@lenne.tech/cli 1.41.1 → 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.
- package/build/lib/dev-patches.js +123 -13
- package/build/lib/dev-ticket.js +11 -1
- package/package.json +2 -7
package/build/lib/dev-patches.js
CHANGED
|
@@ -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
|
-
*
|
|
17
|
-
* form
|
|
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:
|
|
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 =
|
|
202
|
-
const bridgeEnd =
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
package/build/lib/dev-ticket.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.41.
|
|
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,7 +92,6 @@
|
|
|
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": {
|