@aws-blocks/create-block 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +174 -0
- package/dist/index.js +789 -0
- package/dist/index.test.js +247 -0
- package/package.json +37 -0
- package/templates/primitive/DESIGN.md +27 -0
- package/templates/primitive/LICENSE +174 -0
- package/templates/primitive/README.md +43 -0
- package/templates/primitive/api-extractor.json +4 -0
- package/templates/primitive/package.json +43 -0
- package/templates/primitive/src/errors.ts +12 -0
- package/templates/primitive/src/index.aws.ts +34 -0
- package/templates/primitive/src/index.browser.ts +12 -0
- package/templates/primitive/src/index.cdk.test.ts +47 -0
- package/templates/primitive/src/index.cdk.ts +34 -0
- package/templates/primitive/src/index.mock.ts +46 -0
- package/templates/primitive/src/index.test.ts +29 -0
- package/templates/primitive/src/parity.test.ts +16 -0
- package/templates/primitive/src/types.ts +13 -0
- package/templates/primitive/tsconfig.json +11 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { access, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
/** Templates ship next to the built CLI (packages/create-block/templates). */
|
|
12
|
+
const TEMPLATES_DIR = resolve(__dirname, '..', 'templates');
|
|
13
|
+
// ─── Pure helpers (unit-tested) ──────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a user-supplied block name into a PascalCase class name and validate
|
|
16
|
+
* it. A leading `BB`/`Bb` prefix is stripped (the naming convention forbids it —
|
|
17
|
+
* `KVStore`, not `BBKVStore`).
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeClassName(raw) {
|
|
20
|
+
// Strip a leading BB prefix only when it's unambiguously a prefix: an explicit
|
|
21
|
+
// separator (`bb-`, `bb_`) or `bb` immediately followed by an uppercase letter
|
|
22
|
+
// (`BBKVStore` → `KVStore`). Leaves names like `BBox` untouched.
|
|
23
|
+
// `[Bb]{2}` matches the prefix in any case; the `(?=[A-Z])` lookahead stays
|
|
24
|
+
// case-sensitive (no `i` flag) so `BBox` isn't mangled into `ox`.
|
|
25
|
+
return raw.replace(/^[Bb]{2}(?:[-_]|(?=[A-Z]))/, '');
|
|
26
|
+
}
|
|
27
|
+
/** Validate an npm scope (the part after `@`, before `/`). */
|
|
28
|
+
export function validateScope(scope) {
|
|
29
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(scope)) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: `--scope "${scope}" is not a valid npm scope (lowercase letters, digits, and ._- ; must not start with ._-)`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
37
|
+
export function validateClassName(name) {
|
|
38
|
+
if (!name)
|
|
39
|
+
return { ok: false, reason: 'a block name is required' };
|
|
40
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: `"${name}" must be PascalCase (start with an uppercase letter, letters/digits only) — e.g. "SearchIndex"`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { ok: true };
|
|
47
|
+
}
|
|
48
|
+
/** `DemoStore` → `demo-store`, `SQLCache` → `sql-cache`, `HTTPQueue` → `http-queue`. */
|
|
49
|
+
export function toKebabCase(pascal) {
|
|
50
|
+
return pascal
|
|
51
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // HTTPQueue → HTTP-Queue
|
|
52
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2') // demoStore → demo-Store
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
/** Compute folder / package name from the class name and mode. */
|
|
56
|
+
export function deriveNames(className, mode, scope) {
|
|
57
|
+
const suffix = toKebabCase(className);
|
|
58
|
+
const folder = `bb-${suffix}`;
|
|
59
|
+
const org = mode === 'contributor' ? 'aws-blocks' : scope;
|
|
60
|
+
return { className, suffix, folder, pkgName: `@${org}/bb-${suffix}` };
|
|
61
|
+
}
|
|
62
|
+
/** Replace the two template tokens in a file's text. */
|
|
63
|
+
export function substituteTokens(content, tokens) {
|
|
64
|
+
return content.replace(/__BB_CLASS__/g, tokens.className).replace(/__BB_PKG_NAME__/g, tokens.pkgName);
|
|
65
|
+
}
|
|
66
|
+
// ─── Filesystem helpers ──────────────────────────────────────────────────────
|
|
67
|
+
async function exists(path) {
|
|
68
|
+
return access(path).then(() => true, () => false);
|
|
69
|
+
}
|
|
70
|
+
async function confirm(message) {
|
|
71
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
72
|
+
return new Promise((res) => {
|
|
73
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
74
|
+
rl.close();
|
|
75
|
+
res(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function ask(message, fallback) {
|
|
80
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
81
|
+
return new Promise((res) => {
|
|
82
|
+
rl.question(`${message} `, (answer) => {
|
|
83
|
+
rl.close();
|
|
84
|
+
res(answer.trim() || fallback);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Walk up from `startDir` looking for the AWS Blocks monorepo root: a directory
|
|
90
|
+
* whose `package.json` declares a `workspaces` array that includes
|
|
91
|
+
* `packages/blocks`, and which actually contains `packages/blocks`. Returns the
|
|
92
|
+
* root path in contributor mode, or `null` (external mode).
|
|
93
|
+
*/
|
|
94
|
+
export async function findMonorepoRoot(startDir) {
|
|
95
|
+
let dir = resolve(startDir);
|
|
96
|
+
// Bound the walk to the filesystem root.
|
|
97
|
+
for (;;) {
|
|
98
|
+
const pkgPath = join(dir, 'package.json');
|
|
99
|
+
if (await exists(pkgPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
102
|
+
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : [];
|
|
103
|
+
if (ws.includes('packages/blocks') && (await exists(join(dir, 'packages', 'blocks')))) {
|
|
104
|
+
return dir;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Unparseable package.json — keep walking up.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const parent = dirname(dir);
|
|
112
|
+
if (parent === dir)
|
|
113
|
+
return null;
|
|
114
|
+
dir = parent;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** npm `workspaces` may be a string array or `{ packages: [...] }`. Return the globs. */
|
|
118
|
+
export function normalizeWorkspaces(ws) {
|
|
119
|
+
if (Array.isArray(ws))
|
|
120
|
+
return ws.filter((w) => typeof w === 'string');
|
|
121
|
+
if (ws && typeof ws === 'object' && Array.isArray(ws.packages)) {
|
|
122
|
+
return ws.packages.filter((w) => typeof w === 'string');
|
|
123
|
+
}
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Walk up from `startDir` for a *customer* monorepo root: a `package.json` that
|
|
128
|
+
* declares npm `workspaces` but is NOT the AWS Blocks framework repo (that's
|
|
129
|
+
* contributor mode). Returns `{ root, pkg }` or `null` (→ standalone external).
|
|
130
|
+
*/
|
|
131
|
+
export async function findCustomerWorkspaceRoot(startDir) {
|
|
132
|
+
let dir = resolve(startDir);
|
|
133
|
+
for (;;) {
|
|
134
|
+
const pkgPath = join(dir, 'package.json');
|
|
135
|
+
if (await exists(pkgPath)) {
|
|
136
|
+
try {
|
|
137
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
138
|
+
const ws = normalizeWorkspaces(pkg.workspaces);
|
|
139
|
+
const isBlocksRepo = ws.includes('packages/blocks') && (await exists(join(dir, 'packages', 'blocks')));
|
|
140
|
+
if (ws.length > 0 && !isBlocksRepo)
|
|
141
|
+
return { root: dir, pkg };
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Unparseable package.json — keep walking up.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const parent = dirname(dir);
|
|
148
|
+
if (parent === dir)
|
|
149
|
+
return null;
|
|
150
|
+
dir = parent;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Extract an npm scope from a package name (`@acme/app` → `acme`). */
|
|
154
|
+
export function scopeFromPkgName(name) {
|
|
155
|
+
if (typeof name !== 'string')
|
|
156
|
+
return null;
|
|
157
|
+
const m = name.match(/^@([^/]+)\//);
|
|
158
|
+
return m ? m[1] : null;
|
|
159
|
+
}
|
|
160
|
+
/** Does an existing `workspaces` glob already cover `packages/<folder>`? */
|
|
161
|
+
export function workspacesCover(ws, entry) {
|
|
162
|
+
if (ws.includes(entry))
|
|
163
|
+
return true;
|
|
164
|
+
const slash = entry.lastIndexOf('/');
|
|
165
|
+
if (slash < 0)
|
|
166
|
+
return false;
|
|
167
|
+
const parent = entry.slice(0, slash);
|
|
168
|
+
return ws.includes(`${parent}/*`) || ws.includes(`${parent}/**`);
|
|
169
|
+
}
|
|
170
|
+
/** Recursively list every file (not directory) under `root`, as absolute paths. */
|
|
171
|
+
async function listFiles(root) {
|
|
172
|
+
const out = [];
|
|
173
|
+
async function walk(dir) {
|
|
174
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
175
|
+
const full = join(dir, entry.name);
|
|
176
|
+
if (entry.isDirectory())
|
|
177
|
+
await walk(full);
|
|
178
|
+
else
|
|
179
|
+
out.push(full);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
await walk(root);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Copy one template directory into `targetDir`, substituting tokens in every
|
|
187
|
+
* file's contents. Overlaying (calling twice) overwrites files with the same
|
|
188
|
+
* relative path and records the write once. In `--dry-run` mode nothing is
|
|
189
|
+
* written; `planned` collects the resulting file set.
|
|
190
|
+
*/
|
|
191
|
+
async function copyDir(templateDir, targetDir, tokens, dryRun, planned) {
|
|
192
|
+
if (!(await exists(templateDir))) {
|
|
193
|
+
throw new Error(`Template not found: ${templateDir} (is create-block built?)`);
|
|
194
|
+
}
|
|
195
|
+
for (const src of await listFiles(templateDir)) {
|
|
196
|
+
const rel = relative(templateDir, src);
|
|
197
|
+
const dest = join(targetDir, rel);
|
|
198
|
+
planned.set(dest, { path: dest, action: planned.has(dest) ? 'overwrite' : 'create' });
|
|
199
|
+
if (dryRun)
|
|
200
|
+
continue;
|
|
201
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
202
|
+
await writeFile(dest, substituteTokens(await readFile(src, 'utf-8'), tokens));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Materialize the `primitive` block template into `targetDir`. */
|
|
206
|
+
async function copyTemplate(targetDir, tokens, dryRun, planned) {
|
|
207
|
+
await copyDir(join(TEMPLATES_DIR, 'primitive'), targetDir, tokens, dryRun, planned);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve a registry-installable range for an `@aws-blocks/*` dependency. The
|
|
211
|
+
* templates pin the versions used *inside the monorepo*, which don't match the
|
|
212
|
+
* published registry — so outside contributor mode we re-pin to the latest
|
|
213
|
+
* published version (`^x.y.z`). Falls back to `latest` when offline / unknown.
|
|
214
|
+
*/
|
|
215
|
+
function resolvePublishedRange(pkgName) {
|
|
216
|
+
// Escape hatch for hermetic tests (avoid a network call to the registry).
|
|
217
|
+
if (process.env.CREATE_BLOCK_SKIP_REGISTRY)
|
|
218
|
+
return 'latest';
|
|
219
|
+
try {
|
|
220
|
+
// execFileSync (argv array, no shell) — pkgName never touches a shell string.
|
|
221
|
+
const v = execFileSync('npm', ['view', pkgName, 'version'], {
|
|
222
|
+
encoding: 'utf-8',
|
|
223
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
224
|
+
}).trim();
|
|
225
|
+
if (/^\d+\.\d+\.\d+/.test(v))
|
|
226
|
+
return `^${v}`;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Offline or unpublished — fall back to the floating tag.
|
|
230
|
+
}
|
|
231
|
+
return 'latest';
|
|
232
|
+
}
|
|
233
|
+
/** A standalone `scripts/generate-version.mjs` for out-of-monorepo packages. */
|
|
234
|
+
const STANDALONE_VERSION_SCRIPT = `#!/usr/bin/env node
|
|
235
|
+
// Auto-generated by @aws-blocks/create-block. Regenerates src/version.ts from
|
|
236
|
+
// the block name (argv[2]) and this package's version. Standalone — no monorepo.
|
|
237
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
238
|
+
|
|
239
|
+
const bbName = process.argv[2];
|
|
240
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
241
|
+
writeFileSync(
|
|
242
|
+
new URL('../src/version.ts', import.meta.url),
|
|
243
|
+
\`// Auto-generated — do not edit manually\\nexport const BB_NAME = '\${bbName}';\\nexport const BB_VERSION = '\${pkg.version}';\\n\`,
|
|
244
|
+
);
|
|
245
|
+
`;
|
|
246
|
+
/**
|
|
247
|
+
* External / customer mode fixup: make the generated package build and install
|
|
248
|
+
* outside the monorepo. The shipped template's `prebuild` calls the monorepo's
|
|
249
|
+
* `scripts/generate-version.mjs` and its deps pin monorepo-internal versions —
|
|
250
|
+
* neither works from the registry. Point `prebuild` at a standalone
|
|
251
|
+
* `scripts/generate-version.mjs`, re-pin `@aws-blocks/*` deps to published
|
|
252
|
+
* versions, add the `aws-blocks` discovery keyword, drop the core-coupled CDK
|
|
253
|
+
* synth test, and swap the tsconfig for a standalone one.
|
|
254
|
+
*/
|
|
255
|
+
async function fixupForExternal(targetDir, className, dryRun) {
|
|
256
|
+
if (dryRun)
|
|
257
|
+
return; // nothing on disk to fix up in a preview
|
|
258
|
+
const pkgPath = join(targetDir, 'package.json');
|
|
259
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
260
|
+
pkg.scripts ??= {};
|
|
261
|
+
// Point prebuild at a small standalone .mjs (written below) instead of the
|
|
262
|
+
// monorepo's scripts/ — a real file avoids cross-shell quoting issues (npm
|
|
263
|
+
// runs scripts through cmd.exe on Windows).
|
|
264
|
+
pkg.scripts.prebuild = `node scripts/generate-version.mjs ${className}`;
|
|
265
|
+
pkg.keywords = Array.from(new Set([...(pkg.keywords ?? []), 'aws-blocks']));
|
|
266
|
+
// Re-pin @aws-blocks/* deps to versions that exist on the registry.
|
|
267
|
+
for (const deps of [pkg.dependencies, pkg.peerDependencies]) {
|
|
268
|
+
if (!deps)
|
|
269
|
+
continue;
|
|
270
|
+
for (const name of Object.keys(deps)) {
|
|
271
|
+
if (name.startsWith('@aws-blocks/'))
|
|
272
|
+
deps[name] = resolvePublishedRange(name);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!dryRun)
|
|
276
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
277
|
+
// Write the standalone prebuild helper referenced by pkg.scripts.prebuild.
|
|
278
|
+
if (!dryRun) {
|
|
279
|
+
const scriptPath = join(targetDir, 'scripts', 'generate-version.mjs');
|
|
280
|
+
await mkdir(dirname(scriptPath), { recursive: true });
|
|
281
|
+
await writeFile(scriptPath, STANDALONE_VERSION_SCRIPT);
|
|
282
|
+
}
|
|
283
|
+
// The CDK *synth test's* harness depends on how @aws-blocks/core attaches a
|
|
284
|
+
// block to its Stack, which varies by core version — so it isn't portable to
|
|
285
|
+
// an arbitrary installed core. Drop it from out-of-monorepo packages; the
|
|
286
|
+
// runtime + parity tests (the customer-relevant, portable ones) still ship,
|
|
287
|
+
// and the CDK construct itself (index.cdk.ts) is unchanged. Author a CDK test
|
|
288
|
+
// against your own stack setup if you need one.
|
|
289
|
+
const cdkTest = join(targetDir, 'src', 'index.cdk.test.ts');
|
|
290
|
+
if (!dryRun && (await exists(cdkTest)))
|
|
291
|
+
await rm(cdkTest);
|
|
292
|
+
// The shipped tsconfig extends the monorepo base and references sibling
|
|
293
|
+
// packages by path — neither exists outside the repo. Replace it with a
|
|
294
|
+
// self-contained config so `tsc --build` works standalone.
|
|
295
|
+
const tsconfigPath = join(targetDir, 'tsconfig.json');
|
|
296
|
+
if (await exists(tsconfigPath)) {
|
|
297
|
+
const standalone = {
|
|
298
|
+
compilerOptions: {
|
|
299
|
+
target: 'ES2022',
|
|
300
|
+
module: 'ES2022',
|
|
301
|
+
moduleResolution: 'bundler',
|
|
302
|
+
strict: true,
|
|
303
|
+
esModuleInterop: true,
|
|
304
|
+
skipLibCheck: true,
|
|
305
|
+
forceConsistentCasingInFileNames: true,
|
|
306
|
+
declaration: true,
|
|
307
|
+
declarationMap: true,
|
|
308
|
+
composite: true,
|
|
309
|
+
incremental: true,
|
|
310
|
+
outDir: './dist',
|
|
311
|
+
rootDir: './src',
|
|
312
|
+
},
|
|
313
|
+
include: ['src/**/*'],
|
|
314
|
+
};
|
|
315
|
+
if (!dryRun)
|
|
316
|
+
await writeFile(tsconfigPath, `${JSON.stringify(standalone, null, 2)}\n`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Contributor fixup: re-pin the generated block's `@aws-blocks/*` deps to the
|
|
321
|
+
* monorepo's *current local* versions (`^x.y.z`, as sibling BBs do). The
|
|
322
|
+
* templates carry a snapshot version that drifts as core is bumped; without this
|
|
323
|
+
* the pinned range stops matching the local workspace and npm won't link it.
|
|
324
|
+
*/
|
|
325
|
+
async function repinContributorDeps(root, targetDir, dryRun) {
|
|
326
|
+
if (dryRun)
|
|
327
|
+
return; // nothing on disk to re-pin in a preview
|
|
328
|
+
const pkgPath = join(targetDir, 'package.json');
|
|
329
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
330
|
+
let changed = false;
|
|
331
|
+
for (const deps of [pkg.dependencies, pkg.peerDependencies]) {
|
|
332
|
+
if (!deps)
|
|
333
|
+
continue;
|
|
334
|
+
for (const name of Object.keys(deps)) {
|
|
335
|
+
if (!name.startsWith('@aws-blocks/'))
|
|
336
|
+
continue;
|
|
337
|
+
const local = name.slice('@aws-blocks/'.length);
|
|
338
|
+
try {
|
|
339
|
+
const sibling = JSON.parse(await readFile(join(root, 'packages', local, 'package.json'), 'utf-8'));
|
|
340
|
+
if (sibling.version) {
|
|
341
|
+
deps[name] = `^${sibling.version}`;
|
|
342
|
+
changed = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Not a local package (or unreadable) — leave the template pin as-is.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (changed && !dryRun)
|
|
351
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
352
|
+
}
|
|
353
|
+
// ─── Contributor-mode monorepo wiring ────────────────────────────────────────
|
|
354
|
+
const BEGIN_MARKER = '// <!-- BEGIN:generated-block-exports -->';
|
|
355
|
+
const END_MARKER = '// <!-- END:generated-block-exports -->';
|
|
356
|
+
/**
|
|
357
|
+
* Insert `entry` between the generated-exports markers in `content`, adding the
|
|
358
|
+
* marker pair at the end of the file if it is not present yet. Idempotent: if
|
|
359
|
+
* the entry already appears inside the block it is left untouched.
|
|
360
|
+
*/
|
|
361
|
+
export function insertBetweenMarkers(content, entry) {
|
|
362
|
+
let text = content;
|
|
363
|
+
if (!text.includes(BEGIN_MARKER)) {
|
|
364
|
+
const trimmed = text.replace(/\s*$/, '');
|
|
365
|
+
text = `${trimmed}\n\n${BEGIN_MARKER}\n${END_MARKER}\n`;
|
|
366
|
+
}
|
|
367
|
+
const begin = text.indexOf(BEGIN_MARKER) + BEGIN_MARKER.length;
|
|
368
|
+
const end = text.indexOf(END_MARKER);
|
|
369
|
+
const region = text.slice(begin, end);
|
|
370
|
+
if (region.includes(entry.trim()))
|
|
371
|
+
return text; // already wired — idempotent
|
|
372
|
+
const updated = `${region.replace(/\s*$/, '')}\n${entry}\n`;
|
|
373
|
+
return text.slice(0, begin) + updated + text.slice(end);
|
|
374
|
+
}
|
|
375
|
+
async function wireContributor(root, names, dryRun) {
|
|
376
|
+
const edits = [];
|
|
377
|
+
const warnings = [];
|
|
378
|
+
const { className, suffix, folder, pkgName } = names;
|
|
379
|
+
// Both editors record whether they actually changed anything, so the printed
|
|
380
|
+
// summary distinguishes a real edit from "already present".
|
|
381
|
+
const editJson = async (path, mutate, label) => {
|
|
382
|
+
const obj = JSON.parse(await readFile(path, 'utf-8'));
|
|
383
|
+
const before = JSON.stringify(obj);
|
|
384
|
+
mutate(obj);
|
|
385
|
+
const changed = JSON.stringify(obj) !== before;
|
|
386
|
+
if (changed && !dryRun)
|
|
387
|
+
await writeFile(path, `${JSON.stringify(obj, null, 2)}\n`);
|
|
388
|
+
edits.push(changed ? label : `${label} (already present)`);
|
|
389
|
+
};
|
|
390
|
+
const editText = async (path, mutate, label) => {
|
|
391
|
+
const before = await readFile(path, 'utf-8');
|
|
392
|
+
const after = mutate(before);
|
|
393
|
+
if (after !== before && !dryRun)
|
|
394
|
+
await writeFile(path, after);
|
|
395
|
+
edits.push(after !== before ? label : `${label} (already present)`);
|
|
396
|
+
};
|
|
397
|
+
// All touchpoints run inside try/finally so a mid-way failure still returns
|
|
398
|
+
// the edits that already landed (the caller prints them + the warning).
|
|
399
|
+
try {
|
|
400
|
+
// 1. Root workspaces — append packages/<folder> if absent.
|
|
401
|
+
await editJson(join(root, 'package.json'), (o) => {
|
|
402
|
+
o.workspaces ??= [];
|
|
403
|
+
if (!o.workspaces.includes(`packages/${folder}`))
|
|
404
|
+
o.workspaces.push(`packages/${folder}`);
|
|
405
|
+
}, 'root package.json → workspaces');
|
|
406
|
+
// 2. Umbrella runtime re-export (with JSDoc) + type re-export.
|
|
407
|
+
await editText(join(root, 'packages/blocks/src/index.ts'), (s) => insertBetweenMarkers(s, `/**\n * **${className}** — TODO: one-line summary shown in IDE hover.\n *\n * Package: \`${pkgName}\`\n * Full docs: \`README.md\` in the package directory above.\n */\nexport { ${className}, ${className}Errors } from '${pkgName}';\nexport type { ${className}Options } from '${pkgName}';`), 'packages/blocks/src/index.ts → re-export');
|
|
408
|
+
// 3. Umbrella CDK re-export (terse).
|
|
409
|
+
await editText(join(root, 'packages/blocks/src/index.cdk.ts'), (s) => insertBetweenMarkers(s, `export { ${className}, ${className}Errors } from '${pkgName}';\nexport type { ${className}Options } from '${pkgName}';`), 'packages/blocks/src/index.cdk.ts → re-export');
|
|
410
|
+
// 4. Umbrella package.json: dependency + vendorize map entry.
|
|
411
|
+
await editJson(join(root, 'packages/blocks/package.json'), (o) => {
|
|
412
|
+
o.dependencies ??= {};
|
|
413
|
+
o.dependencies[pkgName] = '^0.1.0';
|
|
414
|
+
o['aws-blocks'] ??= {};
|
|
415
|
+
o['aws-blocks'].vendorize ??= {};
|
|
416
|
+
o['aws-blocks'].vendorize[pkgName] = [className];
|
|
417
|
+
}, 'packages/blocks/package.json → dependencies + vendorize');
|
|
418
|
+
// 5. Umbrella tsconfig project reference.
|
|
419
|
+
await editJson(join(root, 'packages/blocks/tsconfig.json'), (o) => {
|
|
420
|
+
o.references ??= [];
|
|
421
|
+
if (!o.references.some((r) => r.path === `../${folder}`)) {
|
|
422
|
+
o.references.push({ path: `../${folder}` });
|
|
423
|
+
}
|
|
424
|
+
}, 'packages/blocks/tsconfig.json → reference');
|
|
425
|
+
// 6. Comprehensive test app: dependency + starter test.
|
|
426
|
+
const compPkg = join(root, 'test-apps/comprehensive/package.json');
|
|
427
|
+
if (await exists(compPkg)) {
|
|
428
|
+
await editJson(compPkg, (o) => {
|
|
429
|
+
o.dependencies ??= {};
|
|
430
|
+
o.dependencies[pkgName] = '*';
|
|
431
|
+
}, 'test-apps/comprehensive/package.json → dependency');
|
|
432
|
+
const testPath = join(root, `test-apps/comprehensive/test/${suffix}.test.ts`);
|
|
433
|
+
if (!(await exists(testPath))) {
|
|
434
|
+
const starter = starterComprehensiveTest(className, pkgName);
|
|
435
|
+
if (!dryRun)
|
|
436
|
+
await writeFile(testPath, starter);
|
|
437
|
+
edits.push(`test-apps/comprehensive/test/${suffix}.test.ts → starter (author TODO)`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
warnings.push('test-apps/comprehensive not found — skipped test-app wiring');
|
|
442
|
+
}
|
|
443
|
+
// 7. Changeset.
|
|
444
|
+
const changesetName = `add-${folder}-${randomBytes(3).toString('hex')}`;
|
|
445
|
+
const changesetPath = join(root, `.changeset/${changesetName}.md`);
|
|
446
|
+
const changeset = `---\n"${pkgName}": minor\n"@aws-blocks/blocks": patch\n---\n\nAdd \`${className}\` Building Block (\`${pkgName}\`) and re-export it from \`@aws-blocks/blocks\`.\n\nTODO: describe what this block does and its public surface before release.\n`;
|
|
447
|
+
if (!dryRun)
|
|
448
|
+
await writeFile(changesetPath, changeset);
|
|
449
|
+
edits.push(`.changeset/${changesetName}.md`);
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
warnings.push(`wiring stopped early: ${e.message}. ` +
|
|
453
|
+
`Completed: ${edits.join('; ') || 'nothing'}. Finish the remaining touchpoints by hand (see AGENTS.md).`);
|
|
454
|
+
}
|
|
455
|
+
return { edits, warnings };
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Customer-mode wiring: register `packages/<folder>` in the customer's root
|
|
459
|
+
* `workspaces` so `npm install` links it and their app can import it without
|
|
460
|
+
* publishing. Skips the edit when an existing glob (e.g. `packages/*`) already
|
|
461
|
+
* covers it. Does not touch the app's own package.json or source.
|
|
462
|
+
*/
|
|
463
|
+
async function wireCustomer(root, customerPkg, names, dryRun) {
|
|
464
|
+
const edits = [];
|
|
465
|
+
const warnings = [];
|
|
466
|
+
const entry = `packages/${names.folder}`;
|
|
467
|
+
const ws = normalizeWorkspaces(customerPkg.workspaces);
|
|
468
|
+
if (workspacesCover(ws, entry)) {
|
|
469
|
+
edits.push(`workspaces already cover ${entry} (no package.json edit needed)`);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
const pkgPath = join(root, 'package.json');
|
|
473
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
474
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
475
|
+
pkg.workspaces.push(entry);
|
|
476
|
+
}
|
|
477
|
+
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
|
|
478
|
+
pkg.workspaces.packages.push(entry);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
pkg.workspaces = [entry];
|
|
482
|
+
}
|
|
483
|
+
if (!dryRun)
|
|
484
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
485
|
+
edits.push(`root package.json → workspaces += "${entry}"`);
|
|
486
|
+
}
|
|
487
|
+
return { edits, warnings };
|
|
488
|
+
}
|
|
489
|
+
function starterComprehensiveTest(className, pkgName) {
|
|
490
|
+
return `// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
491
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
492
|
+
|
|
493
|
+
// TODO(author): flesh this out. Instantiate ${className} against the app's Scope
|
|
494
|
+
// in test-apps/comprehensive/aws-blocks/index.ts, expose it via the ApiNamespace,
|
|
495
|
+
// and assert the end-to-end typed DX with ZERO type casts (see AGENTS.md).
|
|
496
|
+
import { test } from 'node:test';
|
|
497
|
+
import assert from 'node:assert';
|
|
498
|
+
import { ${className} } from '${pkgName}';
|
|
499
|
+
|
|
500
|
+
test('${className}: scaffolded placeholder — replace with real e2e coverage', () => {
|
|
501
|
+
assert.ok(${className}, '${className} should be importable from ${pkgName}');
|
|
502
|
+
});
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
// ─── Verification ────────────────────────────────────────────────────────────
|
|
506
|
+
function verify(root, pkgName) {
|
|
507
|
+
try {
|
|
508
|
+
execSync(`npm run build -w ${pkgName}`, { cwd: root, stdio: 'inherit' });
|
|
509
|
+
execSync(`npm test -w ${pkgName}`, { cwd: root, stdio: 'inherit' });
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// ─── Arg parsing ─────────────────────────────────────────────────────────────
|
|
517
|
+
export function parseArgs(argv) {
|
|
518
|
+
const opts = { yes: false, skipInstall: false, skipVerify: false, dryRun: false, help: false };
|
|
519
|
+
for (let i = 0; i < argv.length; i++) {
|
|
520
|
+
const arg = argv[i];
|
|
521
|
+
// Consume the next token as this flag's value, rejecting a missing value
|
|
522
|
+
// or another flag (so `--dir --yes` errors instead of silently eating --yes).
|
|
523
|
+
const takeValue = () => {
|
|
524
|
+
const next = argv[i + 1];
|
|
525
|
+
if (next === undefined || next.startsWith('-')) {
|
|
526
|
+
throw new Error(`${arg} requires a value`);
|
|
527
|
+
}
|
|
528
|
+
i++;
|
|
529
|
+
return next;
|
|
530
|
+
};
|
|
531
|
+
switch (arg) {
|
|
532
|
+
case '--help':
|
|
533
|
+
case '-h':
|
|
534
|
+
opts.help = true;
|
|
535
|
+
break;
|
|
536
|
+
case '--yes':
|
|
537
|
+
case '-y':
|
|
538
|
+
opts.yes = true;
|
|
539
|
+
break;
|
|
540
|
+
case '--skip-install':
|
|
541
|
+
opts.skipInstall = true;
|
|
542
|
+
break;
|
|
543
|
+
case '--skip-verify':
|
|
544
|
+
opts.skipVerify = true;
|
|
545
|
+
break;
|
|
546
|
+
case '--dry-run':
|
|
547
|
+
opts.dryRun = true;
|
|
548
|
+
break;
|
|
549
|
+
case '--dir':
|
|
550
|
+
opts.dir = takeValue();
|
|
551
|
+
break;
|
|
552
|
+
case '--scope':
|
|
553
|
+
opts.scope = takeValue();
|
|
554
|
+
break;
|
|
555
|
+
default:
|
|
556
|
+
if (arg.startsWith('-'))
|
|
557
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
558
|
+
if (opts.className)
|
|
559
|
+
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
560
|
+
opts.className = arg;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return opts;
|
|
564
|
+
}
|
|
565
|
+
function printUsage() {
|
|
566
|
+
console.log(`
|
|
567
|
+
create-block — scaffold a new AWS Blocks Building Block
|
|
568
|
+
|
|
569
|
+
Usage:
|
|
570
|
+
npm create @aws-blocks/block@latest <ClassName> [options]
|
|
571
|
+
npx @aws-blocks/create-block <ClassName> [options]
|
|
572
|
+
|
|
573
|
+
Arguments:
|
|
574
|
+
<ClassName> PascalCase block class name, no "BB" prefix (e.g. SearchIndex)
|
|
575
|
+
|
|
576
|
+
Options:
|
|
577
|
+
--dir <path> target directory (default: derived from the package name)
|
|
578
|
+
--scope <npm-scope> npm scope for external mode (default: your-org)
|
|
579
|
+
--yes, -y accept defaults / skip confirmation
|
|
580
|
+
--skip-install do not run npm install (external mode)
|
|
581
|
+
--skip-verify do not build + test the generated block afterward
|
|
582
|
+
--dry-run print what would be generated without writing anything
|
|
583
|
+
--help, -h show this help
|
|
584
|
+
|
|
585
|
+
Modes (auto-detected):
|
|
586
|
+
contributor run inside the aws-blocks monorepo → generates packages/bb-<name>
|
|
587
|
+
and wires it into @aws-blocks/blocks, the root workspaces, the
|
|
588
|
+
comprehensive test app, and a changeset.
|
|
589
|
+
customer run inside your own npm-workspaces repo → generates
|
|
590
|
+
packages/bb-<name>, registers it in your root workspaces, and
|
|
591
|
+
npm-installs so your app can import it (no publish). App code
|
|
592
|
+
is not modified.
|
|
593
|
+
external run anywhere else → generates a standalone @<scope>/bb-<name>
|
|
594
|
+
package (keywords: ["aws-blocks"]), no workspace wiring.
|
|
595
|
+
|
|
596
|
+
What it generates:
|
|
597
|
+
A Building Block — a Scope subclass with one strongly-typed API, backed by four
|
|
598
|
+
conditional-export entries selected per execution context:
|
|
599
|
+
src/index.mock.ts local dev + tests (the default + types entry)
|
|
600
|
+
src/index.aws.ts deployed Lambda runtime (real AWS SDK calls)
|
|
601
|
+
src/index.cdk.ts cdk synth — provisions infra, grants IAM, synthGuard stubs
|
|
602
|
+
src/index.browser.ts browser stub (re-exports the public types + errors)
|
|
603
|
+
src/types.ts, src/errors.ts, README.md, DESIGN.md, package.json, tsconfig.json
|
|
604
|
+
The code is a storage-agnostic skeleton with one example method and TODO markers —
|
|
605
|
+
fill it in with your block's real API.
|
|
606
|
+
|
|
607
|
+
Next steps (for humans and coding agents):
|
|
608
|
+
1. Implement the API in src/. Keep index.mock.ts and index.aws.ts behaviorally
|
|
609
|
+
identical, and add a synthGuard stub in index.cdk.ts for every runtime method.
|
|
610
|
+
2. Return only JSON-serializable values; get()-style reads return null for
|
|
611
|
+
not-found (don't throw). Derive resource names from this.fullId.
|
|
612
|
+
3. Fill in README.md / DESIGN.md and the TODOs, then build + test.
|
|
613
|
+
Reference: packages/bb-kv-store is the canonical worked example.
|
|
614
|
+
|
|
615
|
+
Examples:
|
|
616
|
+
npm create @aws-blocks/block@latest SearchIndex
|
|
617
|
+
npx @aws-blocks/create-block SearchIndex --scope acme --dir ./bb-search-index
|
|
618
|
+
`);
|
|
619
|
+
}
|
|
620
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
621
|
+
export async function run(argv, cwd) {
|
|
622
|
+
let opts;
|
|
623
|
+
try {
|
|
624
|
+
opts = parseArgs(argv);
|
|
625
|
+
}
|
|
626
|
+
catch (e) {
|
|
627
|
+
console.error(`Error: ${e.message}`);
|
|
628
|
+
printUsage();
|
|
629
|
+
return 1;
|
|
630
|
+
}
|
|
631
|
+
if (opts.help) {
|
|
632
|
+
printUsage();
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
// Resolve the block name.
|
|
636
|
+
let className = opts.className ? normalizeClassName(opts.className) : '';
|
|
637
|
+
if (!className && !opts.yes) {
|
|
638
|
+
className = normalizeClassName(await ask('Block class name (PascalCase, e.g. SearchIndex):', ''));
|
|
639
|
+
}
|
|
640
|
+
const nameCheck = validateClassName(className);
|
|
641
|
+
if (!nameCheck.ok) {
|
|
642
|
+
console.error(`Error: ${nameCheck.reason}`);
|
|
643
|
+
return 1;
|
|
644
|
+
}
|
|
645
|
+
// Validate a user-supplied --scope up front (it flows into package.json).
|
|
646
|
+
if (opts.scope) {
|
|
647
|
+
const scopeCheck = validateScope(opts.scope);
|
|
648
|
+
if (!scopeCheck.ok) {
|
|
649
|
+
console.error(`Error: ${scopeCheck.reason}`);
|
|
650
|
+
return 1;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
// Detect mode: AWS Blocks monorepo (contributor) → customer workspace → standalone.
|
|
654
|
+
const monorepoRoot = await findMonorepoRoot(cwd);
|
|
655
|
+
const customer = monorepoRoot ? null : await findCustomerWorkspaceRoot(cwd);
|
|
656
|
+
const mode = monorepoRoot ? 'contributor' : customer ? 'customer' : 'external';
|
|
657
|
+
// Resolve the npm scope + derived names.
|
|
658
|
+
const scope = mode === 'customer'
|
|
659
|
+
? (opts.scope ?? scopeFromPkgName(customer?.pkg?.name) ?? 'app')
|
|
660
|
+
: (opts.scope ?? 'your-org');
|
|
661
|
+
const names = deriveNames(className, mode, scope);
|
|
662
|
+
// Resolve target directory.
|
|
663
|
+
const targetDir = mode === 'contributor'
|
|
664
|
+
? join(monorepoRoot, 'packages', names.folder)
|
|
665
|
+
: mode === 'customer'
|
|
666
|
+
? join(customer.root, 'packages', names.folder)
|
|
667
|
+
: resolve(cwd, opts.dir ?? names.folder);
|
|
668
|
+
if (await exists(targetDir)) {
|
|
669
|
+
const isEmpty = (await readdir(targetDir).catch(() => [])).length === 0;
|
|
670
|
+
if (!isEmpty) {
|
|
671
|
+
console.error(`Error: target directory already exists and is not empty: ${targetDir}`);
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Summary + confirm.
|
|
676
|
+
console.log('');
|
|
677
|
+
console.log(` Block: ${names.className}`);
|
|
678
|
+
console.log(` Package: ${names.pkgName}`);
|
|
679
|
+
const contextRoot = mode === 'contributor' ? monorepoRoot : mode === 'customer' ? customer.root : null;
|
|
680
|
+
console.log(` Mode: ${mode}${contextRoot ? ` (${mode === 'contributor' ? 'monorepo' : 'workspace'}: ${contextRoot})` : ''}`);
|
|
681
|
+
console.log(` Target: ${targetDir}`);
|
|
682
|
+
console.log('');
|
|
683
|
+
if (opts.dryRun)
|
|
684
|
+
console.log(' (--dry-run: no files will be written)\n');
|
|
685
|
+
if (!opts.yes && !opts.dryRun && !(await confirm('Scaffold this block?'))) {
|
|
686
|
+
console.log('Aborted.');
|
|
687
|
+
return 0;
|
|
688
|
+
}
|
|
689
|
+
// Generate.
|
|
690
|
+
const planned = new Map();
|
|
691
|
+
await copyTemplate(targetDir, { className: names.className, pkgName: names.pkgName }, opts.dryRun, planned);
|
|
692
|
+
// Contributor mode uses the monorepo's shared build (scripts/, tsconfig.base);
|
|
693
|
+
// customer + external need a self-contained build.
|
|
694
|
+
if (mode !== 'contributor')
|
|
695
|
+
await fixupForExternal(targetDir, names.className, opts.dryRun);
|
|
696
|
+
let wire = null;
|
|
697
|
+
if (mode === 'contributor') {
|
|
698
|
+
await repinContributorDeps(monorepoRoot, targetDir, opts.dryRun);
|
|
699
|
+
wire = await wireContributor(monorepoRoot, names, opts.dryRun);
|
|
700
|
+
}
|
|
701
|
+
else if (mode === 'customer') {
|
|
702
|
+
const c = customer;
|
|
703
|
+
wire = await wireCustomer(c.root, c.pkg, names, opts.dryRun);
|
|
704
|
+
}
|
|
705
|
+
if (opts.dryRun) {
|
|
706
|
+
console.log('Would create:');
|
|
707
|
+
for (const p of planned.values())
|
|
708
|
+
console.log(` + ${relative(cwd, p.path)}`);
|
|
709
|
+
if (wire) {
|
|
710
|
+
console.log('Would wire:');
|
|
711
|
+
for (const e of wire.edits)
|
|
712
|
+
console.log(` ~ ${e}`);
|
|
713
|
+
}
|
|
714
|
+
return 0;
|
|
715
|
+
}
|
|
716
|
+
console.log(`\nCreated ${planned.size} files in ${relative(cwd, targetDir) || '.'}`);
|
|
717
|
+
if (wire) {
|
|
718
|
+
console.log(mode === 'contributor' ? 'Wired:' : 'Linked:');
|
|
719
|
+
for (const e of wire.edits)
|
|
720
|
+
console.log(` ~ ${e}`);
|
|
721
|
+
for (const w of wire.warnings)
|
|
722
|
+
console.log(` ! ${w}`);
|
|
723
|
+
if (mode === 'contributor') {
|
|
724
|
+
// Regenerate the README catalog table (idempotent, safe to fail).
|
|
725
|
+
try {
|
|
726
|
+
execSync('npm run sync-docs', { cwd: monorepoRoot, stdio: 'pipe' });
|
|
727
|
+
console.log(' ~ packages/blocks/README.md catalog (npm run sync-docs)');
|
|
728
|
+
}
|
|
729
|
+
catch (e) {
|
|
730
|
+
console.log(` ! npm run sync-docs failed (run it manually): ${e.message.split('\n')[0]}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Install so the workspace symlink / package deps resolve. Customer installs
|
|
735
|
+
// at the workspace root (links the sub-package); external installs in-package.
|
|
736
|
+
const installCwd = mode === 'customer' ? customer.root : mode === 'external' ? targetDir : null;
|
|
737
|
+
if (installCwd && !opts.skipInstall) {
|
|
738
|
+
try {
|
|
739
|
+
execSync('npm install', { cwd: installCwd, stdio: 'inherit' });
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
console.log('! npm install failed — run it manually.');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// Verify (build + test the new package). Skipped when install was skipped in
|
|
746
|
+
// customer/external mode, since the workspace link wouldn't exist yet.
|
|
747
|
+
const verifyRoot = mode === 'contributor'
|
|
748
|
+
? monorepoRoot
|
|
749
|
+
: mode === 'customer'
|
|
750
|
+
? customer.root
|
|
751
|
+
: null;
|
|
752
|
+
const canVerify = mode === 'contributor' || !opts.skipInstall;
|
|
753
|
+
if (verifyRoot && canVerify && !opts.skipVerify) {
|
|
754
|
+
console.log('\nVerifying (build + test)...');
|
|
755
|
+
if (!verify(verifyRoot, names.pkgName)) {
|
|
756
|
+
console.log('! Verification failed — inspect the build output above.');
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
printNextSteps(mode, names);
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
function printNextSteps(mode, names) {
|
|
763
|
+
console.log('\nNext steps:');
|
|
764
|
+
if (mode === 'contributor') {
|
|
765
|
+
console.log(` 1. Implement ${names.className}'s API in packages/${names.folder}/src/.`);
|
|
766
|
+
console.log(` 2. Add a real ${names.className} instance + assertions to test-apps/comprehensive`);
|
|
767
|
+
console.log(` (aws-blocks/index.ts and test/${names.suffix}.test.ts — zero type casts).`);
|
|
768
|
+
console.log(` 3. Fill in the TODO summaries in packages/blocks/src/index.ts and the changeset.`);
|
|
769
|
+
console.log(' 4. npm run build && npm run lint:deps && npm test && npm run test:e2e:local');
|
|
770
|
+
}
|
|
771
|
+
else if (mode === 'customer') {
|
|
772
|
+
console.log(` 1. Implement ${names.className}'s API in packages/${names.folder}/src/.`);
|
|
773
|
+
console.log(` 2. Import it in your backend: import { ${names.className} } from '${names.pkgName}';`);
|
|
774
|
+
console.log(` (it's linked into your workspace — no publish needed).`);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
console.log(` 1. cd ${names.folder} && npm run build && npm test`);
|
|
778
|
+
console.log(` 2. Implement ${names.className}'s API in src/, then publish (keywords: ["aws-blocks"]).`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// Only execute when invoked as the CLI (not when imported by tests).
|
|
782
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
783
|
+
run(process.argv.slice(2), process.cwd())
|
|
784
|
+
.then((code) => process.exit(code))
|
|
785
|
+
.catch((e) => {
|
|
786
|
+
console.error(e);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
});
|
|
789
|
+
}
|