@lenne.tech/cli 1.48.0 → 1.49.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.
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENV_PREFIX = exports.CROSS_ENV_PACKAGE = exports.CROSS_ENV_VERSION = void 0;
4
+ exports.bareEnvPrefixes = bareEnvPrefixes;
5
+ exports.ensureCrossEnvDependency = ensureCrossEnvDependency;
6
+ exports.usesPosixEnvPrefix = usesPosixEnvPrefix;
7
+ exports.withCrossEnv = withCrossEnv;
8
+ /**
9
+ * Environment prefixes in GENERATED package.json scripts.
10
+ *
11
+ * npm and pnpm run scripts through cmd.exe on Windows, which knows no
12
+ * `VAR=value command` form: it reads `NODE_ENV` as the program name and aborts
13
+ * with "'NODE_ENV' is not recognized as an internal or external command"
14
+ * (reported from a Windows laptop, 2026-09-16, on a project generated by lt
15
+ * 1.47.0: `pnpm run start:local` died there).
16
+ *
17
+ * `cross-env` sets the variable in the child process instead, so the same script
18
+ * works in bash, zsh, PowerShell and cmd.exe.
19
+ */
20
+ /** Pinned exactly: an override target or dependency of ours is never a range (repo rule). */
21
+ exports.CROSS_ENV_VERSION = '10.1.0';
22
+ exports.CROSS_ENV_PACKAGE = 'cross-env';
23
+ /**
24
+ * A POSIX env assignment in front of a command — the form cmd.exe cannot run.
25
+ *
26
+ * Deliberately NOT anchored to the start of the script: the mistake that actually
27
+ * happens when converting is an `&&` chain in which only the first link was
28
+ * wrapped (`cross-env NODE_ENV=x a && NODE_ENV=x b`), and an anchored expression
29
+ * reports that as clean.
30
+ */
31
+ exports.ENV_PREFIX = /(^|[\s;&|(])[A-Z_][A-Z0-9_]*=\S/;
32
+ /** `cross-env` plus the assignments it carries — those are the covered ones. */
33
+ const CROSS_ENV_ASSIGNMENTS = /cross-env\s+(?:[A-Z_][A-Z0-9_]*=\S+\s+)+/g;
34
+ /**
35
+ * Bare `VAR=value` prefixes in `script`, i.e. the ones cmd.exe would choke on.
36
+ * Assignments that `cross-env` carries are removed first, so a correctly
37
+ * converted script — including `cross-env A=1 B=2 cmd` — reports nothing.
38
+ */
39
+ function bareEnvPrefixes(script) {
40
+ const rest = script.replace(CROSS_ENV_ASSIGNMENTS, '');
41
+ const found = [];
42
+ for (const match of rest.matchAll(/(?:^|[\s;&|(])([A-Z_][A-Z0-9_]*=\S+)/g)) {
43
+ found.push(match[1]);
44
+ }
45
+ return found;
46
+ }
47
+ /**
48
+ * Make sure a generated project can run the scripts we just wrote.
49
+ *
50
+ * Lands in `dependencies`, NOT `devDependencies`: production trees are installed
51
+ * without dev packages — the starter's Dockerfile uses
52
+ * `pnpm install --frozen-lockfile --prod` and `pnpm deploy --prod` — and the scripts
53
+ * run there (`start:prod`, `dp:prod` → `migrate:prod:up`) are exactly the env-carrying
54
+ * ones. Measured by the starter session on two real `--prod` installs from one
55
+ * lockfile: as a devDependency `node_modules/.bin/cross-env` is missing, as a
56
+ * dependency it is there. (The framework repos keep it in devDependencies: their
57
+ * scripts never reach a customer image, and nest-server is published to npm, where a
58
+ * production dependency would be forced on every consumer.)
59
+ *
60
+ * Only added when the project does not declare it anywhere already — the starters ship
61
+ * it themselves, and a second entry would fight theirs on every update. An existing
62
+ * entry keeps its group and version, devDependencies included.
63
+ *
64
+ * @param pkg Parsed package.json of the generated project; mutated in place.
65
+ * @returns true when the dependency was added.
66
+ */
67
+ function ensureCrossEnvDependency(pkg) {
68
+ const declared = [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies, pkg.optionalDependencies].some((group) => group && typeof group === 'object' && exports.CROSS_ENV_PACKAGE in group);
69
+ if (declared) {
70
+ return false;
71
+ }
72
+ if (!pkg.dependencies || typeof pkg.dependencies !== 'object') {
73
+ pkg.dependencies = {};
74
+ }
75
+ pkg.dependencies[exports.CROSS_ENV_PACKAGE] = exports.CROSS_ENV_VERSION;
76
+ pkg.dependencies = Object.fromEntries(Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b)));
77
+ return true;
78
+ }
79
+ /** True when `script` still carries a bare `VAR=value` prefix. */
80
+ function usesPosixEnvPrefix(script) {
81
+ return bareEnvPrefixes(script).length > 0;
82
+ }
83
+ /**
84
+ * Prefix a command with `cross-env VAR=value …`.
85
+ *
86
+ * @param env Variables to set, e.g. `{ NODE_ENV: 'production' }`.
87
+ * @param command The command to run.
88
+ */
89
+ function withCrossEnv(env, command) {
90
+ const assignments = Object.entries(env)
91
+ .map(([name, value]) => `${name}=${value}`)
92
+ .join(' ');
93
+ return `cross-env ${assignments} ${command}`;
94
+ }
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.applyPendingMigrations = applyPendingMigrations;
12
13
  exports.findCompiledEntry = findCompiledEntry;
13
14
  exports.isApiCompiledRequested = isApiCompiledRequested;
14
15
  exports.resolveApiRuntime = resolveApiRuntime;
@@ -29,6 +30,33 @@ const node_path_1 = require("node:path");
29
30
  const dev_process_1 = require("./dev-process");
30
31
  /** Candidate compiled entry points, in preference order. Single-sourced so `lt dev test` agrees. */
31
32
  const COMPILED_ENTRIES = ['dist/src/main.js', 'dist/main.js'];
33
+ /**
34
+ * Apply the project's pending migrations (`<pm> run migrate:up`) ahead of an API
35
+ * that is started WITHOUT its own `start` script.
36
+ *
37
+ * lt projects chain the migration into that script (`migrate:up && start:local`),
38
+ * so every path that starts the compiled bundle directly — `lt dev up
39
+ * --api-compiled` and `lt dev test` — drops the migration along with it. On a
40
+ * database that outlives the run, that leaves the API on a state no deployed
41
+ * environment has: a changed unique index never reached the `lt dev test` DB, and
42
+ * the suite failed with a 409 that looked like a bug in the feature (DEV-3289).
43
+ *
44
+ * `env` must be the environment the API itself gets. The migration store resolves
45
+ * its database from the same `config.env.ts` + `NSC__MONGOOSE__URI` merge, so
46
+ * sharing the env is what makes both reach the same database.
47
+ *
48
+ * A project without a `migrate:up` script is `skipped`, never an error — nest-base
49
+ * (Prisma) projects have none. A migration killed by a signal (`null`) is `failed`.
50
+ */
51
+ function applyPendingMigrations(options) {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ const { apiDir, env, pm } = options;
54
+ if (!hasScript(apiDir, 'migrate:up'))
55
+ return { status: 'skipped' };
56
+ const exitCode = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('migrate:up'), { cwd: apiDir, env });
57
+ return exitCode === 0 ? { status: 'applied' } : { exitCode, status: 'failed' };
58
+ });
59
+ }
32
60
  /** Resolve the compiled API entry point in `apiDir`, or `undefined` if none was built. */
33
61
  function findCompiledEntry(apiDir) {
34
62
  return COMPILED_ENTRIES.map((rel) => (0, node_path_1.join)(apiDir, rel)).find((candidate) => (0, node_fs_1.existsSync)(candidate));
@@ -106,14 +134,12 @@ function startCompiledApi(options) {
106
134
  const build = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('build'), { cwd: apiDir, env });
107
135
  const entry = findCompiledEntry(apiDir);
108
136
  if (build === 0 && entry) {
109
- if (hasScript(apiDir, 'migrate:up')) {
110
- const migrate = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('migrate:up'), { cwd: apiDir, env });
111
- if (migrate !== 0) {
112
- // Parity with `migrate:up && start:local`: a failed migration must PREVENT the server
113
- // from starting rather than boot it against a half-migrated DB behind a "Started" banner.
114
- log.warn(`migrate:up failed (exit ${String(migrate)}) — API NOT started (would run on an un-migrated DB).`);
115
- return undefined;
116
- }
137
+ const migration = yield applyPendingMigrations({ apiDir, env, pm });
138
+ if (migration.status === 'failed') {
139
+ // Parity with `migrate:up && start:local`: a failed migration must PREVENT the server
140
+ // from starting rather than boot it against a half-migrated DB behind a "Started" banner.
141
+ log.warn(`migrate:up failed (exit ${String(migration.exitCode)}) — API NOT started (would run on an un-migrated DB).`);
142
+ return undefined;
117
143
  }
118
144
  return (0, dev_process_1.spawnDetached)(resolveApiRuntime(apiDir), [entry], {
119
145
  cwd: apiDir,
@@ -74,16 +74,20 @@ function buildCommand(bin, name) {
74
74
  },
75
75
  };
76
76
  }
77
+ /**
78
+ * Manager behind an override like `LT_PM_BIN`.
79
+ *
80
+ * Reads the file NAME, split on both separators and stripped of a Windows launcher
81
+ * extension: there the binary is `C:\\…\\pnpm.cmd`, which neither `endsWith('pnpm')`
82
+ * nor `includes('/pnpm')` matched — the override then resolved to `unknown` and the
83
+ * caller drove the wrong manager.
84
+ */
77
85
  function inferNameFromBin(bin) {
78
- const lower = bin.toLowerCase();
79
- if (lower.endsWith('pnpm') || lower.includes('/pnpm')) {
80
- return 'pnpm';
81
- }
82
- if (lower.endsWith('yarn') || lower.includes('/yarn')) {
83
- return 'yarn';
84
- }
85
- if (lower.endsWith('npm') || lower.includes('/npm')) {
86
- return 'npm';
86
+ var _a;
87
+ const file = (_a = bin.toLowerCase().split(/[\\/]/).pop()) !== null && _a !== void 0 ? _a : '';
88
+ const name = file.replace(/\.(bat|cmd|exe|ps1)$/, '');
89
+ if (name === 'pnpm' || name === 'yarn' || name === 'npm') {
90
+ return name;
87
91
  }
88
92
  return 'unknown';
89
93
  }
@@ -17,9 +17,11 @@ exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
17
17
  exports.buildTestAppEnv = buildTestAppEnv;
18
18
  exports.hasTestSession = hasTestSession;
19
19
  exports.isStackServing = isStackServing;
20
+ exports.migrationFailedError = migrationFailedError;
20
21
  exports.resolveTestSession = resolveTestSession;
21
22
  exports.runShardedTestSession = runShardedTestSession;
22
23
  exports.shardReportDir = shardReportDir;
24
+ exports.startTestApi = startTestApi;
23
25
  exports.tearDownAllTestSessions = tearDownAllTestSessions;
24
26
  exports.tearDownTestSession = tearDownTestSession;
25
27
  exports.testAppEntryCandidates = testAppEntryCandidates;
@@ -30,8 +32,10 @@ exports.unreachableStackError = unreachableStackError;
30
32
  * Brings up a SECOND, fully separate stack (own URLs, own internal ports,
31
33
  * own Caddy block, own database) that runs PARALLEL to — and never touches —
32
34
  * the developer's `lt dev up` session. Used to run the Playwright E2E suite
33
- * against a clean, dedicated database so a developer can keep working in their
34
- * own environment while tests run, and so a test run never pollutes dev data.
35
+ * against a dedicated database so a developer can keep working in their own
36
+ * environment while tests run, and so a test run never pollutes dev data. That
37
+ * database outlives the run — the CLI never drops it — so the API is migrated
38
+ * before it starts, like every other environment (DEV-3289).
35
39
  *
36
40
  * Topology (for slug `svl`):
37
41
  * - dev session : svl.localhost / api.svl.localhost → db `<…>-local`
@@ -99,12 +103,13 @@ const TEST_PORT_BASE = 4500;
99
103
  * `systemSetup.initialAdmin.*` and seeds the admin ONCE on an empty DB (it never
100
104
  * overwrites an existing admin), so a set-up system exists before the suite runs.
101
105
  * A fresh template project's standard auth E2E specs assume such a system — they
102
- * do no self-setup — so without this they fail LOCALLY on the empty per-run DB.
106
+ * do no self-setup — so without this they fail LOCALLY on a test DB that has no
107
+ * users yet (its first run, or after a global-setup reset it).
103
108
  * These are the exact values the lt-monorepo template CI uses
104
109
  * (`.gitlab-ci.yml` / `.github/workflows/test.yml`), keeping CI ↔ local parity.
105
110
  *
106
- * Injected ONLY into the `lt dev test` API process (its DB is fresh + discarded
107
- * per run), NEVER into `lt dev up` — no surprise admin in the persistent dev DB.
111
+ * Injected ONLY into the `lt dev test` API process (its DB is the dedicated
112
+ * `<…>-test` one), NEVER into `lt dev up` — no surprise admin in the dev DB.
108
113
  */
109
114
  exports.TEST_INITIAL_ADMIN_ENV = {
110
115
  NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL: 'ci-admin@test.com',
@@ -276,7 +281,11 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
276
281
  log.info(` app: ${appUrl} → 127.0.0.1:${appPort}`);
277
282
  if (apiUrl)
278
283
  log.info(` api: ${apiUrl} → 127.0.0.1:${apiPort}`);
279
- log.info(` db: mongodb://127.0.0.1/${dbName} (reset before the suite by Playwright global-setup)`);
284
+ // No claim about resetting here: the CLI never drops this DB, and whether a
285
+ // project's global-setup does is the project's business. The claim used to be
286
+ // printed unconditionally and sent a diagnosis looking for a reset that never
287
+ // happened instead of at the un-migrated DB (DEV-3289).
288
+ log.info(` db: mongodb://127.0.0.1/${dbName}`);
280
289
  log.info('');
281
290
  const devEnv = (0, dev_env_1.buildDevEnv)({
282
291
  apiInternalPort: apiPort !== null && apiPort !== void 0 ? apiPort : 0,
@@ -294,43 +303,23 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
294
303
  // hands it to the Playwright child as well.
295
304
  const appEnv = buildTestAppEnv(devEnv.app.env);
296
305
  const pids = {};
297
- // --- API: compiled (`node dist`) for stability; fall back to the project's
298
- // own dev start script. `skipBuild` (sibling shards) reuses the dist the
299
- // first shard produced. Per-component PM detection mirrors `lt dev up`:
300
- // a monorepo with an npm api and a pnpm app must drive each correctly. ---
306
+ // --- API: compiled (`node dist`) for stability, migrated first; falls back to
307
+ // the project's own start script (see `startTestApi`). ---
301
308
  if (layout.apiDir && apiPort) {
302
- const apiPm = (0, dev_package_manager_1.pickPackageManager)(layout.apiDir);
303
- let build = 0;
304
- if (!skipBuild) {
305
- log.info(log.dim('Building API (compiled, for stable long runs) …'));
306
- build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: layout.apiDir, env: process.env });
307
- }
308
- const entry = (0, dev_api_launch_1.findCompiledEntry)(layout.apiDir);
309
- // Seed a throwaway initial admin into the fresh, isolated test DB so the
310
- // standard auth E2E specs run against a set-up system — locally exactly like
311
- // the lt-monorepo template CI. Defaults first so an explicitly inherited
309
+ // Seed a throwaway initial admin into the isolated test DB so the standard
310
+ // auth E2E specs run against a set-up system — locally exactly like the
311
+ // lt-monorepo template CI. Defaults first so an explicitly inherited
312
312
  // `NSC__…INITIAL_ADMIN__…` still wins (deliberate override respected). Only
313
313
  // reached from `lt dev test` — `lt dev up` never calls bringUpTestSession.
314
314
  const apiEnv = Object.assign(Object.assign(Object.assign({}, exports.TEST_INITIAL_ADMIN_ENV), devEnv.api.env), { NODE_ENV: 'local' });
315
- let apiSpawn;
316
- if (build === 0 && entry) {
317
- // Runtime follows the PROJECT, never a hardcoded default: a `nest-base` API
318
- // bundles with `Bun.build({ target: 'bun' })` and dies under node with
319
- // "__require is not a function" (DEV-3208).
320
- apiSpawn = (0, dev_process_1.spawnDetached)((0, dev_api_launch_1.resolveApiRuntime)(layout.apiDir), [entry], {
321
- cwd: layout.apiDir,
322
- env: apiEnv,
323
- logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
324
- });
325
- }
326
- else {
327
- log.warn(`compiled API not available — falling back to \`${apiPm.bin} start\` (ts-node).`);
328
- apiSpawn = (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
329
- cwd: layout.apiDir,
330
- env: apiEnv,
331
- logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
332
- });
333
- }
315
+ const apiSpawn = yield startTestApi({
316
+ apiDir: layout.apiDir,
317
+ apiEnv,
318
+ dbName,
319
+ log,
320
+ logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
321
+ skipBuild,
322
+ });
334
323
  if (apiSpawn)
335
324
  pids.api = apiSpawn.pid;
336
325
  }
@@ -515,6 +504,19 @@ function hasTestSession(root) {
515
504
  function isStackServing(status) {
516
505
  return status > 0 && ![502, 503, 504].includes(status);
517
506
  }
507
+ /**
508
+ * The abort thrown when the test DB cannot be migrated.
509
+ *
510
+ * An abort rather than a warning for the same reason as `unreachableStackError`:
511
+ * a suite run on an un-migrated DB tests a data state no environment has, and
512
+ * whatever it reports is about the test setup, not about the feature. Unlike the
513
+ * API's boot, `migrate:up` runs in the foreground, so its output is already on
514
+ * screen right above this message.
515
+ */
516
+ function migrationFailedError(dbName, exitCode) {
517
+ return new Error(`migrate:up failed (exit ${String(exitCode)}) against the test DB "${dbName}" — test API not started, ` +
518
+ 'because a suite on an un-migrated DB tests a state no environment has. The migration output is above.');
519
+ }
518
520
  /** Build the dedicated test identity + test DB name for a project. */
519
521
  function resolveTestSession(layout, baseIdentity, shardIndex, devDbName) {
520
522
  const names = testStackNames(shardIndex);
@@ -599,6 +601,50 @@ function runShardedTestSession(layout, baseIdentity, log, opts) {
599
601
  function shardReportDir(root, shardIndex) {
600
602
  return (0, path_1.join)(root, '.lt-dev', `shard.${shardIndex}.playwright-report`);
601
603
  }
604
+ /**
605
+ * Start the test stack's API: build → apply pending migrations → start the
606
+ * compiled bundle, or fall back to the project's own `start` script when no
607
+ * bundle is available.
608
+ *
609
+ * The compiled start bypasses that `start` script and therefore the
610
+ * `migrate:up` it chains in front of the server. The test DB outlives every
611
+ * run — the CLI never drops it — so without the explicit step here it drifted
612
+ * behind every environment that does migrate (DEV-3289). The migration runs
613
+ * with `apiEnv`, the API's own env, so both resolve the same database, and
614
+ * before the spawn, because the API syncs its indexes against whatever it finds
615
+ * on boot. A failed migration throws `migrationFailedError`.
616
+ *
617
+ * `skipBuild` (sibling shards) reuses the dist the first shard produced but
618
+ * still migrates: every shard has a database of its own. The fallback does not
619
+ * migrate itself — the `start` script it runs owns that, exactly as under
620
+ * `lt dev up`. Per-directory package manager, like `lt dev up`: a monorepo with
621
+ * an npm API and a pnpm App must drive each with its own.
622
+ */
623
+ function startTestApi(opts) {
624
+ return __awaiter(this, void 0, void 0, function* () {
625
+ const { apiDir, apiEnv, dbName, log, logFile, skipBuild } = opts;
626
+ const apiPm = (0, dev_package_manager_1.pickPackageManager)(apiDir);
627
+ let build = 0;
628
+ if (!skipBuild) {
629
+ log.info(log.dim('Building API (compiled, for stable long runs) …'));
630
+ build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: apiDir, env: process.env });
631
+ }
632
+ const entry = (0, dev_api_launch_1.findCompiledEntry)(apiDir);
633
+ if (build === 0 && entry) {
634
+ const migration = yield (0, dev_api_launch_1.applyPendingMigrations)({ apiDir, env: apiEnv, pm: apiPm });
635
+ if (migration.status === 'failed')
636
+ throw migrationFailedError(dbName, migration.exitCode);
637
+ if (migration.status === 'skipped')
638
+ log.info(log.dim(`no migrate:up script — ${dbName} is not migrated`));
639
+ // Runtime follows the PROJECT, never a hardcoded default: a `nest-base` API
640
+ // bundles with `Bun.build({ target: 'bun' })` and dies under node with
641
+ // "__require is not a function" (DEV-3208).
642
+ return (0, dev_process_1.spawnDetached)((0, dev_api_launch_1.resolveApiRuntime)(apiDir), [entry], { cwd: apiDir, env: apiEnv, logFile });
643
+ }
644
+ log.warn(`compiled API not available — falling back to \`${apiPm.bin} start\` (ts-node).`);
645
+ return (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), { cwd: apiDir, env: apiEnv, logFile });
646
+ });
647
+ }
602
648
  /**
603
649
  * Tear down the unsharded test stack AND every sharded stack discovered on disk
604
650
  * (`state.test.<i>.json` in `.lt-dev/`). Used by `lt dev test down` so a
@@ -20,6 +20,17 @@ exports.failRun = failRun;
20
20
  * buffered output, including the spinner's own failure message, which is the one
21
21
  * line the operator actually needs.
22
22
  *
23
+ * That choice carries a precondition worth stating, because it was violated:
24
+ * setting `process.exitCode` only ends the process once **the event loop drains**,
25
+ * and `bin/lt` has no terminal `process.exit()` after `run()`. A running ora
26
+ * spinner holds the loop open on two counts — its `setInterval` for the animation
27
+ * and the `stdin.resume()` its stdin discarder performs. So a command that called
28
+ * `failRun` and returned while a spinner was still spinning did not exit at all:
29
+ * no message, no prompt back, and the terminal left in raw mode. The remedy is to
30
+ * stop the spinner, NOT to switch this helper to `process.exit()` — that would
31
+ * trade a hang for the truncated-output bug this helper exists to avoid.
32
+ * `__tests__/spinner-lifetime.test.ts` enforces the spinner half.
33
+ *
23
34
  * **Guarded by `fromGluegunMenu`**, like the CLI's other exit-code call sites
24
35
  * (`dev test`, `dev tunnel`, `tools ocr`, `workspace-integration`): inside the
25
36
  * interactive `lt` menu a command is one step of a longer session, and failing
@@ -90,9 +90,10 @@ const ts = __importStar(require("typescript"));
90
90
  * Detection runs on the TypeScript AST, not on regex-stripped text. A regex
91
91
  * "lexer" has no string/template/regex-literal state, so a `/*` or `//` inside a
92
92
  * literal earlier in the file silently erased the guard and triggered the very
93
- * overwrite this function must avoid. `lib/strip-comments.ts` solves the comment
94
- * half properly (TS scanner) and would have been the right reuse; the AST solves
95
- * comments AND nesting in one step, and recognises a backtick require for free.
93
+ * overwrite this function must avoid. Scanning is not enough either: a standalone
94
+ * `ts.createScanner` loop desynchronises on the first interpolating template
95
+ * literal (that is what retired `lib/strip-comments.ts`). The AST solves comments
96
+ * AND nesting in one step, and recognises a backtick require for free.
96
97
  *
97
98
  * ## Recoverability
98
99
  *
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILD_GUARD_FILE = exports.COMPILED_MIGRATE_ARGS = exports.MIGRATE_ENVIRONMENTS = void 0;
4
+ exports.deployedMigrateScripts = deployedMigrateScripts;
5
+ exports.missingBuildGuardWarning = missingBuildGuardWarning;
6
+ const path_1 = require("path");
7
+ const cross_env_1 = require("./cross-env");
8
+ /** Script suffix → `NODE_ENV` value. `prod` maps to `production`, the name Nest config keys off. */
9
+ exports.MIGRATE_ENVIRONMENTS = [
10
+ ['develop', 'develop'],
11
+ ['test', 'test'],
12
+ ['preview', 'preview'],
13
+ ['prod', 'production'],
14
+ ];
15
+ /** Where the build puts the compiled migrations and the store helper. */
16
+ exports.COMPILED_MIGRATE_ARGS = '--store ./dist/migrations-utils/migrate.js --migrations-dir ./dist/migrations';
17
+ /**
18
+ * Build guard in front of every deployed script — the same wiring nest-server-starter
19
+ * uses (`node scripts/… && <migrate>`), so the chain fails BEFORE migrate starts and
20
+ * `dp:prod` never reaches the server start.
21
+ *
22
+ * It is needed because the failure is otherwise SILENT: measured with nest-server
23
+ * 11.41.2, a missing migrations directory prints "treating as empty" and a missing or
24
+ * empty one both end in "No pending migrations", exit 0. An unbuilt checkout would
25
+ * report a successful migration run that never happened, and the server would then come
26
+ * up against an unmigrated database.
27
+ */
28
+ exports.BUILD_GUARD_FILE = 'require-built-migrations.mjs';
29
+ const BUILD_GUARD = `node scripts/${exports.BUILD_GUARD_FILE} && `;
30
+ /**
31
+ * Build the four deployed migrate scripts.
32
+ *
33
+ * @param invocation How this project starts the migrate CLI: `'migrate'` in npm mode
34
+ * (the binary of `@lenne.tech/nest-server`, a production dependency),
35
+ * `'node ./dist/bin/migrate.js'` in vendor mode (the shim `copy:bin`
36
+ * ships into `dist/`).
37
+ */
38
+ function deployedMigrateScripts(invocation) {
39
+ const scripts = {};
40
+ for (const [scriptEnv, nodeEnv] of exports.MIGRATE_ENVIRONMENTS) {
41
+ scripts[`migrate:${scriptEnv}:up`] =
42
+ BUILD_GUARD + (0, cross_env_1.withCrossEnv)({ NODE_ENV: nodeEnv }, `${invocation} up ${exports.COMPILED_MIGRATE_ARGS}`);
43
+ }
44
+ return scripts;
45
+ }
46
+ /**
47
+ * Warn when the build guard the deployed scripts call is not in the project.
48
+ *
49
+ * The file ships with nest-server-starter, which BOTH modes clone as their base
50
+ * (`server.ts`: "Both npm and vendor mode clone nest-server-starter as the base"), and
51
+ * nothing in the conversion prunes `scripts/` wholesale — only
52
+ * `strip-api-mode-markers.mjs`, `scripts/vendor/` and, in REST mode,
53
+ * `run-spectaql.mjs`. So the CLI does NOT ship its own copy: a second, nearly identical
54
+ * file is exactly the drift we are trying to avoid.
55
+ *
56
+ * What it does instead is fail loudly if the assumption stops holding — e.g. a project
57
+ * built from an older starter. Same treatment as `migrations-utils/mongo-uri.js`:
58
+ * without the file every deployed migrate script dies at the first `&&`.
59
+ *
60
+ * @returns A warning message, or null when the guard is present.
61
+ */
62
+ function missingBuildGuardWarning(filesystem, projectDir) {
63
+ if (filesystem.exists((0, path_1.join)(projectDir, 'scripts', exports.BUILD_GUARD_FILE))) {
64
+ return null;
65
+ }
66
+ return (` ⚠ scripts/${exports.BUILD_GUARD_FILE} is missing — the deployed migrate:<env>:up scripts call it ` +
67
+ 'before migrating and would fail immediately. It ships with nest-server-starter; add it ' +
68
+ '(or update the starter this project was created from).');
69
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.fileImportedSpecifiers = fileImportedSpecifiers;
37
+ exports.importedSpecifiers = importedSpecifiers;
38
+ exports.isPackageImport = isPackageImport;
39
+ exports.isRelativeCoreImport = isRelativeCoreImport;
40
+ /**
41
+ * The module specifiers a file imports — read from the syntax tree, never from text.
42
+ *
43
+ * The detectors that ask "does this file still import X?" used to search the raw source
44
+ * and blank out comments first, because a docblock that DOCUMENTS the conversion quotes
45
+ * the very syntax they look for (`nest-server-starter`'s
46
+ * `tests/unit/bootstrap-diagnostics.spec.ts` names `from '@lenne.tech/nest-server'` in
47
+ * prose, and the detector told the user to rewrite imports that file does not have).
48
+ *
49
+ * That workaround does not hold. `stripComments` drives a standalone `ts.createScanner`
50
+ * in a plain `scan()` loop, and a template literal WITH a substitution cannot be read
51
+ * that way: the scanner hands back `TemplateHead` and needs `reScanTemplateToken()` to
52
+ * continue. Without it every token after the first `` `x${…}` `` is misread, so comments
53
+ * from there on pass through verbatim. Bisected on `src/templates/check/check.mjs`: the
54
+ * first surviving comment appears right after `` `${s.toFixed(1)}s` `` on line 70, and a
55
+ * two-line fixture reproduces it. A plain template without substitution is fine; regex
56
+ * literals and division — the first suspects — are not the cause.
57
+ * Since template literals are everywhere, degradation was present in 23 of 76 real
58
+ * frontend files and 21 of 50 backend files; only the absence of a needle-quoting
59
+ * comment BELOW such a template kept it from producing false alarms.
60
+ *
61
+ * Asking the parser removes the whole class: a comment is not part of the AST, so there
62
+ * is nothing to strip and nothing to get wrong.
63
+ */
64
+ const ts = __importStar(require("typescript"));
65
+ /** `<script>` / `<script setup>` blocks of a Vue SFC, which is not valid TypeScript as a whole. */
66
+ const VUE_SCRIPT_BLOCK = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
67
+ /**
68
+ * Same, for a file on disk. A `.vue` SFC is not valid TypeScript, so its `<script>`
69
+ * blocks are extracted first and parsed individually — the template and style blocks
70
+ * cannot contain imports and are skipped. Everything else is parsed as-is.
71
+ *
72
+ * @returns The specifiers, or `[]` when the file cannot be read.
73
+ */
74
+ function fileImportedSpecifiers(filePath, read) {
75
+ const source = read(filePath);
76
+ if (!source) {
77
+ return [];
78
+ }
79
+ if (!filePath.endsWith('.vue')) {
80
+ return importedSpecifiers(source, filePath);
81
+ }
82
+ const specifiers = [];
83
+ for (const [, block] of source.matchAll(VUE_SCRIPT_BLOCK)) {
84
+ specifiers.push(...importedSpecifiers(block, `${filePath}.ts`));
85
+ }
86
+ return specifiers;
87
+ }
88
+ /**
89
+ * Every module specifier `source` imports, exports from, dynamically imports or
90
+ * requires. Duplicates are preserved; order follows the file.
91
+ *
92
+ * Covers `import x from 'y'`, `import 'y'`, `export … from 'y'`, `export * from 'y'`,
93
+ * `import y = require('y')`, `await import('y')` and `require('y')`.
94
+ *
95
+ * @param source The file's text.
96
+ * @param fileName Only used to pick the parser dialect (`.tsx` vs `.ts`).
97
+ */
98
+ function importedSpecifiers(source, fileName = 'file.ts') {
99
+ const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
100
+ const specifiers = [];
101
+ const literalText = (node) => node && ts.isStringLiteralLike(node) ? node.text : null;
102
+ const visit = (node) => {
103
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
104
+ const text = literalText(node.moduleSpecifier);
105
+ if (text !== null) {
106
+ specifiers.push(text);
107
+ }
108
+ }
109
+ else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
110
+ const text = literalText(node.moduleReference.expression);
111
+ if (text !== null) {
112
+ specifiers.push(text);
113
+ }
114
+ }
115
+ else if (ts.isCallExpression(node)) {
116
+ const callee = node.expression;
117
+ const isRequire = ts.isIdentifier(callee) && callee.text === 'require';
118
+ const isDynamicImport = callee.kind === ts.SyntaxKind.ImportKeyword;
119
+ if (isRequire || isDynamicImport) {
120
+ const text = literalText(node.arguments[0]);
121
+ if (text !== null) {
122
+ specifiers.push(text);
123
+ }
124
+ }
125
+ }
126
+ ts.forEachChild(node, visit);
127
+ };
128
+ visit(sourceFile);
129
+ return specifiers;
130
+ }
131
+ /** True when `specifier` is `pkg` itself or a deep import of it (`pkg/sub/path`). */
132
+ function isPackageImport(specifier, pkg) {
133
+ return specifier === pkg || specifier.startsWith(`${pkg}/`);
134
+ }
135
+ /**
136
+ * True when `specifier` is a relative path whose last segment is `core`, or that leads
137
+ * into a `core` directory — `./core`, `../core`, `../../core/utils`.
138
+ */
139
+ function isRelativeCoreImport(specifier) {
140
+ if (!specifier.startsWith('.')) {
141
+ return false;
142
+ }
143
+ return specifier.split('/').includes('core');
144
+ }