@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
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.findSpinnerLeaks = findSpinnerLeaks;
|
|
37
|
+
/**
|
|
38
|
+
* Finds progress spinners that can be left running.
|
|
39
|
+
*
|
|
40
|
+
* gluegun's `spin(text)` is `require('ora')(text).start()`. ora 4.0.2 with the
|
|
41
|
+
* default `discardStdin: true` does three things on start that only a stop undoes:
|
|
42
|
+
* it puts the terminal into raw mode (remembering the value it found), it calls
|
|
43
|
+
* `stdin.resume()` and adds a `data` listener, and it starts a `setInterval` for
|
|
44
|
+
* the animation.
|
|
45
|
+
*
|
|
46
|
+
* A spinner that is never stopped therefore does not merely look untidy — the
|
|
47
|
+
* interval and the resumed stdin keep the Node event loop alive, so the process
|
|
48
|
+
* never exits, and the terminal stays in raw mode. That combination is how
|
|
49
|
+
* `lt fullstack init` could reach the end of its work and simply not return, with
|
|
50
|
+
* a spinner still turning. `failRun` deliberately sets `process.exitCode` instead
|
|
51
|
+
* of calling `process.exit()` (so a failure message is never truncated), which
|
|
52
|
+
* means "the event loop drains" is a precondition the CLI relies on.
|
|
53
|
+
*
|
|
54
|
+
* ## What "stopped" means here
|
|
55
|
+
*
|
|
56
|
+
* This is a dominance approximation, not a full control-flow analysis, and it is
|
|
57
|
+
* deliberately conservative in the direction that avoids false alarms — a guard
|
|
58
|
+
* that flags correct code gets switched off. A statement list definitely stops a
|
|
59
|
+
* spinner when it contains a direct stop call, or an `if` whose BOTH branches
|
|
60
|
+
* stop, or a `try` whose body and `catch` both stop (or whose `finally` stops).
|
|
61
|
+
* Anything else does not count.
|
|
62
|
+
*
|
|
63
|
+
* Two questions are asked per spinner:
|
|
64
|
+
*
|
|
65
|
+
* 1. Can the end of its enclosing block be reached without a stop? That is the
|
|
66
|
+
* silent case — the command falls off the end, exit code 0, spinner running.
|
|
67
|
+
* 2. Is there a `return` after it that no stop dominates? That is the early-exit
|
|
68
|
+
* case — an error path that prints and leaves.
|
|
69
|
+
*
|
|
70
|
+
* Returns inside a nested function are ignored: they leave the callback, not the
|
|
71
|
+
* command.
|
|
72
|
+
*/
|
|
73
|
+
const ts = __importStar(require("typescript"));
|
|
74
|
+
/** Methods that stop an ora spinner. `stopAndPersist` is what `succeed`/`fail`/… call. */
|
|
75
|
+
const STOP_METHODS = new Set(['fail', 'info', 'stop', 'stopAndPersist', 'succeed', 'warn']);
|
|
76
|
+
/**
|
|
77
|
+
* Every spinner in `source` that can outlive the code path that started it.
|
|
78
|
+
*
|
|
79
|
+
* @param source The file's text.
|
|
80
|
+
* @param fileName Only used to pick the parser dialect.
|
|
81
|
+
*/
|
|
82
|
+
function findSpinnerLeaks(source, fileName = 'file.ts') {
|
|
83
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
|
|
84
|
+
const leaks = [];
|
|
85
|
+
const lineOf = (node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
86
|
+
for (const declaration of spinnerDeclarations(sourceFile)) {
|
|
87
|
+
const name = declaration.name.getText(sourceFile);
|
|
88
|
+
const block = enclosingBlock(declaration);
|
|
89
|
+
if (!block) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const returns = returnsAfter(block, declaration);
|
|
93
|
+
// `return spinner;` hands ownership to the caller, which is then responsible
|
|
94
|
+
// for stopping it — a factory like `logger.ts#spin` is not a leak. One such
|
|
95
|
+
// return exempts the whole declaration, because the spinner's lifetime is no
|
|
96
|
+
// longer decided in this function.
|
|
97
|
+
if (returns.some((statement) => handsOverOwnership(statement, name))) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const after = statementsAfter(block, declaration);
|
|
101
|
+
if (!after.some((statement) => definitelyStops(statement, name, sourceFile))) {
|
|
102
|
+
leaks.push({
|
|
103
|
+
line: lineOf(declaration),
|
|
104
|
+
reason: after.length === 0 ? 'never-stopped' : 'falls-through',
|
|
105
|
+
spinner: name,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (const returnStatement of returns) {
|
|
109
|
+
if (!dominatedByStop(returnStatement, block, declaration, name, sourceFile)) {
|
|
110
|
+
leaks.push({ line: lineOf(returnStatement), reason: 'return-without-stop', spinner: name });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return leaks.sort((a, b) => a.line - b.line);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* True when executing `statement` always stops `name`.
|
|
118
|
+
*
|
|
119
|
+
* Only the shapes the CLI actually uses are recognised. Everything unrecognised
|
|
120
|
+
* answers false, so an unusual construct produces a finding to look at rather
|
|
121
|
+
* than a silent pass.
|
|
122
|
+
*/
|
|
123
|
+
function definitelyStops(statement, name, sourceFile) {
|
|
124
|
+
if (ts.isExpressionStatement(statement)) {
|
|
125
|
+
return isStopCall(statement.expression, name, sourceFile);
|
|
126
|
+
}
|
|
127
|
+
if (ts.isBlock(statement)) {
|
|
128
|
+
return statement.statements.some((child) => definitelyStops(child, name, sourceFile));
|
|
129
|
+
}
|
|
130
|
+
if (ts.isIfStatement(statement)) {
|
|
131
|
+
// Only an if/else that stops on BOTH sides stops unconditionally.
|
|
132
|
+
return (!!statement.elseStatement &&
|
|
133
|
+
definitelyStops(statement.thenStatement, name, sourceFile) &&
|
|
134
|
+
definitelyStops(statement.elseStatement, name, sourceFile));
|
|
135
|
+
}
|
|
136
|
+
if (ts.isTryStatement(statement)) {
|
|
137
|
+
if (statement.finallyBlock && definitelyStops(statement.finallyBlock, name, sourceFile)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return (definitelyStops(statement.tryBlock, name, sourceFile) &&
|
|
141
|
+
!!statement.catchClause &&
|
|
142
|
+
definitelyStops(statement.catchClause.block, name, sourceFile));
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* True when some stop of `name` is guaranteed to run before `returnStatement`.
|
|
148
|
+
*
|
|
149
|
+
* Walks out from the return towards `block`, and at each level checks the
|
|
150
|
+
* statements that precede the one we came from — a stop in any of them dominates.
|
|
151
|
+
*/
|
|
152
|
+
function dominatedByStop(returnStatement, block, declaration, name, sourceFile) {
|
|
153
|
+
let child = returnStatement;
|
|
154
|
+
let parent = returnStatement.parent;
|
|
155
|
+
while (parent) {
|
|
156
|
+
if (ts.isBlock(parent) || ts.isSourceFile(parent)) {
|
|
157
|
+
for (const statement of parent.statements) {
|
|
158
|
+
if (statement === child || statement.getStart(sourceFile) >= child.getStart(sourceFile)) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (statement.getStart(sourceFile) > declaration.getStart(sourceFile) &&
|
|
162
|
+
definitelyStops(statement, name, sourceFile)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (parent === block) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
child = parent;
|
|
171
|
+
parent = parent.parent;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
/** The nearest enclosing block of a declaration — the scope its spinner must not outlive. */
|
|
176
|
+
function enclosingBlock(node) {
|
|
177
|
+
let current = node.parent;
|
|
178
|
+
while (current) {
|
|
179
|
+
if (ts.isBlock(current)) {
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
182
|
+
current = current.parent;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
/** True for `return spinner;` — the caller becomes responsible for stopping it. */
|
|
187
|
+
function handsOverOwnership(statement, name) {
|
|
188
|
+
return !!statement.expression && ts.isIdentifier(statement.expression) && statement.expression.text === name;
|
|
189
|
+
}
|
|
190
|
+
/** True for `x.succeed(…)` / `x.fail(…)` / … on the spinner called `name`. */
|
|
191
|
+
function isStopCall(expression, name, sourceFile) {
|
|
192
|
+
if (!ts.isCallExpression(expression) || !ts.isPropertyAccessExpression(expression.expression)) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const access = expression.expression;
|
|
196
|
+
return access.expression.getText(sourceFile) === name && STOP_METHODS.has(access.name.text);
|
|
197
|
+
}
|
|
198
|
+
/** Every `return` after `declaration` inside `block`, skipping nested functions. */
|
|
199
|
+
function returnsAfter(block, declaration) {
|
|
200
|
+
const found = [];
|
|
201
|
+
const start = declaration.getStart();
|
|
202
|
+
const visit = (node) => {
|
|
203
|
+
if (ts.isFunctionLike(node)) {
|
|
204
|
+
return; // A return there leaves the callback, not the command.
|
|
205
|
+
}
|
|
206
|
+
if (ts.isReturnStatement(node) && node.getStart() > start) {
|
|
207
|
+
found.push(node);
|
|
208
|
+
}
|
|
209
|
+
ts.forEachChild(node, visit);
|
|
210
|
+
};
|
|
211
|
+
for (const statement of block.statements) {
|
|
212
|
+
if (statement.getStart() > start) {
|
|
213
|
+
visit(statement);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return found;
|
|
217
|
+
}
|
|
218
|
+
/** `const x = spin(…)` declarations, including `toolbox.print.spin(…)`. */
|
|
219
|
+
function spinnerDeclarations(sourceFile) {
|
|
220
|
+
const found = [];
|
|
221
|
+
const visit = (node) => {
|
|
222
|
+
if (ts.isVariableDeclaration(node) &&
|
|
223
|
+
ts.isIdentifier(node.name) &&
|
|
224
|
+
node.initializer &&
|
|
225
|
+
ts.isCallExpression(node.initializer)) {
|
|
226
|
+
const callee = node.initializer.expression;
|
|
227
|
+
const name = ts.isPropertyAccessExpression(callee)
|
|
228
|
+
? callee.name.text
|
|
229
|
+
: ts.isIdentifier(callee)
|
|
230
|
+
? callee.text
|
|
231
|
+
: '';
|
|
232
|
+
if (name === 'spin') {
|
|
233
|
+
found.push(node);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
ts.forEachChild(node, visit);
|
|
237
|
+
};
|
|
238
|
+
visit(sourceFile);
|
|
239
|
+
return found;
|
|
240
|
+
}
|
|
241
|
+
/** The statements of `block` that follow `declaration`. */
|
|
242
|
+
function statementsAfter(block, declaration) {
|
|
243
|
+
const start = declaration.getStart();
|
|
244
|
+
return block.statements.filter((statement) => statement.getStart() > start);
|
|
245
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stripAugmentationBlocks = stripAugmentationBlocks;
|
|
4
4
|
exports.stripVendorSchemaAugmentation = stripVendorSchemaAugmentation;
|
|
5
|
+
const glob_1 = require("glob");
|
|
5
6
|
const fs_utils_1 = require("./fs-utils");
|
|
6
7
|
/**
|
|
7
8
|
* Drop `declare module '<nuxt|@nuxt>/schema' { … }` blocks that only augment
|
|
@@ -118,7 +119,6 @@ function stripAugmentationBlocks(source, onSkip) {
|
|
|
118
119
|
* is still there and only the transform knows it.
|
|
119
120
|
*/
|
|
120
121
|
function stripVendorSchemaAugmentation(options) {
|
|
121
|
-
var _a;
|
|
122
122
|
const { coreDir, filesystem } = options;
|
|
123
123
|
if (!filesystem.isDirectory(coreDir))
|
|
124
124
|
return { touched: [], warnings: [] };
|
|
@@ -128,7 +128,18 @@ function stripVendorSchemaAugmentation(options) {
|
|
|
128
128
|
return { touched: [], warnings: [] };
|
|
129
129
|
const touched = [];
|
|
130
130
|
const warnings = [];
|
|
131
|
-
|
|
131
|
+
// `glob`, not `filesystem.find`: gluegun's find is fs-jetpack, which builds its
|
|
132
|
+
// matcher by concatenating the resolved absolute base path in front of the glob
|
|
133
|
+
// (`fs-jetpack/lib/utils/matcher.js` → `convertPatternToAbsolutePath`). On Windows
|
|
134
|
+
// that base is backslash-separated and minimatch reads a backslash inside a
|
|
135
|
+
// PATTERN as an escape, so `D:\…\core/**/*.ts` collapses to `D:coresrc/**/*.ts`
|
|
136
|
+
// and matches nothing — minimatch normalises separators on the file side only.
|
|
137
|
+
// The strip then reported success while the TS2310 augmentation stayed in every
|
|
138
|
+
// Windows-converted project. `glob` keeps pattern and base directory apart and
|
|
139
|
+
// normalises separators itself. Same fix as `api-mode.ts`'s `globFiles`. glob does
|
|
140
|
+
// not follow symlinked directories, unlike jetpack — which is the direction the
|
|
141
|
+
// guard above already wants: nothing inside a linked checkout gets rewritten.
|
|
142
|
+
for (const file of (0, glob_1.globSync)('**/*.ts', { absolute: true, cwd: coreDir, dot: true, nodir: true })) {
|
|
132
143
|
const content = filesystem.read(file);
|
|
133
144
|
if (!content || !content.includes('PublicRuntimeConfig'))
|
|
134
145
|
continue;
|
|
@@ -33,6 +33,7 @@ exports.healVendorClaudeMd = healVendorClaudeMd;
|
|
|
33
33
|
exports.insertVendorBlockIfMissing = insertVendorBlockIfMissing;
|
|
34
34
|
exports.removeVendorBlock = removeVendorBlock;
|
|
35
35
|
exports.upsertVendorBlock = upsertVendorBlock;
|
|
36
|
+
const path_1 = require("path");
|
|
36
37
|
exports.BACKEND_VENDOR_MARKER = '<!-- lt-vendor-marker -->';
|
|
37
38
|
exports.FRONTEND_VENDOR_MARKER = '<!-- lt-vendor-marker-frontend -->';
|
|
38
39
|
exports.ROOT_VENDOR_MARKER = '<!-- lt-vendor-marker-root -->';
|
|
@@ -182,15 +183,20 @@ function healVendorClaudeMd(fs, state) {
|
|
|
182
183
|
changed.push(path);
|
|
183
184
|
}
|
|
184
185
|
};
|
|
186
|
+
// `join`, not string concatenation with `/`: the returned paths are reported
|
|
187
|
+
// to the user and compared by callers, so they have to be the same spelling
|
|
188
|
+
// the rest of the CLI produces. Concatenating `/` onto a Windows directory
|
|
189
|
+
// yields `C:\ws\projects\api/CLAUDE.md` — it opens fine, but it matches
|
|
190
|
+
// nothing and reads like a bug in the output.
|
|
185
191
|
if (state.apiDir) {
|
|
186
|
-
apply(
|
|
192
|
+
apply((0, path_1.join)(state.apiDir, 'CLAUDE.md'), exports.BACKEND_VENDOR_MARKER, state.backendVendor ? buildBackendVendorBlock() : null);
|
|
187
193
|
}
|
|
188
194
|
if (state.appDir) {
|
|
189
|
-
apply(
|
|
195
|
+
apply((0, path_1.join)(state.appDir, 'CLAUDE.md'), exports.FRONTEND_VENDOR_MARKER, state.frontendVendor ? buildFrontendVendorBlock() : null);
|
|
190
196
|
}
|
|
191
197
|
if (state.workspaceRoot) {
|
|
192
198
|
const anyVendor = state.backendVendor || state.frontendVendor;
|
|
193
|
-
apply(
|
|
199
|
+
apply((0, path_1.join)(state.workspaceRoot, 'CLAUDE.md'), exports.ROOT_VENDOR_MARKER, anyVendor ? buildRootVendorBlock({ backend: state.backendVendor, frontend: state.frontendVendor }) : null);
|
|
194
200
|
}
|
|
195
201
|
return changed;
|
|
196
202
|
}
|
|
@@ -242,6 +248,3 @@ function blockRegex(marker) {
|
|
|
242
248
|
function escapeRegExp(value) {
|
|
243
249
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
244
250
|
}
|
|
245
|
-
function joinPath(dir, file) {
|
|
246
|
-
return dir.endsWith('/') ? `${dir}${file}` : `${dir}/${file}`;
|
|
247
|
-
}
|
|
@@ -81,6 +81,7 @@ exports.runStandaloneWorkspaceGate = runStandaloneWorkspaceGate;
|
|
|
81
81
|
exports.shouldProceedAsStandalone = shouldProceedAsStandalone;
|
|
82
82
|
exports.writeApiConfig = writeApiConfig;
|
|
83
83
|
const yaml = __importStar(require("js-yaml"));
|
|
84
|
+
const path_1 = require("path");
|
|
84
85
|
const ensure_root_dockerignore_1 = require("./ensure-root-dockerignore");
|
|
85
86
|
const hoist_workspace_pnpm_config_1 = require("./hoist-workspace-pnpm-config");
|
|
86
87
|
const remove_nested_lockfiles_1 = require("./remove-nested-lockfiles");
|
|
@@ -104,11 +105,20 @@ function detectSubProjectContext(startDir, filesystem) {
|
|
|
104
105
|
const appDir = filesystem.path(root, 'projects', 'app');
|
|
105
106
|
// Resolve absolute prefixes for a clean startsWith compare. We use
|
|
106
107
|
// the gluegun-resolved paths because all callers go through it.
|
|
108
|
+
//
|
|
109
|
+
// The prefix MUST carry the OS separator, not a literal `/`:
|
|
110
|
+
// `filesystem.path` resolves through Node's `path`, so on Windows both
|
|
111
|
+
// sides come back with backslashes and an `${apiDir}/` prefix can never
|
|
112
|
+
// match. Every cwd below `projects\api\` then read as "not a
|
|
113
|
+
// sub-project", and the standalone gate in `runStandaloneWorkspaceGate`
|
|
114
|
+
// waved the user through instead of pointing them at the workspace root
|
|
115
|
+
// — the exact case (cloning a sibling tree INSIDE an existing
|
|
116
|
+
// sub-project) the check exists to prevent.
|
|
107
117
|
const startAbs = filesystem.path(startDir);
|
|
108
|
-
if (startAbs === apiDir || startAbs.startsWith(
|
|
118
|
+
if (startAbs === apiDir || startAbs.startsWith(apiDir + path_1.sep)) {
|
|
109
119
|
return { kind: 'api', subProjectDir: apiDir, workspaceRoot: root };
|
|
110
120
|
}
|
|
111
|
-
if (startAbs === appDir || startAbs.startsWith(
|
|
121
|
+
if (startAbs === appDir || startAbs.startsWith(appDir + path_1.sep)) {
|
|
112
122
|
return { kind: 'app', subProjectDir: appDir, workspaceRoot: root };
|
|
113
123
|
}
|
|
114
124
|
return null;
|