@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.
- package/build/commands/dev/test.js +5 -3
- package/build/commands/fullstack/init.js +12 -3
- package/build/commands/git/squash.js +4 -1
- package/build/extensions/api-mode.js +69 -0
- package/build/extensions/frontend-helper.js +11 -12
- package/build/extensions/server.js +42 -32
- 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 +34 -8
- package/build/lib/dev-package-manager.js +13 -9
- package/build/lib/dev-test-session.js +85 -39
- package/build/lib/fail-run.js +11 -0
- 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/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
|
@@ -23,8 +23,10 @@ const dev_ticket_1 = require("../../lib/dev-ticket");
|
|
|
23
23
|
* App mode (default) runs the Playwright suite against a fully ISOLATED test
|
|
24
24
|
* stack (own URLs / ports / Caddy block / dedicated `<…>-test` database) that
|
|
25
25
|
* runs parallel to — and never touches — the developer's `lt dev up` session.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* That DB outlives the run, so pending migrations are applied to it before the
|
|
27
|
+
* API starts; a project's Playwright global-setup may reset it on top (the CLI
|
|
28
|
+
* does not). The stack is torn down automatically when the run finishes
|
|
29
|
+
* (residue-free).
|
|
28
30
|
*
|
|
29
31
|
* API mode (`--api`) runs the standalone API test suite (`pnpm test:e2e` in the
|
|
30
32
|
* API), which already isolates itself on its own DB — no stack is brought up.
|
|
@@ -225,7 +227,7 @@ const TestCommand = {
|
|
|
225
227
|
try {
|
|
226
228
|
const ctx = yield (0, dev_test_session_1.bringUpTestSession)(layout, identity, log, { devDbName });
|
|
227
229
|
const env = Object.assign(Object.assign(Object.assign(Object.assign({}, process.env), readBridgeEnv(layout.root)), {
|
|
228
|
-
//
|
|
230
|
+
// THIS db, for a project whose global-setup resets or cleans it (allow-listed).
|
|
229
231
|
MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` }), (ctx.apiLogPath ? { NEST_SERVER_LOG: ctx.apiLogPath } : {}));
|
|
230
232
|
if (debug) {
|
|
231
233
|
env.PWDEBUG = '1';
|
|
@@ -488,7 +488,7 @@ const NewCommand = {
|
|
|
488
488
|
yield system.run(`cd ${projectDir} && git init --initial-branch=dev`);
|
|
489
489
|
}
|
|
490
490
|
catch (err) {
|
|
491
|
-
|
|
491
|
+
ngBaseSpinner.fail(`Failed to initialize git: ${err.message}`);
|
|
492
492
|
(0, fail_run_1.failRun)(toolbox);
|
|
493
493
|
return;
|
|
494
494
|
}
|
|
@@ -498,7 +498,7 @@ const NewCommand = {
|
|
|
498
498
|
yield system.run(`cd ${projectDir} && git remote add origin ${gitLink}`);
|
|
499
499
|
}
|
|
500
500
|
catch (err) {
|
|
501
|
-
|
|
501
|
+
ngBaseSpinner.fail(`Failed to add remote: ${err.message}`);
|
|
502
502
|
(0, fail_run_1.failRun)(toolbox);
|
|
503
503
|
return;
|
|
504
504
|
}
|
|
@@ -526,7 +526,7 @@ const NewCommand = {
|
|
|
526
526
|
});
|
|
527
527
|
}
|
|
528
528
|
if (!frontendResult.success) {
|
|
529
|
-
|
|
529
|
+
ngBaseSpinner.fail(`Failed to set up ${frontend} frontend: ${frontendResult.path}`);
|
|
530
530
|
(0, fail_run_1.failRun)(toolbox);
|
|
531
531
|
return;
|
|
532
532
|
}
|
|
@@ -774,6 +774,15 @@ const NewCommand = {
|
|
|
774
774
|
// For tests
|
|
775
775
|
return `new workspace ${projectDir} with ${name}`;
|
|
776
776
|
}
|
|
777
|
+
else {
|
|
778
|
+
// The frontend setup reported success but left no `projects/app`. Without this
|
|
779
|
+
// branch the command fell off the end here: no message, no failing exit code,
|
|
780
|
+
// and `ngBaseSpinner` still spinning — which keeps the event loop alive (see
|
|
781
|
+
// the spinner note in CLAUDE.md), so the process never returned at all.
|
|
782
|
+
ngBaseSpinner.fail(`${frontend} was set up but "${projectDir}/projects/app" does not exist — cannot continue`);
|
|
783
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
777
786
|
}),
|
|
778
787
|
};
|
|
779
788
|
exports.default = NewCommand;
|
|
@@ -129,7 +129,10 @@ const NewCommand = {
|
|
|
129
129
|
const mergeBaseSpin = spin(`Get merge ${base}`);
|
|
130
130
|
const mergeBase = yield git.getMergeBase(base);
|
|
131
131
|
if (!mergeBase) {
|
|
132
|
-
error
|
|
132
|
+
// `fail` rather than `error` + `return`: a running spinner holds the event
|
|
133
|
+
// loop open (ora resumes stdin and keeps a render interval), so the old
|
|
134
|
+
// shape did not just print and leave — the command never exited at all.
|
|
135
|
+
mergeBaseSpin.fail(`No merge base found with ${base}!`);
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
138
|
mergeBaseSpin.succeed();
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.ApiMode = void 0;
|
|
13
13
|
const glob_1 = require("glob");
|
|
14
14
|
const path_1 = require("path");
|
|
15
|
+
const config_env_graphql_1 = require("../lib/config-env-graphql");
|
|
15
16
|
/**
|
|
16
17
|
* API Mode processing for nest-server-starter template
|
|
17
18
|
*
|
|
@@ -46,11 +47,18 @@ class ApiMode {
|
|
|
46
47
|
// REST mode: remove graphql regions, keep rest regions
|
|
47
48
|
yield this.removeMode(projectPath, manifest, 'graphql', 'rest');
|
|
48
49
|
yield this.modifyConfigEnvForRest(projectPath);
|
|
50
|
+
this.assertGraphQlDisabled(projectPath);
|
|
49
51
|
}
|
|
50
52
|
else if (mode === 'GraphQL') {
|
|
51
53
|
// GraphQL mode: remove rest regions, keep graphql regions
|
|
52
54
|
yield this.removeMode(projectPath, manifest, 'rest', 'graphql');
|
|
53
55
|
}
|
|
56
|
+
// Both conversions above must leave no marker behind. Verified rather than
|
|
57
|
+
// assumed: a strip that quietly does nothing reports success just as loudly as
|
|
58
|
+
// one that worked (see assertNoMarkersRemain).
|
|
59
|
+
if (mode !== 'Both') {
|
|
60
|
+
this.assertNoMarkersRemain(projectPath, mode === 'Rest' ? 'graphql' : 'rest');
|
|
61
|
+
}
|
|
54
62
|
// Remove manifest and strip-markers script
|
|
55
63
|
this.filesystem.remove(manifestPath);
|
|
56
64
|
this.filesystem.remove((0, path_1.join)(projectPath, 'scripts', 'strip-api-mode-markers.mjs'));
|
|
@@ -167,6 +175,67 @@ class ApiMode {
|
|
|
167
175
|
this.cleanOrphanImports(projectPath);
|
|
168
176
|
});
|
|
169
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Final check of the REST conversion: every environment block in `config.env.ts`
|
|
180
|
+
* must disable GraphQL.
|
|
181
|
+
*
|
|
182
|
+
* `CoreModule.forRoot` reads a missing `graphQl` as ENABLED, so a block without it
|
|
183
|
+
* yields a project that fails at START time with `Cannot determine a GraphQL output
|
|
184
|
+
* type for the "arguments"` — long after the generator reported success. Every step
|
|
185
|
+
* before this one REPLACES an existing property; none of them adds a missing one, so
|
|
186
|
+
* nothing guaranteed the outcome.
|
|
187
|
+
*
|
|
188
|
+
* Repairs what it can (insert the switch) and throws when a block still lacks it,
|
|
189
|
+
* rather than handing over a project that cannot boot.
|
|
190
|
+
*/
|
|
191
|
+
assertGraphQlDisabled(projectPath) {
|
|
192
|
+
const configPath = (0, path_1.join)(projectPath, 'src', 'config.env.ts');
|
|
193
|
+
const content = this.filesystem.read(configPath);
|
|
194
|
+
if (!content) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const { added, content: repaired } = (0, config_env_graphql_1.disableGraphQlInEveryEnvBlock)(content);
|
|
198
|
+
if (added.length > 0) {
|
|
199
|
+
this.filesystem.write(configPath, repaired);
|
|
200
|
+
}
|
|
201
|
+
const offenders = (0, config_env_graphql_1.envBlocksWithoutGraphQlDisabled)(repaired);
|
|
202
|
+
if (offenders.length > 0) {
|
|
203
|
+
throw new Error(`REST conversion incomplete: src/config.env.ts has no \`graphQl: false\` in ${offenders.join(', ')}. ` +
|
|
204
|
+
'The framework treats a missing switch as GraphQL ENABLED, and the server would fail to start. ' +
|
|
205
|
+
'Add it to those environment blocks and re-run.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Refuse to hand over a project that still carries markers of the removed mode.
|
|
210
|
+
*
|
|
211
|
+
* A leftover `// #region graphql` announces a mode the project does not have — and,
|
|
212
|
+
* worse, means the region CONTENT is still there: resolvers, graphql-only wiring,
|
|
213
|
+
* e2e specs. That is what a silent no-op looks like from the outside, and the strip
|
|
214
|
+
* used to be exactly that on Windows (fixed in the glob helper above): it reported
|
|
215
|
+
* success while touching nothing.
|
|
216
|
+
*
|
|
217
|
+
* Cheap, and it fails at generation time instead of in the user's editor.
|
|
218
|
+
*/
|
|
219
|
+
assertNoMarkersRemain(projectPath, removedMarker) {
|
|
220
|
+
const marked = [];
|
|
221
|
+
for (const dir of ['src', 'tests']) {
|
|
222
|
+
const base = (0, path_1.join)(projectPath, dir);
|
|
223
|
+
if (!this.filesystem.exists(base)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
for (const file of this.globFiles(base, '**/*.ts')) {
|
|
227
|
+
const content = this.filesystem.read(file);
|
|
228
|
+
if ((content === null || content === void 0 ? void 0 : content.includes(`// #region ${removedMarker}`)) || (content === null || content === void 0 ? void 0 : content.includes(`// #endregion ${removedMarker}`))) {
|
|
229
|
+
marked.push(`${dir}/${(0, path_1.basename)(file)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (marked.length > 0) {
|
|
234
|
+
throw new Error(`API-mode conversion incomplete: ${marked.length} file(s) still carry \`// #region ${removedMarker}\` ` +
|
|
235
|
+
`markers (${marked.slice(0, 5).join(', ')}${marked.length > 5 ? ', …' : ''}). ` +
|
|
236
|
+
'The region content was not removed, so this project still contains code of a mode it does not have.');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
170
239
|
/**
|
|
171
240
|
* Resolve a POSIX-style glob pattern against `baseDir` and return the
|
|
172
241
|
* matching files as absolute, OS-native paths.
|
|
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.FrontendHelper = void 0;
|
|
13
13
|
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
14
14
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
15
|
-
const
|
|
15
|
+
const module_specifiers_1 = require("../lib/module-specifiers");
|
|
16
16
|
const strip_vendor_schema_augmentation_1 = require("../lib/strip-vendor-schema-augmentation");
|
|
17
17
|
const vendor_claude_md_1 = require("../lib/vendor-claude-md");
|
|
18
18
|
/**
|
|
@@ -399,7 +399,7 @@ class FrontendHelper {
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
// ── Post-conversion verification ─────────────────────────────────────
|
|
402
|
-
const stale = this.findStaleFrontendImports(dest,
|
|
402
|
+
const stale = this.findStaleFrontendImports(dest, module_specifiers_1.isRelativeCoreImport);
|
|
403
403
|
if (stale.length > 0) {
|
|
404
404
|
const { print } = this.toolbox;
|
|
405
405
|
print.warning(`${stale.length} file(s) still contain relative core imports after npm conversion:`);
|
|
@@ -716,8 +716,7 @@ class FrontendHelper {
|
|
|
716
716
|
].join('\n'));
|
|
717
717
|
}
|
|
718
718
|
// ── Post-conversion verification ─────────────────────────────────────
|
|
719
|
-
|
|
720
|
-
const staleImports = this.findStaleFrontendImports(dest, /(?:^|\s)(?:import|from)\s+['"][^'"]*@lenne\.tech\/nuxt-extensions/m, 'app/core/');
|
|
719
|
+
const staleImports = this.findStaleFrontendImports(dest, (specifier) => (0, module_specifiers_1.isPackageImport)(specifier, '@lenne.tech/nuxt-extensions'), 'app/core/');
|
|
721
720
|
if (staleImports.length > 0) {
|
|
722
721
|
const { print } = this.toolbox;
|
|
723
722
|
print.warning(`${staleImports.length} file(s) still contain '@lenne.tech/nuxt-extensions' imports after vendor conversion:`);
|
|
@@ -826,20 +825,20 @@ class FrontendHelper {
|
|
|
826
825
|
/**
|
|
827
826
|
* Scan consumer files for stale imports matching a pattern.
|
|
828
827
|
*/
|
|
829
|
-
findStaleFrontendImports(appDir,
|
|
828
|
+
findStaleFrontendImports(appDir, matches, skipPathContaining) {
|
|
830
829
|
const { filesystem } = this.toolbox;
|
|
831
830
|
const allFiles = this.walkConsumerFiles(appDir);
|
|
832
831
|
const stale = [];
|
|
833
832
|
for (const absFile of allFiles) {
|
|
834
833
|
if (skipPathContaining && absFile.includes(skipPathContaining))
|
|
835
834
|
continue;
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
const
|
|
842
|
-
if (matches) {
|
|
835
|
+
// Ask the parser for the file's module specifiers instead of searching its text.
|
|
836
|
+
// A docblock that DOCUMENTS the conversion quotes the very import syntax a text
|
|
837
|
+
// search looks for, and blanking comments first does not hold either — see
|
|
838
|
+
// src/lib/module-specifiers.ts. A comment is not part of the AST, so the whole
|
|
839
|
+
// false-positive class is gone rather than worked around.
|
|
840
|
+
const specifiers = (0, module_specifiers_1.fileImportedSpecifiers)(absFile, (path) => filesystem.read(path) || undefined);
|
|
841
|
+
if (specifiers.some(matches)) {
|
|
843
842
|
stale.push(absFile.replace(`${appDir}/`, ''));
|
|
844
843
|
}
|
|
845
844
|
}
|
|
@@ -49,8 +49,10 @@ const path_1 = require("path");
|
|
|
49
49
|
const ts = __importStar(require("typescript"));
|
|
50
50
|
const adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
|
|
51
51
|
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
52
|
+
const cross_env_1 = require("../lib/cross-env");
|
|
52
53
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
53
|
-
const
|
|
54
|
+
const migrate_scripts_1 = require("../lib/migrate-scripts");
|
|
55
|
+
const module_specifiers_1 = require("../lib/module-specifiers");
|
|
54
56
|
const vendor_claude_md_1 = require("../lib/vendor-claude-md");
|
|
55
57
|
/**
|
|
56
58
|
* Server helper functions
|
|
@@ -1556,10 +1558,16 @@ class Server {
|
|
|
1556
1558
|
scripts['migrate:up'] = `node ./bin/migrate.js up ${migrateArgs}`;
|
|
1557
1559
|
scripts['migrate:down'] = `node ./bin/migrate.js down ${migrateArgs}`;
|
|
1558
1560
|
scripts['migrate:list'] = `node ./bin/migrate.js list ${migrateArgs}`;
|
|
1559
|
-
scripts
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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);
|
|
1563
1571
|
// Make the production build carry a runnable migration setup.
|
|
1564
1572
|
//
|
|
1565
1573
|
// The image has no ts-node, and the docker entrypoint looks for
|
|
@@ -1579,7 +1587,8 @@ class Server {
|
|
|
1579
1587
|
// deliberate customization survives a re-run (older starters simply
|
|
1580
1588
|
// lack them → still repaired).
|
|
1581
1589
|
if (typeof scripts['copy:bin'] !== 'string') {
|
|
1582
|
-
|
|
1590
|
+
// `|| exit 0`, not `|| true`: cmd.exe has no `true` command.
|
|
1591
|
+
scripts['copy:bin'] = 'cpy ./bin ./dist/ || exit 0';
|
|
1583
1592
|
}
|
|
1584
1593
|
if (typeof scripts['copy:migrations'] !== 'string') {
|
|
1585
1594
|
scripts['copy:migrations'] =
|
|
@@ -1887,13 +1896,10 @@ class Server {
|
|
|
1887
1896
|
// Scan all consumer files for stale bare-specifier imports that the
|
|
1888
1897
|
// codemod should have rewritten. A single miss causes a compile error,
|
|
1889
1898
|
// so catching it here with a clear message saves the user debugging time.
|
|
1890
|
-
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
1893
|
-
|
|
1894
|
-
// which the codemod legitimately leaves untouched. A naive substring
|
|
1895
|
-
// match flagged config.env.ts's JSDoc path as a false positive.
|
|
1896
|
-
/(?: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'));
|
|
1897
1903
|
if (staleImports.length > 0) {
|
|
1898
1904
|
const { print } = this.toolbox;
|
|
1899
1905
|
print.warning(`⚠ ${staleImports.length} file(s) still contain '@lenne.tech/nest-server' imports after vendor conversion:`);
|
|
@@ -2417,11 +2423,16 @@ class Server {
|
|
|
2417
2423
|
scripts['migrate:up'] = `migrate up ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2418
2424
|
scripts['migrate:down'] = `migrate down ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2419
2425
|
scripts['migrate:list'] = `migrate list ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2420
|
-
//
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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);
|
|
2425
2436
|
filesystem.write(`${dest}/package.json`, pkg);
|
|
2426
2437
|
}
|
|
2427
2438
|
// ── 5. Remove vendor artifacts ──────────────────────────────────────
|
|
@@ -2498,7 +2509,7 @@ class Server {
|
|
|
2498
2509
|
// Scan all consumer files for stale relative imports that still resolve
|
|
2499
2510
|
// to the (now deleted) src/core/ directory. These would be silent
|
|
2500
2511
|
// compile errors.
|
|
2501
|
-
const staleRelativeImports = this.findStaleImports(dest,
|
|
2512
|
+
const staleRelativeImports = this.findStaleImports(dest, module_specifiers_1.isRelativeCoreImport);
|
|
2502
2513
|
if (staleRelativeImports.length > 0) {
|
|
2503
2514
|
const { print } = this.toolbox;
|
|
2504
2515
|
print.warning(`⚠ ${staleRelativeImports.length} file(s) still contain relative core imports after npm conversion:`);
|
|
@@ -2514,11 +2525,10 @@ class Server {
|
|
|
2514
2525
|
* rewritten by a mode conversion. Returns a list of file paths that still
|
|
2515
2526
|
* contain matches.
|
|
2516
2527
|
*
|
|
2517
|
-
* @param dest
|
|
2518
|
-
* @param
|
|
2519
|
-
* @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')`
|
|
2520
2530
|
*/
|
|
2521
|
-
findStaleImports(dest,
|
|
2531
|
+
findStaleImports(dest, matches) {
|
|
2522
2532
|
// Patterns are relative to `dest` and passed to `glob` rather than
|
|
2523
2533
|
// `filesystem.find`: jetpack concatenates the resolved base path in front of
|
|
2524
2534
|
// the pattern, and minimatch treats a backslash inside a PATTERN as an escape,
|
|
@@ -2538,15 +2548,15 @@ class Server {
|
|
|
2538
2548
|
for (const glob of globs) {
|
|
2539
2549
|
const files = (0, glob_1.globSync)(glob, { absolute: true, cwd: dest, dot: true, nodir: true });
|
|
2540
2550
|
for (const file of files) {
|
|
2541
|
-
|
|
2542
|
-
//
|
|
2543
|
-
//
|
|
2544
|
-
//
|
|
2545
|
-
//
|
|
2546
|
-
// and
|
|
2547
|
-
//
|
|
2548
|
-
const
|
|
2549
|
-
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)) {
|
|
2550
2560
|
stale.push(file.replace(`${dest}/`, ''));
|
|
2551
2561
|
}
|
|
2552
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
|
+
}
|