@lenne.tech/cli 1.41.0 → 1.41.2

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.
@@ -12,6 +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 strip_comments_1 = require("../lib/strip-comments");
15
16
  const vendor_claude_md_1 = require("../lib/vendor-claude-md");
16
17
  /**
17
18
  * Frontend helper functions for project scaffolding
@@ -813,7 +814,11 @@ class FrontendHelper {
813
814
  for (const absFile of allFiles) {
814
815
  if (skipPathContaining && absFile.includes(skipPathContaining))
815
816
  continue;
816
- const content = filesystem.read(absFile) || '';
817
+ // Strip comments first a docblock that DOCUMENTS the conversion legitimately quotes the
818
+ // very import syntax this looks for, and would otherwise be reported as a file the user has
819
+ // to fix by hand. Same false-positive class that hit the backend detector on
820
+ // nest-server-starter's bootstrap-diagnostics.spec.ts.
821
+ const content = (0, strip_comments_1.stripComments)(filesystem.read(absFile) || '');
817
822
  const matches = typeof needle === 'string' ? content.includes(needle) : needle.test(content);
818
823
  if (matches) {
819
824
  stale.push(absFile.replace(`${appDir}/`, ''));
@@ -48,6 +48,7 @@ const path_1 = require("path");
48
48
  const ts = __importStar(require("typescript"));
49
49
  const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
50
50
  const markdown_table_1 = require("../lib/markdown-table");
51
+ const strip_comments_1 = require("../lib/strip-comments");
51
52
  const vendor_claude_md_1 = require("../lib/vendor-claude-md");
52
53
  /**
53
54
  * Server helper functions
@@ -2494,7 +2495,14 @@ class Server {
2494
2495
  recursive: true,
2495
2496
  }) || [];
2496
2497
  for (const file of files) {
2497
- const content = this.filesystem.read(file) || '';
2498
+ const raw = this.filesystem.read(file) || '';
2499
+ // Strip comments before matching. The keyword-anchored pattern is not enough on its own:
2500
+ // a docblock that DOCUMENTS the conversion legitimately quotes the very syntax it looks
2501
+ // for — nest-server-starter's `tests/unit/bootstrap-diagnostics.spec.ts` contains
2502
+ // "rewrites `from '@lenne.tech/nest-server'` to a relative `./core` path", which matched
2503
+ // and told the user to rewrite imports that file does not have. A detector that reads
2504
+ // comments as code produces false alarms on exactly the files that explain it best.
2505
+ const content = (0, strip_comments_1.stripComments)(raw);
2498
2506
  if (pattern ? pattern.test(content) : content.includes(needle)) {
2499
2507
  stale.push(file.replace(`${dest}/`, ''));
2500
2508
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.addToGitignore = addToGitignore;
4
4
  exports.autoPatch = autoPatch;
5
+ exports.canonicaliseBridgeSpan = canonicaliseBridgeSpan;
5
6
  exports.patchApiConfig = patchApiConfig;
6
7
  exports.patchClaudeMd = patchClaudeMd;
7
8
  exports.patchNuxtConfig = patchNuxtConfig;
@@ -13,10 +14,33 @@ exports.patchPlaywrightConfig = patchPlaywrightConfig;
13
14
  * defaults and make it env-aware so it can be served behind Caddy
14
15
  * under `https://<slug>.localhost`.
15
16
  *
16
- * Each patch is a regex-based replace that matches only the legacy
17
- * form. Already-patched files are no-ops.
17
+ * Most patches are regex-based replaces that match only the legacy
18
+ * form; the marker-bracketed `lt-dev:bridge` block in
19
+ * playwright.config.ts is located by its markers instead. Already-patched
20
+ * files are no-ops — including when the consumer's own formatter has since
21
+ * restyled an injected block: that block is compared SEMANTICALLY (see
22
+ * `BRIDGE_VERSION` / `normaliseBridgeBlock`), not byte-for-byte.
18
23
  */
19
24
  const fs_1 = require("fs");
25
+ /**
26
+ * Version of the `lt-dev:bridge` block emitted into a consumer's
27
+ * `playwright.config.ts`. It travels IN the markers
28
+ * (`// >>> lt-dev:bridge v2 >>>`), which is what makes a genuine upgrade
29
+ * detectable independently of formatting.
30
+ *
31
+ * **Bump this on every change to `bridgeBlock`** — including a
32
+ * formatting-only one. The content comparison deliberately ignores quote
33
+ * style and whitespace (the consumer's formatter owns that file), so
34
+ * without a bump a cosmetic fix would never reach already-patched
35
+ * projects. That is exactly what happened to 1.32.1's `"utf8"` → `'utf8'`
36
+ * fix, which is why the version exists.
37
+ */
38
+ const BRIDGE_VERSION = 2;
39
+ /** Matches any bridge marker, versioned or not — v1 shipped without one. */
40
+ const BRIDGE_START_RE = /\/\/ >>> lt-dev:bridge(?: v(\d+))? >>>/;
41
+ const BRIDGE_END_RE = /\/\/ <<< lt-dev:bridge(?: v(\d+))? <<</;
42
+ /** Imports the bridge block owns; a stray copy outside it must be dropped. */
43
+ const BRIDGE_IMPORT_RE = /^\s*import \{[^}]*__ltDev(?:Exists|Read|Dirname|Resolve)[^}]*\} from '[^']*';?\s*$/gm;
20
44
  /** Append entry to .gitignore if not already present. */
21
45
  function addToGitignore(root, entry) {
22
46
  const path = `${root}/.gitignore`;
@@ -40,6 +64,29 @@ function autoPatch(file) {
40
64
  return patchPlaywrightConfig(file);
41
65
  return { file, patched: false, replacements: 0 };
42
66
  }
67
+ /**
68
+ * Replace the `lt-dev:bridge` span inside a whole file with its canonical
69
+ * comparison form, leaving every other byte untouched.
70
+ *
71
+ * For callers that must decide "is this file exactly what `autoPatch` would
72
+ * produce, or did a developer edit it?" — most importantly
73
+ * `dev-ticket.ts#isPristineLtDevPatch`, which gates whether `lt ticket stop`
74
+ * may discard a dirty config. Since the patcher deliberately tolerates the
75
+ * consumer formatter restyling the block, such a comparison MUST tolerate it
76
+ * too; otherwise a formatter-touched config reads as real developer work and
77
+ * the worktree removal is refused.
78
+ *
79
+ * Everything outside the markers stays byte-exact on purpose — being lenient
80
+ * there could let genuine work be silently discarded.
81
+ */
82
+ function canonicaliseBridgeSpan(content) {
83
+ const start = BRIDGE_START_RE.exec(content);
84
+ const end = BRIDGE_END_RE.exec(content);
85
+ if (!start || !end || end.index <= start.index)
86
+ return content;
87
+ const endsAt = end.index + end[0].length;
88
+ return (content.slice(0, start.index) + normaliseBridgeBlock(content.slice(start.index, endsAt)) + content.slice(endsAt));
89
+ }
43
90
  /**
44
91
  * API: make the server listen port honour `process.env.PORT` (injected by
45
92
  * `lt dev up` for its Caddy upstream). Handles two patterns found in
@@ -164,8 +211,12 @@ function patchNuxtConfig(file) {
164
211
  * any env.
165
212
  *
166
213
  * Patches applied (each idempotent):
167
- * 1. Top-of-file: `if (existsSync('.lt-dev/.env')) loadEnv(...)` block,
168
- * bracketed by `// >>> lt-dev:bridge >>>` markers.
214
+ * 1. Top-of-file: a dotenv loader that searches UP from cwd for
215
+ * `.lt-dev/.env`, bracketed by `// >>> lt-dev:bridge vN >>>` markers.
216
+ * Re-injected when the marker's version differs from `BRIDGE_VERSION`
217
+ * (a genuine upgrade) or when the block's CODE differs semantically
218
+ * (tampering / corruption) — but NOT when the consumer's formatter
219
+ * merely restyled it, which owns this file and must stay free to.
169
220
  * 2. Hardcoded baseURL/host/url for `http://localhost:3001` →
170
221
  * `process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`.
171
222
  * 3. `webServer` wrapped in an `LT_DEV_ACTIVE` guard so Playwright reuses
@@ -180,6 +231,7 @@ function patchNuxtConfig(file) {
180
231
  * 6. `slowMo: 10` → `0` (pointless per-action delay, multiplied across shards).
181
232
  */
182
233
  function patchPlaywrightConfig(file) {
234
+ var _a;
183
235
  if (!(0, fs_1.existsSync)(file))
184
236
  return { file, patched: false, replacements: 0 };
185
237
  const before = (0, fs_1.readFileSync)(file, 'utf8');
@@ -198,8 +250,8 @@ function patchPlaywrightConfig(file) {
198
250
  // a direct `playwright test` run) usually sit in `projects/app`. The
199
251
  // original cwd-only resolve missed it, so direct runs fell back to
200
252
  // `localhost:3001` and could collide with a parallel project.
201
- const bridgeStart = '// >>> lt-dev:bridge >>>';
202
- const bridgeEnd = '// <<< lt-dev:bridge <<<';
253
+ const bridgeStart = `// >>> lt-dev:bridge v${BRIDGE_VERSION} >>>`;
254
+ const bridgeEnd = `// <<< lt-dev:bridge v${BRIDGE_VERSION} <<<`;
203
255
  const bridgeBlock = [
204
256
  bridgeStart,
205
257
  '// Auto-load <root>/.lt-dev/.env when `lt dev up` is active so',
@@ -228,16 +280,47 @@ function patchPlaywrightConfig(file) {
228
280
  '}',
229
281
  bridgeEnd,
230
282
  ].join('\n');
231
- const bridgeStartIdx = after.indexOf(bridgeStart);
232
- const bridgeEndIdx = after.indexOf(bridgeEnd);
233
- if (bridgeStartIdx === -1) {
283
+ // Locate ANY bridge marker, versioned or not — v1 shipped unversioned and
284
+ // must still be recognised (and upgraded) rather than double-injected.
285
+ const startMatch = BRIDGE_START_RE.exec(after);
286
+ const endMatch = BRIDGE_END_RE.exec(after);
287
+ const bridgeStartIdx = startMatch ? startMatch.index : -1;
288
+ const bridgeEndIdx = endMatch ? endMatch.index : -1;
289
+ // A start marker without a well-ordered end marker is a corrupted block: the
290
+ // slice arithmetic below would duplicate everything between the two (and the
291
+ // user's code with it). `patchClaudeMd` already guards its span this way.
292
+ const spanIsSane = bridgeStartIdx !== -1 && bridgeEndIdx > bridgeStartIdx;
293
+ if (bridgeStartIdx === -1 || !spanIsSane) {
294
+ // Markers present but unusable (e.g. reversed). Strip the STRAY MARKERS
295
+ // ONLY — never the text between them: with the markers out of order that
296
+ // text is the user's own code, not our block, so slicing the span out
297
+ // would delete their work (and slicing it in would duplicate it).
298
+ if (!spanIsSane)
299
+ after = after.replace(BRIDGE_START_RE, '').replace(BRIDGE_END_RE, '');
300
+ // A formatter with organize-imports may have hoisted the block's imports
301
+ // out of the markers; leaving them behind would duplicate the `__ltDev*`
302
+ // bindings and break the consumer's config with a duplicate-identifier
303
+ // error. They are ours to own, so it is safe to drop them.
304
+ after = after.replace(BRIDGE_IMPORT_RE, '').replace(/^\n+/, '');
234
305
  after = `${bridgeBlock}\n${after}`;
235
306
  count++;
236
307
  }
237
- else if (bridgeEndIdx !== -1) {
238
- const rebuilt = after.slice(0, bridgeStartIdx) + bridgeBlock + after.slice(bridgeEndIdx + bridgeEnd.length);
239
- if (rebuilt !== after) {
240
- after = rebuilt;
308
+ else {
309
+ const existing = after.slice(bridgeStartIdx, bridgeEndIdx + endMatch[0].length);
310
+ // Two independent reasons to re-inject:
311
+ // (a) VERSION — the marker carries the block's version, so a genuine
312
+ // upgrade is detected regardless of how the consumer formatted it.
313
+ // This is what keeps formatting-only fixes shippable (see
314
+ // BRIDGE_VERSION); an unversioned v1 marker yields `undefined` here.
315
+ // (b) CODE — the block's code differs semantically, i.e. it was tampered
316
+ // with or corrupted. Cosmetic reformatting by the consumer's own
317
+ // formatter is deliberately NOT a reason: rewriting it back on every
318
+ // run is what used to leave playwright.config.ts permanently dirty in
319
+ // the working tree, with formatter and patcher flipping it forever.
320
+ const existingVersion = Number((_a = startMatch[1]) !== null && _a !== void 0 ? _a : 0);
321
+ if (existingVersion !== BRIDGE_VERSION || normaliseBridgeBlock(existing) !== normaliseBridgeBlock(bridgeBlock)) {
322
+ const head = after.slice(0, bridgeStartIdx).replace(BRIDGE_IMPORT_RE, '');
323
+ after = head + bridgeBlock + after.slice(bridgeEndIdx + endMatch[0].length);
241
324
  count++;
242
325
  }
243
326
  }
@@ -307,3 +390,30 @@ function patchPlaywrightConfig(file) {
307
390
  (0, fs_1.writeFileSync)(file, after, 'utf8');
308
391
  return { file, patched: true, replacements: count };
309
392
  }
393
+ /**
394
+ * Canonical form of a bridge block for comparison purposes.
395
+ *
396
+ * Compares only the CODE: comment lines are dropped entirely, so rewording
397
+ * a comment never triggers a rewrite — and, crucially, code that a reflow
398
+ * folded behind a `//` VANISHES from the comparison and is therefore
399
+ * detected as changed. A plain `\s+ → ' '` collapse would erase newlines
400
+ * instead, which makes a fully commented-out (inert) loader normalise
401
+ * identically to a live one: the block would silently never load
402
+ * `.lt-dev/.env` again and Playwright would fall back to `localhost:3001`.
403
+ *
404
+ * Quote style and intra-line whitespace are normalised away because the
405
+ * consumer's formatter owns this file (which direction it flips is
406
+ * project-specific — do not assume). Blind spot to keep in mind: a change
407
+ * that differs ONLY in quotes or whitespace is invisible here, which is
408
+ * what `BRIDGE_VERSION` is for.
409
+ */
410
+ function normaliseBridgeBlock(s) {
411
+ return s
412
+ .split(/\r?\n/)
413
+ .map((l) => l.trim())
414
+ .filter((l) => l !== '' && !l.startsWith('//'))
415
+ .join(' ')
416
+ .replace(/['"]/g, '"')
417
+ .replace(/\s+/g, ' ')
418
+ .trim();
419
+ }
@@ -824,7 +824,17 @@ function isPristineLtDevPatch(worktreePath, relPath) {
824
824
  try {
825
825
  (0, fs_1.writeFileSync)(tmp, head, 'utf8');
826
826
  (0, dev_patches_1.autoPatch)(tmp);
827
- return (0, fs_1.readFileSync)(tmp, 'utf8') === current;
827
+ const derived = (0, fs_1.readFileSync)(tmp, 'utf8');
828
+ if (derived === current)
829
+ return true;
830
+ // The patcher deliberately lets the consumer's formatter restyle the
831
+ // injected `lt-dev:bridge` block (quote style, wrapping) without rewriting
832
+ // it — so a byte-exact comparison would classify a merely reformatted
833
+ // playwright.config.ts as real developer work and make `lt ticket stop`
834
+ // refuse to remove the worktree. Compare the bridge span in its canonical
835
+ // form; everything OUTSIDE the markers stays byte-exact, so genuine edits
836
+ // are still never auto-discarded.
837
+ return (0, dev_patches_1.canonicaliseBridgeSpan)(derived) === (0, dev_patches_1.canonicaliseBridgeSpan)(current);
828
838
  }
829
839
  catch (_c) {
830
840
  return false;
@@ -0,0 +1,78 @@
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.stripComments = stripComments;
37
+ const ts = __importStar(require("typescript"));
38
+ /**
39
+ * Removes comments from TypeScript/JavaScript source, preserving everything else verbatim.
40
+ *
41
+ * Exists because detectors that search source for import specifiers must not read comments as
42
+ * code. A keyword-anchored regex is not sufficient on its own: a docblock that DOCUMENTS an import
43
+ * rewrite legitimately quotes the exact syntax the detector looks for. nest-server-starter's
44
+ * `tests/unit/bootstrap-diagnostics.spec.ts` contains
45
+ *
46
+ * * CLI's vendor conversion rewrites `from '@lenne.tech/nest-server'` to a relative `./core` path
47
+ *
48
+ * which matched `/(?:from|import|…)\s*['"]@lenne\.tech\/nest-server['"]/` and made
49
+ * `lt fullstack init --framework-mode vendor` warn about imports that file does not have.
50
+ *
51
+ * Uses the TypeScript scanner rather than a regex, so string literals, template literals and
52
+ * regex literals containing `//` or `/*` are handled correctly by construction — a hand-rolled
53
+ * stripper trips over `'https://…'` and over `/* ` inside a string.
54
+ *
55
+ * Comment characters are replaced with spaces instead of being deleted, so byte offsets and line
56
+ * numbers of the surrounding code stay unchanged — a caller can still report a meaningful
57
+ * position from a match.
58
+ *
59
+ * @param source - TypeScript or JavaScript source text
60
+ * @returns The source with every comment blanked out
61
+ *
62
+ * @example
63
+ * stripComments("// from 'pkg'\nimport x from 'pkg';")
64
+ * // => " \nimport x from 'pkg';"
65
+ */
66
+ function stripComments(source) {
67
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, /* skipTrivia */ false, ts.LanguageVariant.Standard, source);
68
+ let result = '';
69
+ let token = scanner.scan();
70
+ while (token !== ts.SyntaxKind.EndOfFileToken) {
71
+ const text = scanner.getTokenText();
72
+ const isComment = token === ts.SyntaxKind.SingleLineCommentTrivia || token === ts.SyntaxKind.MultiLineCommentTrivia;
73
+ // Keep newlines so line numbers survive; blank everything else in the comment.
74
+ result += isComment ? text.replace(/[^\n]/g, ' ') : text;
75
+ token = scanner.scan();
76
+ }
77
+ return result;
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.41.0",
3
+ "version": "1.41.2",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -46,14 +46,10 @@
46
46
  "start:build": "npm run build && node bin/lt --compiled-build",
47
47
  "start:compiled": "node bin/lt --compiled-build",
48
48
  "test": "jest --testTimeout=60000",
49
- "watch": "jest --watch",
50
- "release": "standard-version && git push --follow-tags origin main",
51
- "release:minor": "standard-version --release-as minor && git push --follow-tags origin main",
52
- "release:major": "standard-version --release-as major && git push --follow-tags origin main"
49
+ "watch": "jest --watch"
53
50
  },
54
51
  "files": [
55
52
  "tsconfig.json",
56
- "tslint.json",
57
53
  "build",
58
54
  "LICENSE",
59
55
  "README.md",
@@ -96,14 +92,19 @@
96
92
  "jest": "30.4.2",
97
93
  "prettier": "3.8.3",
98
94
  "rimraf": "6.1.3",
99
- "standard-version": "9.5.0",
100
95
  "ts-jest": "29.4.11"
101
96
  },
102
97
  "//overrides": {
103
- "semver@*": "Force latest semver 7.x across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep."
98
+ "semver@*": "Force latest semver 7.x across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep.",
99
+ "brace-expansion@<1.1.16": "DoS via exponential-time expansion of consecutive non-expanding {} groups (GHSA-3jxr-9vmj-r5cp, high). Transitive via dotgitignore/eslint/fs-jetpack/glob/test-exclude > minimatch. One bounded key per affected major so each can only raise a vulnerable version, never cap a patched one - remove once minimatch requests the patched ranges.",
100
+ "brace-expansion@>=2.0.0 <2.1.2": "Same advisory, 2.x line.",
101
+ "brace-expansion@>=5.0.0 <5.0.7": "Same advisory, 5.x line. Floored at >=5.0.0 so a future 3.x/4.x dependency is not silently forced across two majors."
104
102
  },
105
103
  "overrides": {
106
- "semver@*": "7.8.5"
104
+ "semver@*": "7.8.5",
105
+ "brace-expansion@<1.1.16": "1.1.16",
106
+ "brace-expansion@>=2.0.0 <2.1.2": "2.1.2",
107
+ "brace-expansion@>=5.0.0 <5.0.7": "5.0.7"
107
108
  },
108
109
  "jest": {
109
110
  "testEnvironment": "node",