@vercube/nitro 1.0.0-beta.2 → 1.1.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 (2) hide show
  1. package/dist/index.mjs +13 -369
  2. package/package.json +6 -5
package/dist/index.mjs CHANGED
@@ -1,11 +1,10 @@
1
1
  import { createNitroApp } from "./runtime/App.mjs";
2
2
  import { destroyContainer } from "@vercube/di";
3
3
  import { defu } from "defu";
4
- import { join, relative, resolve } from "pathe";
4
+ import { join, resolve } from "pathe";
5
5
  import { watch } from "chokidar";
6
6
  import { readFileSync } from "node:fs";
7
- import { glob } from "tinyglobby";
8
- import { parseSync } from "oxc-parser";
7
+ import { IMPORT_SOURCE, MIDDLEWARE_IMPORT_SOURCE, SERVICE_IMPORT_SOURCE, scanFiles, transformMiddleware, transformRoute, transformService } from "@vercube/scan";
9
8
  //#region src/helpers/helpers.ts
10
9
  function useVercubeApp() {
11
10
  return globalThis.__vercubeApp__;
@@ -38,208 +37,14 @@ function validateTypescript(nitro) {
38
37
  }
39
38
  //#endregion
40
39
  //#region src/_internal/scan.ts
41
- const GLOB_SCAN_PATTERN = "**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}";
42
- async function scanFiles(nitro, name) {
43
- return await Promise.all(nitro.options.scanDirs.map((dir) => scanDir(nitro, dir, name))).then((r) => r.flat());
44
- }
45
- async function scanDir(nitro, dir, name) {
46
- return (await glob(join(name, GLOB_SCAN_PATTERN), {
47
- cwd: dir,
48
- dot: true,
49
- absolute: true
50
- }).catch((error) => {
51
- if (error?.code === "ENOTDIR") {
52
- nitro.logger.warn(`Ignoring \`${join(dir, name)}\`. It must be a directory.`);
53
- return [];
54
- }
55
- throw error;
56
- })).map((fullPath) => {
57
- return {
58
- fullPath,
59
- path: relative(join(dir, name), fullPath)
60
- };
61
- }).sort((a, b) => a.path.localeCompare(b.path));
62
- }
63
- //#endregion
64
- //#region src/build/Routes.ts
65
- /**
66
- * Mapping of decorator names to their corresponding uppercase HTTP method strings.
67
- * Used for O(1) lookup and simultaneous method name resolution.
68
- */
69
- const HTTP_METHODS = {
70
- Get: "GET",
71
- Post: "POST",
72
- Put: "PUT",
73
- Delete: "DELETE",
74
- Patch: "PATCH",
75
- Options: "OPTIONS",
76
- Head: "HEAD"
77
- };
78
- /**
79
- * Pre-compiled regular expression for extracting named route parameters.
80
- * Matches path segments prefixed with ':' (e.g. ':id', ':slug').
81
- * Uses the global flag for iterative matching via `exec`.
82
- */
83
- const PARAM_RE = /:([^/]+)/g;
84
- /**
85
- * Internal module path used for generated import statements.
86
- */
87
- const IMPORT_SOURCE = "#internal/vercube-route-plugin";
88
- /**
89
- * Reads a controller file from disk and returns all routes defined in classes
90
- * decorated with `Controller` and methods decorated with HTTP decorators (Get, Post, etc.).
91
- * @param route - File info (path and fullPath) to analyze.
92
- * @returns List of `RouteInfo` for every route in the file.
93
- */
94
- async function transformRoute(route) {
95
- return extractRoutes(readFileSync(route.fullPath, "utf8")).map((parsedRoute) => ({
96
- ...parsedRoute,
97
- ...route
98
- }));
99
- }
100
- /**
101
- * Extracts all route definitions from the given TypeScript/JavaScript source code.
102
- *
103
- * Parses the source using `oxc-parser` and traverses the resulting AST to find
104
- * classes decorated with `@Controller(path)`. For each such class, it inspects
105
- * method definitions for HTTP method decorators (`@Get`, `@Post`, `@Put`, `@Delete`,
106
- * `@Patch`, `@Options`, `@Head`) and constructs full route paths by concatenating
107
- * the controller base path with the method-level path.
108
- *
109
- * Generates appropriate import statements based on the export style:
110
- * - `export default class Foo` → `import Foo from '...'`
111
- * - `export class Foo` → `import { Foo } from '...'`
112
- *
113
- * Supports classes declared via `export class`, `export default class`,
114
- * and plain `class` declarations.
115
- *
116
- * @param code - The raw TypeScript or JavaScript source code string to analyze.
117
- * @returns An array of {@link RouteInfo} objects representing all discovered routes.
118
- */
119
- function extractRoutes(code) {
120
- const ast = parseSync("file.ts", code).program;
121
- const routes = [];
122
- for (const node of ast.body) {
123
- const classInfo = getClassNode$2(node);
124
- if (!classInfo) continue;
125
- const { classNode, isDefault } = classInfo;
126
- const basePath = extractDecoratorArg(classNode.decorators, "Controller");
127
- if (basePath === null) continue;
128
- const className = classNode.id?.name;
129
- if (!className) continue;
130
- const importStatement = isDefault ? `import ${className} from '${IMPORT_SOURCE}';` : `import { ${className} } from '${IMPORT_SOURCE}';`;
131
- for (const member of classNode.body?.body ?? []) {
132
- if (member.type !== "MethodDefinition") continue;
133
- for (const decorator of member.decorators ?? []) {
134
- const expr = decorator.expression;
135
- if (expr?.type !== "CallExpression") continue;
136
- const method = HTTP_METHODS[expr.callee?.name];
137
- if (!method) continue;
138
- const arg = expr.arguments?.[0];
139
- const fullRoute = normalizePath(basePath + (arg?.type === "Literal" && typeof arg.value === "string" ? arg.value : ""));
140
- routes.push({
141
- import: importStatement,
142
- importClassName: className,
143
- route: fullRoute,
144
- method,
145
- fullPath: "",
146
- path: "",
147
- params: extractParams(fullRoute)
148
- });
149
- }
150
- }
151
- }
152
- return routes;
153
- }
154
40
  /**
155
- * Unwraps export declarations to retrieve the underlying class node,
156
- * along with metadata about the export style.
157
- *
158
- * Handles the following AST patterns:
159
- * - `ExportDefaultDeclaration` wrapping a `ClassDeclaration` → `isDefault: true`
160
- * - `ExportNamedDeclaration` wrapping a `ClassDeclaration` → `isDefault: false`
161
- * - Direct `ClassDeclaration` or `Class` nodes → `isDefault: false`
162
- *
163
- * @param node - An AST node from the program body to inspect.
164
- * @returns An object containing the class AST node and whether it is a default export, or `null` if not a class.
41
+ * Scans every configured Nitro scan directory for files inside `name`.
42
+ * @param nitro - The nitro instance (provides `options.scanDirs` and `logger`).
43
+ * @param name - The subdirectory to scan within each scan dir.
44
+ * @returns The discovered files.
165
45
  */
166
- function getClassNode$2(node) {
167
- if (node.type === "ExportDefaultDeclaration") {
168
- const decl = node.declaration;
169
- if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
170
- classNode: decl,
171
- isDefault: true
172
- };
173
- }
174
- if (node.type === "ExportNamedDeclaration") {
175
- const decl = node.declaration;
176
- if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
177
- classNode: decl,
178
- isDefault: false
179
- };
180
- }
181
- if (node?.type === "ClassDeclaration" || node?.type === "Class") return {
182
- classNode: node,
183
- isDefault: false
184
- };
185
- return null;
186
- }
187
- /**
188
- * Searches a list of decorators for a `CallExpression` matching the given name
189
- * and extracts its first string argument.
190
- *
191
- * This is used to retrieve the path argument from decorators like `@Controller('/api/foo')`.
192
- * If the decorator is found but has no string argument, an empty string is returned.
193
- * If no matching decorator is found, `null` is returned.
194
- *
195
- * @param decorators - The array of decorator AST nodes to search, or `undefined` if none exist.
196
- * @param name - The decorator function name to match against (e.g. 'Controller').
197
- * @returns The string argument value, an empty string if no argument is provided, or `null` if the decorator is not found.
198
- */
199
- function extractDecoratorArg(decorators, name) {
200
- if (!decorators) return null;
201
- for (const dec of decorators) {
202
- const expr = dec.expression;
203
- if (expr?.type === "CallExpression" && expr.callee?.name === name) {
204
- const arg = expr.arguments?.[0];
205
- if (arg?.type === "Literal" && typeof arg.value === "string") return arg.value;
206
- return "";
207
- }
208
- }
209
- return null;
210
- }
211
- /**
212
- * Normalizes a route path by removing duplicate slashes and ensuring
213
- * the path starts with a single leading slash.
214
- *
215
- * Splits the path on '/' separators, filters out empty segments,
216
- * and rejoins with single '/' separators.
217
- *
218
- * @param path - The raw concatenated path string to normalize.
219
- * @returns A cleaned path string with a leading slash and no duplicate separators.
220
- */
221
- function normalizePath(path) {
222
- return "/" + path.split("/").filter(Boolean).join("/");
223
- }
224
- /**
225
- * Extracts named route parameters from a path string.
226
- *
227
- * Scans the path for segments prefixed with ':' using a pre-compiled
228
- * regular expression and returns an array of parameter names with
229
- * the ':' prefix stripped.
230
- *
231
- * Resets the regex `lastIndex` after each invocation to ensure
232
- * consistent behavior across repeated calls.
233
- *
234
- * @param route - The normalized route path to scan for parameters.
235
- * @returns An array of parameter name strings, empty if none are found.
236
- */
237
- function extractParams(route) {
238
- const params = [];
239
- let match;
240
- while (match = PARAM_RE.exec(route)) params.push(match[1]);
241
- PARAM_RE.lastIndex = 0;
242
- return params;
46
+ async function scanFiles$1(nitro, name) {
47
+ return scanFiles(nitro.options.scanDirs, name, nitro.logger);
243
48
  }
244
49
  //#endregion
245
50
  //#region src/setup/Routes.ts
@@ -257,7 +62,7 @@ async function setupRoutes(nitro) {
257
62
  handler: `@vercube/nitro/runtime/handler`,
258
63
  lazy: true
259
64
  })));
260
- nitro.options.ignore = [...new Set([...nitro.options.ignore ?? [], ...routes.map((route) => route.fullPath.replace(nitro.options.rootDir, "").replace(String(nitro.options?.serverDir), "").replace("src/", ""))])];
65
+ nitro.options.ignore = [.../* @__PURE__ */ new Set([...nitro.options.ignore ?? [], ...routes.map((route) => route.fullPath.replace(nitro.options.rootDir, "").replace(String(nitro.options?.serverDir), "").replace("src/", ""))])];
261
66
  nitro.routing.sync();
262
67
  }
263
68
  /**
@@ -278,7 +83,7 @@ async function getTransformedRoutes(nitro) {
278
83
  * @returns The file info of the routes
279
84
  */
280
85
  async function scanRoutes(nitro) {
281
- return Promise.all([scanFiles(nitro, nitro.options.apiDir || "api"), scanFiles(nitro, nitro.options.routesDir || "routes")]).then((r) => r.flat());
86
+ return Promise.all([scanFiles$1(nitro, nitro.options.apiDir || "api"), scanFiles$1(nitro, nitro.options.routesDir || "routes")]).then((r) => r.flat());
282
87
  }
283
88
  /**
284
89
  * Clears the routes from the handlers
@@ -335,83 +140,6 @@ function setupHooks(nitro, options) {
335
140
  });
336
141
  }
337
142
  //#endregion
338
- //#region src/build/Middleware.ts
339
- /**
340
- * Internal module path used as a placeholder in generated import statements.
341
- * Replaced with the actual file path during setup.
342
- */
343
- const MIDDLEWARE_IMPORT_SOURCE = "#internal/vercube-middleware-source";
344
- /**
345
- * Reads a file from disk and returns all classes extending `BaseMiddleware` found within it.
346
- */
347
- async function transformMiddleware(file) {
348
- return extractMiddlewares(readFileSync(file.fullPath, "utf8")).map((m) => ({
349
- ...m,
350
- ...file
351
- }));
352
- }
353
- /**
354
- * Extracts all class definitions that extend `BaseMiddleware` from the given source code.
355
- *
356
- * Parses the source using `oxc-parser` and traverses the AST to find classes
357
- * whose `superClass` resolves to the identifier `BaseMiddleware`. Generates
358
- * appropriate import statements based on whether the class is a default or named export.
359
- *
360
- * @param code - The raw TypeScript or JavaScript source code string to analyze.
361
- * @returns An array of {@link MiddlewareInfo} objects for all discovered middleware classes.
362
- */
363
- function extractMiddlewares(code) {
364
- const ast = parseSync("file.ts", code).program;
365
- const middlewares = [];
366
- for (const node of ast.body) {
367
- const classInfo = getClassNode$1(node);
368
- if (!classInfo) continue;
369
- const { classNode, isDefault } = classInfo;
370
- if (!extendsBaseMiddleware(classNode)) continue;
371
- const className = classNode.id?.name;
372
- if (!className) continue;
373
- const importStatement = isDefault ? `import ${className} from '${MIDDLEWARE_IMPORT_SOURCE}';` : `import { ${className} } from '${MIDDLEWARE_IMPORT_SOURCE}';`;
374
- middlewares.push({
375
- import: importStatement,
376
- importClassName: className,
377
- fullPath: "",
378
- path: ""
379
- });
380
- }
381
- return middlewares;
382
- }
383
- /**
384
- * Returns true if the class node extends `BaseMiddleware`.
385
- */
386
- function extendsBaseMiddleware(classNode) {
387
- return classNode.superClass?.type === "Identifier" && classNode.superClass.name === "BaseMiddleware";
388
- }
389
- /**
390
- * Unwraps export declarations to retrieve the underlying class node,
391
- * along with metadata about the export style.
392
- */
393
- function getClassNode$1(node) {
394
- if (node.type === "ExportDefaultDeclaration") {
395
- const decl = node.declaration;
396
- if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
397
- classNode: decl,
398
- isDefault: true
399
- };
400
- }
401
- if (node.type === "ExportNamedDeclaration") {
402
- const decl = node.declaration;
403
- if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
404
- classNode: decl,
405
- isDefault: false
406
- };
407
- }
408
- if (node?.type === "ClassDeclaration" || node?.type === "Class") return {
409
- classNode: node,
410
- isDefault: false
411
- };
412
- return null;
413
- }
414
- //#endregion
415
143
  //#region src/setup/Middleware.ts
416
144
  /**
417
145
  * Gets the transformed middleware from the middleware directory.
@@ -428,95 +156,11 @@ async function getTransformedMiddlewares(nitro) {
428
156
  * excludes those files from Nitro's native middleware handling.
429
157
  */
430
158
  async function scanMiddlewares(nitro) {
431
- const files = await scanFiles(nitro, "middleware");
432
- nitro.options.ignore = [...new Set([...nitro.options.ignore ?? [], ...files.map((file) => file.fullPath.replace(nitro.options.rootDir, "").replace(String(nitro.options?.serverDir), "").replace("src/", ""))])];
159
+ const files = await scanFiles$1(nitro, "middleware");
160
+ nitro.options.ignore = [.../* @__PURE__ */ new Set([...nitro.options.ignore ?? [], ...files.map((file) => file.fullPath.replace(nitro.options.rootDir, "").replace(String(nitro.options?.serverDir), "").replace("src/", ""))])];
433
161
  return files;
434
162
  }
435
163
  //#endregion
436
- //#region src/build/Services.ts
437
- /**
438
- * Internal module path used as a placeholder in generated import statements.
439
- * Replaced with the actual file path during setup.
440
- */
441
- const SERVICE_IMPORT_SOURCE = "#internal/vercube-service-source";
442
- /**
443
- * Reads a file from disk and returns all `@Injectable`-decorated classes found within it.
444
- */
445
- async function transformService(file) {
446
- return extractServices(readFileSync(file.fullPath, "utf8")).map((s) => ({
447
- ...s,
448
- ...file
449
- }));
450
- }
451
- /**
452
- * Extracts all `@Injectable`-decorated class definitions from the given source code.
453
- *
454
- * Parses the source using `oxc-parser` and traverses the AST to find classes
455
- * annotated with `@Injectable()`. Generates appropriate import statements based
456
- * on whether the class is a default or named export.
457
- *
458
- * @param code - The raw TypeScript or JavaScript source code string to analyze.
459
- * @returns An array of {@link ServiceInfo} objects for all discovered injectable classes.
460
- */
461
- function extractServices(code) {
462
- const ast = parseSync("file.ts", code).program;
463
- const services = [];
464
- for (const node of ast.body) {
465
- const classInfo = getClassNode(node);
466
- if (!classInfo) continue;
467
- const { classNode, isDefault } = classInfo;
468
- if (!hasDecorator(classNode.decorators, "Injectable")) continue;
469
- const className = classNode.id?.name;
470
- if (!className) continue;
471
- const importStatement = isDefault ? `import ${className} from '${SERVICE_IMPORT_SOURCE}';` : `import { ${className} } from '${SERVICE_IMPORT_SOURCE}';`;
472
- services.push({
473
- import: importStatement,
474
- importClassName: className,
475
- fullPath: "",
476
- path: ""
477
- });
478
- }
479
- return services;
480
- }
481
- /**
482
- * Unwraps export declarations to retrieve the underlying class node,
483
- * along with metadata about the export style.
484
- */
485
- function getClassNode(node) {
486
- if (node.type === "ExportDefaultDeclaration") {
487
- const decl = node.declaration;
488
- if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
489
- classNode: decl,
490
- isDefault: true
491
- };
492
- }
493
- if (node.type === "ExportNamedDeclaration") {
494
- const decl = node.declaration;
495
- if (decl?.type === "ClassDeclaration" || decl?.type === "Class") return {
496
- classNode: decl,
497
- isDefault: false
498
- };
499
- }
500
- if (node?.type === "ClassDeclaration" || node?.type === "Class") return {
501
- classNode: node,
502
- isDefault: false
503
- };
504
- return null;
505
- }
506
- /**
507
- * Returns true if the given decorators array contains a decorator matching `name`.
508
- * Supports both `@Name` identifier and `@Name()` call expression styles.
509
- */
510
- function hasDecorator(decorators, name) {
511
- if (!decorators) return false;
512
- for (const dec of decorators) {
513
- const expr = dec.expression;
514
- if (expr?.type === "CallExpression" && expr.callee?.name === name) return true;
515
- if (expr?.type === "Identifier" && expr.name === name) return true;
516
- }
517
- return false;
518
- }
519
- //#endregion
520
164
  //#region src/setup/Services.ts
521
165
  /**
522
166
  * Scans the project source directory for `@Injectable`-decorated classes and returns
@@ -535,7 +179,7 @@ async function getTransformedServices(nitro, scanDirs) {
535
179
  * from `.` to cover all subdirectories without adding an extra path segment.
536
180
  */
537
181
  async function scanServices(nitro, scanDirs) {
538
- return Promise.all(scanDirs?.map((dir) => scanFiles(nitro, dir)) ?? [scanFiles(nitro, ".")]).then((r) => r.flat());
182
+ return Promise.all(scanDirs?.map((dir) => scanFiles$1(nitro, dir)) ?? [scanFiles$1(nitro, ".")]).then((r) => r.flat());
539
183
  }
540
184
  //#endregion
541
185
  //#region src/plugin/VercubePlugin.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercube/nitro",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.1.0",
4
4
  "description": "Nitro module for Vercube framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,12 +31,13 @@
31
31
  "chokidar": "5.0.0",
32
32
  "defu": "6.1.7",
33
33
  "nitro": "3.0.260610-beta",
34
- "oxc-parser": "0.135.0",
34
+ "oxc-parser": "0.138.0",
35
35
  "pathe": "2.0.3",
36
36
  "tinyglobby": "0.2.17",
37
- "@vercube/core": "1.0.0-beta.2",
38
- "@vercube/storage": "1.0.0-beta.2",
39
- "@vercube/di": "1.0.0-beta.2"
37
+ "@vercube/core": "1.1.0",
38
+ "@vercube/scan": "1.1.0",
39
+ "@vercube/di": "1.1.0",
40
+ "@vercube/storage": "1.1.0"
40
41
  },
41
42
  "publishConfig": {
42
43
  "access": "public"