@expressots/cli 4.0.0-preview.2 ā 4.0.0-preview.3
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/bin/cicd/cli.d.ts +1 -1
- package/bin/cicd/cli.js +3 -1
- package/bin/cicd/form.js +5 -4
- package/bin/cli.d.ts +1 -5
- package/bin/cli.js +56 -6
- package/bin/commands/project.commands.js +233 -26
- package/bin/containerize/cli.d.ts +1 -1
- package/bin/containerize/cli.js +1 -1
- package/bin/containerize/form.js +49 -51
- package/bin/containerize/generators/ci-generator.js +16 -12
- package/bin/containerize/generators/docker-compose-generator.js +3 -2
- package/bin/containerize/generators/dockerfile-generator.js +50 -28
- package/bin/containerize/generators/kubernetes-generator.js +5 -4
- package/bin/costs/cli.d.ts +1 -1
- package/bin/costs/cli.js +4 -2
- package/bin/dev/cli.d.ts +1 -1
- package/bin/dev/cli.js +3 -1
- package/bin/generate/cli.d.ts +1 -1
- package/bin/generate/templates/nonopinionated/config.tpl +12 -12
- package/bin/generate/templates/nonopinionated/event.tpl +10 -10
- package/bin/generate/templates/nonopinionated/guard.tpl +18 -18
- package/bin/generate/templates/nonopinionated/handler.tpl +12 -12
- package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -27
- package/bin/generate/templates/opinionated/config.tpl +47 -47
- package/bin/generate/templates/opinionated/event.tpl +15 -15
- package/bin/generate/templates/opinionated/guard.tpl +41 -41
- package/bin/generate/templates/opinionated/handler.tpl +23 -23
- package/bin/generate/templates/opinionated/interceptor.tpl +50 -50
- package/bin/generate/utils/command-utils.d.ts +13 -2
- package/bin/generate/utils/command-utils.js +50 -17
- package/bin/generate/utils/opinionated-cmd.js +19 -12
- package/bin/help/cli.d.ts +1 -1
- package/bin/help/command-help-registry.d.ts +23 -0
- package/bin/help/command-help-registry.js +303 -0
- package/bin/help/command-help.d.ts +36 -0
- package/bin/help/command-help.js +56 -0
- package/bin/help/form.js +127 -30
- package/bin/help/main-help.d.ts +8 -0
- package/bin/help/main-help.js +126 -0
- package/bin/help/render.d.ts +32 -0
- package/bin/help/render.js +46 -0
- package/bin/info/cli.d.ts +1 -1
- package/bin/info/form.d.ts +1 -1
- package/bin/info/form.js +11 -11
- package/bin/migrate/cli.d.ts +1 -1
- package/bin/migrate/cli.js +3 -1
- package/bin/migrate/form.js +4 -3
- package/bin/new/cli.d.ts +5 -1
- package/bin/new/cli.js +62 -14
- package/bin/new/form.d.ts +3 -1
- package/bin/new/form.js +338 -23
- package/bin/profile/cli.d.ts +1 -1
- package/bin/profile/cli.js +3 -1
- package/bin/profile/form.js +5 -4
- package/bin/providers/create/form.js +53 -4
- package/bin/studio/cli.js +9 -3
- package/bin/templates/cli.js +7 -5
- package/bin/utils/add-module-to-container.d.ts +14 -3
- package/bin/utils/add-module-to-container.js +330 -111
- package/bin/utils/cli-ui.d.ts +20 -1
- package/bin/utils/cli-ui.js +41 -3
- package/bin/utils/update-tsconfig-paths.js +73 -33
- package/package.json +22 -13
package/bin/new/form.js
CHANGED
|
@@ -10,6 +10,7 @@ const degit_1 = __importDefault(require("degit"));
|
|
|
10
10
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const cli_1 = require("../cli");
|
|
13
14
|
const center_text_1 = require("../utils/center-text");
|
|
14
15
|
const change_package_info_1 = require("../utils/change-package-info");
|
|
15
16
|
const cli_ui_1 = require("../utils/cli-ui");
|
|
@@ -176,6 +177,7 @@ function copyDirectorySync(src, dest) {
|
|
|
176
177
|
var Template;
|
|
177
178
|
(function (Template) {
|
|
178
179
|
Template["application"] = "Application :: Full-featured ExpressoTS application. (Recommended)";
|
|
180
|
+
Template["applicationWithEvents"] = "Application with Events :: Application template pre-wired with the type-safe Event Bus example.";
|
|
179
181
|
Template["micro"] = "Micro :: A minimalistic template for building micro APIs and serverless functions.";
|
|
180
182
|
})(Template || (Template = {}));
|
|
181
183
|
/**
|
|
@@ -194,6 +196,7 @@ var MiddlewarePreset;
|
|
|
194
196
|
*/
|
|
195
197
|
const TEMPLATE_FOLDERS = {
|
|
196
198
|
Application: "application",
|
|
199
|
+
"Application with Events": "application-with-events",
|
|
197
200
|
Micro: "micro",
|
|
198
201
|
};
|
|
199
202
|
/**
|
|
@@ -202,10 +205,87 @@ const TEMPLATE_FOLDERS = {
|
|
|
202
205
|
const PRESET_CODE = {
|
|
203
206
|
API: `this.Middleware.applyPreset("api");`,
|
|
204
207
|
Web: `this.Middleware.applyPreset("web");`,
|
|
205
|
-
GraphQL:
|
|
208
|
+
GraphQL: [
|
|
209
|
+
`this.Middleware.applyPreset("graphql");`,
|
|
210
|
+
``,
|
|
211
|
+
` const apolloServer = new ApolloServer({ typeDefs, resolvers });`,
|
|
212
|
+
` await apolloServer.start();`,
|
|
213
|
+
` this.Middleware.add({`,
|
|
214
|
+
` path: "/graphql",`,
|
|
215
|
+
` middlewares: [expressMiddleware(apolloServer)],`,
|
|
216
|
+
` });`,
|
|
217
|
+
].join("\n"),
|
|
206
218
|
Microservice: `this.Middleware.applyPreset("microservice");`,
|
|
207
219
|
Minimal: `this.Middleware.parse();`,
|
|
208
220
|
};
|
|
221
|
+
/**
|
|
222
|
+
* Extra imports that specific presets need appended to app.ts.
|
|
223
|
+
*/
|
|
224
|
+
const PRESET_IMPORTS = {
|
|
225
|
+
GraphQL: [
|
|
226
|
+
`import { ApolloServer } from "@apollo/server";`,
|
|
227
|
+
`import { expressMiddleware } from "@as-integrations/express5";`,
|
|
228
|
+
`import { typeDefs, resolvers } from "./graphql/schema";`,
|
|
229
|
+
].join("\n"),
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Per-preset runtime dependencies. The base application template ships only
|
|
233
|
+
* express + framework packages; each preset declares exactly which optional
|
|
234
|
+
* middleware packages it needs so scaffolded projects stay lean.
|
|
235
|
+
*
|
|
236
|
+
* Versions are pinned with `^` ranges matching the middleware-resolver
|
|
237
|
+
* registry expectations in `@expressots/core`.
|
|
238
|
+
*/
|
|
239
|
+
const PRESET_DEPENDENCIES = {
|
|
240
|
+
API: {
|
|
241
|
+
dependencies: {
|
|
242
|
+
compression: "^1.8.1",
|
|
243
|
+
cors: "^2.8.6",
|
|
244
|
+
"express-rate-limit": "^8.5.1",
|
|
245
|
+
helmet: "^8.1.0",
|
|
246
|
+
},
|
|
247
|
+
devDependencies: {
|
|
248
|
+
"@types/compression": "^1.7.5",
|
|
249
|
+
"@types/cors": "^2.8.17",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
Web: {
|
|
253
|
+
dependencies: {
|
|
254
|
+
compression: "^1.8.1",
|
|
255
|
+
"cookie-parser": "^1.4.7",
|
|
256
|
+
cors: "^2.8.6",
|
|
257
|
+
helmet: "^8.1.0",
|
|
258
|
+
},
|
|
259
|
+
devDependencies: {
|
|
260
|
+
"@types/compression": "^1.7.5",
|
|
261
|
+
"@types/cookie-parser": "^1.4.8",
|
|
262
|
+
"@types/cors": "^2.8.17",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
GraphQL: {
|
|
266
|
+
dependencies: {
|
|
267
|
+
"@apollo/server": "^5.5.1",
|
|
268
|
+
"@as-integrations/express5": "^1.1.2",
|
|
269
|
+
compression: "^1.8.1",
|
|
270
|
+
cors: "^2.8.6",
|
|
271
|
+
graphql: "^16.14.0",
|
|
272
|
+
helmet: "^8.1.0",
|
|
273
|
+
},
|
|
274
|
+
devDependencies: {
|
|
275
|
+
"@types/compression": "^1.7.5",
|
|
276
|
+
"@types/cors": "^2.8.17",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
Microservice: {
|
|
280
|
+
dependencies: {
|
|
281
|
+
compression: "^1.8.1",
|
|
282
|
+
},
|
|
283
|
+
devDependencies: {
|
|
284
|
+
"@types/compression": "^1.7.5",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
Minimal: {},
|
|
288
|
+
};
|
|
209
289
|
/**
|
|
210
290
|
* Apply the selected middleware preset to the generated app.ts
|
|
211
291
|
*/
|
|
@@ -219,10 +299,101 @@ function applyMiddlewarePreset(directory, preset) {
|
|
|
219
299
|
const presetName = presetMatch ? presetMatch[1] : "API";
|
|
220
300
|
const presetCode = PRESET_CODE[presetName] || PRESET_CODE["API"];
|
|
221
301
|
let content = node_fs_1.default.readFileSync(appTsPath, "utf-8");
|
|
302
|
+
// Inject preset-specific imports after the existing import block.
|
|
303
|
+
// Match the first blank line (handles both LF and CRLF endings).
|
|
304
|
+
const extraImports = PRESET_IMPORTS[presetName];
|
|
305
|
+
if (extraImports) {
|
|
306
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
307
|
+
content = content.replace(new RegExp(`${eol}${eol}`), `${eol}${extraImports}${eol}${eol}`);
|
|
308
|
+
}
|
|
222
309
|
// Replace the placeholder with the preset code
|
|
223
310
|
content = content.replace(/\/\/ __MIDDLEWARE_PRESET_PLACEHOLDER__/, presetCode);
|
|
224
311
|
node_fs_1.default.writeFileSync(appTsPath, content, "utf-8");
|
|
225
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* GraphQL schema scaffold content. Provides sample typeDefs and resolvers
|
|
315
|
+
* so the generated project has a working `/graphql` endpoint out of the box.
|
|
316
|
+
*/
|
|
317
|
+
const GRAPHQL_SCHEMA_CONTENT = `export const typeDefs = \`#graphql
|
|
318
|
+
type Query {
|
|
319
|
+
hello: String!
|
|
320
|
+
health: HealthStatus!
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
type Mutation {
|
|
324
|
+
echo(message: String!): EchoResponse!
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
type HealthStatus {
|
|
328
|
+
status: String!
|
|
329
|
+
timestamp: String!
|
|
330
|
+
uptime: Float!
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
type EchoResponse {
|
|
334
|
+
message: String!
|
|
335
|
+
receivedAt: String!
|
|
336
|
+
}
|
|
337
|
+
\`;
|
|
338
|
+
|
|
339
|
+
export const resolvers = {
|
|
340
|
+
Query: {
|
|
341
|
+
hello: () => "Hello from ExpressoTS GraphQL!",
|
|
342
|
+
health: () => ({
|
|
343
|
+
status: "ok",
|
|
344
|
+
timestamp: new Date().toISOString(),
|
|
345
|
+
uptime: process.uptime(),
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
Mutation: {
|
|
349
|
+
echo: (_: unknown, { message }: { message: string }) => ({
|
|
350
|
+
message,
|
|
351
|
+
receivedAt: new Date().toISOString(),
|
|
352
|
+
}),
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
`;
|
|
356
|
+
/**
|
|
357
|
+
* Create additional source files required by specific presets.
|
|
358
|
+
* For example, the GraphQL preset ships a starter schema + resolvers.
|
|
359
|
+
*/
|
|
360
|
+
function createPresetFiles(directory, preset) {
|
|
361
|
+
const presetMatch = preset.match(/^(\w+) ::/);
|
|
362
|
+
const presetName = presetMatch ? presetMatch[1] : "API";
|
|
363
|
+
if (presetName === "GraphQL") {
|
|
364
|
+
const graphqlDir = node_path_1.default.join(directory, "src", "graphql");
|
|
365
|
+
if (!node_fs_1.default.existsSync(graphqlDir)) {
|
|
366
|
+
node_fs_1.default.mkdirSync(graphqlDir, { recursive: true });
|
|
367
|
+
}
|
|
368
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(graphqlDir, "schema.ts"), GRAPHQL_SCHEMA_CONTENT, "utf-8");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Inject preset-specific dependencies into the scaffolded project's
|
|
373
|
+
* package.json **before** `npm install` runs, so everything resolves
|
|
374
|
+
* in a single install step.
|
|
375
|
+
*/
|
|
376
|
+
function injectPresetDependencies(directory, preset) {
|
|
377
|
+
const pkgPath = node_path_1.default.join(directory, "package.json");
|
|
378
|
+
if (!node_fs_1.default.existsSync(pkgPath))
|
|
379
|
+
return;
|
|
380
|
+
const presetMatch = preset.match(/^(\w+) ::/);
|
|
381
|
+
const presetName = presetMatch ? presetMatch[1] : "API";
|
|
382
|
+
const presetDeps = PRESET_DEPENDENCIES[presetName];
|
|
383
|
+
if (!presetDeps)
|
|
384
|
+
return;
|
|
385
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8"));
|
|
386
|
+
if (presetDeps.dependencies) {
|
|
387
|
+
pkg.dependencies = { ...pkg.dependencies, ...presetDeps.dependencies };
|
|
388
|
+
}
|
|
389
|
+
if (presetDeps.devDependencies) {
|
|
390
|
+
pkg.devDependencies = {
|
|
391
|
+
...pkg.devDependencies,
|
|
392
|
+
...presetDeps.devDependencies,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf-8");
|
|
396
|
+
}
|
|
226
397
|
/**
|
|
227
398
|
* Enable local template mode for testing.
|
|
228
399
|
* Opt-in via `EXPRESSOTS_DEV=1` and `EXPRESSOTS_USE_LOCAL_TEMPLATES=1`.
|
|
@@ -244,18 +415,101 @@ const SKIP_INSTALL_FOR_TESTING = process.env.EXPRESSOTS_DEV === "1" &&
|
|
|
244
415
|
* For production: this will be replaced with the actual path
|
|
245
416
|
*/
|
|
246
417
|
const LOCAL_TEMPLATES_PATH = node_path_1.default.resolve(__dirname, "../../../templates");
|
|
418
|
+
/**
|
|
419
|
+
* Optional override for the templates ref/tag.
|
|
420
|
+
*
|
|
421
|
+
* Useful during the preview window before the matching `vX.Y.Z` tag exists
|
|
422
|
+
* on `expressots/templates`. Setting `EXPRESSOTS_TEMPLATE_REF=feature/v4.0`
|
|
423
|
+
* makes `expressots new` clone from that branch instead of the version tag.
|
|
424
|
+
*
|
|
425
|
+
* This is intentionally NOT gated by `EXPRESSOTS_DEV`: even an installed
|
|
426
|
+
* CLI consumer can opt into a custom ref to test pre-release scaffolds.
|
|
427
|
+
*/
|
|
428
|
+
const TEMPLATE_REF_OVERRIDE = process.env.EXPRESSOTS_TEMPLATE_REF?.trim() || "";
|
|
429
|
+
/**
|
|
430
|
+
* Resolve the degit ref to use when fetching a template.
|
|
431
|
+
*
|
|
432
|
+
* Priority:
|
|
433
|
+
* 1. Explicit override via `EXPRESSOTS_TEMPLATE_REF`
|
|
434
|
+
* 2. The version-pinned tag matching this CLI build (`v${BUNDLE_VERSION}`)
|
|
435
|
+
*/
|
|
436
|
+
function resolveTemplateRef() {
|
|
437
|
+
if (TEMPLATE_REF_OVERRIDE)
|
|
438
|
+
return TEMPLATE_REF_OVERRIDE;
|
|
439
|
+
return `v${cli_1.BUNDLE_VERSION}`;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Build the full degit URL for a given template folder.
|
|
443
|
+
*/
|
|
444
|
+
function buildTemplateRepo(templateFolder, ref) {
|
|
445
|
+
return `expressots/templates/${templateFolder}#${ref}`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Detect whether the running CLI is a preview build (e.g.
|
|
449
|
+
* `4.0.0-preview.3`). During the preview window the matching templates tag
|
|
450
|
+
* may not yet exist on GitHub, so we allow a soft fallback to the active
|
|
451
|
+
* release branch.
|
|
452
|
+
*/
|
|
453
|
+
function isPreviewBuild() {
|
|
454
|
+
return /-(?:preview|alpha|beta|rc)\b/i.test(cli_1.BUNDLE_VERSION);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Fallback ref used when the primary ref is missing AND we are running a
|
|
458
|
+
* preview build. Matches the framework's working branch.
|
|
459
|
+
*/
|
|
460
|
+
const PREVIEW_FALLBACK_REF = "feature/v4.0";
|
|
461
|
+
/**
|
|
462
|
+
* Clone a template from the public `expressots/templates` repo via degit.
|
|
463
|
+
*
|
|
464
|
+
* On `MISSING_REF` we attempt one graceful retry against the preview
|
|
465
|
+
* fallback branch ā this keeps `npx @expressots/cli@next new` usable during
|
|
466
|
+
* the window between a CLI publish and the matching templates-tag push.
|
|
467
|
+
* For non-preview builds we let the error propagate so the user gets a
|
|
468
|
+
* loud, accurate diagnostic.
|
|
469
|
+
*/
|
|
470
|
+
async function cloneFromGitHub({ templateFolder, targetDir, progressBar, }) {
|
|
471
|
+
const primaryRef = resolveTemplateRef();
|
|
472
|
+
const primaryRepo = buildTemplateRepo(templateFolder, primaryRef);
|
|
473
|
+
try {
|
|
474
|
+
await (0, degit_1.default)(primaryRepo, { force: false }).clone(targetDir);
|
|
475
|
+
progressBar.update(30, { doing: "Template ready" });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
const isMissingRef = err?.code === "MISSING_REF";
|
|
480
|
+
const canFallback = isMissingRef && !TEMPLATE_REF_OVERRIDE && isPreviewBuild();
|
|
481
|
+
if (!canFallback)
|
|
482
|
+
throw err;
|
|
483
|
+
// Tag for this preview hasn't been pushed yet; transparently retry
|
|
484
|
+
// against the working branch and surface a one-line warning so the
|
|
485
|
+
// user knows what they actually got. Written to stdout while the bar
|
|
486
|
+
// renders on stderr, so the streams do not interfere.
|
|
487
|
+
console.log(chalk_1.default.yellow(`\nā Templates tag "${primaryRef}" not found on GitHub yet ā falling back to "${PREVIEW_FALLBACK_REF}". ` +
|
|
488
|
+
`Set EXPRESSOTS_TEMPLATE_REF=<branch-or-tag> to override.`));
|
|
489
|
+
const fallbackRepo = buildTemplateRepo(templateFolder, PREVIEW_FALLBACK_REF);
|
|
490
|
+
await (0, degit_1.default)(fallbackRepo, { force: false }).clone(targetDir);
|
|
491
|
+
progressBar.update(30, { doing: "Template ready (fallback ref)" });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
247
494
|
/**
|
|
248
495
|
* Main project creation form
|
|
249
496
|
*/
|
|
250
497
|
const projectForm = async (projectName, args) => {
|
|
251
498
|
let answer;
|
|
252
|
-
const [packageManager, template, directory, preset] = args;
|
|
499
|
+
const [packageManager, template, directory, preset, events] = args;
|
|
253
500
|
if (packageManager && template) {
|
|
501
|
+
const resolvedPreset = preset ??
|
|
502
|
+
(template === "application"
|
|
503
|
+
? "api"
|
|
504
|
+
: undefined);
|
|
254
505
|
answer = {
|
|
255
506
|
name: projectName,
|
|
256
507
|
packageManager: packageManager,
|
|
257
508
|
template: Template[template],
|
|
258
|
-
preset:
|
|
509
|
+
preset: resolvedPreset
|
|
510
|
+
? MiddlewarePreset[resolvedPreset]
|
|
511
|
+
: undefined,
|
|
512
|
+
events: events,
|
|
259
513
|
confirm: true,
|
|
260
514
|
};
|
|
261
515
|
}
|
|
@@ -293,6 +547,7 @@ const projectForm = async (projectName, args) => {
|
|
|
293
547
|
]);
|
|
294
548
|
// Only show preset selection for Application template
|
|
295
549
|
let presetAnswer = {};
|
|
550
|
+
let eventsAnswer = {};
|
|
296
551
|
if (baseAnswers.template.startsWith("Application")) {
|
|
297
552
|
presetAnswer = await inquirer_1.default.prompt([
|
|
298
553
|
{
|
|
@@ -308,6 +563,19 @@ const projectForm = async (projectName, args) => {
|
|
|
308
563
|
],
|
|
309
564
|
},
|
|
310
565
|
]);
|
|
566
|
+
// Opt-in to the type-safe Event Bus example. Defaults to No
|
|
567
|
+
// so the API/Web/GraphQL/etc. presets stay focused on what
|
|
568
|
+
// the user actually asked for. Picking Yes swaps the
|
|
569
|
+
// scaffold to `application-with-events` (extra event class
|
|
570
|
+
// + handler + `setupEventSystemForExpress` wiring).
|
|
571
|
+
eventsAnswer = await inquirer_1.default.prompt([
|
|
572
|
+
{
|
|
573
|
+
type: "confirm",
|
|
574
|
+
name: "events",
|
|
575
|
+
message: "Include the type-safe Event Bus example? (adds a sample event + handler)",
|
|
576
|
+
default: false,
|
|
577
|
+
},
|
|
578
|
+
]);
|
|
311
579
|
}
|
|
312
580
|
const confirmAnswer = await inquirer_1.default.prompt([
|
|
313
581
|
{
|
|
@@ -320,6 +588,7 @@ const projectForm = async (projectName, args) => {
|
|
|
320
588
|
answer = {
|
|
321
589
|
...baseAnswers,
|
|
322
590
|
...presetAnswer,
|
|
591
|
+
...eventsAnswer,
|
|
323
592
|
...confirmAnswer,
|
|
324
593
|
};
|
|
325
594
|
}
|
|
@@ -339,15 +608,33 @@ const projectForm = async (projectName, args) => {
|
|
|
339
608
|
process.exit(1);
|
|
340
609
|
}
|
|
341
610
|
await checkIfPackageManagerExists(answer.packageManager);
|
|
342
|
-
|
|
611
|
+
process.stdout.write(`\n ${chalk_1.default.dim("Creating")} ${chalk_1.default.bold.green(answer.name)}\n\n`);
|
|
612
|
+
const termCols = typeof process.stdout.columns === "number" &&
|
|
613
|
+
process.stdout.columns > 0
|
|
614
|
+
? process.stdout.columns
|
|
615
|
+
: 80;
|
|
616
|
+
const barsize = Math.max(20, Math.min(40, termCols - 22));
|
|
343
617
|
const progressBar = new cli_progress_1.SingleBar({
|
|
344
|
-
format: "
|
|
345
|
-
chalk_1.default.
|
|
346
|
-
"
|
|
618
|
+
format: " {bar} " +
|
|
619
|
+
chalk_1.default.bold("{percentage}") +
|
|
620
|
+
chalk_1.default.dim("%") +
|
|
621
|
+
" " +
|
|
622
|
+
chalk_1.default.dim("{doing}"),
|
|
623
|
+
barCompleteChar: "\u2588",
|
|
624
|
+
barIncompleteChar: "\u2591",
|
|
625
|
+
formatBar: (progress, options) => {
|
|
626
|
+
const completeSize = Math.round(progress * (options.barsize ?? barsize));
|
|
627
|
+
const incompleteSize = (options.barsize ?? barsize) - completeSize;
|
|
628
|
+
return (chalk_1.default.green("\u2588".repeat(completeSize)) +
|
|
629
|
+
chalk_1.default.dim("\u2591".repeat(incompleteSize)));
|
|
630
|
+
},
|
|
631
|
+
barsize,
|
|
347
632
|
hideCursor: true,
|
|
348
|
-
|
|
633
|
+
clearOnComplete: false,
|
|
634
|
+
linewrap: false,
|
|
635
|
+
}, cli_progress_1.Presets.legacy);
|
|
349
636
|
progressBar.start(100, 0, {
|
|
350
|
-
doing: "
|
|
637
|
+
doing: "Fetching template",
|
|
351
638
|
});
|
|
352
639
|
// Extract template name from selection
|
|
353
640
|
const templateMatch = answer.template.match(/(.*) ::/);
|
|
@@ -357,7 +644,15 @@ const projectForm = async (projectName, args) => {
|
|
|
357
644
|
process.exit(1);
|
|
358
645
|
}
|
|
359
646
|
const templateName = templateMatch[1];
|
|
360
|
-
|
|
647
|
+
// The "Application with Events" template is no longer a top-level
|
|
648
|
+
// choice. When the user opts into events on the Application track
|
|
649
|
+
// (or passes `--events`), swap the folder so we still pull from
|
|
650
|
+
// `application-with-events`. The folder split is preserved on disk
|
|
651
|
+
// for now so we keep two minimal sources of truth.
|
|
652
|
+
let templateFolder = TEMPLATE_FOLDERS[templateName];
|
|
653
|
+
if (templateName === "Application" && answer.events) {
|
|
654
|
+
templateFolder = TEMPLATE_FOLDERS["Application with Events"];
|
|
655
|
+
}
|
|
361
656
|
if (!templateFolder) {
|
|
362
657
|
progressBar.stop();
|
|
363
658
|
(0, cli_ui_1.printError)(`Unknown template: ${templateName}`, "new");
|
|
@@ -376,23 +671,47 @@ const projectForm = async (projectName, args) => {
|
|
|
376
671
|
node_fs_1.default.mkdirSync(answer.name, { recursive: true });
|
|
377
672
|
// Copy template files
|
|
378
673
|
copyDirectorySync(localTemplatePath, answer.name);
|
|
379
|
-
progressBar.update(30, { doing: "Template
|
|
674
|
+
progressBar.update(30, { doing: "Template ready" });
|
|
380
675
|
}
|
|
381
676
|
else {
|
|
382
677
|
// GITHUB MODE (production)
|
|
383
|
-
// Pinned to the
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
await
|
|
388
|
-
|
|
678
|
+
// Pinned to the templates tag matching this CLI's published
|
|
679
|
+
// version (e.g. CLI 4.0.0-preview.3 -> templates/v4.0.0-preview.3).
|
|
680
|
+
// BUNDLE_VERSION reads from this package's package.json, so a
|
|
681
|
+
// CLI release and its templates tag move together.
|
|
682
|
+
await cloneFromGitHub({
|
|
683
|
+
templateFolder,
|
|
684
|
+
targetDir: answer.name,
|
|
685
|
+
progressBar,
|
|
686
|
+
});
|
|
389
687
|
}
|
|
390
688
|
}
|
|
391
689
|
catch (err) {
|
|
392
690
|
console.log("\n");
|
|
393
|
-
|
|
691
|
+
// Surface the real failure cause so users can self-diagnose
|
|
692
|
+
// instead of guessing at "folder not empty" every time.
|
|
693
|
+
const msg = err?.message ? String(err.message) : String(err);
|
|
694
|
+
const code = err?.code ? ` [${err.code}]` : "";
|
|
695
|
+
if (err?.code === "DEST_NOT_EMPTY" ||
|
|
696
|
+
/already exists|not empty/i.test(msg)) {
|
|
697
|
+
(0, cli_ui_1.printError)(`Target folder "${answer.name}" already exists or is not empty`, answer.name);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
(0, cli_ui_1.printError)(`Failed to scaffold project${code}: ${msg}`, answer.name);
|
|
701
|
+
}
|
|
394
702
|
process.exit(1);
|
|
395
703
|
}
|
|
704
|
+
// Apply preset files + placeholder substitution BEFORE install so
|
|
705
|
+
// that a failed/skipped install still leaves the user with a
|
|
706
|
+
// runnable scaffold (the middleware preset placeholder must not
|
|
707
|
+
// leak into src/app.ts as a literal comment).
|
|
708
|
+
if (answer.preset &&
|
|
709
|
+
(templateFolder === "application" ||
|
|
710
|
+
templateFolder === "application-with-events")) {
|
|
711
|
+
injectPresetDependencies(answer.name, answer.preset);
|
|
712
|
+
createPresetFiles(answer.name, answer.preset);
|
|
713
|
+
applyMiddlewarePreset(answer.name, answer.preset);
|
|
714
|
+
}
|
|
396
715
|
if (SKIP_INSTALL_FOR_TESTING) {
|
|
397
716
|
progressBar.update(90, {
|
|
398
717
|
doing: "Skipping install (testing mode)",
|
|
@@ -411,11 +730,7 @@ const projectForm = async (projectName, args) => {
|
|
|
411
730
|
// Progress should already be at 90% from packageManagerInstall
|
|
412
731
|
// Only update if we skipped installation
|
|
413
732
|
if (!SKIP_INSTALL_FOR_TESTING) {
|
|
414
|
-
progressBar.update(90, { doing: "Finalizing
|
|
415
|
-
}
|
|
416
|
-
// Apply middleware preset for Application template
|
|
417
|
-
if (answer.preset && templateFolder === "application") {
|
|
418
|
-
applyMiddlewarePreset(answer.name, answer.preset);
|
|
733
|
+
progressBar.update(90, { doing: "Finalizing" });
|
|
419
734
|
}
|
|
420
735
|
(0, change_package_info_1.changePackageName)({
|
|
421
736
|
directory: answer.name,
|
package/bin/profile/cli.d.ts
CHANGED
package/bin/profile/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.profileCommand = void 0;
|
|
4
4
|
const form_1 = require("./form");
|
|
5
|
+
const cli_ui_1 = require("../utils/cli-ui");
|
|
5
6
|
const profileCommand = () => {
|
|
6
7
|
return {
|
|
7
8
|
command: "profile <action> [target]",
|
|
@@ -84,7 +85,8 @@ const profileCommand = () => {
|
|
|
84
85
|
await (0, form_1.showProfileReport)(options);
|
|
85
86
|
break;
|
|
86
87
|
default:
|
|
87
|
-
|
|
88
|
+
(0, cli_ui_1.printError)(`Unknown action: ${action}`, "profile");
|
|
89
|
+
process.exit(1);
|
|
88
90
|
}
|
|
89
91
|
},
|
|
90
92
|
};
|
package/bin/profile/form.js
CHANGED
|
@@ -10,11 +10,12 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
10
10
|
const dockerfile_analyzer_1 = require("./analyzers/dockerfile-analyzer");
|
|
11
11
|
const image_analyzer_1 = require("./analyzers/image-analyzer");
|
|
12
12
|
const optimizers_1 = require("./optimizers");
|
|
13
|
+
const cli_ui_1 = require("../utils/cli-ui");
|
|
13
14
|
/**
|
|
14
15
|
* Profile a Dockerfile for issues and recommendations
|
|
15
16
|
*/
|
|
16
17
|
async function profileContainer(options) {
|
|
17
|
-
|
|
18
|
+
(0, cli_ui_1.printSection)("š ExpressoTS Container Profiler");
|
|
18
19
|
const cwd = process.cwd();
|
|
19
20
|
const dockerfilePath = path_1.default.join(cwd, options.dockerfile);
|
|
20
21
|
if (!fs_1.default.existsSync(dockerfilePath)) {
|
|
@@ -32,7 +33,7 @@ exports.profileContainer = profileContainer;
|
|
|
32
33
|
* Profile a built Docker image
|
|
33
34
|
*/
|
|
34
35
|
async function profileImage(options) {
|
|
35
|
-
|
|
36
|
+
(0, cli_ui_1.printSection)("š ExpressoTS Image Profiler");
|
|
36
37
|
if (!options.target) {
|
|
37
38
|
console.log(chalk_1.default.red("Error: Please specify an image name."));
|
|
38
39
|
console.log(chalk_1.default.gray("Usage: expressots profile image <image-name>"));
|
|
@@ -71,7 +72,7 @@ exports.profileImage = profileImage;
|
|
|
71
72
|
* Generate and optionally apply optimizations
|
|
72
73
|
*/
|
|
73
74
|
async function optimizeContainer(options) {
|
|
74
|
-
|
|
75
|
+
(0, cli_ui_1.printSection)("ā” ExpressoTS Container Optimizer");
|
|
75
76
|
const cwd = process.cwd();
|
|
76
77
|
const dockerfilePath = path_1.default.join(cwd, options.dockerfile);
|
|
77
78
|
if (!fs_1.default.existsSync(dockerfilePath)) {
|
|
@@ -110,7 +111,7 @@ exports.optimizeContainer = optimizeContainer;
|
|
|
110
111
|
* Show a comprehensive profile report
|
|
111
112
|
*/
|
|
112
113
|
async function showProfileReport(options) {
|
|
113
|
-
|
|
114
|
+
(0, cli_ui_1.printSection)("š ExpressoTS Container Profile Report");
|
|
114
115
|
const cwd = process.cwd();
|
|
115
116
|
const dockerfilePath = path_1.default.join(cwd, options.dockerfile);
|
|
116
117
|
if (!fs_1.default.existsSync(dockerfilePath)) {
|
|
@@ -7,9 +7,44 @@ exports.createExternalProvider = void 0;
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const degit_1 = __importDefault(require("degit"));
|
|
9
9
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
|
+
const cli_1 = require("../../cli");
|
|
10
11
|
const center_text_1 = require("../../utils/center-text");
|
|
11
12
|
const change_package_info_1 = require("../../utils/change-package-info");
|
|
12
13
|
const cli_ui_1 = require("../../utils/cli-ui");
|
|
14
|
+
/**
|
|
15
|
+
* Override the templates ref/tag, mirroring `EXPRESSOTS_TEMPLATE_REF` in the
|
|
16
|
+
* `new` command. Lets users target a branch (e.g. `feature/v4.0`) before the
|
|
17
|
+
* matching version tag has been pushed.
|
|
18
|
+
*/
|
|
19
|
+
const TEMPLATE_REF_OVERRIDE = process.env.EXPRESSOTS_TEMPLATE_REF?.trim() || "";
|
|
20
|
+
const PREVIEW_FALLBACK_REF = "feature/v4.0";
|
|
21
|
+
function isPreviewBuild() {
|
|
22
|
+
return /-(?:preview|alpha|beta|rc)\b/i.test(cli_1.BUNDLE_VERSION);
|
|
23
|
+
}
|
|
24
|
+
function resolveProviderRef() {
|
|
25
|
+
if (TEMPLATE_REF_OVERRIDE)
|
|
26
|
+
return TEMPLATE_REF_OVERRIDE;
|
|
27
|
+
return `v${cli_1.BUNDLE_VERSION}`;
|
|
28
|
+
}
|
|
29
|
+
async function cloneProviderTemplate(targetDir) {
|
|
30
|
+
const primaryRef = resolveProviderRef();
|
|
31
|
+
const primaryRepo = `expressots/templates/provider#${primaryRef}`;
|
|
32
|
+
try {
|
|
33
|
+
await (0, degit_1.default)(primaryRepo, { force: false }).clone(targetDir);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const isMissingRef = err?.code === "MISSING_REF";
|
|
38
|
+
const canFallback = isMissingRef && !TEMPLATE_REF_OVERRIDE && isPreviewBuild();
|
|
39
|
+
if (!canFallback)
|
|
40
|
+
throw err;
|
|
41
|
+
console.log(chalk_1.default.yellow(`\nā Templates tag "${primaryRef}" not found on GitHub yet ā falling back to "${PREVIEW_FALLBACK_REF}". ` +
|
|
42
|
+
`Set EXPRESSOTS_TEMPLATE_REF=<branch-or-tag> to override.`));
|
|
43
|
+
await (0, degit_1.default)(`expressots/templates/provider#${PREVIEW_FALLBACK_REF}`, {
|
|
44
|
+
force: false,
|
|
45
|
+
}).clone(targetDir);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
13
48
|
async function printInfo(providerName) {
|
|
14
49
|
console.log("\n");
|
|
15
50
|
console.log("š Provider", chalk_1.default.green(providerName), "created successfully!");
|
|
@@ -39,9 +74,15 @@ const createExternalProvider = async (provider) => {
|
|
|
39
74
|
]);
|
|
40
75
|
}
|
|
41
76
|
try {
|
|
42
|
-
// Pinned to the
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
// Pinned to the templates tag matching this CLI's published version,
|
|
78
|
+
// same policy as `expressots new`. BUNDLE_VERSION reads from the
|
|
79
|
+
// CLI's own package.json so the ref always tracks the release.
|
|
80
|
+
//
|
|
81
|
+
// Mirrors the preview-fallback logic in `new/form.ts`: during the
|
|
82
|
+
// preview window the matching `vX.Y.Z` tag may not yet be on
|
|
83
|
+
// `expressots/templates`, so we soft-fall back to the active
|
|
84
|
+
// release branch and warn rather than failing opaquely.
|
|
85
|
+
await cloneProviderTemplate(providerInfo.providerName);
|
|
45
86
|
(0, change_package_info_1.changePackageName)({
|
|
46
87
|
directory: providerInfo.providerName,
|
|
47
88
|
name: providerInfo.providerName,
|
|
@@ -51,7 +92,15 @@ const createExternalProvider = async (provider) => {
|
|
|
51
92
|
}
|
|
52
93
|
catch (err) {
|
|
53
94
|
console.log("\n");
|
|
54
|
-
|
|
95
|
+
const msg = err?.message ? String(err.message) : String(err);
|
|
96
|
+
const code = err?.code ? ` [${err.code}]` : "";
|
|
97
|
+
if (err?.code === "DEST_NOT_EMPTY" ||
|
|
98
|
+
/already exists|not empty/i.test(msg)) {
|
|
99
|
+
(0, cli_ui_1.printError)(`Target folder "${providerInfo.providerName}" already exists or is not empty`, "");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
(0, cli_ui_1.printError)(`Failed to scaffold provider${code}: ${msg}`, "");
|
|
103
|
+
}
|
|
55
104
|
reject(err);
|
|
56
105
|
}
|
|
57
106
|
});
|
package/bin/studio/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
11
11
|
const ora_1 = __importDefault(require("ora"));
|
|
12
12
|
const fs_1 = require("fs");
|
|
13
13
|
const path_1 = require("path");
|
|
14
|
+
const cli_1 = require("../cli");
|
|
14
15
|
const safe_spawn_1 = require("../utils/safe-spawn");
|
|
15
16
|
/**
|
|
16
17
|
* Check if @expressots/studio is installed
|
|
@@ -33,19 +34,24 @@ async function installStudio() {
|
|
|
33
34
|
try {
|
|
34
35
|
const hasYarn = (0, fs_1.existsSync)((0, path_1.resolve)(process.cwd(), "yarn.lock"));
|
|
35
36
|
const hasPnpm = (0, fs_1.existsSync)((0, path_1.resolve)(process.cwd(), "pnpm-lock.yaml"));
|
|
37
|
+
// Pin the studio install to the same minor as the running CLI so
|
|
38
|
+
// `expressots studio` from a preview-N CLI fetches a matching
|
|
39
|
+
// preview-N studio. Falls back to a caret on the major if the CLI
|
|
40
|
+
// version isn't a valid prerelease (defensive).
|
|
41
|
+
const studioSpec = `@expressots/studio@^${cli_1.BUNDLE_VERSION}`;
|
|
36
42
|
let pkgManager;
|
|
37
43
|
let args;
|
|
38
44
|
if (hasPnpm) {
|
|
39
45
|
pkgManager = "pnpm";
|
|
40
|
-
args = ["add", "-D",
|
|
46
|
+
args = ["add", "-D", studioSpec];
|
|
41
47
|
}
|
|
42
48
|
else if (hasYarn) {
|
|
43
49
|
pkgManager = "yarn";
|
|
44
|
-
args = ["add", "-D",
|
|
50
|
+
args = ["add", "-D", studioSpec];
|
|
45
51
|
}
|
|
46
52
|
else {
|
|
47
53
|
pkgManager = "npm";
|
|
48
|
-
args = ["install", "-D",
|
|
54
|
+
args = ["install", "-D", studioSpec];
|
|
49
55
|
}
|
|
50
56
|
// `safeSpawnSync` (cross-spawn) resolves the Windows `.cmd` shim
|
|
51
57
|
// and properly escapes argv for cmd.exe. The argv here is a
|