@lenne.tech/cli 1.9.6 → 1.10.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.
Files changed (30) hide show
  1. package/README.md +83 -0
  2. package/build/commands/fullstack/init.js +108 -4
  3. package/build/commands/fullstack/update.js +129 -0
  4. package/build/commands/server/add-property.js +29 -2
  5. package/build/commands/server/create.js +41 -3
  6. package/build/commands/server/module.js +58 -25
  7. package/build/commands/server/object.js +26 -5
  8. package/build/commands/server/permissions.js +20 -6
  9. package/build/commands/server/test.js +7 -1
  10. package/build/commands/status.js +13 -1
  11. package/build/config/vendor-runtime-deps.json +9 -0
  12. package/build/extensions/api-mode.js +19 -3
  13. package/build/extensions/server.js +1028 -3
  14. package/build/lib/framework-detection.js +167 -0
  15. package/build/templates/nest-server-module/inputs/template-create.input.ts.ejs +1 -1
  16. package/build/templates/nest-server-module/inputs/template.input.ts.ejs +1 -1
  17. package/build/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs +1 -1
  18. package/build/templates/nest-server-module/template.controller.ts.ejs +1 -1
  19. package/build/templates/nest-server-module/template.model.ts.ejs +1 -1
  20. package/build/templates/nest-server-module/template.module.ts.ejs +1 -1
  21. package/build/templates/nest-server-module/template.resolver.ts.ejs +1 -1
  22. package/build/templates/nest-server-module/template.service.ts.ejs +1 -1
  23. package/build/templates/nest-server-object/template-create.input.ts.ejs +1 -1
  24. package/build/templates/nest-server-object/template.input.ts.ejs +1 -1
  25. package/build/templates/nest-server-object/template.object.ts.ejs +1 -1
  26. package/build/templates/nest-server-tests/tests.e2e-spec.ts.ejs +1 -1
  27. package/build/templates/vendor-scripts/check-vendor-freshness.mjs +131 -0
  28. package/build/templates/vendor-scripts/propose-upstream-pr.ts +269 -0
  29. package/build/templates/vendor-scripts/sync-from-upstream.ts +250 -0
  30. package/package.json +16 -8
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const path_1 = require("path");
16
+ const framework_detection_1 = require("../../lib/framework-detection");
16
17
  const object_1 = __importDefault(require("./object"));
17
18
  /**
18
19
  * Detect controller type based on existing modules
@@ -272,27 +273,43 @@ const NewCommand = {
272
273
  const inputTemplate = server.propsForInput(props, { modelName: name, nullable: true });
273
274
  const createTemplate = server.propsForInput(props, { create: true, modelName: name, nullable: false });
274
275
  const modelTemplate = server.propsForModel(props, { modelName: name });
276
+ // Compute the correct framework-import specifier for each generated file.
277
+ // In vendored projects this resolves to a relative path to src/core (depth
278
+ // depends on file location); in npm projects it stays '@lenne.tech/nest-server'.
279
+ const importFor = (target) => (0, framework_detection_1.getFrameworkImportSpecifier)(path, target);
275
280
  // nest-server-module/inputs/xxx.input.ts
281
+ const inputTarget = (0, path_1.join)(directory, 'inputs', `${nameKebab}.input.ts`);
276
282
  yield template.generate({
277
- props: { imports: inputTemplate.imports, nameCamel, nameKebab, namePascal, props: inputTemplate.props },
278
- target: (0, path_1.join)(directory, 'inputs', `${nameKebab}.input.ts`),
283
+ props: {
284
+ frameworkImport: importFor(inputTarget),
285
+ imports: inputTemplate.imports,
286
+ nameCamel,
287
+ nameKebab,
288
+ namePascal,
289
+ props: inputTemplate.props,
290
+ },
291
+ target: inputTarget,
279
292
  template: 'nest-server-module/inputs/template.input.ts.ejs',
280
293
  });
281
294
  if (controller === 'Rest' || controller === 'Both') {
295
+ const controllerTarget = (0, path_1.join)(directory, `${nameKebab}.controller.ts`);
282
296
  yield template.generate({
283
297
  props: {
298
+ frameworkImport: importFor(controllerTarget),
284
299
  lowercase: name.toLowerCase(),
285
300
  nameCamel: camelCase(name),
286
301
  nameKebab: kebabCase(name),
287
302
  namePascal: pascalCase(name),
288
303
  },
289
- target: (0, path_1.join)(directory, `${nameKebab}.controller.ts`),
304
+ target: controllerTarget,
290
305
  template: 'nest-server-module/template.controller.ts.ejs',
291
306
  });
292
307
  }
293
308
  // nest-server-module/inputs/xxx-create.input.ts
309
+ const createInputTarget = (0, path_1.join)(directory, 'inputs', `${nameKebab}-create.input.ts`);
294
310
  yield template.generate({
295
311
  props: {
312
+ frameworkImport: importFor(createInputTarget),
296
313
  imports: createTemplate.imports,
297
314
  isGql: controller === 'GraphQL' || controller === 'Both',
298
315
  nameCamel,
@@ -300,18 +317,27 @@ const NewCommand = {
300
317
  namePascal,
301
318
  props: createTemplate.props,
302
319
  },
303
- target: (0, path_1.join)(directory, 'inputs', `${nameKebab}-create.input.ts`),
320
+ target: createInputTarget,
304
321
  template: 'nest-server-module/inputs/template-create.input.ts.ejs',
305
322
  });
306
323
  // nest-server-module/output/find-and-count-xxxs-result.output.ts
324
+ const facOutputTarget = (0, path_1.join)(directory, 'outputs', `find-and-count-${nameKebab}s-result.output.ts`);
307
325
  yield template.generate({
308
- props: { isGql: controller === 'GraphQL' || controller === 'Both', nameCamel, nameKebab, namePascal },
309
- target: (0, path_1.join)(directory, 'outputs', `find-and-count-${nameKebab}s-result.output.ts`),
326
+ props: {
327
+ frameworkImport: importFor(facOutputTarget),
328
+ isGql: controller === 'GraphQL' || controller === 'Both',
329
+ nameCamel,
330
+ nameKebab,
331
+ namePascal,
332
+ },
333
+ target: facOutputTarget,
310
334
  template: 'nest-server-module/outputs/template-fac-result.output.ts.ejs',
311
335
  });
312
336
  // nest-server-module/xxx.model.ts
337
+ const modelTarget = (0, path_1.join)(directory, `${nameKebab}.model.ts`);
313
338
  yield template.generate({
314
339
  props: {
340
+ frameworkImport: importFor(modelTarget),
315
341
  imports: modelTemplate.imports,
316
342
  isGql: controller === 'GraphQL' || controller === 'Both',
317
343
  mappings: modelTemplate.mappings,
@@ -320,27 +346,36 @@ const NewCommand = {
320
346
  namePascal,
321
347
  props: modelTemplate.props,
322
348
  },
323
- target: (0, path_1.join)(directory, `${nameKebab}.model.ts`),
349
+ target: modelTarget,
324
350
  template: 'nest-server-module/template.model.ts.ejs',
325
351
  });
326
352
  // nest-server-module/xxx.module.ts
353
+ const moduleTarget = (0, path_1.join)(directory, `${nameKebab}.module.ts`);
327
354
  yield template.generate({
328
- props: { controller, nameCamel, nameKebab, namePascal },
329
- target: (0, path_1.join)(directory, `${nameKebab}.module.ts`),
355
+ props: { controller, frameworkImport: importFor(moduleTarget), nameCamel, nameKebab, namePascal },
356
+ target: moduleTarget,
330
357
  template: 'nest-server-module/template.module.ts.ejs',
331
358
  });
332
359
  if (controller === 'GraphQL' || controller === 'Both') {
333
360
  // nest-server-module/xxx.resolver.ts
361
+ const resolverTarget = (0, path_1.join)(directory, `${nameKebab}.resolver.ts`);
334
362
  yield template.generate({
335
- props: { nameCamel, nameKebab, namePascal },
336
- target: (0, path_1.join)(directory, `${nameKebab}.resolver.ts`),
363
+ props: { frameworkImport: importFor(resolverTarget), nameCamel, nameKebab, namePascal },
364
+ target: resolverTarget,
337
365
  template: 'nest-server-module/template.resolver.ts.ejs',
338
366
  });
339
367
  }
340
368
  // nest-server-module/xxx.service.ts
369
+ const serviceTarget = (0, path_1.join)(directory, `${nameKebab}.service.ts`);
341
370
  yield template.generate({
342
- props: { isGql: controller === 'GraphQL' || controller === 'Both', nameCamel, nameKebab, namePascal },
343
- target: (0, path_1.join)(directory, `${nameKebab}.service.ts`),
371
+ props: {
372
+ frameworkImport: importFor(serviceTarget),
373
+ isGql: controller === 'GraphQL' || controller === 'Both',
374
+ nameCamel,
375
+ nameKebab,
376
+ namePascal,
377
+ },
378
+ target: serviceTarget,
344
379
  template: 'nest-server-module/template.service.ts.ejs',
345
380
  });
346
381
  generateSpinner.succeed('Files generated');
@@ -365,20 +400,18 @@ const NewCommand = {
365
400
  });
366
401
  // Ensure forwardRef is imported from @nestjs/common
367
402
  const serverModuleContent = filesystem.read(serverModule);
368
- if (serverModuleContent && !serverModuleContent.includes('forwardRef')) {
369
- // Add forwardRef to @nestjs/common import
370
- yield patching.patch(serverModule, {
371
- insert: '$1, forwardRef$2',
372
- replace: /from '@nestjs\/common'(.*?)}/,
373
- });
374
- }
375
- else if (serverModuleContent &&
403
+ if (serverModuleContent &&
376
404
  serverModuleContent.includes('@nestjs/common') &&
377
- !serverModuleContent.match(/forwardRef.*@nestjs\/common|@nestjs\/common.*forwardRef/)) {
378
- // forwardRef exists but not in @nestjs/common import - add it
405
+ !serverModuleContent.match(/import\s*\{[^}]*forwardRef[^}]*}\s*from\s+['"]@nestjs\/common['"]/)) {
406
+ // Add forwardRef into the existing `import { ... } from '@nestjs/common'`
407
+ // statement by inserting it before the closing brace. The regex
408
+ // captures two groups: (1) everything up to (but not including) the
409
+ // closing brace of the named-import list and (2) the closing brace
410
+ // plus the `from '@nestjs/common'` clause. The replacement wedges
411
+ // `, forwardRef` in between.
379
412
  yield patching.patch(serverModule, {
380
- insert: '$1, forwardRef$2',
381
- replace: /(\w+)\s*}\s*from\s+'@nestjs\/common'/,
413
+ insert: "$1, forwardRef$2",
414
+ replace: /(import\s*\{\s*[^}]*?)(\s*\}\s*from\s+['"]@nestjs\/common['"])/,
382
415
  });
383
416
  }
384
417
  }
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const path_1 = require("path");
16
+ const framework_detection_1 = require("../../lib/framework-detection");
16
17
  const module_1 = __importDefault(require("./module"));
17
18
  /**
18
19
  * Create a new server object
@@ -133,21 +134,41 @@ const NewCommand = {
133
134
  const inputTemplate = server.propsForInput(props, { modelName: name, nullable: true });
134
135
  const createTemplate = server.propsForInput(props, { create: true, modelName: name, nullable: false });
135
136
  const objectTemplate = server.propsForModel(props, { modelName: name });
137
+ // Framework-import specifier (bare in npm mode, relative in vendored mode)
138
+ const importFor = (target) => (0, framework_detection_1.getFrameworkImportSpecifier)(path, target);
136
139
  // nest-server-module/inputs/xxx.input.ts
140
+ const inputTarget = (0, path_1.join)(directory, `${nameKebab}.input.ts`);
137
141
  yield template.generate({
138
- props: { imports: inputTemplate.imports, nameCamel, nameKebab, namePascal, props: inputTemplate.props },
139
- target: (0, path_1.join)(directory, `${nameKebab}.input.ts`),
142
+ props: {
143
+ frameworkImport: importFor(inputTarget),
144
+ imports: inputTemplate.imports,
145
+ nameCamel,
146
+ nameKebab,
147
+ namePascal,
148
+ props: inputTemplate.props,
149
+ },
150
+ target: inputTarget,
140
151
  template: 'nest-server-object/template.input.ts.ejs',
141
152
  });
142
153
  // nest-server-object/inputs/xxx-create.input.ts
154
+ const createInputTarget = (0, path_1.join)(directory, `${nameKebab}-create.input.ts`);
143
155
  yield template.generate({
144
- props: { imports: createTemplate.imports, nameCamel, nameKebab, namePascal, props: createTemplate.props },
145
- target: (0, path_1.join)(directory, `${nameKebab}-create.input.ts`),
156
+ props: {
157
+ frameworkImport: importFor(createInputTarget),
158
+ imports: createTemplate.imports,
159
+ nameCamel,
160
+ nameKebab,
161
+ namePascal,
162
+ props: createTemplate.props,
163
+ },
164
+ target: createInputTarget,
146
165
  template: 'nest-server-object/template-create.input.ts.ejs',
147
166
  });
148
167
  // nest-server-module/xxx.model.ts
168
+ const objectTarget = (0, path_1.join)(directory, `${nameKebab}.object.ts`);
149
169
  yield template.generate({
150
170
  props: {
171
+ frameworkImport: importFor(objectTarget),
151
172
  imports: objectTemplate.imports,
152
173
  mappings: objectTemplate.mappings,
153
174
  nameCamel,
@@ -155,7 +176,7 @@ const NewCommand = {
155
176
  namePascal,
156
177
  props: objectTemplate.props,
157
178
  },
158
- target: (0, path_1.join)(directory, `${nameKebab}.object.ts`),
179
+ target: objectTarget,
159
180
  template: 'nest-server-object/template.object.ts.ejs',
160
181
  });
161
182
  generateSpinner.succeed('Files generated');
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const child_process_1 = require("child_process");
13
13
  const fs_1 = require("fs");
14
14
  const path_1 = require("path");
15
+ const framework_detection_1 = require("../../lib/framework-detection");
15
16
  // ────────────────────────────────────────────────────────────────────────────
16
17
  // Helper functions (alphabetically sorted per ESLint rules)
17
18
  // ────────────────────────────────────────────────────────────────────────────
@@ -82,11 +83,23 @@ function generateJson(report, projectPath) {
82
83
  */
83
84
  function loadScanner(projectPath) {
84
85
  return __awaiter(this, void 0, void 0, function* () {
85
- // Try 1: Load from project's nest-server (preferred)
86
- const scannerPaths = [
87
- (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist', 'core', 'modules', 'permissions', 'permissions-scanner'),
88
- (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist', 'core', 'modules', 'permissions', 'permissions-scanner.js'),
89
- ];
86
+ // Build an ordered list of candidate paths, mode-aware:
87
+ //
88
+ // - Vendored projects: the framework code lives in src/core/ as
89
+ // project-compiled TypeScript. After `nest build` the compiled output
90
+ // lands under dist/src/core/modules/permissions/. Before the build,
91
+ // the .ts source sits at src/core/modules/permissions/permissions-scanner.ts
92
+ // (which Node's `require()` cannot load directly). We try the dist
93
+ // variant first and fall back to the bundled CLI scanner if the
94
+ // project hasn't been built yet.
95
+ // - npm projects: classic path under node_modules/@lenne.tech/nest-server/dist.
96
+ const scannerPaths = [];
97
+ if ((0, framework_detection_1.isVendoredProject)(projectPath)) {
98
+ scannerPaths.push((0, path_1.join)(projectPath, 'dist', 'src', 'core', 'modules', 'permissions', 'permissions-scanner'), (0, path_1.join)(projectPath, 'dist', 'src', 'core', 'modules', 'permissions', 'permissions-scanner.js'), (0, path_1.join)(projectPath, 'dist', 'core', 'modules', 'permissions', 'permissions-scanner'), (0, path_1.join)(projectPath, 'dist', 'core', 'modules', 'permissions', 'permissions-scanner.js'));
99
+ }
100
+ else {
101
+ scannerPaths.push((0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist', 'core', 'modules', 'permissions', 'permissions-scanner'), (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist', 'core', 'modules', 'permissions', 'permissions-scanner.js'));
102
+ }
90
103
  for (const scannerPath of scannerPaths) {
91
104
  try {
92
105
  const mod = require(scannerPath);
@@ -101,7 +114,8 @@ function loadScanner(projectPath) {
101
114
  // Not available at this path
102
115
  }
103
116
  }
104
- // Try 2: Use CLI's bundled fallback scanner
117
+ // Fallback: Use CLI's bundled scanner. Covers both "not yet built" (vendor
118
+ // mode before `nest build`) and "framework not installed" (detached clone).
105
119
  try {
106
120
  const fallback = require('../../lib/fallback-scanner');
107
121
  if (typeof fallback.scanPermissions === 'function') {
@@ -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
  const path_1 = require("path");
13
+ const framework_detection_1 = require("../../lib/framework-detection");
13
14
  /**
14
15
  * Create a new server
15
16
  */
@@ -55,7 +56,12 @@ const NewCommand = {
55
56
  const generateSpinner = spin('Generate test file');
56
57
  // nest-server-tests/tests.e2e-spec.ts.ejs
57
58
  yield template.generate({
58
- props: { nameCamel, nameKebab, namePascal },
59
+ props: {
60
+ frameworkImport: (0, framework_detection_1.getFrameworkImportSpecifier)(path, filePath),
61
+ nameCamel,
62
+ nameKebab,
63
+ namePascal,
64
+ },
59
65
  target: filePath,
60
66
  template: 'nest-server-tests/tests.e2e-spec.ts.ejs',
61
67
  });
@@ -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
  const path_1 = require("path");
13
+ const framework_detection_1 = require("../lib/framework-detection");
13
14
  /**
14
15
  * Show project status and context
15
16
  */
@@ -27,6 +28,7 @@ const StatusCommand = {
27
28
  info(colors.dim('─'.repeat(50)));
28
29
  const projectInfo = {
29
30
  configFiles: [],
31
+ frameworkMode: null,
30
32
  gitBranch: null,
31
33
  gitRoot: null,
32
34
  hasGit: false,
@@ -56,8 +58,12 @@ const StatusCommand = {
56
58
  projectInfo.packageVersion = packageJson.version || null;
57
59
  // Detect project type
58
60
  const deps = Object.assign(Object.assign({}, packageJson.dependencies), packageJson.devDependencies);
59
- if (deps['@lenne.tech/nest-server']) {
61
+ // A project is a nest-server project if it EITHER has the npm dep
62
+ // (classic) OR has vendored the core/ directory. The frameworkMode
63
+ // field records which of the two modes this project runs in.
64
+ if (deps['@lenne.tech/nest-server'] || (0, framework_detection_1.isVendoredProject)(cwd)) {
60
65
  projectInfo.projectType = 'nest-server';
66
+ projectInfo.frameworkMode = (0, framework_detection_1.detectFrameworkMode)(cwd);
61
67
  }
62
68
  else if (deps['@nestjs/core']) {
63
69
  projectInfo.projectType = 'nestjs';
@@ -120,6 +126,12 @@ const StatusCommand = {
120
126
  info(` Version: ${projectInfo.packageVersion}`);
121
127
  }
122
128
  info(` Type: ${formatProjectType(projectInfo.projectType)}`);
129
+ if (projectInfo.frameworkMode) {
130
+ const modeLabel = projectInfo.frameworkMode === 'vendor'
131
+ ? 'vendor (src/core/, VENDOR.md)'
132
+ : 'npm (@lenne.tech/nest-server dependency)';
133
+ info(` Framework: ${modeLabel}`);
134
+ }
123
135
  }
124
136
  if (projectInfo.hasGit) {
125
137
  info('');
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "./vendor-runtime-deps.schema.json",
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
+ ]
7
+ }
8
+ </content>
9
+ </invoke>
@@ -373,9 +373,25 @@ class ApiMode {
373
373
  }
374
374
  i++;
375
375
  }
376
- // Get the non-import part of the file
377
- const maxImportEnd = importLines.reduce((max, il) => Math.max(max, il.end), -1);
378
- const codeContent = lines.slice(maxImportEnd + 1).join('\n');
376
+ // Build the "code content" view against which import usage is checked.
377
+ //
378
+ // Previous implementation: `lines.slice(maxImportEnd + 1)` — only the
379
+ // lines AFTER the last import. That breaks for files where imports and
380
+ // top-level code are interleaved (e.g. a helper `const` declared between
381
+ // two import groups). Those inter-import usages were never seen, so the
382
+ // still-used identifiers got pruned.
383
+ //
384
+ // Fix: build a mask where all import lines are blanked out but every
385
+ // other line is preserved, so inter-import usages still count.
386
+ const importLineSet = new Set();
387
+ for (const imp of importLines) {
388
+ for (let j = imp.start; j <= imp.end; j++) {
389
+ importLineSet.add(j);
390
+ }
391
+ }
392
+ const codeContent = lines
393
+ .map((line, idx) => (importLineSet.has(idx) ? '' : line))
394
+ .join('\n');
379
395
  // Check each import
380
396
  const linesToRemove = new Set();
381
397
  for (const imp of importLines) {