@lenne.tech/cli 1.47.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.
- package/build/commands/dev/test.js +5 -3
- package/build/commands/fullstack/init.js +12 -3
- package/build/commands/git/force-pull.js +11 -2
- package/build/commands/git/rebase.js +11 -1
- package/build/commands/git/reset.js +36 -4
- package/build/commands/git/squash.js +4 -1
- package/build/commands/git/update.js +42 -6
- package/build/extensions/api-mode.js +101 -8
- package/build/extensions/frontend-helper.js +11 -12
- package/build/extensions/git.js +23 -25
- package/build/extensions/server.js +66 -49
- package/build/lib/check-template-sync.js +62 -2
- package/build/lib/config-env-graphql.js +106 -0
- package/build/lib/cross-env.js +94 -0
- package/build/lib/dev-api-launch.js +92 -14
- package/build/lib/dev-package-manager.js +13 -9
- package/build/lib/dev-process.js +66 -30
- package/build/lib/dev-state.js +13 -2
- package/build/lib/dev-test-session.js +166 -55
- package/build/lib/fail-run.js +11 -0
- package/build/lib/git-env.js +46 -0
- package/build/lib/heal-oxlintrc.js +20 -3
- package/build/lib/heal-vendor-migrate-store.js +4 -3
- package/build/lib/migrate-scripts.js +69 -0
- package/build/lib/module-specifiers.js +144 -0
- package/build/lib/spinner-lifetime.js +245 -0
- package/build/lib/strip-vendor-schema-augmentation.js +13 -2
- package/build/lib/vendor-claude-md.js +9 -6
- package/build/lib/workspace-integration.js +12 -2
- package/build/templates/check/check.mjs +59 -9
- package/build/templates/check/lt-monorepo-pin.json +4 -4
- package/docs/commands.md +16 -12
- package/package.json +1 -1
- package/build/lib/strip-comments.js +0 -78
|
@@ -44,12 +44,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.Server = void 0;
|
|
46
46
|
const crypto = __importStar(require("crypto"));
|
|
47
|
+
const glob_1 = require("glob");
|
|
47
48
|
const path_1 = require("path");
|
|
48
49
|
const ts = __importStar(require("typescript"));
|
|
49
50
|
const adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
|
|
50
51
|
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
52
|
+
const cross_env_1 = require("../lib/cross-env");
|
|
51
53
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
52
|
-
const
|
|
54
|
+
const migrate_scripts_1 = require("../lib/migrate-scripts");
|
|
55
|
+
const module_specifiers_1 = require("../lib/module-specifiers");
|
|
53
56
|
const vendor_claude_md_1 = require("../lib/vendor-claude-md");
|
|
54
57
|
/**
|
|
55
58
|
* Server helper functions
|
|
@@ -1232,13 +1235,15 @@ class Server {
|
|
|
1232
1235
|
// only as type annotations — any runtime `new Request(...)` calls refer
|
|
1233
1236
|
// to the global Fetch API Request, not the express one.
|
|
1234
1237
|
const expressImportRegex = /^import\s+\{([^}]*)\}\s+from\s+['"]express['"]\s*;?\s*$/gm;
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1238
|
+
// `glob`, not `filesystem.find`: jetpack builds its matcher by concatenating the
|
|
1239
|
+
// RESOLVED base path in front of the pattern, and minimatch reads a backslash
|
|
1240
|
+
// inside a PATTERN as an escape. On Windows `C:\…\core` + `**/*.ts` therefore
|
|
1241
|
+
// collapses to a literal that matches nothing, so this loop silently patched
|
|
1242
|
+
// no files at all. (minimatch 3 normalised the pattern's separators; the repo
|
|
1243
|
+
// now resolves the hoisted minimatch 10, which does not — see package.json.)
|
|
1244
|
+
// `glob` keeps base and pattern apart and returns absolute OS-native paths.
|
|
1245
|
+
const vendoredTsFiles = (0, glob_1.globSync)('**/*.ts', { absolute: true, cwd: coreDir, dot: true, nodir: true });
|
|
1246
|
+
for (const absPath of vendoredTsFiles) {
|
|
1242
1247
|
if (!filesystem.exists(absPath))
|
|
1243
1248
|
continue;
|
|
1244
1249
|
const content = filesystem.read(absPath) || '';
|
|
@@ -1553,10 +1558,16 @@ class Server {
|
|
|
1553
1558
|
scripts['migrate:up'] = `node ./bin/migrate.js up ${migrateArgs}`;
|
|
1554
1559
|
scripts['migrate:down'] = `node ./bin/migrate.js down ${migrateArgs}`;
|
|
1555
1560
|
scripts['migrate:list'] = `node ./bin/migrate.js list ${migrateArgs}`;
|
|
1556
|
-
scripts
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1561
|
+
// The deployed scripts run the COMPILED migrations under dist/, not the
|
|
1562
|
+
// sources: a production tree has no ts-node (devDependency), so the
|
|
1563
|
+
// `--compiler ts:…` form dies there. `copy:bin` ships the shim into
|
|
1564
|
+
// dist/bin. See src/lib/migrate-scripts.ts.
|
|
1565
|
+
Object.assign(scripts, (0, migrate_scripts_1.deployedMigrateScripts)('node ./dist/bin/migrate.js'));
|
|
1566
|
+
const guardWarning = (0, migrate_scripts_1.missingBuildGuardWarning)(filesystem, dest);
|
|
1567
|
+
if (guardWarning) {
|
|
1568
|
+
this.toolbox.print.warning(guardWarning);
|
|
1569
|
+
}
|
|
1570
|
+
(0, cross_env_1.ensureCrossEnvDependency)(pkg);
|
|
1560
1571
|
// Make the production build carry a runnable migration setup.
|
|
1561
1572
|
//
|
|
1562
1573
|
// The image has no ts-node, and the docker entrypoint looks for
|
|
@@ -1576,7 +1587,8 @@ class Server {
|
|
|
1576
1587
|
// deliberate customization survives a re-run (older starters simply
|
|
1577
1588
|
// lack them → still repaired).
|
|
1578
1589
|
if (typeof scripts['copy:bin'] !== 'string') {
|
|
1579
|
-
|
|
1590
|
+
// `|| exit 0`, not `|| true`: cmd.exe has no `true` command.
|
|
1591
|
+
scripts['copy:bin'] = 'cpy ./bin ./dist/ || exit 0';
|
|
1580
1592
|
}
|
|
1581
1593
|
if (typeof scripts['copy:migrations'] !== 'string') {
|
|
1582
1594
|
scripts['copy:migrations'] =
|
|
@@ -1884,13 +1896,10 @@ class Server {
|
|
|
1884
1896
|
// Scan all consumer files for stale bare-specifier imports that the
|
|
1885
1897
|
// codemod should have rewritten. A single miss causes a compile error,
|
|
1886
1898
|
// so catching it here with a clear message saves the user debugging time.
|
|
1887
|
-
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
|
|
1891
|
-
// which the codemod legitimately leaves untouched. A naive substring
|
|
1892
|
-
// match flagged config.env.ts's JSDoc path as a false positive.
|
|
1893
|
-
/(?:from|import|export|require)\s*\(?\s*['"]@lenne\.tech\/nest-server(?:\/[^'"]*)?['"]/);
|
|
1899
|
+
// A specifier, not a text match: a JSDoc path like
|
|
1900
|
+
// `node_modules/@lenne.tech/nest-server/...` is prose and never reaches the AST,
|
|
1901
|
+
// which is what used to make config.env.ts a false positive.
|
|
1902
|
+
const staleImports = this.findStaleImports(dest, (specifier) => (0, module_specifiers_1.isPackageImport)(specifier, '@lenne.tech/nest-server'));
|
|
1894
1903
|
if (staleImports.length > 0) {
|
|
1895
1904
|
const { print } = this.toolbox;
|
|
1896
1905
|
print.warning(`⚠ ${staleImports.length} file(s) still contain '@lenne.tech/nest-server' imports after vendor conversion:`);
|
|
@@ -2414,11 +2423,16 @@ class Server {
|
|
|
2414
2423
|
scripts['migrate:up'] = `migrate up ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2415
2424
|
scripts['migrate:down'] = `migrate down ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2416
2425
|
scripts['migrate:list'] = `migrate list ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2417
|
-
//
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2426
|
+
// Deployed scripts: compiled migrations under dist/, no --compiler (no ts-node
|
|
2427
|
+
// in a production tree), via cross-env for cmd.exe. `migrate` is the binary of
|
|
2428
|
+
// @lenne.tech/nest-server, a production dependency in npm mode — the same path
|
|
2429
|
+
// docker-entrypoint.sh uses for this mode.
|
|
2430
|
+
Object.assign(scripts, (0, migrate_scripts_1.deployedMigrateScripts)('migrate'));
|
|
2431
|
+
const guardWarning = (0, migrate_scripts_1.missingBuildGuardWarning)(filesystem, dest);
|
|
2432
|
+
if (guardWarning) {
|
|
2433
|
+
this.toolbox.print.warning(guardWarning);
|
|
2434
|
+
}
|
|
2435
|
+
(0, cross_env_1.ensureCrossEnvDependency)(pkg);
|
|
2422
2436
|
filesystem.write(`${dest}/package.json`, pkg);
|
|
2423
2437
|
}
|
|
2424
2438
|
// ── 5. Remove vendor artifacts ──────────────────────────────────────
|
|
@@ -2495,7 +2509,7 @@ class Server {
|
|
|
2495
2509
|
// Scan all consumer files for stale relative imports that still resolve
|
|
2496
2510
|
// to the (now deleted) src/core/ directory. These would be silent
|
|
2497
2511
|
// compile errors.
|
|
2498
|
-
const staleRelativeImports = this.findStaleImports(dest,
|
|
2512
|
+
const staleRelativeImports = this.findStaleImports(dest, module_specifiers_1.isRelativeCoreImport);
|
|
2499
2513
|
if (staleRelativeImports.length > 0) {
|
|
2500
2514
|
const { print } = this.toolbox;
|
|
2501
2515
|
print.warning(`⚠ ${staleRelativeImports.length} file(s) still contain relative core imports after npm conversion:`);
|
|
@@ -2511,35 +2525,38 @@ class Server {
|
|
|
2511
2525
|
* rewritten by a mode conversion. Returns a list of file paths that still
|
|
2512
2526
|
* contain matches.
|
|
2513
2527
|
*
|
|
2514
|
-
* @param dest
|
|
2515
|
-
* @param
|
|
2516
|
-
* @param pattern Optional regex for more flexible matching
|
|
2528
|
+
* @param dest Project root directory
|
|
2529
|
+
* @param matches Predicate on a module SPECIFIER, e.g. `(s) => isPackageImport(s, 'pkg')`
|
|
2517
2530
|
*/
|
|
2518
|
-
findStaleImports(dest,
|
|
2531
|
+
findStaleImports(dest, matches) {
|
|
2532
|
+
// Patterns are relative to `dest` and passed to `glob` rather than
|
|
2533
|
+
// `filesystem.find`: jetpack concatenates the resolved base path in front of
|
|
2534
|
+
// the pattern, and minimatch treats a backslash inside a PATTERN as an escape,
|
|
2535
|
+
// so on Windows every slash-bearing pattern below matched nothing. This
|
|
2536
|
+
// function would then report "no stale imports" for a project that has them —
|
|
2537
|
+
// a false clean bill of health on the one check meant to catch a broken
|
|
2538
|
+
// conversion. See the same fix at the vendored-express patch above.
|
|
2519
2539
|
const globs = [
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2540
|
+
'src/server/**/*.ts',
|
|
2541
|
+
'src/main.ts',
|
|
2542
|
+
'src/config.env.ts',
|
|
2543
|
+
'tests/**/*.ts',
|
|
2544
|
+
'migrations/**/*.ts',
|
|
2545
|
+
'scripts/**/*.ts',
|
|
2526
2546
|
];
|
|
2527
2547
|
const stale = [];
|
|
2528
2548
|
for (const glob of globs) {
|
|
2529
|
-
const files =
|
|
2530
|
-
matching: glob.replace(`${dest}/`, ''),
|
|
2531
|
-
recursive: true,
|
|
2532
|
-
}) || [];
|
|
2549
|
+
const files = (0, glob_1.globSync)(glob, { absolute: true, cwd: dest, dot: true, nodir: true });
|
|
2533
2550
|
for (const file of files) {
|
|
2534
|
-
|
|
2535
|
-
//
|
|
2536
|
-
//
|
|
2537
|
-
//
|
|
2538
|
-
//
|
|
2539
|
-
// and
|
|
2540
|
-
//
|
|
2541
|
-
const
|
|
2542
|
-
if (
|
|
2551
|
+
// Ask the parser for the file's module specifiers rather than searching its
|
|
2552
|
+
// text. A docblock that DOCUMENTS the conversion quotes the very syntax a text
|
|
2553
|
+
// search looks for — nest-server-starter's `tests/unit/bootstrap-diagnostics.spec.ts`
|
|
2554
|
+
// names `from '@lenne.tech/nest-server'` in prose, and the detector told the user
|
|
2555
|
+
// to rewrite imports that file does not have. Blanking comments first was the old
|
|
2556
|
+
// answer and does not hold (see src/lib/module-specifiers.ts); a comment is not
|
|
2557
|
+
// part of the AST at all.
|
|
2558
|
+
const specifiers = (0, module_specifiers_1.fileImportedSpecifiers)(file, (path) => this.filesystem.read(path) || undefined);
|
|
2559
|
+
if (specifiers.some(matches)) {
|
|
2543
2560
|
stale.push(file.replace(`${dest}/`, ''));
|
|
2544
2561
|
}
|
|
2545
2562
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CHECK_TEMPLATE_PIN_FILE = exports.CHECK_TEMPLATE_REPOSITORY = void 0;
|
|
4
4
|
exports.readCheckTemplatePin = readCheckTemplatePin;
|
|
5
5
|
exports.syncCheckTemplate = syncCheckTemplate;
|
|
6
|
+
exports.spawnedSiblings = spawnedSiblings;
|
|
6
7
|
exports.verifyCheckTemplate = verifyCheckTemplate;
|
|
7
8
|
/**
|
|
8
9
|
* Keep `src/templates/check/` a verbatim copy of a PINNED lt-monorepo state.
|
|
@@ -77,6 +78,14 @@ function syncCheckTemplate(options) {
|
|
|
77
78
|
(0, fs_1.rmSync)((0, path_1.join)(templateDir, stale), { force: true });
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
// Refuse rather than ship silently: whether a spawned sibling belongs in the
|
|
82
|
+
// template or whether lt-monorepo should drop the call is a human decision.
|
|
83
|
+
const spawned = spawnedSiblings(templateDir, Object.keys(files)).filter((name) => !(name in files));
|
|
84
|
+
if (spawned.length > 0) {
|
|
85
|
+
throw new Error(`${ref}: the template starts sibling script(s) it does not ship: ${spawned.join(', ')}. ` +
|
|
86
|
+
'resolveCopySet only follows imports, so these would be missing in every generated project. ' +
|
|
87
|
+
'Add them to the wrapper\'s import closure, ship them deliberately, or remove the call upstream.');
|
|
88
|
+
}
|
|
80
89
|
const pin = {
|
|
81
90
|
commit,
|
|
82
91
|
files: sortKeys(files),
|
|
@@ -91,10 +100,54 @@ function syncCheckTemplate(options) {
|
|
|
91
100
|
(0, fs_1.rmSync)(checkout, { force: true, recursive: true });
|
|
92
101
|
}
|
|
93
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* A sibling script the wrapper SPAWNS rather than imports.
|
|
105
|
+
*
|
|
106
|
+
* `resolveCopySet` follows imports, so a `node scripts/x.mjs` — or a spawn/execFile
|
|
107
|
+
* with such a path — is invisible to it: the file is never shipped, the pin test stays
|
|
108
|
+
* green, and only the generated project breaks. Matched on the path inside a STRING
|
|
109
|
+
* rather than on the call shape, so `spawn('node', ['scripts/x.mjs'])`,
|
|
110
|
+
* `execSync('bash scripts/x.sh')` and a bare `'scripts/x.mjs'` are all covered.
|
|
111
|
+
*
|
|
112
|
+
* Comment LINES are skipped before matching. These files document their own behaviour
|
|
113
|
+
* in backtick-quoted prose, and a backtick is a string delimiter to a regex: the first
|
|
114
|
+
* draft reported `bash scripts/audit.sh` out of a comment on `check.mjs:103` and would
|
|
115
|
+
* have refused every sync from then on. `stripComments` is deliberately NOT used —
|
|
116
|
+
* measured on this very file, it stops blanking at offset 3329, where a regex literal
|
|
117
|
+
* (`/^projects\//`) is followed by a division (`ms / 1000`) and the standalone
|
|
118
|
+
* TypeScript scanner, having no parser context, can no longer tell the two apart.
|
|
119
|
+
*/
|
|
120
|
+
const SPAWNED_SIBLING = /['"`](?:[\w./-]+\s+)*(?:\.\/)?scripts\/([\w.-]+\.(?:mjs|cjs|js|sh))/g;
|
|
121
|
+
/**
|
|
122
|
+
* Sibling scripts the template STARTS instead of importing, as `scripts/<name>`.
|
|
123
|
+
* Deduplicated, and the wrapper's own name is never reported.
|
|
124
|
+
*/
|
|
125
|
+
function spawnedSiblings(templateDir, files) {
|
|
126
|
+
const found = new Set();
|
|
127
|
+
for (const name of files) {
|
|
128
|
+
const file = (0, path_1.join)(templateDir, name);
|
|
129
|
+
if (!(0, fs_1.existsSync)(file)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
for (const line of (0, fs_1.readFileSync)(file, 'utf8').split('\n')) {
|
|
133
|
+
const code = line.trimStart();
|
|
134
|
+
if (code.startsWith('//') || code.startsWith('*') || code.startsWith('/*')) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const [, sibling] of code.matchAll(SPAWNED_SIBLING)) {
|
|
138
|
+
if (sibling !== 'check.mjs' && sibling !== name) {
|
|
139
|
+
found.add(sibling);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [...found].sort();
|
|
145
|
+
}
|
|
94
146
|
/**
|
|
95
147
|
* Differences between the template dir and its pin; empty when they match.
|
|
96
|
-
* Checks
|
|
97
|
-
*
|
|
148
|
+
* Checks three directions: every pinned file with its hash, every file the wrapper
|
|
149
|
+
* imports is pinned (a hand-added import would otherwise slip by), and no file is
|
|
150
|
+
* SPAWNED that nothing ships.
|
|
98
151
|
*/
|
|
99
152
|
function verifyCheckTemplate(templateDir, pin) {
|
|
100
153
|
const problems = [];
|
|
@@ -113,6 +166,13 @@ function verifyCheckTemplate(templateDir, pin) {
|
|
|
113
166
|
problems.push(`${name}: imported by the wrapper but not pinned`);
|
|
114
167
|
}
|
|
115
168
|
}
|
|
169
|
+
for (const sibling of spawnedSiblings(templateDir, [...Object.keys(pin.files), ...actual])) {
|
|
170
|
+
if (!(sibling in pin.files)) {
|
|
171
|
+
problems.push(`${sibling}: STARTED by the template (\`node scripts/${sibling}\`) but not shipped — ` +
|
|
172
|
+
'a generated project would call a file it does not have. Decide deliberately: add it to the ' +
|
|
173
|
+
'template, or have lt-monorepo drop the call.');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
116
176
|
const version = (0, heal_check_wrapper_1.readWrapperVersion)((0, path_1.join)(templateDir, 'check.mjs'));
|
|
117
177
|
if (version !== pin.version) {
|
|
118
178
|
problems.push(`check.mjs: marker ${version !== null && version !== void 0 ? version : '(none)'} does not match pinned version ${pin.version}`);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.disableGraphQlInEveryEnvBlock = disableGraphQlInEveryEnvBlock;
|
|
4
|
+
exports.envBlocksWithoutGraphQlDisabled = envBlocksWithoutGraphQlDisabled;
|
|
5
|
+
/**
|
|
6
|
+
* Add `graphQl: false` to every environment block that has no `graphQl` property, and
|
|
7
|
+
* replace a `graphQl: { … }` that survived with `false`.
|
|
8
|
+
*
|
|
9
|
+
* Line endings are preserved: a CRLF file stays CRLF.
|
|
10
|
+
*/
|
|
11
|
+
function disableGraphQlInEveryEnvBlock(content) {
|
|
12
|
+
var _a, _b, _c;
|
|
13
|
+
const { project, sourceFile } = parse(content);
|
|
14
|
+
const added = [];
|
|
15
|
+
for (const [name, block] of envBlocks(sourceFile)) {
|
|
16
|
+
const property = block.getProperty('graphQl');
|
|
17
|
+
if (!property) {
|
|
18
|
+
block.insertPropertyAssignment(0, { initializer: 'false', name: 'graphQl' });
|
|
19
|
+
added.push(name);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const initializer = (_b = (_a = property).getInitializer) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
23
|
+
if (initializer && initializer.getText() !== 'false') {
|
|
24
|
+
property.setInitializer('false');
|
|
25
|
+
added.push(name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const written = added.length > 0 ? sourceFile.getFullText() : content;
|
|
29
|
+
(_c = project.removeSourceFile) === null || _c === void 0 ? void 0 : _c.call(project, sourceFile);
|
|
30
|
+
return { added, content: restoreLineEndings(content, written) };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Environment blocks that do NOT disable GraphQL — empty when the file is fine.
|
|
34
|
+
* Used as the conversion's final check.
|
|
35
|
+
*/
|
|
36
|
+
function envBlocksWithoutGraphQlDisabled(content) {
|
|
37
|
+
var _a, _b, _c;
|
|
38
|
+
const { project, sourceFile } = parse(content);
|
|
39
|
+
const offenders = [];
|
|
40
|
+
for (const [name, block] of envBlocks(sourceFile)) {
|
|
41
|
+
const property = block.getProperty('graphQl');
|
|
42
|
+
const initializer = property && ((_b = (_a = property).getInitializer) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
43
|
+
if (!initializer || initializer.getText() !== 'false') {
|
|
44
|
+
offenders.push(name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
(_c = project.removeSourceFile) === null || _c === void 0 ? void 0 : _c.call(project, sourceFile);
|
|
48
|
+
return offenders;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The environment blocks of a `config.env.ts`: the object-literal properties of the
|
|
52
|
+
* exported `config` object, including the ones passed to a `merge(…)` call (the shape
|
|
53
|
+
* the starter uses). Only direct children count — a nested `mongoose: { … }` is not an
|
|
54
|
+
* environment.
|
|
55
|
+
*/
|
|
56
|
+
function envBlocks(sourceFile) {
|
|
57
|
+
var _a, _b;
|
|
58
|
+
const { SyntaxKind } = require('ts-morph');
|
|
59
|
+
const roots = [];
|
|
60
|
+
for (const statement of sourceFile.getVariableStatements()) {
|
|
61
|
+
for (const declaration of statement.getDeclarations()) {
|
|
62
|
+
if (declaration.getName() !== 'config') {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const initializer = declaration.getInitializer();
|
|
66
|
+
if (!initializer) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (initializer.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
70
|
+
roots.push(initializer);
|
|
71
|
+
}
|
|
72
|
+
if (initializer.getKind() === SyntaxKind.CallExpression) {
|
|
73
|
+
for (const argument of initializer.getArguments()) {
|
|
74
|
+
if (argument.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
75
|
+
roots.push(argument);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const blocks = [];
|
|
82
|
+
for (const root of roots) {
|
|
83
|
+
for (const property of root.getProperties()) {
|
|
84
|
+
if (property.getKind() !== SyntaxKind.PropertyAssignment) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const initializer = (_b = (_a = property).getInitializer) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
88
|
+
if ((initializer === null || initializer === void 0 ? void 0 : initializer.getKind()) === SyntaxKind.ObjectLiteralExpression) {
|
|
89
|
+
blocks.push([property.getName(), initializer]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return blocks;
|
|
94
|
+
}
|
|
95
|
+
function parse(content) {
|
|
96
|
+
const { Project } = require('ts-morph');
|
|
97
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: true });
|
|
98
|
+
return { project, sourceFile: project.createSourceFile('config.env.ts', content, { overwrite: true }) };
|
|
99
|
+
}
|
|
100
|
+
/** ts-morph writes LF; a file that came in as CRLF goes back out as CRLF. */
|
|
101
|
+
function restoreLineEndings(original, written) {
|
|
102
|
+
if (!original.includes('\r\n')) {
|
|
103
|
+
return written;
|
|
104
|
+
}
|
|
105
|
+
return written.replace(/\r?\n/g, '\r\n');
|
|
106
|
+
}
|
|
@@ -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,8 +9,10 @@ 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;
|
|
15
|
+
exports.resolveApiRuntime = resolveApiRuntime;
|
|
14
16
|
exports.startCompiledApi = startCompiledApi;
|
|
15
17
|
/**
|
|
16
18
|
* Launch strategy for the API process under `lt dev up`.
|
|
@@ -28,6 +30,33 @@ const node_path_1 = require("node:path");
|
|
|
28
30
|
const dev_process_1 = require("./dev-process");
|
|
29
31
|
/** Candidate compiled entry points, in preference order. Single-sourced so `lt dev test` agrees. */
|
|
30
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
|
+
}
|
|
31
60
|
/** Resolve the compiled API entry point in `apiDir`, or `undefined` if none was built. */
|
|
32
61
|
function findCompiledEntry(apiDir) {
|
|
33
62
|
return COMPILED_ENTRIES.map((rel) => (0, node_path_1.join)(apiDir, rel)).find((candidate) => (0, node_fs_1.existsSync)(candidate));
|
|
@@ -47,6 +76,50 @@ function isApiCompiledRequested(options = {}) {
|
|
|
47
76
|
const affirmative = (value) => value === true || ['1', 'true', 'yes'].includes(String(value).toLowerCase());
|
|
48
77
|
return affirmative(options.apiCompiled) || affirmative(options['api-compiled']);
|
|
49
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Which runtime to start the API's COMPILED bundle with.
|
|
81
|
+
*
|
|
82
|
+
* A bundle is only portable across runtimes by accident. `nest-base` builds with
|
|
83
|
+
* `Bun.build({ target: 'bun' })`, whose output calls `__require` — a Bun-only
|
|
84
|
+
* shim. Started under node it dies in milliseconds with "__require is not a
|
|
85
|
+
* function" (DEV-3208), and because the caller used to only WARN when the API
|
|
86
|
+
* never answered, the whole Playwright suite then ran against a dead API: every
|
|
87
|
+
* data-dependent spec passed trivially or skipped, so a run that checked nothing
|
|
88
|
+
* looked exactly like one that checked everything.
|
|
89
|
+
*
|
|
90
|
+
* Detected from the project rather than configured, because the project already
|
|
91
|
+
* states it three times over. Any one signal is enough:
|
|
92
|
+
* - a `bun.lock` / `bun.lockb` IN THIS DIRECTORY (not a nested package's),
|
|
93
|
+
* - `engines.bun` in its package.json,
|
|
94
|
+
* - a `build` / `start` script that invokes `bun` as the COMMAND.
|
|
95
|
+
*
|
|
96
|
+
* The script check matches `bun` as a whole word at a command position, never as
|
|
97
|
+
* a substring: `rollup --bundle` and `node -r bunyan …` are Node projects, and a
|
|
98
|
+
* loose `includes('bun')` would switch them to a runtime they never installed.
|
|
99
|
+
*
|
|
100
|
+
* Defaults to `node` whenever nothing says otherwise, including an unreadable or
|
|
101
|
+
* missing package.json — the historical behaviour, and the safe one: guessing
|
|
102
|
+
* `bun` for a project that does not have it installed turns a working start into
|
|
103
|
+
* an exit-127.
|
|
104
|
+
*
|
|
105
|
+
* This deliberately does NOT live in `dev-package-manager`: that module resolves
|
|
106
|
+
* which PACKAGE MANAGER drives a directory (`pnpm run build`), a separate axis
|
|
107
|
+
* from which runtime executes an already-built bundle. A Bun project whose
|
|
108
|
+
* scripts pnpm happens to run still needs `bun dist/main.js`.
|
|
109
|
+
*/
|
|
110
|
+
function resolveApiRuntime(apiDir) {
|
|
111
|
+
var _a, _b, _c;
|
|
112
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(apiDir, 'bun.lock')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(apiDir, 'bun.lockb')))
|
|
113
|
+
return 'bun';
|
|
114
|
+
const pkg = readPackageJson(apiDir);
|
|
115
|
+
if (!pkg)
|
|
116
|
+
return 'node';
|
|
117
|
+
if ((_a = pkg.engines) === null || _a === void 0 ? void 0 : _a.bun)
|
|
118
|
+
return 'bun';
|
|
119
|
+
// `bun` as the command itself, or after a shell separator (`&&`, `;`, `|`).
|
|
120
|
+
const invokesBun = /(?:^|[&|;]\s*)bun\b/;
|
|
121
|
+
return [(_b = pkg.scripts) === null || _b === void 0 ? void 0 : _b.build, (_c = pkg.scripts) === null || _c === void 0 ? void 0 : _c.start].some((script) => script && invokesBun.test(script)) ? 'bun' : 'node';
|
|
122
|
+
}
|
|
50
123
|
/**
|
|
51
124
|
* Build the API and start it compiled (`node dist/src/main.js`). Applies pending
|
|
52
125
|
* migrations first for parity with the ts-node path it replaces (`<pm> run start`
|
|
@@ -61,16 +134,18 @@ function startCompiledApi(options) {
|
|
|
61
134
|
const build = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('build'), { cwd: apiDir, env });
|
|
62
135
|
const entry = findCompiledEntry(apiDir);
|
|
63
136
|
if (build === 0 && entry) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
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;
|
|
72
143
|
}
|
|
73
|
-
return (0, dev_process_1.spawnDetached)(
|
|
144
|
+
return (0, dev_process_1.spawnDetached)(resolveApiRuntime(apiDir), [entry], {
|
|
145
|
+
cwd: apiDir,
|
|
146
|
+
env: Object.assign(Object.assign({}, env), { NODE_ENV: 'local' }),
|
|
147
|
+
logFile,
|
|
148
|
+
});
|
|
74
149
|
}
|
|
75
150
|
log.warn(`compiled API unavailable (build exit ${String(build)}) — falling back to \`${pm.bin} start\` (ts-node).`);
|
|
76
151
|
return (0, dev_process_1.spawnDetached)(pm.bin, pm.runScript('start'), { cwd: apiDir, env, logFile });
|
|
@@ -78,12 +153,15 @@ function startCompiledApi(options) {
|
|
|
78
153
|
}
|
|
79
154
|
/** True when `package.json` in `apiDir` defines a script named `name`. */
|
|
80
155
|
function hasScript(apiDir, name) {
|
|
81
|
-
var _a;
|
|
156
|
+
var _a, _b;
|
|
157
|
+
return typeof ((_b = (_a = readPackageJson(apiDir)) === null || _a === void 0 ? void 0 : _a.scripts) === null || _b === void 0 ? void 0 : _b[name]) === 'string';
|
|
158
|
+
}
|
|
159
|
+
/** The parsed `package.json` in `apiDir`, or `undefined` when it is missing or unparsable. */
|
|
160
|
+
function readPackageJson(apiDir) {
|
|
82
161
|
try {
|
|
83
|
-
|
|
84
|
-
return typeof ((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a[name]) === 'string';
|
|
162
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(apiDir, 'package.json'), 'utf8'));
|
|
85
163
|
}
|
|
86
|
-
catch (
|
|
87
|
-
return
|
|
164
|
+
catch (_a) {
|
|
165
|
+
return undefined;
|
|
88
166
|
}
|
|
89
167
|
}
|