@lenne.tech/cli 1.11.1 → 1.12.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/claude/shortcuts.js +5 -0
- package/build/commands/fullstack/init.js +21 -0
- package/build/commands/fullstack/update.js +1 -4
- package/build/commands/git/reset.js +2 -2
- package/build/commands/git/update.js +3 -3
- package/build/commands/server/add-property.js +1 -3
- package/build/commands/server/module.js +1 -1
- package/build/config/vendor-runtime-deps.json +1 -5
- package/build/extensions/api-mode.js +123 -5
- package/build/extensions/frontend-helper.js +59 -32
- package/build/extensions/git.js +4 -5
- package/build/extensions/server.js +80 -42
- package/build/lib/frontend-framework-detection.js +1 -2
- package/build/lib/hoist-workspace-pnpm-config.js +97 -0
- package/build/lib/markdown-table.js +33 -0
- package/docs/VENDOR-MODE-WORKFLOW.md +73 -0
- package/docs/commands.md +1 -1
- package/package.json +17 -9
|
@@ -30,6 +30,11 @@ const CLAUDE_SHORTCUTS = [
|
|
|
30
30
|
command: 'claude --dangerously-skip-permissions --resume',
|
|
31
31
|
description: 'Select and resume previous session',
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
alias: 'cf',
|
|
35
|
+
command: 'LT_PLUGIN_HOOKS_SKIP=1 claude --dangerously-skip-permissions',
|
|
36
|
+
description: 'Start Claude Code in fast mode (skip lenne.tech plugin detect hooks)',
|
|
37
|
+
},
|
|
33
38
|
];
|
|
34
39
|
/**
|
|
35
40
|
* Install Claude Code shell shortcuts
|
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
12
13
|
/**
|
|
13
14
|
* Create a new fullstack workspace
|
|
14
15
|
*/
|
|
@@ -456,6 +457,14 @@ const NewCommand = {
|
|
|
456
457
|
else {
|
|
457
458
|
serverSpinner.warn('Nest Server Starter not integrated');
|
|
458
459
|
}
|
|
460
|
+
// Hoist workspace-scoped pnpm config out of sub-projects. pnpm only
|
|
461
|
+
// honors `pnpm.overrides`, `pnpm.onlyBuiltDependencies`, and
|
|
462
|
+
// `pnpm.ignoredOptionalDependencies` at the workspace root; leaving
|
|
463
|
+
// them in projects/api/package.json or projects/app/package.json
|
|
464
|
+
// causes `WARN The field … was found in … This will not take
|
|
465
|
+
// effect. You should configure … at the root of the workspace
|
|
466
|
+
// instead.` and silently disables CVE overrides.
|
|
467
|
+
(0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({ filesystem, projectDir, subProjects: ['projects/api', 'projects/app'] });
|
|
459
468
|
// Install all packages
|
|
460
469
|
const installSpinner = spin('Install all packages');
|
|
461
470
|
try {
|
|
@@ -467,6 +476,18 @@ const NewCommand = {
|
|
|
467
476
|
installSpinner.fail(`Failed to install packages: ${err.message}`);
|
|
468
477
|
return;
|
|
469
478
|
}
|
|
479
|
+
// Post-install format pass. processApiMode (run earlier in
|
|
480
|
+
// setupServerForFullstack) and convertAppCloneToVendored rewrite
|
|
481
|
+
// source files, leaving whitespace artifacts that oxfmt flags in
|
|
482
|
+
// `pnpm run format:check` (multi-line arrays/imports after region
|
|
483
|
+
// stripping, import-path rewrites that now fit single-line). The
|
|
484
|
+
// formatter is only available after install, so we normalize here.
|
|
485
|
+
if (apiMode && filesystem.isDirectory(`${projectDir}/projects/api`)) {
|
|
486
|
+
yield toolbox.apiMode.formatProject(`${projectDir}/projects/api`);
|
|
487
|
+
}
|
|
488
|
+
if (isNuxt && filesystem.isDirectory(`${projectDir}/projects/app`)) {
|
|
489
|
+
yield toolbox.apiMode.formatProject(`${projectDir}/projects/app`);
|
|
490
|
+
}
|
|
470
491
|
// Create initial commit after everything is set up
|
|
471
492
|
try {
|
|
472
493
|
yield system.run(`cd ${projectDir} && git add . && git commit -m "Initial commit"`);
|
|
@@ -126,10 +126,7 @@ const NewCommand = {
|
|
|
126
126
|
info(colors.dim('─'.repeat(60)));
|
|
127
127
|
info('');
|
|
128
128
|
// Detect frontend project
|
|
129
|
-
const appCandidates = [
|
|
130
|
-
(0, path_1.join)(cwd, 'projects', 'app'),
|
|
131
|
-
(0, path_1.join)(cwd, 'packages', 'app'),
|
|
132
|
-
].filter((p) => Boolean(p));
|
|
129
|
+
const appCandidates = [(0, path_1.join)(cwd, 'projects', 'app'), (0, path_1.join)(cwd, 'packages', 'app')].filter((p) => Boolean(p));
|
|
133
130
|
let appDir;
|
|
134
131
|
for (const candidate of appCandidates) {
|
|
135
132
|
if (filesystem.exists((0, path_1.join)(candidate, 'nuxt.config.ts')) || filesystem.exists((0, path_1.join)(candidate, 'package.json'))) {
|
|
@@ -45,8 +45,8 @@ const NewCommand = {
|
|
|
45
45
|
error('No current branch!');
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
// Check remote
|
|
49
|
-
const remoteBranch = yield system.run(`git ls-remote --heads origin ${branch}`);
|
|
48
|
+
// Check remote (use short SSH timeout so ls-remote doesn't hang in offline environments)
|
|
49
|
+
const remoteBranch = yield system.run(`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git ls-remote --heads origin ${branch} 2>/dev/null || true`);
|
|
50
50
|
if (!remoteBranch) {
|
|
51
51
|
error(`No remote branch ${branch} found!`);
|
|
52
52
|
return;
|
|
@@ -47,8 +47,8 @@ const NewCommand = {
|
|
|
47
47
|
info('');
|
|
48
48
|
info(`Current branch: ${branch}`);
|
|
49
49
|
info('');
|
|
50
|
-
// Fetch to see incoming changes
|
|
51
|
-
yield run('git fetch');
|
|
50
|
+
// Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline)
|
|
51
|
+
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true');
|
|
52
52
|
// Check for incoming commits
|
|
53
53
|
const incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline 2>/dev/null || echo ""`);
|
|
54
54
|
const commits = (incomingCommits === null || incomingCommits === void 0 ? void 0 : incomingCommits.trim().split('\n').filter((c) => c)) || [];
|
|
@@ -78,7 +78,7 @@ const NewCommand = {
|
|
|
78
78
|
const timer = startTimer();
|
|
79
79
|
// Update
|
|
80
80
|
const updateSpin = spin(`Update branch ${branch}`);
|
|
81
|
-
yield run('git fetch && git pull --rebase');
|
|
81
|
+
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true && GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git pull --rebase');
|
|
82
82
|
updateSpin.succeed();
|
|
83
83
|
// Install packages (unless skipped) with correctly detected package manager (supports monorepo lockfiles)
|
|
84
84
|
if (!skipInstall) {
|
|
@@ -434,9 +434,7 @@ const NewCommand = {
|
|
|
434
434
|
// path to src/core whose depth depends on the model file location.
|
|
435
435
|
// We search for BOTH forms so this works regardless of how the file
|
|
436
436
|
// was originally generated.
|
|
437
|
-
const vendoredSpec = (0, framework_detection_1.isVendoredProject)(path)
|
|
438
|
-
? (0, framework_detection_1.getFrameworkImportSpecifier)(path, modelPath)
|
|
439
|
-
: null;
|
|
437
|
+
const vendoredSpec = (0, framework_detection_1.isVendoredProject)(path) ? (0, framework_detection_1.getFrameworkImportSpecifier)(path, modelPath) : null;
|
|
440
438
|
let existingImports = moduleFile.getImportDeclaration('@lenne.tech/nest-server');
|
|
441
439
|
if (!existingImports && vendoredSpec) {
|
|
442
440
|
existingImports = moduleFile.getImportDeclaration(vendoredSpec);
|
|
@@ -410,7 +410,7 @@ const NewCommand = {
|
|
|
410
410
|
// plus the `from '@nestjs/common'` clause. The replacement wedges
|
|
411
411
|
// `, forwardRef` in between.
|
|
412
412
|
yield patching.patch(serverModule, {
|
|
413
|
-
insert:
|
|
413
|
+
insert: '$1, forwardRef$2',
|
|
414
414
|
replace: /(import\s*\{\s*[^}]*?)(\s*\}\s*from\s+['"]@nestjs\/common['"])/,
|
|
415
415
|
});
|
|
416
416
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./vendor-runtime-deps.schema.json",
|
|
3
3
|
"description": "Upstream @lenne.tech/nest-server devDependencies that are actually needed at runtime in a consumer project. When vendoring, these entries are promoted from upstream devDependencies into the project's dependencies so production runtime has them available.",
|
|
4
|
-
"runtimeHelpers": [
|
|
5
|
-
"find-file-up"
|
|
6
|
-
]
|
|
4
|
+
"runtimeHelpers": ["find-file-up"]
|
|
7
5
|
}
|
|
8
|
-
</content>
|
|
9
|
-
</invoke>
|
|
@@ -55,6 +55,50 @@ class ApiMode {
|
|
|
55
55
|
this.filesystem.remove((0, path_1.join)(projectPath, 'scripts', 'strip-api-mode-markers.mjs'));
|
|
56
56
|
// Remove strip-markers script from package.json
|
|
57
57
|
this.removeScriptFromPackageJson(projectPath, 'strip-markers');
|
|
58
|
+
// NOTE: auto-format of the stripped files happens separately in
|
|
59
|
+
// `formatProject()`, which MUST be called by the caller AFTER
|
|
60
|
+
// `pnpm install`. At this point the project's formatter (oxfmt) is
|
|
61
|
+
// not yet on disk, so running it here would silently no-op.
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run the project's `format` (or `format:fix`) npm script, if it exists.
|
|
66
|
+
* Call this AFTER the project's dependencies have been installed —
|
|
67
|
+
* otherwise the formatter (e.g. oxfmt) isn't available yet and the
|
|
68
|
+
* pass silently no-ops.
|
|
69
|
+
*
|
|
70
|
+
* Used after region stripping to normalize whitespace artifacts the
|
|
71
|
+
* formatter would otherwise flag in `format:check` (e.g. collapsing
|
|
72
|
+
* `providers: [\n X,\n]` to `providers: [X]` once graphql items were
|
|
73
|
+
* removed). Failures are non-fatal so a misbehaving formatter never
|
|
74
|
+
* blocks init.
|
|
75
|
+
*/
|
|
76
|
+
formatProject(projectPath) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
var _a, _b, _c;
|
|
79
|
+
const pkgPath = (0, path_1.join)(projectPath, 'package.json');
|
|
80
|
+
const pkgRaw = this.filesystem.read(pkgPath);
|
|
81
|
+
if (!pkgRaw)
|
|
82
|
+
return;
|
|
83
|
+
let pkg;
|
|
84
|
+
try {
|
|
85
|
+
pkg = JSON.parse(pkgRaw);
|
|
86
|
+
}
|
|
87
|
+
catch (_d) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const scripts = (_a = pkg.scripts) !== null && _a !== void 0 ? _a : {};
|
|
91
|
+
const formatScript = scripts.format ? 'format' : scripts['format:fix'] ? 'format:fix' : null;
|
|
92
|
+
if (!formatScript)
|
|
93
|
+
return;
|
|
94
|
+
const { pm, system } = this.toolbox;
|
|
95
|
+
const runner = (_c = (_b = pm === null || pm === void 0 ? void 0 : pm.run) === null || _b === void 0 ? void 0 : _b.call(pm, formatScript, pm.detect(projectPath))) !== null && _c !== void 0 ? _c : `pnpm run ${formatScript}`;
|
|
96
|
+
try {
|
|
97
|
+
yield system.run(`cd "${projectPath}" && ${runner}`);
|
|
98
|
+
}
|
|
99
|
+
catch (_e) {
|
|
100
|
+
// Non-fatal: the user can run format manually if this misbehaves.
|
|
101
|
+
}
|
|
58
102
|
});
|
|
59
103
|
}
|
|
60
104
|
/**
|
|
@@ -143,13 +187,77 @@ class ApiMode {
|
|
|
143
187
|
if (!content) {
|
|
144
188
|
continue;
|
|
145
189
|
}
|
|
146
|
-
|
|
190
|
+
// Special-case config.env.ts in REST mode: simply deleting the
|
|
191
|
+
// `graphQl: { … }` property is not enough — `CoreModule.forRoot`
|
|
192
|
+
// treats `graphQl === undefined` as enabled and tries to build
|
|
193
|
+
// the GraphQL schema anyway (which then fails on core models
|
|
194
|
+
// like CoreHealthCheckResult that reference the JSON scalar).
|
|
195
|
+
// Replace each stripped `// #region graphql … // #endregion
|
|
196
|
+
// graphql` block with an explicit `graphQl: false,` so GraphQL
|
|
197
|
+
// is cleanly disabled.
|
|
198
|
+
let processed;
|
|
199
|
+
if (removeMarker === 'graphql' && file.endsWith('/config.env.ts')) {
|
|
200
|
+
processed = this.replaceGraphqlRegionsWithDisabled(content, keepMarker);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
processed = this.processFileRegions(content, removeMarker, keepMarker);
|
|
204
|
+
}
|
|
147
205
|
if (processed !== content) {
|
|
148
206
|
this.filesystem.write(file, processed);
|
|
149
207
|
}
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Like `processFileRegions` with removeMarker='graphql', but a
|
|
213
|
+
* stripped region that contains a `graphQl:` property assignment is
|
|
214
|
+
* replaced with `graphQl: false,` (at the region's indent) instead of
|
|
215
|
+
* being deleted outright. Other graphql-regions (e.g. wrapping
|
|
216
|
+
* `execAfterInit: 'pnpm run docs:bootstrap'`) are deleted as usual.
|
|
217
|
+
*
|
|
218
|
+
* Rationale: `CoreModule.forRoot` treats `options.graphQl === undefined`
|
|
219
|
+
* as enabled, so dropping the property silently keeps GraphQL active
|
|
220
|
+
* and later crashes when the schema references scalars that were
|
|
221
|
+
* purged in REST mode.
|
|
222
|
+
*
|
|
223
|
+
* Preserves keepMarker behaviour (strip marker lines only, keep content).
|
|
224
|
+
*/
|
|
225
|
+
replaceGraphqlRegionsWithDisabled(content, keepMarker) {
|
|
226
|
+
const lines = content.split('\n');
|
|
227
|
+
const result = [];
|
|
228
|
+
let inRegion = false;
|
|
229
|
+
let regionIndent = '';
|
|
230
|
+
let regionBuffer = [];
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
if (trimmed === '// #region graphql') {
|
|
234
|
+
inRegion = true;
|
|
235
|
+
regionIndent = line.slice(0, line.indexOf('//'));
|
|
236
|
+
regionBuffer = [];
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (trimmed === '// #endregion graphql') {
|
|
240
|
+
inRegion = false;
|
|
241
|
+
// Only emit a replacement if the stripped block actually
|
|
242
|
+
// contained a `graphQl:` assignment.
|
|
243
|
+
if (regionBuffer.some((l) => /\bgraphQl\s*:/.test(l))) {
|
|
244
|
+
result.push(`${regionIndent}graphQl: false,`);
|
|
245
|
+
}
|
|
246
|
+
regionBuffer = [];
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inRegion) {
|
|
250
|
+
regionBuffer.push(line);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Keep-marker lines (e.g. `// #region rest`) are dropped; content between them stays.
|
|
254
|
+
if (trimmed === `// #region ${keepMarker}` || trimmed === `// #endregion ${keepMarker}`) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
result.push(line);
|
|
258
|
+
}
|
|
259
|
+
return result.join('\n');
|
|
260
|
+
}
|
|
153
261
|
/**
|
|
154
262
|
* Process region markers in file content
|
|
155
263
|
*/
|
|
@@ -261,8 +369,20 @@ class ApiMode {
|
|
|
261
369
|
}
|
|
262
370
|
catch (_b) {
|
|
263
371
|
// If ts-morph is not available or fails, fall back to regex
|
|
264
|
-
this.modifyConfigEnvForRestFallback(projectPath);
|
|
265
372
|
}
|
|
373
|
+
// Safety net: always run the regex fallback too. ts-morph only
|
|
374
|
+
// traverses direct ObjectLiteralExpression properties, so configs
|
|
375
|
+
// that wrap env-blocks in helper functions (e.g. `local:
|
|
376
|
+
// localConfig(...)`) are skipped silently. The regex is idempotent
|
|
377
|
+
// — if ts-morph already replaced `graphQl: {...}` with
|
|
378
|
+
// `graphQl: false` it's a no-op, but if ts-morph missed a wrapped
|
|
379
|
+
// occurrence the regex catches it. Without this, REST-mode
|
|
380
|
+
// projects built from a starter that lacks explicit
|
|
381
|
+
// `// #region graphql` markers would end up with `graphQl:
|
|
382
|
+
// undefined`, which `CoreModule.forRoot` treats as ENABLED, and
|
|
383
|
+
// the GraphQL schema build crashes on core models that still
|
|
384
|
+
// reference the JSON scalar.
|
|
385
|
+
this.modifyConfigEnvForRestFallback(projectPath);
|
|
266
386
|
});
|
|
267
387
|
}
|
|
268
388
|
/**
|
|
@@ -389,9 +509,7 @@ class ApiMode {
|
|
|
389
509
|
importLineSet.add(j);
|
|
390
510
|
}
|
|
391
511
|
}
|
|
392
|
-
const codeContent = lines
|
|
393
|
-
.map((line, idx) => (importLineSet.has(idx) ? '' : line))
|
|
394
|
-
.join('\n');
|
|
512
|
+
const codeContent = lines.map((line, idx) => (importLineSet.has(idx) ? '' : line)).join('\n');
|
|
395
513
|
// Check each import
|
|
396
514
|
const linesToRemove = new Set();
|
|
397
515
|
for (const imp of importLines) {
|
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.FrontendHelper = void 0;
|
|
13
|
+
const markdown_table_1 = require("../lib/markdown-table");
|
|
13
14
|
/**
|
|
14
15
|
* Frontend helper functions for project scaffolding
|
|
15
16
|
* Provides reusable methods for setting up Nuxt and Angular frontends
|
|
@@ -273,9 +274,7 @@ class FrontendHelper {
|
|
|
273
274
|
const { print } = this.toolbox;
|
|
274
275
|
print.warning('');
|
|
275
276
|
print.warning('VENDOR.md documents local patches in app/core/ that will be LOST:');
|
|
276
|
-
const rows = localChangesSection[0]
|
|
277
|
-
.split('\n')
|
|
278
|
-
.filter((l) => /^\|\s*\d{4}-/.test(l));
|
|
277
|
+
const rows = localChangesSection[0].split('\n').filter((l) => /^\|\s*\d{4}-/.test(l));
|
|
279
278
|
for (const row of rows.slice(0, 5)) {
|
|
280
279
|
print.info(` ${row.trim()}`);
|
|
281
280
|
}
|
|
@@ -307,8 +306,7 @@ class FrontendHelper {
|
|
|
307
306
|
// Unhook freshness from check/check:fix/check:naf
|
|
308
307
|
for (const scriptName of ['check', 'check:fix', 'check:naf']) {
|
|
309
308
|
if ((_a = scripts[scriptName]) === null || _a === void 0 ? void 0 : _a.includes('check:vendor-freshness')) {
|
|
310
|
-
scripts[scriptName] = scripts[scriptName]
|
|
311
|
-
.replace(/pnpm run check:vendor-freshness && /g, '');
|
|
309
|
+
scripts[scriptName] = scripts[scriptName].replace(/pnpm run check:vendor-freshness && /g, '');
|
|
312
310
|
}
|
|
313
311
|
}
|
|
314
312
|
}
|
|
@@ -354,7 +352,7 @@ class FrontendHelper {
|
|
|
354
352
|
*/
|
|
355
353
|
convertAppCloneToVendored(options) {
|
|
356
354
|
return __awaiter(this, void 0, void 0, function* () {
|
|
357
|
-
const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nuxt-extensions.git'
|
|
355
|
+
const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nuxt-extensions.git' } = options;
|
|
358
356
|
const path = require('node:path');
|
|
359
357
|
const { filesystem, system } = this.toolbox;
|
|
360
358
|
const coreDir = path.join(dest, 'app', 'core');
|
|
@@ -501,14 +499,14 @@ class FrontendHelper {
|
|
|
501
499
|
"var f=require('fs'),h=require('https');",
|
|
502
500
|
"try{var c=f.readFileSync('app/core/VENDOR.md','utf8')}catch(e){process.exit(0)}",
|
|
503
501
|
'var m=c.match(/Baseline-Version[^0-9]*(\\d+\\.\\d+\\.\\d+)/);',
|
|
504
|
-
|
|
502
|
+
"if(!m){process.stderr.write(String.fromCharCode(9888)+' vendor-freshness: no baseline\\n');process.exit(0)}",
|
|
505
503
|
'var v=m[1];',
|
|
506
504
|
"h.get('https://registry.npmjs.org/@lenne.tech/nuxt-extensions/latest',function(r){",
|
|
507
505
|
"var d='';r.on('data',function(c){d+=c});r.on('end',function(){",
|
|
508
|
-
|
|
506
|
+
'try{var l=JSON.parse(d).version;',
|
|
509
507
|
"if(v===l)console.log('vendor core up-to-date (v'+v+')');",
|
|
510
508
|
"else process.stderr.write('vendor core v'+v+', latest v'+l+'\\n')",
|
|
511
|
-
|
|
509
|
+
"}catch(e){}})}).on('error',function(){});",
|
|
512
510
|
'setTimeout(function(){process.exit(0)},5000)',
|
|
513
511
|
'"',
|
|
514
512
|
].join('');
|
|
@@ -525,7 +523,20 @@ class FrontendHelper {
|
|
|
525
523
|
hookFreshness('check:fix');
|
|
526
524
|
hookFreshness('check:naf');
|
|
527
525
|
}
|
|
528
|
-
|
|
526
|
+
// Sort dependency maps alphabetically so merged-in entries
|
|
527
|
+
// (e.g. upstream `@nuxt/kit`) end up in the expected position
|
|
528
|
+
// and the generated package.json passes oxfmt/format:check.
|
|
529
|
+
const sortObjectKeys = (obj) => {
|
|
530
|
+
if (!obj)
|
|
531
|
+
return obj;
|
|
532
|
+
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
533
|
+
};
|
|
534
|
+
pkg.dependencies = sortObjectKeys(pkg.dependencies);
|
|
535
|
+
pkg.devDependencies = sortObjectKeys(pkg.devDependencies);
|
|
536
|
+
pkg.peerDependencies = sortObjectKeys(pkg.peerDependencies);
|
|
537
|
+
// Ensure trailing newline — oxfmt with the starter's .editorconfig
|
|
538
|
+
// `insert_final_newline = true` requires it.
|
|
539
|
+
filesystem.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
529
540
|
}
|
|
530
541
|
}
|
|
531
542
|
// ── 6. CLAUDE.md: prepend vendor marker + merge upstream sections ────
|
|
@@ -544,8 +555,8 @@ class FrontendHelper {
|
|
|
544
555
|
'project code. There is **no** `@lenne.tech/nuxt-extensions` npm dependency.',
|
|
545
556
|
'',
|
|
546
557
|
'- **Read framework code from `app/core/**`** — not from `node_modules/`.',
|
|
547
|
-
|
|
548
|
-
|
|
558
|
+
"- **nuxt.config.ts** references `'./app/core/module'` instead of",
|
|
559
|
+
" `'@lenne.tech/nuxt-extensions'`.",
|
|
549
560
|
'- **Baseline + patch log** live in `app/core/VENDOR.md`. Log any',
|
|
550
561
|
' substantial local change there so the `nuxt-extensions-core-updater`',
|
|
551
562
|
' agent can classify it at sync time.',
|
|
@@ -595,13 +606,41 @@ class FrontendHelper {
|
|
|
595
606
|
'# @lenne.tech/nuxt-extensions (vendored)',
|
|
596
607
|
'',
|
|
597
608
|
'This directory is a curated vendor copy of `@lenne.tech/nuxt-extensions`.',
|
|
598
|
-
'It is first-class project code, not a node_modules shadow copy
|
|
599
|
-
'
|
|
600
|
-
'
|
|
609
|
+
'It is first-class project code, not a node_modules shadow copy — but it',
|
|
610
|
+
'is **not a fork**. The copy exists so Claude Code (and humans) can read',
|
|
611
|
+
'framework internals directly. Log substantial local changes in the',
|
|
612
|
+
'"Local changes" table below so the `nuxt-extensions-core-updater` agent',
|
|
613
|
+
'can classify them at sync time.',
|
|
601
614
|
'',
|
|
602
615
|
'Unlike the backend (nest-server) vendoring, no flatten-fix is needed —',
|
|
603
616
|
'the nuxt-extensions source structure is already flat.',
|
|
604
617
|
'',
|
|
618
|
+
'## Modification Policy',
|
|
619
|
+
'',
|
|
620
|
+
'Edit `app/core/` **only** when the change is generally useful to every',
|
|
621
|
+
'@lenne.tech/nuxt-extensions consumer:',
|
|
622
|
+
'',
|
|
623
|
+
'- Bugfixes that apply to every consumer',
|
|
624
|
+
'- Broad framework enhancements (new composables, better defaults, SSR fixes)',
|
|
625
|
+
'- Security vulnerability fixes',
|
|
626
|
+
'- Type/config compatibility fixes every consumer would hit',
|
|
627
|
+
'',
|
|
628
|
+
'Everything else stays **outside** `app/core/`. Project-specific business',
|
|
629
|
+
'rules, customer branding, and proprietary integrations belong in project',
|
|
630
|
+
'code (`app/composables/`, `app/components/`, `app/middleware/`, plugin',
|
|
631
|
+
'overrides).',
|
|
632
|
+
'',
|
|
633
|
+
'Generally-useful changes **MUST** be submitted as an upstream PR to',
|
|
634
|
+
'https://github.com/lenneTech/nuxt-extensions. Run',
|
|
635
|
+
'`/lt-dev:frontend:contribute-nuxt-extensions-core` to prepare it — the',
|
|
636
|
+
'agent filters cosmetic commits, categorizes local changes as',
|
|
637
|
+
'upstream-candidate vs. project-specific, and writes PR drafts for human',
|
|
638
|
+
"review. Letting useful fixes rot in one project's vendor tree is an",
|
|
639
|
+
'anti-pattern: they belong upstream so every consumer benefits and the',
|
|
640
|
+
'local patch disappears on the next sync.',
|
|
641
|
+
'',
|
|
642
|
+
'When in doubt, ask before editing `app/core/`.',
|
|
643
|
+
'',
|
|
605
644
|
'## Baseline',
|
|
606
645
|
'',
|
|
607
646
|
'- **Upstream-Repo:** https://github.com/lenneTech/nuxt-extensions',
|
|
@@ -612,22 +651,15 @@ class FrontendHelper {
|
|
|
612
651
|
'',
|
|
613
652
|
'## Sync history',
|
|
614
653
|
'',
|
|
615
|
-
'
|
|
616
|
-
'| ---- | ---- | -- | ----- |',
|
|
617
|
-
`| ${today} | — | ${syncHistoryTo} | scaffolded by lt CLI |`,
|
|
654
|
+
...(0, markdown_table_1.formatMarkdownTable)(['Date', 'From', 'To', 'Notes'], [[today, '—', syncHistoryTo, 'scaffolded by lt CLI']]),
|
|
618
655
|
'',
|
|
619
656
|
'## Local changes',
|
|
620
657
|
'',
|
|
621
|
-
'
|
|
622
|
-
'| ---- | ------ | ----- | ------ | ------ |',
|
|
623
|
-
'| — | — | (none, pristine) | initial vendor | — |',
|
|
658
|
+
...(0, markdown_table_1.formatMarkdownTable)(['Date', 'Commit', 'Scope', 'Reason', 'Status'], [['—', '—', '(none, pristine)', 'initial vendor', '—']]),
|
|
624
659
|
'',
|
|
625
660
|
'## Upstream PRs',
|
|
626
661
|
'',
|
|
627
|
-
'
|
|
628
|
-
'| -- | ----- | ------- | ------ |',
|
|
629
|
-
'| — | (none yet) | — | — |',
|
|
630
|
-
'',
|
|
662
|
+
...(0, markdown_table_1.formatMarkdownTable)(['PR', 'Title', 'Commits', 'Status'], [['—', '(none yet)', '—', '—']]),
|
|
631
663
|
].join('\n'));
|
|
632
664
|
}
|
|
633
665
|
// ── Post-conversion verification ─────────────────────────────────────
|
|
@@ -749,9 +781,7 @@ class FrontendHelper {
|
|
|
749
781
|
if (skipPathContaining && absFile.includes(skipPathContaining))
|
|
750
782
|
continue;
|
|
751
783
|
const content = filesystem.read(absFile) || '';
|
|
752
|
-
const matches = typeof needle === 'string'
|
|
753
|
-
? content.includes(needle)
|
|
754
|
-
: needle.test(content);
|
|
784
|
+
const matches = typeof needle === 'string' ? content.includes(needle) : needle.test(content);
|
|
755
785
|
if (matches) {
|
|
756
786
|
stale.push(absFile.replace(`${appDir}/`, ''));
|
|
757
787
|
}
|
|
@@ -800,10 +830,7 @@ class FrontendHelper {
|
|
|
800
830
|
walkConsumerFiles(appDir) {
|
|
801
831
|
const fs = require('node:fs');
|
|
802
832
|
const path = require('node:path');
|
|
803
|
-
const searchDirs = [
|
|
804
|
-
path.join(appDir, 'app'),
|
|
805
|
-
path.join(appDir, 'tests'),
|
|
806
|
-
];
|
|
833
|
+
const searchDirs = [path.join(appDir, 'app'), path.join(appDir, 'tests')];
|
|
807
834
|
const allFiles = [];
|
|
808
835
|
const walk = (dir) => {
|
|
809
836
|
try {
|
package/build/extensions/git.js
CHANGED
|
@@ -119,8 +119,8 @@ class Git {
|
|
|
119
119
|
const result = [];
|
|
120
120
|
// Toolbox features
|
|
121
121
|
const { system } = this.toolbox;
|
|
122
|
-
// Get branches
|
|
123
|
-
const branches = yield system.run('git fetch
|
|
122
|
+
// Get branches (use short SSH timeout so fetch doesn't hang in offline environments)
|
|
123
|
+
const branches = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null; git show-branch --list');
|
|
124
124
|
branches.split('\n').forEach((item) => {
|
|
125
125
|
const matches = item.match(/\[(.*?)]/);
|
|
126
126
|
if (matches) {
|
|
@@ -251,11 +251,10 @@ class Git {
|
|
|
251
251
|
if (opts.spin) {
|
|
252
252
|
searchSpin = spin(opts.spinText);
|
|
253
253
|
}
|
|
254
|
-
// Update infos
|
|
255
|
-
const fetch = yield system.run('git fetch');
|
|
254
|
+
// Update infos (use short SSH timeout so fetch doesn't hang in offline environments)
|
|
255
|
+
const fetch = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true');
|
|
256
256
|
if (fetch.length && !fetch.startsWith('remote')) {
|
|
257
257
|
info(`Could not update infos ${fetch.length}`);
|
|
258
|
-
process.exit(1);
|
|
259
258
|
}
|
|
260
259
|
// Search branch
|
|
261
260
|
if (opts.exact) {
|
|
@@ -46,6 +46,7 @@ exports.Server = void 0;
|
|
|
46
46
|
const crypto = __importStar(require("crypto"));
|
|
47
47
|
const path_1 = require("path");
|
|
48
48
|
const ts = __importStar(require("typescript"));
|
|
49
|
+
const markdown_table_1 = require("../lib/markdown-table");
|
|
49
50
|
/**
|
|
50
51
|
* Server helper functions
|
|
51
52
|
*/
|
|
@@ -749,6 +750,13 @@ class Server {
|
|
|
749
750
|
catch (err) {
|
|
750
751
|
return { method: result.method, path: dest, success: false };
|
|
751
752
|
}
|
|
753
|
+
// Post-install format pass. processApiMode may have left whitespace
|
|
754
|
+
// artifacts (multi-line arrays/imports) that the formatter flags in
|
|
755
|
+
// format:check; oxfmt is only available after install, so we run it
|
|
756
|
+
// here.
|
|
757
|
+
if (apiMode) {
|
|
758
|
+
yield apiModeHelper.formatProject(dest);
|
|
759
|
+
}
|
|
752
760
|
}
|
|
753
761
|
return { method: result.method, path: dest, success: true };
|
|
754
762
|
});
|
|
@@ -890,9 +898,11 @@ class Server {
|
|
|
890
898
|
*
|
|
891
899
|
* In vendor mode we additionally clone `@lenne.tech/nest-server` to
|
|
892
900
|
* /tmp, copy its framework kernel (`src/core/`, `src/index.ts`,
|
|
893
|
-
* `src/core.module.ts`, `src/test/`, `src/
|
|
894
|
-
* `
|
|
895
|
-
*
|
|
901
|
+
* `src/core.module.ts`, `src/test/`, `src/types/`, `LICENSE`,
|
|
902
|
+
* `bin/migrate.js`) into the project at `src/core/` applying the
|
|
903
|
+
* flatten-fix, place upstream `src/templates/` at `<project>/src/templates/`
|
|
904
|
+
* (outside core/ so the runtime resolver finds it at the same relative
|
|
905
|
+
* path as in npm mode), remove `@lenne.tech/nest-server` from the
|
|
896
906
|
* project's `package.json`, merge the framework's transitive deps into
|
|
897
907
|
* the project's own deps, and run an AST-based codemod that rewrites
|
|
898
908
|
* every `from '@lenne.tech/nest-server'` import in consumer code
|
|
@@ -994,7 +1004,10 @@ class Server {
|
|
|
994
1004
|
//
|
|
995
1005
|
// Upstream layout: src/core/ (framework sub-dir) + src/index.ts +
|
|
996
1006
|
// src/core.module.ts + src/test/ + src/templates/ + src/types/.
|
|
997
|
-
// Target layout:
|
|
1007
|
+
// Target layout: most things flat under <project>/src/core/, with
|
|
1008
|
+
// one exception: src/templates/ stays at the same upstream location
|
|
1009
|
+
// (<project>/src/templates/) because the runtime email-template
|
|
1010
|
+
// resolver uses __dirname-relative lookup that must match npm mode.
|
|
998
1011
|
//
|
|
999
1012
|
// We WIPE the starter's (non-existent in npm mode) src/core/ first
|
|
1000
1013
|
// just to guarantee idempotency when users run this twice.
|
|
@@ -1006,7 +1019,15 @@ class Server {
|
|
|
1006
1019
|
[`${tmpClone}/src/index.ts`, `${coreDir}/index.ts`],
|
|
1007
1020
|
[`${tmpClone}/src/core.module.ts`, `${coreDir}/core.module.ts`],
|
|
1008
1021
|
[`${tmpClone}/src/test`, `${coreDir}/test`],
|
|
1009
|
-
|
|
1022
|
+
// src/templates/ stays OUTSIDE src/core/ at its upstream location so
|
|
1023
|
+
// the runtime template resolver (which computes
|
|
1024
|
+
// `__dirname + '../../../templates'` from within
|
|
1025
|
+
// src/core/modules/better-auth/) finds E-Mail templates at the same
|
|
1026
|
+
// relative path as in npm mode (node_modules/@lenne.tech/nest-server/
|
|
1027
|
+
// src/templates/). Keeping templates as first-class project files
|
|
1028
|
+
// outside core/ also lets projects customize them without touching
|
|
1029
|
+
// the vendored framework tree.
|
|
1030
|
+
[`${tmpClone}/src/templates`, `${dest}/src/templates`],
|
|
1010
1031
|
[`${tmpClone}/src/types`, `${coreDir}/types`],
|
|
1011
1032
|
[`${tmpClone}/LICENSE`, `${coreDir}/LICENSE`],
|
|
1012
1033
|
];
|
|
@@ -1395,14 +1416,14 @@ class Server {
|
|
|
1395
1416
|
"var f=require('fs'),h=require('https');",
|
|
1396
1417
|
"try{var c=f.readFileSync('src/core/VENDOR.md','utf8')}catch(e){process.exit(0)}",
|
|
1397
1418
|
'var m=c.match(/Baseline-Version[^0-9]*(\\d+\\.\\d+\\.\\d+)/);',
|
|
1398
|
-
|
|
1419
|
+
"if(!m){process.stderr.write(String.fromCharCode(9888)+' vendor-freshness: no baseline\\n');process.exit(0)}",
|
|
1399
1420
|
'var v=m[1];',
|
|
1400
1421
|
"h.get('https://registry.npmjs.org/@lenne.tech/nest-server/latest',function(r){",
|
|
1401
1422
|
"var d='';r.on('data',function(c){d+=c});r.on('end',function(){",
|
|
1402
|
-
|
|
1423
|
+
'try{var l=JSON.parse(d).version;',
|
|
1403
1424
|
"if(v===l)console.log('vendor core up-to-date (v'+v+')');",
|
|
1404
1425
|
"else process.stderr.write('vendor core v'+v+', latest v'+l+'\\n')",
|
|
1405
|
-
|
|
1426
|
+
"}catch(e){}})}).on('error',function(){});",
|
|
1406
1427
|
'setTimeout(function(){process.exit(0)},5000)',
|
|
1407
1428
|
'"',
|
|
1408
1429
|
].join('');
|
|
@@ -1417,7 +1438,8 @@ class Server {
|
|
|
1417
1438
|
return;
|
|
1418
1439
|
const installPrefix = 'pnpm install && ';
|
|
1419
1440
|
if (existing.startsWith(installPrefix)) {
|
|
1420
|
-
scripts[scriptName] =
|
|
1441
|
+
scripts[scriptName] =
|
|
1442
|
+
`${installPrefix}pnpm run check:vendor-freshness && ${existing.slice(installPrefix.length)}`;
|
|
1421
1443
|
}
|
|
1422
1444
|
else {
|
|
1423
1445
|
scripts[scriptName] = `pnpm run check:vendor-freshness && ${existing}`;
|
|
@@ -1485,7 +1507,7 @@ class Server {
|
|
|
1485
1507
|
' * Vendor-mode stub for extras/sync-packages.mjs.',
|
|
1486
1508
|
' *',
|
|
1487
1509
|
' * The original script is designed for npm-mode projects where',
|
|
1488
|
-
|
|
1510
|
+
' * `@lenne.tech/nest-server` is an installed dependency and',
|
|
1489
1511
|
' * `pnpm run update` pulls the latest upstream deps into the',
|
|
1490
1512
|
' * project package.json.',
|
|
1491
1513
|
' *',
|
|
@@ -1547,7 +1569,7 @@ class Server {
|
|
|
1547
1569
|
'',
|
|
1548
1570
|
'- **Read framework code from `src/core/**`** — not from `node_modules/`.',
|
|
1549
1571
|
'- **Generated imports use relative paths** to `src/core`, e.g.',
|
|
1550
|
-
|
|
1572
|
+
" `import { CrudService } from '../../../core';`",
|
|
1551
1573
|
' The exact depth depends on the file location. `lt server module`',
|
|
1552
1574
|
' computes it automatically.',
|
|
1553
1575
|
'- **Baseline + patch log** live in `src/core/VENDOR.md`. Log any',
|
|
@@ -1615,17 +1637,48 @@ class Server {
|
|
|
1615
1637
|
'',
|
|
1616
1638
|
'This directory is a curated vendor copy of the `core/` tree from',
|
|
1617
1639
|
'@lenne.tech/nest-server. It is first-class project code, not a',
|
|
1618
|
-
'node_modules shadow copy
|
|
1619
|
-
'
|
|
1620
|
-
'
|
|
1640
|
+
'node_modules shadow copy — but it is **not a fork**. The copy',
|
|
1641
|
+
'exists so Claude Code (and humans) can read framework internals',
|
|
1642
|
+
'directly. Log substantial local changes in the "Local changes"',
|
|
1643
|
+
'table below so the `nest-server-core-updater` agent can classify',
|
|
1644
|
+
'them at sync time.',
|
|
1621
1645
|
'',
|
|
1622
1646
|
'The flatten-fix was applied during `lt fullstack init`: the',
|
|
1623
1647
|
'upstream `src/index.ts`, `src/core.module.ts`, `src/test/`,',
|
|
1624
|
-
'`src/
|
|
1625
|
-
'
|
|
1626
|
-
'
|
|
1648
|
+
'`src/types/`, and `LICENSE` were moved under `src/core/` and',
|
|
1649
|
+
'their relative `./core/…` specifiers were stripped. The upstream',
|
|
1650
|
+
'`src/templates/` tree (E-Mail templates) was placed at the',
|
|
1651
|
+
'project root `src/templates/` (outside `src/core/`) so the',
|
|
1652
|
+
'runtime template resolver finds them at the same relative path',
|
|
1653
|
+
'as in npm mode. See the init code in',
|
|
1627
1654
|
'`lenneTech/cli/src/extensions/server.ts#convertCloneToVendored`.',
|
|
1628
1655
|
'',
|
|
1656
|
+
'## Modification Policy',
|
|
1657
|
+
'',
|
|
1658
|
+
'Edit `src/core/` **only** when the change is generally useful to every',
|
|
1659
|
+
'@lenne.tech/nest-server consumer:',
|
|
1660
|
+
'',
|
|
1661
|
+
'- Bugfixes that apply to every consumer',
|
|
1662
|
+
'- Broad framework enhancements',
|
|
1663
|
+
'- Security vulnerability fixes',
|
|
1664
|
+
'- Build/TypeScript compatibility fixes every consumer would hit',
|
|
1665
|
+
'',
|
|
1666
|
+
'Everything else stays **outside** `src/core/`. Project-specific',
|
|
1667
|
+
'business rules, customer enums, and proprietary integrations',
|
|
1668
|
+
'belong in project code via modification, inheritance, extension,',
|
|
1669
|
+
'or `ICoreModuleOverrides`.',
|
|
1670
|
+
'',
|
|
1671
|
+
'Generally-useful changes **MUST** be submitted as an upstream PR',
|
|
1672
|
+
'to https://github.com/lenneTech/nest-server. Run',
|
|
1673
|
+
'`/lt-dev:backend:contribute-nest-server-core` to prepare it — the',
|
|
1674
|
+
'agent filters cosmetic commits, categorizes local changes as',
|
|
1675
|
+
'upstream-candidate vs. project-specific, and writes PR drafts for',
|
|
1676
|
+
"human review. Letting useful fixes rot in one project's vendor",
|
|
1677
|
+
'tree is an anti-pattern: they belong upstream so every consumer',
|
|
1678
|
+
'benefits and the local patch disappears on the next sync.',
|
|
1679
|
+
'',
|
|
1680
|
+
'When in doubt, ask before editing `src/core/`.',
|
|
1681
|
+
'',
|
|
1629
1682
|
'## Baseline',
|
|
1630
1683
|
'',
|
|
1631
1684
|
'- **Upstream-Repo:** https://github.com/lenneTech/nest-server',
|
|
@@ -1636,22 +1689,15 @@ class Server {
|
|
|
1636
1689
|
'',
|
|
1637
1690
|
'## Sync history',
|
|
1638
1691
|
'',
|
|
1639
|
-
'
|
|
1640
|
-
'| ---- | ---- | -- | ----- |',
|
|
1641
|
-
`| ${today} | — | ${syncHistoryTo} | scaffolded by lt CLI |`,
|
|
1692
|
+
...(0, markdown_table_1.formatMarkdownTable)(['Date', 'From', 'To', 'Notes'], [[today, '—', syncHistoryTo, 'scaffolded by lt CLI']]),
|
|
1642
1693
|
'',
|
|
1643
1694
|
'## Local changes',
|
|
1644
1695
|
'',
|
|
1645
|
-
'
|
|
1646
|
-
'| ---- | ------ | ----- | ------ | ------ |',
|
|
1647
|
-
'| — | — | (none, pristine) | initial vendor | — |',
|
|
1696
|
+
...(0, markdown_table_1.formatMarkdownTable)(['Date', 'Commit', 'Scope', 'Reason', 'Status'], [['—', '—', '(none, pristine)', 'initial vendor', '—']]),
|
|
1648
1697
|
'',
|
|
1649
1698
|
'## Upstream PRs',
|
|
1650
1699
|
'',
|
|
1651
|
-
'
|
|
1652
|
-
'| -- | ----- | ------- | ------ |',
|
|
1653
|
-
'| — | (none yet) | — | — |',
|
|
1654
|
-
'',
|
|
1700
|
+
...(0, markdown_table_1.formatMarkdownTable)(['PR', 'Title', 'Commits', 'Status'], [['—', '(none yet)', '—', '—']]),
|
|
1655
1701
|
].join('\n'));
|
|
1656
1702
|
}
|
|
1657
1703
|
// ── Post-conversion verification ──────────────────────────────────────
|
|
@@ -1842,10 +1888,7 @@ class Server {
|
|
|
1842
1888
|
if (!this.filesystem.exists(tsconfigPath)) {
|
|
1843
1889
|
return;
|
|
1844
1890
|
}
|
|
1845
|
-
const EXCLUDE_ENTRIES = [
|
|
1846
|
-
'src/core/modules/migrate/templates/**/*.template.ts',
|
|
1847
|
-
'src/core/test/**/*.ts',
|
|
1848
|
-
];
|
|
1891
|
+
const EXCLUDE_ENTRIES = ['src/core/modules/migrate/templates/**/*.template.ts', 'src/core/test/**/*.ts'];
|
|
1849
1892
|
try {
|
|
1850
1893
|
// The upstream tsconfig files may contain comments — standard JSON parse
|
|
1851
1894
|
// breaks on them. Use a regex-based patch as a fallback.
|
|
@@ -2080,9 +2123,7 @@ class Server {
|
|
|
2080
2123
|
print.warning('');
|
|
2081
2124
|
print.warning('⚠ VENDOR.md documents local patches in src/core/ that will be LOST:');
|
|
2082
2125
|
// Extract non-header table rows
|
|
2083
|
-
const rows = localChangesSection[0]
|
|
2084
|
-
.split('\n')
|
|
2085
|
-
.filter((l) => /^\|\s*\d{4}-/.test(l));
|
|
2126
|
+
const rows = localChangesSection[0].split('\n').filter((l) => /^\|\s*\d{4}-/.test(l));
|
|
2086
2127
|
for (const row of rows.slice(0, 5)) {
|
|
2087
2128
|
print.info(` ${row.trim()}`);
|
|
2088
2129
|
}
|
|
@@ -2112,10 +2153,7 @@ class Server {
|
|
|
2112
2153
|
for (const sourceFile of project.getSourceFiles()) {
|
|
2113
2154
|
let modified = false;
|
|
2114
2155
|
// Static imports + re-exports
|
|
2115
|
-
for (const decl of [
|
|
2116
|
-
...sourceFile.getImportDeclarations(),
|
|
2117
|
-
...sourceFile.getExportDeclarations(),
|
|
2118
|
-
]) {
|
|
2156
|
+
for (const decl of [...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations()]) {
|
|
2119
2157
|
const spec = decl.getModuleSpecifierValue();
|
|
2120
2158
|
if (!spec)
|
|
2121
2159
|
continue;
|
|
@@ -2182,7 +2220,8 @@ class Server {
|
|
|
2182
2220
|
const migrateCompiler = 'ts:./node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/helpers/ts-compiler.js';
|
|
2183
2221
|
const migrateStore = '--store ./migrations-utils/migrate.js --migrations-dir ./migrations';
|
|
2184
2222
|
const migrateTemplate = './node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/templates/migration-project.template.ts';
|
|
2185
|
-
scripts['migrate:create'] =
|
|
2223
|
+
scripts['migrate:create'] =
|
|
2224
|
+
`f() { migrate create "$1" --template-file ${migrateTemplate} --migrations-dir ./migrations --compiler ${migrateCompiler}; }; f`;
|
|
2186
2225
|
scripts['migrate:up'] = `migrate up ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2187
2226
|
scripts['migrate:down'] = `migrate down ${migrateStore} --compiler ${migrateCompiler}`;
|
|
2188
2227
|
scripts['migrate:list'] = `migrate list ${migrateStore} --compiler ${migrateCompiler}`;
|
|
@@ -2262,8 +2301,7 @@ class Server {
|
|
|
2262
2301
|
let content = filesystem.read(gitignorePath) || '';
|
|
2263
2302
|
content = content
|
|
2264
2303
|
.split('\n')
|
|
2265
|
-
.filter((line) => !line.includes('scripts/vendor/sync-results') &&
|
|
2266
|
-
!line.includes('scripts/vendor/upstream-candidates'))
|
|
2304
|
+
.filter((line) => !line.includes('scripts/vendor/sync-results') && !line.includes('scripts/vendor/upstream-candidates'))
|
|
2267
2305
|
.join('\n');
|
|
2268
2306
|
filesystem.write(gitignorePath, content);
|
|
2269
2307
|
}
|
|
@@ -2279,7 +2317,7 @@ class Server {
|
|
|
2279
2317
|
for (const f of staleRelativeImports.slice(0, 10)) {
|
|
2280
2318
|
print.info(` ${f}`);
|
|
2281
2319
|
}
|
|
2282
|
-
print.info(
|
|
2320
|
+
print.info("These imports must be manually rewritten to '@lenne.tech/nest-server'.");
|
|
2283
2321
|
}
|
|
2284
2322
|
});
|
|
2285
2323
|
}
|
|
@@ -81,8 +81,7 @@ function findAppDir(startDir) {
|
|
|
81
81
|
let current = path.resolve(startDir);
|
|
82
82
|
const root = path.parse(current).root;
|
|
83
83
|
while (current !== root) {
|
|
84
|
-
if ((0, node_fs_1.existsSync)(path.join(current, 'nuxt.config.ts')) ||
|
|
85
|
-
(0, node_fs_1.existsSync)(path.join(current, 'nuxt.config.js'))) {
|
|
84
|
+
if ((0, node_fs_1.existsSync)(path.join(current, 'nuxt.config.ts')) || (0, node_fs_1.existsSync)(path.join(current, 'nuxt.config.js'))) {
|
|
86
85
|
return current;
|
|
87
86
|
}
|
|
88
87
|
current = path.dirname(current);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hoistWorkspacePnpmConfig = hoistWorkspacePnpmConfig;
|
|
4
|
+
/**
|
|
5
|
+
* pnpm workspace-scoped fields that must live at the workspace root.
|
|
6
|
+
* When present in sub-project package.json files, pnpm emits:
|
|
7
|
+
*
|
|
8
|
+
* WARN The field "<field>" was found in <path>. This will not take
|
|
9
|
+
* effect. You should configure "<field>" at the root of the workspace
|
|
10
|
+
* instead.
|
|
11
|
+
*
|
|
12
|
+
* Crucially, the WARN also means the values are silently ignored — CVE
|
|
13
|
+
* overrides defined only in projects/api/package.json never reach the
|
|
14
|
+
* install resolver. Hoisting them to the root fixes both the warning
|
|
15
|
+
* and the actual dependency-resolution behavior.
|
|
16
|
+
*/
|
|
17
|
+
const WORKSPACE_SCOPED_PNPM_FIELDS = ['overrides', 'onlyBuiltDependencies', 'ignoredOptionalDependencies'];
|
|
18
|
+
/**
|
|
19
|
+
* Hoist workspace-scoped pnpm config from sub-projects into the root
|
|
20
|
+
* package.json. After this runs, sub-project package.json files no
|
|
21
|
+
* longer have `overrides`, `onlyBuiltDependencies`, or
|
|
22
|
+
* `ignoredOptionalDependencies`, and the root package.json contains
|
|
23
|
+
* the merged union.
|
|
24
|
+
*
|
|
25
|
+
* Idempotent: running twice has the same effect as running once.
|
|
26
|
+
*
|
|
27
|
+
* @param options.filesystem Gluegun filesystem tool
|
|
28
|
+
* @param options.projectDir Workspace root (contains pnpm-workspace.yaml)
|
|
29
|
+
* @param options.subProjects Sub-project dirs relative to projectDir
|
|
30
|
+
*/
|
|
31
|
+
function hoistWorkspacePnpmConfig(options) {
|
|
32
|
+
var _a;
|
|
33
|
+
const { filesystem, projectDir, subProjects } = options;
|
|
34
|
+
const rootPkgPath = `${projectDir}/package.json`;
|
|
35
|
+
if (!filesystem.exists(rootPkgPath))
|
|
36
|
+
return;
|
|
37
|
+
const rootPkg = filesystem.read(rootPkgPath, 'json');
|
|
38
|
+
if (!rootPkg)
|
|
39
|
+
return;
|
|
40
|
+
(_a = rootPkg.pnpm) !== null && _a !== void 0 ? _a : (rootPkg.pnpm = {});
|
|
41
|
+
let rootChanged = false;
|
|
42
|
+
for (const subDir of subProjects) {
|
|
43
|
+
const subPkgPath = `${projectDir}/${subDir}/package.json`;
|
|
44
|
+
if (!filesystem.exists(subPkgPath))
|
|
45
|
+
continue;
|
|
46
|
+
const subPkg = filesystem.read(subPkgPath, 'json');
|
|
47
|
+
if (!(subPkg === null || subPkg === void 0 ? void 0 : subPkg.pnpm))
|
|
48
|
+
continue;
|
|
49
|
+
let subChanged = false;
|
|
50
|
+
for (const field of WORKSPACE_SCOPED_PNPM_FIELDS) {
|
|
51
|
+
const subValue = subPkg.pnpm[field];
|
|
52
|
+
if (subValue === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
rootPkg.pnpm[field] = mergePnpmFieldValue(field, rootPkg.pnpm[field], subValue);
|
|
55
|
+
rootChanged = true;
|
|
56
|
+
delete subPkg.pnpm[field];
|
|
57
|
+
subChanged = true;
|
|
58
|
+
}
|
|
59
|
+
if (subChanged) {
|
|
60
|
+
// If the sub-project's pnpm section is now empty, drop it entirely.
|
|
61
|
+
if (subPkg.pnpm && Object.keys(subPkg.pnpm).length === 0) {
|
|
62
|
+
delete subPkg.pnpm;
|
|
63
|
+
}
|
|
64
|
+
filesystem.write(subPkgPath, `${JSON.stringify(subPkg, null, 2)}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (rootChanged) {
|
|
68
|
+
filesystem.write(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Merge two values for a pnpm workspace-scoped field.
|
|
73
|
+
*
|
|
74
|
+
* pnpm expects:
|
|
75
|
+
* - `overrides` → object ({pkg: version})
|
|
76
|
+
* - `onlyBuiltDependencies` → array of strings
|
|
77
|
+
* - `ignoredOptionalDependencies` → array of strings
|
|
78
|
+
*
|
|
79
|
+
* Arrays: deduplicated, alphabetically sorted union.
|
|
80
|
+
* Objects: sub-project values take precedence over root (sub-projects
|
|
81
|
+
* like nest-server-starter own the authoritative CVE override list for
|
|
82
|
+
* their transitive deps; the root usually only seeds cross-cutting
|
|
83
|
+
* handlebars/minimatch patches).
|
|
84
|
+
*/
|
|
85
|
+
function mergePnpmFieldValue(field, rootValue, subValue) {
|
|
86
|
+
if (field === 'onlyBuiltDependencies' || field === 'ignoredOptionalDependencies') {
|
|
87
|
+
const rootArr = Array.isArray(rootValue) ? rootValue : [];
|
|
88
|
+
const subArr = Array.isArray(subValue) ? subValue : [];
|
|
89
|
+
return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b));
|
|
90
|
+
}
|
|
91
|
+
const rootObj = rootValue && typeof rootValue === 'object' && !Array.isArray(rootValue)
|
|
92
|
+
? rootValue
|
|
93
|
+
: {};
|
|
94
|
+
const subObj = subValue && typeof subValue === 'object' && !Array.isArray(subValue) ? subValue : {};
|
|
95
|
+
const merged = Object.assign(Object.assign({}, rootObj), subObj);
|
|
96
|
+
return Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
|
|
97
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatMarkdownTable = formatMarkdownTable;
|
|
4
|
+
/**
|
|
5
|
+
* Build an oxfmt-compatible Markdown table with padded columns so that the
|
|
6
|
+
* generated file passes `oxfmt --check` without a reformat pass.
|
|
7
|
+
*
|
|
8
|
+
* Column width = max(header length, longest cell length, 3). Cells are
|
|
9
|
+
* padded with trailing spaces; the separator row uses `-` characters of
|
|
10
|
+
* the same width.
|
|
11
|
+
*
|
|
12
|
+
* Character width note: uses JavaScript `.length` (UTF-16 code units),
|
|
13
|
+
* which matches oxfmt's own accounting for typical BMP characters used
|
|
14
|
+
* in VENDOR.md generation (em-dash `—`, backticks, version strings).
|
|
15
|
+
*
|
|
16
|
+
* @param headers Column headers (top row)
|
|
17
|
+
* @param rows Data rows; each row must have `headers.length` cells
|
|
18
|
+
* @returns Lines ready to concatenate with `\n`
|
|
19
|
+
*/
|
|
20
|
+
function formatMarkdownTable(headers, rows) {
|
|
21
|
+
const columnCount = headers.length;
|
|
22
|
+
const widths = headers.map((h, i) => {
|
|
23
|
+
const cellMax = rows.reduce((max, row) => { var _a; return Math.max(max, ((_a = row[i]) !== null && _a !== void 0 ? _a : '').length); }, 0);
|
|
24
|
+
return Math.max(h.length, cellMax, 3);
|
|
25
|
+
});
|
|
26
|
+
const formatRow = (cells) => `| ${cells.map((cell, i) => cell.padEnd(widths[i])).join(' | ')} |`;
|
|
27
|
+
const lines = [formatRow(headers), `| ${widths.map((w) => '-'.repeat(w)).join(' | ')} |`];
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const padded = Array.from({ length: columnCount }, (_, i) => { var _a; return (_a = row[i]) !== null && _a !== void 0 ? _a : ''; });
|
|
30
|
+
lines.push(formatRow(padded));
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
@@ -11,6 +11,7 @@ Practical guide for converting a lenne.tech fullstack project from **npm mode**
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
13
|
- [Prerequisites](#prerequisites)
|
|
14
|
+
- [Vendor Modification Policy](#vendor-modification-policy)
|
|
14
15
|
- [Part 1: Convert npm → vendor](#part-1-convert-npm--vendor)
|
|
15
16
|
- [Part 2: Update in vendor mode](#part-2-update-in-vendor-mode)
|
|
16
17
|
- [Part 3: Roll back vendor → npm](#part-3-roll-back-vendor--npm)
|
|
@@ -32,6 +33,78 @@ Before you start, make sure:
|
|
|
32
33
|
|
|
33
34
|
---
|
|
34
35
|
|
|
36
|
+
## Vendor Modification Policy
|
|
37
|
+
|
|
38
|
+
**Read this before you convert.** It shapes how you work in vendor mode.
|
|
39
|
+
|
|
40
|
+
Vendoring copies the framework source into your project tree:
|
|
41
|
+
|
|
42
|
+
- Backend: `projects/api/src/core/` (from `@lenne.tech/nest-server`)
|
|
43
|
+
- Frontend: `projects/app/app/core/` (from `@lenne.tech/nuxt-extensions`)
|
|
44
|
+
|
|
45
|
+
**This is a comprehension aid, not a fork.** The copy exists so Claude
|
|
46
|
+
Code (and humans) can read framework internals directly — it is **not**
|
|
47
|
+
an invitation to embed project-specific behavior in the framework tree.
|
|
48
|
+
|
|
49
|
+
### When may I edit `core/`?
|
|
50
|
+
|
|
51
|
+
Only when the change is **generally useful to every consumer** of the
|
|
52
|
+
framework:
|
|
53
|
+
|
|
54
|
+
| ✅ Valid reason to edit `core/` | ❌ Belongs in project code instead |
|
|
55
|
+
|--------------------------------|------------------------------------|
|
|
56
|
+
| Bugfix that every consumer hits | Customer-specific business rules |
|
|
57
|
+
| Broad framework enhancement | Project tenant IDs, enums, branding |
|
|
58
|
+
| Security vulnerability fix | Proprietary integration adapters |
|
|
59
|
+
| TypeScript / build compatibility | Project-specific authorization logic |
|
|
60
|
+
|
|
61
|
+
**Project-specific behavior belongs outside `core/`:**
|
|
62
|
+
|
|
63
|
+
- Backend: extend/inherit from core classes, use `ICoreModuleOverrides`
|
|
64
|
+
on `CoreModule.forRoot()`
|
|
65
|
+
- Frontend: use `app/composables/`, `app/components/`,
|
|
66
|
+
`app/middleware/`, or plugin overrides
|
|
67
|
+
|
|
68
|
+
### Every generic change MUST flow back upstream
|
|
69
|
+
|
|
70
|
+
If you fixed or improved something in `core/` that every consumer could
|
|
71
|
+
benefit from, you MUST submit it as a pull request to the corresponding
|
|
72
|
+
upstream repository:
|
|
73
|
+
|
|
74
|
+
| Layer | Command | Upstream |
|
|
75
|
+
|-------|---------|----------|
|
|
76
|
+
| Backend | `/lt-dev:backend:contribute-nest-server-core` | https://github.com/lenneTech/nest-server |
|
|
77
|
+
| Frontend | `/lt-dev:frontend:contribute-nuxt-extensions-core` | https://github.com/lenneTech/nuxt-extensions |
|
|
78
|
+
|
|
79
|
+
The contributor command analyzes your local changes, filters cosmetic
|
|
80
|
+
noise, categorizes commits as upstream-candidate vs. project-specific,
|
|
81
|
+
cherry-picks candidates onto a branch in a fresh upstream clone, and
|
|
82
|
+
drafts a PR body for your review. It **never auto-pushes** — you
|
|
83
|
+
explicitly open the PR via normal GitHub flow.
|
|
84
|
+
|
|
85
|
+
**Why this matters:** Without upstream flow-back, useful fixes rot in
|
|
86
|
+
one project's vendor tree and conflict on every sync. Once merged
|
|
87
|
+
upstream, the next `/lt-dev:*:update-*-core` sync picks the change up
|
|
88
|
+
as upstream-delivered and the local patch disappears — clean state for
|
|
89
|
+
everyone.
|
|
90
|
+
|
|
91
|
+
### Where the policy is documented
|
|
92
|
+
|
|
93
|
+
The same policy is enforced / surfaced in multiple places so you
|
|
94
|
+
encounter it at every relevant touchpoint:
|
|
95
|
+
|
|
96
|
+
- `projects/api/src/core/VENDOR.md` and `projects/app/app/core/VENDOR.md`
|
|
97
|
+
(inside every vendor project, auto-generated by the lt CLI)
|
|
98
|
+
- Skills `nest-server-core-vendoring` and `nuxt-extensions-core-vendoring`
|
|
99
|
+
(lt-dev plugin)
|
|
100
|
+
- Reviewer agents `backend-reviewer` and `frontend-reviewer`
|
|
101
|
+
(flag non-compliant changes in code reviews)
|
|
102
|
+
- The contributor commands themselves
|
|
103
|
+
|
|
104
|
+
When in doubt, **ask before editing `core/`**.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
35
108
|
## Part 1: Convert npm → vendor
|
|
36
109
|
|
|
37
110
|
### Step 1: Check status
|
package/docs/commands.md
CHANGED
|
@@ -230,7 +230,7 @@ lt server convert-mode --to <vendor|npm> [options]
|
|
|
230
230
|
|
|
231
231
|
- **npm → vendor:**
|
|
232
232
|
- Clones `@lenne.tech/nest-server` from GitHub at the specified tag (default: currently installed version)
|
|
233
|
-
- Copies `src/core/`, `src/index.ts`, `src/core.module.ts`, `src/test/`, `src/
|
|
233
|
+
- Copies `src/core/`, `src/index.ts`, `src/core.module.ts`, `src/test/`, `src/types/`, and `LICENSE` to `<api-root>/src/core/`; places upstream `src/templates/` at `<api-root>/src/templates/` (outside `core/` so the runtime E-Mail template resolver works)
|
|
234
234
|
- Applies flatten-fix on `index.ts`, `core.module.ts`, `test.helper.ts`, `core-persistence-model.interface.ts`
|
|
235
235
|
- Rewrites all consumer imports from `'@lenne.tech/nest-server'` to relative paths
|
|
236
236
|
- Merges upstream dependencies dynamically into `package.json`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "lenne.Tech CLI: lt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lenne.Tech",
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
"lt": "bin/lt"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
21
|
+
"c": "npm run check",
|
|
22
|
+
"cf": "npm run check:fix",
|
|
23
|
+
"check": "npm install && npm run format && npm run build && npm run check:start",
|
|
24
|
+
"check:fix": "npm install && npm audit fix && npm run format && npm run lint:fix && npm run build && npm run check:start",
|
|
25
|
+
"check:start": "bash scripts/check-cli-start.sh",
|
|
22
26
|
"postinstall": "node bin/postinstall.js 2>/dev/null || true",
|
|
23
27
|
"build": "npm run lint && npm run test && npm run clean-build && npm run compile && npm run copy-templates",
|
|
24
28
|
"clean-build": "npx rimraf ./build",
|
|
@@ -53,20 +57,19 @@
|
|
|
53
57
|
"bin"
|
|
54
58
|
],
|
|
55
59
|
"dependencies": {
|
|
56
|
-
"@aws-sdk/client-s3": "3.
|
|
60
|
+
"@aws-sdk/client-s3": "3.1032.0",
|
|
57
61
|
"@lenne.tech/cli-plugin-helper": "0.0.14",
|
|
58
62
|
"axios": "1.15.0",
|
|
59
63
|
"bcrypt": "6.0.0",
|
|
60
|
-
"ejs": "5.0.1",
|
|
61
64
|
"glob": "13.0.6",
|
|
62
65
|
"gluegun": "5.2.2",
|
|
63
66
|
"js-sha256": "0.11.1",
|
|
64
67
|
"js-yaml": "4.1.1",
|
|
65
68
|
"lodash": "4.18.1",
|
|
66
69
|
"open": "11.0.0",
|
|
67
|
-
"ts-morph": "
|
|
70
|
+
"ts-morph": "28.0.0",
|
|
68
71
|
"ts-node": "10.9.2",
|
|
69
|
-
"typescript": "6.0.
|
|
72
|
+
"typescript": "6.0.3"
|
|
70
73
|
},
|
|
71
74
|
"devDependencies": {
|
|
72
75
|
"@lenne.tech/eslint-config-ts": "2.1.4",
|
|
@@ -76,23 +79,28 @@
|
|
|
76
79
|
"@types/js-yaml": "4.0.9",
|
|
77
80
|
"@types/lodash": "4.17.24",
|
|
78
81
|
"@types/node": "25.6.0",
|
|
79
|
-
"@typescript-eslint/eslint-plugin": "8.58.
|
|
80
|
-
"@typescript-eslint/parser": "8.58.
|
|
82
|
+
"@typescript-eslint/eslint-plugin": "8.58.2",
|
|
83
|
+
"@typescript-eslint/parser": "8.58.2",
|
|
84
|
+
"ejs": "5.0.2",
|
|
81
85
|
"eslint": "9.39.4",
|
|
82
86
|
"eslint-config-prettier": "10.1.8",
|
|
83
87
|
"husky": "9.1.7",
|
|
84
88
|
"jest": "30.3.0",
|
|
85
|
-
"prettier": "3.8.
|
|
89
|
+
"prettier": "3.8.3",
|
|
86
90
|
"rimraf": "6.1.3",
|
|
87
91
|
"standard-version": "9.5.0",
|
|
88
92
|
"ts-jest": "29.4.9"
|
|
89
93
|
},
|
|
94
|
+
"//overrides": {
|
|
95
|
+
"semver@*": "Force latest semver across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep."
|
|
96
|
+
},
|
|
90
97
|
"overrides": {
|
|
91
98
|
"semver@*": "7.7.4"
|
|
92
99
|
},
|
|
93
100
|
"jest": {
|
|
94
101
|
"testEnvironment": "node",
|
|
95
102
|
"rootDir": "__tests__",
|
|
103
|
+
"testTimeout": 60000,
|
|
96
104
|
"transform": {
|
|
97
105
|
"^.+\\.tsx?$": [
|
|
98
106
|
"ts-jest",
|