@lenne.tech/cli 1.10.0 → 1.11.1
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/README.md +5 -3
- package/build/commands/config/validate.js +2 -0
- package/build/commands/frontend/convert-mode.js +198 -0
- package/build/commands/fullstack/convert-mode.js +368 -0
- package/build/commands/fullstack/init.js +44 -2
- package/build/commands/fullstack/update.js +49 -1
- package/build/commands/server/convert-mode.js +197 -0
- package/build/commands/status.js +81 -2
- package/build/config/vendor-frontend-runtime-deps.json +4 -0
- package/build/extensions/frontend-helper.js +652 -0
- package/build/extensions/server.js +515 -68
- package/build/lib/frontend-framework-detection.js +129 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +972 -0
- package/docs/VENDOR-MODE-WORKFLOW.md +471 -0
- package/docs/commands.md +196 -0
- package/docs/lt.config.md +9 -7
- package/package.json +2 -1
- package/build/templates/vendor-scripts/check-vendor-freshness.mjs +0 -131
- package/build/templates/vendor-scripts/propose-upstream-pr.ts +0 -269
- package/build/templates/vendor-scripts/sync-from-upstream.ts +0 -250
|
@@ -965,13 +965,28 @@ class Server {
|
|
|
965
965
|
// Best-effort — if we can't read upstream pkg, the starter's own
|
|
966
966
|
// deps should still cover most of the framework's needs.
|
|
967
967
|
}
|
|
968
|
+
// Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
|
|
969
|
+
// The nest-server CLAUDE.md contains framework-specific instructions that
|
|
970
|
+
// Claude Code needs to work correctly with the vendored source (API conventions,
|
|
971
|
+
// UnifiedField usage, CrudService patterns, etc.). We capture it before the
|
|
972
|
+
// temp clone is deleted and merge it after the vendor-marker block.
|
|
973
|
+
let upstreamClaudeMd = '';
|
|
974
|
+
try {
|
|
975
|
+
const claudeMdContent = filesystem.read(`${tmpClone}/CLAUDE.md`);
|
|
976
|
+
if (typeof claudeMdContent === 'string') {
|
|
977
|
+
upstreamClaudeMd = claudeMdContent;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch (_b) {
|
|
981
|
+
// Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
|
|
982
|
+
}
|
|
968
983
|
// Snapshot the upstream commit SHA for traceability in VENDOR.md.
|
|
969
984
|
let upstreamCommit = '';
|
|
970
985
|
try {
|
|
971
986
|
const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
|
|
972
987
|
upstreamCommit = (sha || '').trim();
|
|
973
988
|
}
|
|
974
|
-
catch (
|
|
989
|
+
catch (_c) {
|
|
975
990
|
// Non-fatal — VENDOR.md will just show an empty SHA.
|
|
976
991
|
}
|
|
977
992
|
try {
|
|
@@ -1002,13 +1017,39 @@ class Server {
|
|
|
1002
1017
|
}
|
|
1003
1018
|
// Copy bin/migrate.js so the project has a working migrate CLI
|
|
1004
1019
|
// independent of node_modules/@lenne.tech/nest-server.
|
|
1020
|
+
// Overwrite existing file if present (convert-mode on existing project).
|
|
1005
1021
|
if (filesystem.exists(`${tmpClone}/bin/migrate.js`)) {
|
|
1022
|
+
if (filesystem.exists(`${dest}/bin/migrate.js`)) {
|
|
1023
|
+
filesystem.remove(`${dest}/bin/migrate.js`);
|
|
1024
|
+
}
|
|
1006
1025
|
filesystem.copy(`${tmpClone}/bin/migrate.js`, `${dest}/bin/migrate.js`);
|
|
1007
1026
|
}
|
|
1008
1027
|
// Copy migration-guides for vendor-sync agent reference (optional
|
|
1009
1028
|
// but useful — small overhead, big value for the updater agent).
|
|
1029
|
+
// Preserve any project-specific guides by merging instead of overwriting.
|
|
1010
1030
|
if (filesystem.exists(`${tmpClone}/migration-guides`)) {
|
|
1011
|
-
filesystem.
|
|
1031
|
+
if (filesystem.exists(`${dest}/migration-guides`)) {
|
|
1032
|
+
// Project already has migration-guides (maybe custom ones) — merge
|
|
1033
|
+
// upstream files into the existing directory without removing locals.
|
|
1034
|
+
const upstreamGuides = filesystem.find(`${tmpClone}/migration-guides`, {
|
|
1035
|
+
matching: '*.md',
|
|
1036
|
+
recursive: false,
|
|
1037
|
+
}) || [];
|
|
1038
|
+
for (const guide of upstreamGuides) {
|
|
1039
|
+
const basename = require('node:path').basename(guide);
|
|
1040
|
+
const source = `${tmpClone}/migration-guides/${basename}`;
|
|
1041
|
+
const target = `${dest}/migration-guides/${basename}`;
|
|
1042
|
+
if (filesystem.exists(target)) {
|
|
1043
|
+
filesystem.remove(target);
|
|
1044
|
+
}
|
|
1045
|
+
if (filesystem.exists(source)) {
|
|
1046
|
+
filesystem.copy(source, target);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
filesystem.copy(`${tmpClone}/migration-guides`, `${dest}/migration-guides`);
|
|
1052
|
+
}
|
|
1012
1053
|
}
|
|
1013
1054
|
}
|
|
1014
1055
|
finally {
|
|
@@ -1089,6 +1130,43 @@ class Server {
|
|
|
1089
1130
|
}
|
|
1090
1131
|
}
|
|
1091
1132
|
}
|
|
1133
|
+
// Edge: express exports Request/Response as TYPE-ONLY. In npm-mode this
|
|
1134
|
+
// was not a problem because the framework shipped as pre-compiled dist/
|
|
1135
|
+
// and type imports were erased before runtime. In vendor-mode, vitest/vite
|
|
1136
|
+
// evaluates the TypeScript source directly and chokes with:
|
|
1137
|
+
// "The requested module 'express' does not provide an export named 'Response'"
|
|
1138
|
+
// Fix: convert `import { Request, Response } from 'express'` (and variants
|
|
1139
|
+
// with NextFunction etc.) to type-only imports wherever they appear in the
|
|
1140
|
+
// vendored core. This is safe because the core code uses Request/Response
|
|
1141
|
+
// only as type annotations — any runtime `new Request(...)` calls refer
|
|
1142
|
+
// to the global Fetch API Request, not the express one.
|
|
1143
|
+
const expressImportRegex = /^import\s+\{([^}]*)\}\s+from\s+['"]express['"]\s*;?\s*$/gm;
|
|
1144
|
+
const vendoredTsFiles = filesystem.find(coreDir, {
|
|
1145
|
+
matching: '**/*.ts',
|
|
1146
|
+
recursive: true,
|
|
1147
|
+
}) || [];
|
|
1148
|
+
for (const filePath of vendoredTsFiles) {
|
|
1149
|
+
// filesystem.find returns paths relative to jetpack cwd — use absolute resolution
|
|
1150
|
+
const absPath = filePath.startsWith('/') ? filePath : require('node:path').resolve(filePath);
|
|
1151
|
+
if (!filesystem.exists(absPath))
|
|
1152
|
+
continue;
|
|
1153
|
+
const content = filesystem.read(absPath) || '';
|
|
1154
|
+
if (!content.includes("from 'express'") && !content.includes('from "express"'))
|
|
1155
|
+
continue;
|
|
1156
|
+
const patched = content.replace(expressImportRegex, (_match, names) => {
|
|
1157
|
+
const cleanNames = names
|
|
1158
|
+
.split(',')
|
|
1159
|
+
.map((n) => n.trim())
|
|
1160
|
+
.filter((n) => n.length > 0 && n !== 'type')
|
|
1161
|
+
// Strip any pre-existing 'type ' prefix on individual names
|
|
1162
|
+
.map((n) => n.replace(/^type\s+/, ''))
|
|
1163
|
+
.join(', ');
|
|
1164
|
+
return `import type { ${cleanNames} } from 'express';`;
|
|
1165
|
+
});
|
|
1166
|
+
if (patched !== content) {
|
|
1167
|
+
filesystem.write(absPath, patched);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1092
1170
|
// ── 4. Rewrite consumer imports: '@lenne.tech/nest-server' → relative ─
|
|
1093
1171
|
//
|
|
1094
1172
|
// Every .ts file in the starter's src/server/, src/main.ts, tests/,
|
|
@@ -1180,7 +1258,7 @@ class Server {
|
|
|
1180
1258
|
}
|
|
1181
1259
|
}
|
|
1182
1260
|
}
|
|
1183
|
-
catch (
|
|
1261
|
+
catch (_d) {
|
|
1184
1262
|
// skip unreadable file
|
|
1185
1263
|
}
|
|
1186
1264
|
}
|
|
@@ -1208,55 +1286,16 @@ class Server {
|
|
|
1208
1286
|
');',
|
|
1209
1287
|
'',
|
|
1210
1288
|
].join('\n'));
|
|
1211
|
-
// ── 4b.
|
|
1212
|
-
//
|
|
1213
|
-
// These scripts are consumed by the `nest-server-core-updater` and
|
|
1214
|
-
// `nest-server-core-contributor` Claude Code agents and by the
|
|
1215
|
-
// `check`/`check:fix`/`check:naf` pipelines below. They live at
|
|
1216
|
-
// `scripts/vendor/` in every vendored project:
|
|
1217
|
-
//
|
|
1218
|
-
// check-vendor-freshness.mjs — non-blocking warning that the
|
|
1219
|
-
// vendored core is behind the latest upstream release. Hooked
|
|
1220
|
-
// into `check` / `check:fix` / `check:naf`.
|
|
1221
|
-
//
|
|
1222
|
-
// sync-from-upstream.ts — produces a structured diff (upstream
|
|
1223
|
-
// delta vs. baseline, local changes, conflict map) that the
|
|
1224
|
-
// updater agent parses when pulling a new upstream version.
|
|
1289
|
+
// ── 4b. (removed) ─────────────────────────────────────────────────────
|
|
1225
1290
|
//
|
|
1226
|
-
//
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1231
|
-
// are copied into the new project's `scripts/vendor/`.
|
|
1232
|
-
const vendorScriptsSrc = path.join(__dirname, '..', 'templates', 'vendor-scripts');
|
|
1233
|
-
const vendorScriptsDest = `${dest}/scripts/vendor`;
|
|
1234
|
-
if (filesystem.exists(vendorScriptsSrc)) {
|
|
1235
|
-
if (!filesystem.exists(vendorScriptsDest)) {
|
|
1236
|
-
filesystem.dir(vendorScriptsDest);
|
|
1237
|
-
}
|
|
1238
|
-
const vendorScriptFiles = filesystem.find(vendorScriptsSrc, { matching: '*' });
|
|
1239
|
-
for (const src of vendorScriptFiles || []) {
|
|
1240
|
-
const base = path.basename(src);
|
|
1241
|
-
filesystem.copy(src, `${vendorScriptsDest}/${base}`, { overwrite: true });
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
// ── 4c. .gitignore: ignore transient vendor script outputs ──────────
|
|
1291
|
+
// Previous versions copied three maintenance scripts into
|
|
1292
|
+
// `scripts/vendor/` (check-vendor-freshness.mjs, sync-from-upstream.ts,
|
|
1293
|
+
// propose-upstream-pr.ts). The sync + propose scripts duplicated what
|
|
1294
|
+
// the lt-dev Claude Code agents (nest-server-core-updater and
|
|
1295
|
+
// nest-server-core-contributor) do natively — they were dead weight.
|
|
1245
1296
|
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
// scripts/vendor/upstream-candidates/. Both are throw-away analysis
|
|
1249
|
-
// artifacts and must not be committed.
|
|
1250
|
-
const gitignorePath = `${dest}/.gitignore`;
|
|
1251
|
-
if (filesystem.exists(gitignorePath)) {
|
|
1252
|
-
const raw = filesystem.read(gitignorePath) || '';
|
|
1253
|
-
const entries = ['scripts/vendor/sync-results/', 'scripts/vendor/upstream-candidates/'];
|
|
1254
|
-
const missing = entries.filter((e) => !raw.includes(e));
|
|
1255
|
-
if (missing.length > 0) {
|
|
1256
|
-
const block = `\n# Vendor-sync / upstream-contribute output (transient analysis artifacts)\n${missing.join('\n')}\n`;
|
|
1257
|
-
filesystem.write(gitignorePath, raw.trimEnd() + block);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1297
|
+
// The freshness check is now an inline one-liner in package.json
|
|
1298
|
+
// (see step 5 below). VENDOR.md is the sole vendor-specific file.
|
|
1260
1299
|
// ── 5. package.json: remove @lenne.tech/nest-server, add migrate/bin ─
|
|
1261
1300
|
//
|
|
1262
1301
|
// Delete the framework dep — it's no longer needed since src/core/
|
|
@@ -1333,10 +1372,8 @@ class Server {
|
|
|
1333
1372
|
// existing migrate:* scripts are already correct for npm mode; we
|
|
1334
1373
|
// need them pointing at the local bin + local ts-compiler.
|
|
1335
1374
|
//
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
// check into `check` / `check:fix` / `check:naf` as a non-blocking
|
|
1339
|
-
// first step.
|
|
1375
|
+
// Wire the inline vendor-freshness check and hook it into
|
|
1376
|
+
// `check` / `check:fix` / `check:naf` as a non-blocking first step.
|
|
1340
1377
|
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
1341
1378
|
const scripts = pkg.scripts;
|
|
1342
1379
|
const migrateArgs = '--store ./migrations-utils/migrate.js --migrations-dir ./migrations --compiler ts:./migrations-utils/ts-compiler.js';
|
|
@@ -1349,11 +1386,26 @@ class Server {
|
|
|
1349
1386
|
scripts['migrate:test:up'] = `NODE_ENV=test node ./bin/migrate.js up ${migrateArgs}`;
|
|
1350
1387
|
scripts['migrate:preview:up'] = `NODE_ENV=preview node ./bin/migrate.js up ${migrateArgs}`;
|
|
1351
1388
|
scripts['migrate:prod:up'] = `NODE_ENV=production node ./bin/migrate.js up ${migrateArgs}`;
|
|
1352
|
-
// Vendor
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1389
|
+
// Vendor freshness check: reads VENDOR.md baseline version and
|
|
1390
|
+
// compares against npm registry. Non-blocking (always exits 0).
|
|
1391
|
+
// Uses a short inline script — no external file needed.
|
|
1392
|
+
// The heredoc-style approach avoids quote-escaping nightmares.
|
|
1393
|
+
scripts['check:vendor-freshness'] = [
|
|
1394
|
+
'node -e "',
|
|
1395
|
+
"var f=require('fs'),h=require('https');",
|
|
1396
|
+
"try{var c=f.readFileSync('src/core/VENDOR.md','utf8')}catch(e){process.exit(0)}",
|
|
1397
|
+
'var m=c.match(/Baseline-Version[^0-9]*(\\d+\\.\\d+\\.\\d+)/);',
|
|
1398
|
+
'if(!m){process.stderr.write(String.fromCharCode(9888)+\' vendor-freshness: no baseline\\n\');process.exit(0)}',
|
|
1399
|
+
'var v=m[1];',
|
|
1400
|
+
"h.get('https://registry.npmjs.org/@lenne.tech/nest-server/latest',function(r){",
|
|
1401
|
+
"var d='';r.on('data',function(c){d+=c});r.on('end',function(){",
|
|
1402
|
+
"try{var l=JSON.parse(d).version;",
|
|
1403
|
+
"if(v===l)console.log('vendor core up-to-date (v'+v+')');",
|
|
1404
|
+
"else process.stderr.write('vendor core v'+v+', latest v'+l+'\\n')",
|
|
1405
|
+
'}catch(e){}})}).on(\'error\',function(){});',
|
|
1406
|
+
'setTimeout(function(){process.exit(0)},5000)',
|
|
1407
|
+
'"',
|
|
1408
|
+
].join('');
|
|
1357
1409
|
// Hook vendor-freshness as the first step of check / check:fix /
|
|
1358
1410
|
// check:naf. Non-blocking (exit 0 even on mismatch), so it just
|
|
1359
1411
|
// surfaces the warning at the top of the log.
|
|
@@ -1363,9 +1415,6 @@ class Server {
|
|
|
1363
1415
|
return;
|
|
1364
1416
|
if (existing.includes('check:vendor-freshness'))
|
|
1365
1417
|
return;
|
|
1366
|
-
// Inject the freshness check right after the initial `pnpm install`
|
|
1367
|
-
// or at the very beginning of the chain. Looks for a leading
|
|
1368
|
-
// `pnpm install && ` and inserts after it; otherwise prepends.
|
|
1369
1418
|
const installPrefix = 'pnpm install && ';
|
|
1370
1419
|
if (existing.startsWith(installPrefix)) {
|
|
1371
1420
|
scripts[scriptName] = `${installPrefix}pnpm run check:vendor-freshness && ${existing.slice(installPrefix.length)}`;
|
|
@@ -1423,9 +1472,8 @@ class Server {
|
|
|
1423
1472
|
//
|
|
1424
1473
|
// Replace it with a small informational stub that points the user at
|
|
1425
1474
|
// the canonical vendor-update path (the `nest-server-core-updater`
|
|
1426
|
-
// Claude Code agent
|
|
1427
|
-
//
|
|
1428
|
-
// message.
|
|
1475
|
+
// Claude Code agent). Keeps `pnpm run update` from dead-exiting with
|
|
1476
|
+
// a confusing message.
|
|
1429
1477
|
const syncPackagesPath = `${dest}/extras/sync-packages.mjs`;
|
|
1430
1478
|
if (filesystem.exists(syncPackagesPath)) {
|
|
1431
1479
|
filesystem.write(syncPackagesPath, [
|
|
@@ -1444,8 +1492,7 @@ class Server {
|
|
|
1444
1492
|
' * This project runs in VENDOR mode: the framework core/ tree is',
|
|
1445
1493
|
' * copied directly into src/core/ and there is no framework npm',
|
|
1446
1494
|
' * dep to sync. To update the vendored core, use the',
|
|
1447
|
-
' * `nest-server-core-updater` Claude Code agent
|
|
1448
|
-
' * project-local vendor:sync script once it has been added.',
|
|
1495
|
+
' * `nest-server-core-updater` Claude Code agent.',
|
|
1449
1496
|
' */',
|
|
1450
1497
|
'',
|
|
1451
1498
|
"console.warn('');",
|
|
@@ -1519,6 +1566,33 @@ class Server {
|
|
|
1519
1566
|
filesystem.write(apiClaudeMdPath, vendorBlock + existing);
|
|
1520
1567
|
}
|
|
1521
1568
|
}
|
|
1569
|
+
// ── 9c. Merge nest-server CLAUDE.md sections into project CLAUDE.md ──
|
|
1570
|
+
//
|
|
1571
|
+
// The nest-server CLAUDE.md contains framework-specific instructions for
|
|
1572
|
+
// Claude Code (API conventions, UnifiedField usage, CrudService patterns,
|
|
1573
|
+
// etc.). We merge its H2 sections into the project's CLAUDE.md so that
|
|
1574
|
+
// downstream agents (backend-dev, code-reviewer, nest-server-updater)
|
|
1575
|
+
// have accurate framework knowledge out of the box.
|
|
1576
|
+
//
|
|
1577
|
+
// Merge strategy (matches /lt-dev:fullstack:sync-claude-md):
|
|
1578
|
+
// - Section in upstream but NOT in project → ADD at end
|
|
1579
|
+
// - Section in BOTH → KEEP project version (may have customizations)
|
|
1580
|
+
// - Section only in project → KEEP (project-specific content)
|
|
1581
|
+
if (upstreamClaudeMd && filesystem.exists(apiClaudeMdPath)) {
|
|
1582
|
+
const projectContent = filesystem.read(apiClaudeMdPath) || '';
|
|
1583
|
+
const upstreamSections = this.parseH2Sections(upstreamClaudeMd);
|
|
1584
|
+
const projectSections = this.parseH2Sections(projectContent);
|
|
1585
|
+
const newSections = [];
|
|
1586
|
+
for (const [heading, body] of upstreamSections) {
|
|
1587
|
+
if (!projectSections.has(heading)) {
|
|
1588
|
+
newSections.push(`## ${heading}\n\n${body.trim()}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (newSections.length > 0) {
|
|
1592
|
+
const separator = projectContent.endsWith('\n') ? '\n' : '\n\n';
|
|
1593
|
+
filesystem.write(apiClaudeMdPath, `${projectContent}${separator}${newSections.join('\n\n')}\n`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1522
1596
|
// ── 10. VENDOR.md baseline ───────────────────────────────────────────
|
|
1523
1597
|
//
|
|
1524
1598
|
// Record the exact upstream version + commit SHA we vendored from, so
|
|
@@ -1529,7 +1603,7 @@ class Server {
|
|
|
1529
1603
|
const today = new Date().toISOString().slice(0, 10);
|
|
1530
1604
|
const versionLine = upstreamVersion
|
|
1531
1605
|
? `- **Baseline-Version:** ${upstreamVersion}`
|
|
1532
|
-
: '- **Baseline-Version:** (not detected — run
|
|
1606
|
+
: '- **Baseline-Version:** (not detected — run `/lt-dev:backend:update-nest-server-core` to record)';
|
|
1533
1607
|
const commitLine = upstreamCommit
|
|
1534
1608
|
? `- **Baseline-Commit:** \`${upstreamCommit}\``
|
|
1535
1609
|
: '- **Baseline-Commit:** (not detected)';
|
|
@@ -1580,6 +1654,23 @@ class Server {
|
|
|
1580
1654
|
'',
|
|
1581
1655
|
].join('\n'));
|
|
1582
1656
|
}
|
|
1657
|
+
// ── Post-conversion verification ──────────────────────────────────────
|
|
1658
|
+
//
|
|
1659
|
+
// Scan all consumer files for stale bare-specifier imports that the
|
|
1660
|
+
// codemod should have rewritten. A single miss causes a compile error,
|
|
1661
|
+
// so catching it here with a clear message saves the user debugging time.
|
|
1662
|
+
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server');
|
|
1663
|
+
if (staleImports.length > 0) {
|
|
1664
|
+
const { print } = this.toolbox;
|
|
1665
|
+
print.warning(`⚠ ${staleImports.length} file(s) still contain '@lenne.tech/nest-server' imports after vendor conversion:`);
|
|
1666
|
+
for (const f of staleImports.slice(0, 10)) {
|
|
1667
|
+
print.info(` ${f}`);
|
|
1668
|
+
}
|
|
1669
|
+
if (staleImports.length > 10) {
|
|
1670
|
+
print.info(` ... and ${staleImports.length - 10} more`);
|
|
1671
|
+
}
|
|
1672
|
+
print.info('These imports must be manually rewritten to relative paths pointing to src/core.');
|
|
1673
|
+
}
|
|
1583
1674
|
return { upstreamDeps, upstreamDevDeps };
|
|
1584
1675
|
});
|
|
1585
1676
|
}
|
|
@@ -1858,6 +1949,30 @@ class Server {
|
|
|
1858
1949
|
}
|
|
1859
1950
|
this.filesystem.write(claudeMdPath, content);
|
|
1860
1951
|
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Parse a markdown file into a Map of H2 sections.
|
|
1954
|
+
* Key = heading text (without `## `), Value = body text after the heading.
|
|
1955
|
+
* Content before the first H2 heading is stored under key `__preamble__`.
|
|
1956
|
+
*/
|
|
1957
|
+
parseH2Sections(content) {
|
|
1958
|
+
const sections = new Map();
|
|
1959
|
+
const lines = content.split('\n');
|
|
1960
|
+
let currentHeading = '__preamble__';
|
|
1961
|
+
let currentBody = [];
|
|
1962
|
+
for (const line of lines) {
|
|
1963
|
+
const match = /^## (.+)$/.exec(line);
|
|
1964
|
+
if (match) {
|
|
1965
|
+
sections.set(currentHeading, currentBody.join('\n'));
|
|
1966
|
+
currentHeading = match[1].trim();
|
|
1967
|
+
currentBody = [];
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
currentBody.push(line);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
sections.set(currentHeading, currentBody.join('\n'));
|
|
1974
|
+
return sections;
|
|
1975
|
+
}
|
|
1861
1976
|
/**
|
|
1862
1977
|
* Replace secret or private keys in string (e.g. for config files)
|
|
1863
1978
|
*/
|
|
@@ -1882,6 +1997,338 @@ class Server {
|
|
|
1882
1997
|
return `'${secretMap.get(placeholder)}'`;
|
|
1883
1998
|
});
|
|
1884
1999
|
}
|
|
2000
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2001
|
+
// Public mode-conversion API (called by `lt server convert-mode`)
|
|
2002
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2003
|
+
/**
|
|
2004
|
+
* Convert an existing npm-mode API project to vendor mode.
|
|
2005
|
+
*
|
|
2006
|
+
* This is a wrapper around {@link convertCloneToVendored} for use on
|
|
2007
|
+
* projects that were **already created** (not during `lt fullstack init`).
|
|
2008
|
+
* The method detects the currently installed `@lenne.tech/nest-server`
|
|
2009
|
+
* version and vendors from that tag unless `upstreamBranch` overrides it.
|
|
2010
|
+
*/
|
|
2011
|
+
convertToVendorMode(options) {
|
|
2012
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2013
|
+
const { dest, upstreamBranch, upstreamRepoUrl } = options;
|
|
2014
|
+
const { isVendoredProject } = require('../lib/framework-detection');
|
|
2015
|
+
if (isVendoredProject(dest)) {
|
|
2016
|
+
throw new Error('Project is already in vendor mode (src/core/VENDOR.md exists).');
|
|
2017
|
+
}
|
|
2018
|
+
// Verify @lenne.tech/nest-server is currently a dependency
|
|
2019
|
+
const pkg = this.filesystem.read(`${dest}/package.json`, 'json');
|
|
2020
|
+
if (!pkg) {
|
|
2021
|
+
throw new Error('Cannot read package.json');
|
|
2022
|
+
}
|
|
2023
|
+
const allDeps = Object.assign(Object.assign({}, (pkg.dependencies || {})), (pkg.devDependencies || {}));
|
|
2024
|
+
if (!allDeps['@lenne.tech/nest-server']) {
|
|
2025
|
+
throw new Error('@lenne.tech/nest-server is not in dependencies or devDependencies. ' +
|
|
2026
|
+
'Is this an npm-mode lenne.tech API project?');
|
|
2027
|
+
}
|
|
2028
|
+
yield this.convertCloneToVendored({
|
|
2029
|
+
dest,
|
|
2030
|
+
upstreamBranch,
|
|
2031
|
+
upstreamRepoUrl,
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Convert an existing vendor-mode API project back to npm mode.
|
|
2037
|
+
*
|
|
2038
|
+
* Performs the inverse of {@link convertCloneToVendored}:
|
|
2039
|
+
* 1. Read baseline version from VENDOR.md
|
|
2040
|
+
* 2. Delete `src/core/` (the vendored framework)
|
|
2041
|
+
* 3. Rewrite all consumer imports from relative paths back to `@lenne.tech/nest-server`
|
|
2042
|
+
* 4. Restore `@lenne.tech/nest-server` dependency in package.json
|
|
2043
|
+
* 5. Restore migrate scripts to npm paths
|
|
2044
|
+
* 6. Remove vendor-specific scripts and artifacts
|
|
2045
|
+
* 7. Clean up CLAUDE.md vendor marker
|
|
2046
|
+
* 8. Restore tsconfig to npm-mode defaults
|
|
2047
|
+
*/
|
|
2048
|
+
convertToNpmMode(options) {
|
|
2049
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2050
|
+
const { dest, targetVersion } = options;
|
|
2051
|
+
const path = require('path');
|
|
2052
|
+
const { Project, SyntaxKind } = require('ts-morph');
|
|
2053
|
+
const { isVendoredProject } = require('../lib/framework-detection');
|
|
2054
|
+
if (!isVendoredProject(dest)) {
|
|
2055
|
+
throw new Error('Project is not in vendor mode (src/core/VENDOR.md not found).');
|
|
2056
|
+
}
|
|
2057
|
+
const filesystem = this.filesystem;
|
|
2058
|
+
const srcDir = `${dest}/src`;
|
|
2059
|
+
const coreDir = `${srcDir}/core`;
|
|
2060
|
+
// ── 1. Determine target version + warn about local patches ──────────
|
|
2061
|
+
const vendorMd = filesystem.read(`${coreDir}/VENDOR.md`) || '';
|
|
2062
|
+
let version = targetVersion;
|
|
2063
|
+
if (!version) {
|
|
2064
|
+
const match = vendorMd.match(/Baseline-Version:\*{0,2}\s+(\d+\.\d+\.\d+\S*)/);
|
|
2065
|
+
if (match) {
|
|
2066
|
+
version = match[1];
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (!version) {
|
|
2070
|
+
throw new Error('Cannot determine target version. Specify --version or ensure VENDOR.md has a Baseline-Version.');
|
|
2071
|
+
}
|
|
2072
|
+
// Warn if VENDOR.md documents local patches that will be lost
|
|
2073
|
+
const localChangesSection = vendorMd.match(/## Local changes[\s\S]*?(?=## |$)/i);
|
|
2074
|
+
if (localChangesSection) {
|
|
2075
|
+
const hasRealPatches = localChangesSection[0].includes('|') &&
|
|
2076
|
+
!localChangesSection[0].includes('(none, pristine)') &&
|
|
2077
|
+
/\|\s*\d{4}-/.test(localChangesSection[0]);
|
|
2078
|
+
if (hasRealPatches) {
|
|
2079
|
+
const { print } = this.toolbox;
|
|
2080
|
+
print.warning('');
|
|
2081
|
+
print.warning('⚠ VENDOR.md documents local patches in src/core/ that will be LOST:');
|
|
2082
|
+
// Extract non-header table rows
|
|
2083
|
+
const rows = localChangesSection[0]
|
|
2084
|
+
.split('\n')
|
|
2085
|
+
.filter((l) => /^\|\s*\d{4}-/.test(l));
|
|
2086
|
+
for (const row of rows.slice(0, 5)) {
|
|
2087
|
+
print.info(` ${row.trim()}`);
|
|
2088
|
+
}
|
|
2089
|
+
if (rows.length > 5) {
|
|
2090
|
+
print.info(` ... and ${rows.length - 5} more`);
|
|
2091
|
+
}
|
|
2092
|
+
print.warning('Consider running /lt-dev:backend:contribute-nest-server-core first to upstream them.');
|
|
2093
|
+
print.warning('');
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
// ── 2. Rewrite consumer imports: relative → @lenne.tech/nest-server ─
|
|
2097
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
2098
|
+
const globs = [
|
|
2099
|
+
`${srcDir}/server/**/*.ts`,
|
|
2100
|
+
`${srcDir}/main.ts`,
|
|
2101
|
+
`${srcDir}/config.env.ts`,
|
|
2102
|
+
`${dest}/tests/**/*.ts`,
|
|
2103
|
+
`${dest}/migrations/**/*.ts`,
|
|
2104
|
+
`${dest}/migrations-utils/*.ts`,
|
|
2105
|
+
`${dest}/scripts/**/*.ts`,
|
|
2106
|
+
];
|
|
2107
|
+
for (const glob of globs) {
|
|
2108
|
+
project.addSourceFilesAtPaths(glob);
|
|
2109
|
+
}
|
|
2110
|
+
const targetSpecifier = '@lenne.tech/nest-server';
|
|
2111
|
+
const coreAbsPath = path.resolve(coreDir);
|
|
2112
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
2113
|
+
let modified = false;
|
|
2114
|
+
// Static imports + re-exports
|
|
2115
|
+
for (const decl of [
|
|
2116
|
+
...sourceFile.getImportDeclarations(),
|
|
2117
|
+
...sourceFile.getExportDeclarations(),
|
|
2118
|
+
]) {
|
|
2119
|
+
const spec = decl.getModuleSpecifierValue();
|
|
2120
|
+
if (!spec)
|
|
2121
|
+
continue;
|
|
2122
|
+
// Check if this import resolves to src/core (the vendored framework)
|
|
2123
|
+
if (this.isVendoredCoreImport(spec, sourceFile.getFilePath(), coreAbsPath)) {
|
|
2124
|
+
decl.setModuleSpecifier(targetSpecifier);
|
|
2125
|
+
modified = true;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
// Dynamic imports + CJS require
|
|
2129
|
+
sourceFile.forEachDescendant((node) => {
|
|
2130
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
2131
|
+
const expr = node.getExpression().getText();
|
|
2132
|
+
if (expr === 'require' || expr === 'import') {
|
|
2133
|
+
const args = node.getArguments();
|
|
2134
|
+
if (args.length > 0) {
|
|
2135
|
+
const argText = args[0].getText().replace(/['"]/g, '');
|
|
2136
|
+
if (this.isVendoredCoreImport(argText, sourceFile.getFilePath(), coreAbsPath)) {
|
|
2137
|
+
args[0].replaceWithText(`'${targetSpecifier}'`);
|
|
2138
|
+
modified = true;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
if (modified) {
|
|
2145
|
+
sourceFile.saveSync();
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
// Also rewrite .js files in migrations-utils/
|
|
2149
|
+
const jsFiles = filesystem.find(`${dest}/migrations-utils`, { matching: '*.js' }) || [];
|
|
2150
|
+
for (const jsFile of jsFiles) {
|
|
2151
|
+
const content = filesystem.read(jsFile) || '';
|
|
2152
|
+
// Replace any relative require/import that points to src/core
|
|
2153
|
+
const replaced = content.replace(/require\(['"]([^'"]*\/core(?:\/index)?)['"]\)/g, `require('${targetSpecifier}')`);
|
|
2154
|
+
if (replaced !== content) {
|
|
2155
|
+
filesystem.write(jsFile, replaced);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
// ── 3. Delete src/core/ (vendored framework) ────────────────────────
|
|
2159
|
+
filesystem.remove(coreDir);
|
|
2160
|
+
// ── 4. Restore @lenne.tech/nest-server dep + clean package.json ─────
|
|
2161
|
+
const pkg = filesystem.read(`${dest}/package.json`, 'json');
|
|
2162
|
+
if (pkg) {
|
|
2163
|
+
// Add @lenne.tech/nest-server as dependency
|
|
2164
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
2165
|
+
pkg.dependencies['@lenne.tech/nest-server'] = version;
|
|
2166
|
+
// Remove vendor-specific deps that are transitive via nest-server
|
|
2167
|
+
// (they'll come back via node_modules when nest-server is installed)
|
|
2168
|
+
// We only remove deps that are ALSO in nest-server's package.json.
|
|
2169
|
+
// For safety, we don't remove any dep the consumer might use directly.
|
|
2170
|
+
// Remove vendor-specific scripts
|
|
2171
|
+
const scripts = pkg.scripts || {};
|
|
2172
|
+
delete scripts['check:vendor-freshness'];
|
|
2173
|
+
delete scripts['vendor:sync'];
|
|
2174
|
+
delete scripts['vendor:propose-upstream'];
|
|
2175
|
+
// Unhook vendor-freshness from check / check:fix / check:naf
|
|
2176
|
+
for (const key of ['check', 'check:fix', 'check:naf']) {
|
|
2177
|
+
if (scripts[key] && typeof scripts[key] === 'string') {
|
|
2178
|
+
scripts[key] = scripts[key].replace(/pnpm run check:vendor-freshness && /g, '');
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
// Restore migrate scripts to npm-mode paths
|
|
2182
|
+
const migrateCompiler = 'ts:./node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/helpers/ts-compiler.js';
|
|
2183
|
+
const migrateStore = '--store ./migrations-utils/migrate.js --migrations-dir ./migrations';
|
|
2184
|
+
const migrateTemplate = './node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/templates/migration-project.template.ts';
|
|
2185
|
+
scripts['migrate:create'] = `f() { migrate create "$1" --template-file ${migrateTemplate} --migrations-dir ./migrations --compiler ${migrateCompiler}; }; f`;
|
|
2186
|
+
scripts['migrate:up'] = `migrate up ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2187
|
+
scripts['migrate:down'] = `migrate down ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2188
|
+
scripts['migrate:list'] = `migrate list ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2189
|
+
// Env-prefixed migrate scripts
|
|
2190
|
+
for (const env of ['develop', 'test', 'preview', 'prod']) {
|
|
2191
|
+
const nodeEnv = env === 'prod' ? 'production' : env;
|
|
2192
|
+
scripts[`migrate:${env}:up`] = `NODE_ENV=${nodeEnv} migrate up ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2193
|
+
}
|
|
2194
|
+
filesystem.write(`${dest}/package.json`, pkg);
|
|
2195
|
+
}
|
|
2196
|
+
// ── 5. Remove vendor artifacts ──────────────────────────────────────
|
|
2197
|
+
if (filesystem.exists(`${dest}/scripts/vendor`)) {
|
|
2198
|
+
filesystem.remove(`${dest}/scripts/vendor`);
|
|
2199
|
+
}
|
|
2200
|
+
if (filesystem.exists(`${dest}/bin/migrate.js`)) {
|
|
2201
|
+
filesystem.remove(`${dest}/bin/migrate.js`);
|
|
2202
|
+
// Remove bin/ dir if empty
|
|
2203
|
+
const binContents = filesystem.list(`${dest}/bin`) || [];
|
|
2204
|
+
if (binContents.length === 0) {
|
|
2205
|
+
filesystem.remove(`${dest}/bin`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (filesystem.exists(`${dest}/migration-guides`)) {
|
|
2209
|
+
filesystem.remove(`${dest}/migration-guides`);
|
|
2210
|
+
}
|
|
2211
|
+
// Remove migrations-utils/ts-compiler.js (vendor-mode bootstrap)
|
|
2212
|
+
const tsCompilerPath = `${dest}/migrations-utils/ts-compiler.js`;
|
|
2213
|
+
if (filesystem.exists(tsCompilerPath)) {
|
|
2214
|
+
const content = filesystem.read(tsCompilerPath) || '';
|
|
2215
|
+
// Only remove if it's the vendor-mode bootstrap (contains ts-node reference)
|
|
2216
|
+
if (content.includes('ts-node') || content.includes('tsconfig-paths')) {
|
|
2217
|
+
filesystem.remove(tsCompilerPath);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
// Restore extras/sync-packages.mjs if it was replaced with a stub
|
|
2221
|
+
const syncPkgPath = `${dest}/extras/sync-packages.mjs`;
|
|
2222
|
+
if (filesystem.exists(syncPkgPath)) {
|
|
2223
|
+
const content = filesystem.read(syncPkgPath) || '';
|
|
2224
|
+
if (content.includes('vendor mode') || content.includes('vendor:sync')) {
|
|
2225
|
+
// It's the vendor stub — remove it. The user can re-fetch from starter.
|
|
2226
|
+
filesystem.remove(syncPkgPath);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// ── 6. Clean CLAUDE.md vendor marker ────────────────────────────────
|
|
2230
|
+
const claudeMdPath = `${dest}/CLAUDE.md`;
|
|
2231
|
+
if (filesystem.exists(claudeMdPath)) {
|
|
2232
|
+
let content = filesystem.read(claudeMdPath) || '';
|
|
2233
|
+
const marker = '<!-- lt-vendor-marker -->';
|
|
2234
|
+
if (content.includes(marker)) {
|
|
2235
|
+
// Remove everything from marker to the first `---` separator (end of vendor block)
|
|
2236
|
+
content = content.replace(new RegExp(`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?---\\s*\\n?`, ''), '');
|
|
2237
|
+
// Remove leading whitespace/newlines
|
|
2238
|
+
content = content.replace(/^\n+/, '');
|
|
2239
|
+
filesystem.write(claudeMdPath, content);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
// ── 7. Restore tsconfig excludes ────────────────────────────────────
|
|
2243
|
+
// tsconfig files use JSONC (comments + trailing commas), so we cannot
|
|
2244
|
+
// use `filesystem.read(path, 'json')` which calls strict `JSON.parse`.
|
|
2245
|
+
// Instead we do a regex-based removal of vendor-specific exclude entries.
|
|
2246
|
+
for (const tsconfigName of ['tsconfig.json', 'tsconfig.build.json']) {
|
|
2247
|
+
const tsconfigPath = `${dest}/${tsconfigName}`;
|
|
2248
|
+
if (!filesystem.exists(tsconfigPath))
|
|
2249
|
+
continue;
|
|
2250
|
+
let raw = filesystem.read(tsconfigPath) || '';
|
|
2251
|
+
// Remove vendor-specific exclude entries (the string literal + optional trailing comma)
|
|
2252
|
+
raw = raw.replace(/,?\s*"src\/core\/modules\/migrate\/templates\/\*\*\/\*\.template\.ts"/g, '');
|
|
2253
|
+
raw = raw.replace(/,?\s*"src\/core\/test\/\*\*\/\*\.ts"/g, '');
|
|
2254
|
+
// Clean up potential double commas or trailing commas before ]
|
|
2255
|
+
raw = raw.replace(/,\s*,/g, ',');
|
|
2256
|
+
raw = raw.replace(/,\s*]/g, ']');
|
|
2257
|
+
filesystem.write(tsconfigPath, raw);
|
|
2258
|
+
}
|
|
2259
|
+
// ── 8. Remove .gitignore vendor entries ──────────────────────────────
|
|
2260
|
+
const gitignorePath = `${dest}/.gitignore`;
|
|
2261
|
+
if (filesystem.exists(gitignorePath)) {
|
|
2262
|
+
let content = filesystem.read(gitignorePath) || '';
|
|
2263
|
+
content = content
|
|
2264
|
+
.split('\n')
|
|
2265
|
+
.filter((line) => !line.includes('scripts/vendor/sync-results') &&
|
|
2266
|
+
!line.includes('scripts/vendor/upstream-candidates'))
|
|
2267
|
+
.join('\n');
|
|
2268
|
+
filesystem.write(gitignorePath, content);
|
|
2269
|
+
}
|
|
2270
|
+
// ── Post-conversion verification ──────────────────────────────────────
|
|
2271
|
+
//
|
|
2272
|
+
// Scan all consumer files for stale relative imports that still resolve
|
|
2273
|
+
// to the (now deleted) src/core/ directory. These would be silent
|
|
2274
|
+
// compile errors.
|
|
2275
|
+
const staleRelativeImports = this.findStaleImports(dest, '../core', /['"]\.\.?\/[^'"]*core['"]|from\s+['"]\.\.?\/[^'"]*core['"]/);
|
|
2276
|
+
if (staleRelativeImports.length > 0) {
|
|
2277
|
+
const { print } = this.toolbox;
|
|
2278
|
+
print.warning(`⚠ ${staleRelativeImports.length} file(s) still contain relative core imports after npm conversion:`);
|
|
2279
|
+
for (const f of staleRelativeImports.slice(0, 10)) {
|
|
2280
|
+
print.info(` ${f}`);
|
|
2281
|
+
}
|
|
2282
|
+
print.info('These imports must be manually rewritten to \'@lenne.tech/nest-server\'.');
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Scans consumer source files for import specifiers that should have been
|
|
2288
|
+
* rewritten by a mode conversion. Returns a list of file paths that still
|
|
2289
|
+
* contain matches.
|
|
2290
|
+
*
|
|
2291
|
+
* @param dest Project root directory
|
|
2292
|
+
* @param needle Literal string to search for (used when no regex provided)
|
|
2293
|
+
* @param pattern Optional regex for more flexible matching
|
|
2294
|
+
*/
|
|
2295
|
+
findStaleImports(dest, needle, pattern) {
|
|
2296
|
+
const globs = [
|
|
2297
|
+
`${dest}/src/server/**/*.ts`,
|
|
2298
|
+
`${dest}/src/main.ts`,
|
|
2299
|
+
`${dest}/src/config.env.ts`,
|
|
2300
|
+
`${dest}/tests/**/*.ts`,
|
|
2301
|
+
`${dest}/migrations/**/*.ts`,
|
|
2302
|
+
`${dest}/scripts/**/*.ts`,
|
|
2303
|
+
];
|
|
2304
|
+
const stale = [];
|
|
2305
|
+
for (const glob of globs) {
|
|
2306
|
+
const files = this.filesystem.find(dest, {
|
|
2307
|
+
matching: glob.replace(`${dest}/`, ''),
|
|
2308
|
+
recursive: true,
|
|
2309
|
+
}) || [];
|
|
2310
|
+
for (const file of files) {
|
|
2311
|
+
const content = this.filesystem.read(file) || '';
|
|
2312
|
+
if (pattern ? pattern.test(content) : content.includes(needle)) {
|
|
2313
|
+
stale.push(file.replace(`${dest}/`, ''));
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return stale;
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Checks whether an import specifier resolves to the vendored core directory.
|
|
2321
|
+
* Used during vendor→npm import rewriting.
|
|
2322
|
+
*/
|
|
2323
|
+
isVendoredCoreImport(specifier, fromFilePath, coreAbsPath) {
|
|
2324
|
+
if (!specifier.startsWith('.'))
|
|
2325
|
+
return false;
|
|
2326
|
+
const path = require('path');
|
|
2327
|
+
const resolved = path.resolve(path.dirname(fromFilePath), specifier);
|
|
2328
|
+
// Match if the resolved path IS the core dir or is inside it
|
|
2329
|
+
// (e.g., '../../../core' resolves to src/core, '../../../core/index' resolves to src/core/index)
|
|
2330
|
+
return resolved === coreAbsPath || resolved.startsWith(coreAbsPath + path.sep);
|
|
2331
|
+
}
|
|
1885
2332
|
}
|
|
1886
2333
|
exports.Server = Server;
|
|
1887
2334
|
/**
|