@adonisjs/assembler 8.0.0-next.5 → 8.0.0-next.7

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 (36) hide show
  1. package/README.md +87 -59
  2. package/build/chunk-25Q3N5JR.js +392 -0
  3. package/build/chunk-PORDZS62.js +391 -0
  4. package/build/chunk-TIKQQRMX.js +116 -0
  5. package/build/index.d.ts +2 -0
  6. package/build/index.js +825 -430
  7. package/build/src/bundler.d.ts +44 -3
  8. package/build/src/code_scanners/routes_scanner/main.d.ts +49 -9
  9. package/build/src/code_scanners/routes_scanner/main.js +445 -0
  10. package/build/src/code_scanners/routes_scanner/validator_extractor.d.ts +12 -4
  11. package/build/src/code_transformer/main.d.ts +44 -43
  12. package/build/src/code_transformer/main.js +123 -101
  13. package/build/src/code_transformer/rc_file_transformer.d.ts +56 -4
  14. package/build/src/debug.d.ts +12 -0
  15. package/build/src/dev_server.d.ts +38 -9
  16. package/build/src/file_buffer.d.ts +67 -0
  17. package/build/src/file_system.d.ts +45 -7
  18. package/build/src/helpers.d.ts +79 -4
  19. package/build/src/helpers.js +16 -0
  20. package/build/src/hooks.d.ts +224 -0
  21. package/build/src/index_generator/main.d.ts +64 -0
  22. package/build/src/index_generator/main.js +7 -0
  23. package/build/src/index_generator/source.d.ts +60 -0
  24. package/build/src/paths_resolver.d.ts +27 -2
  25. package/build/src/shortcuts_manager.d.ts +42 -4
  26. package/build/src/test_runner.d.ts +56 -10
  27. package/build/src/types/code_scanners.d.ts +138 -24
  28. package/build/src/types/code_transformer.d.ts +61 -19
  29. package/build/src/types/common.d.ts +199 -55
  30. package/build/src/types/hooks.d.ts +235 -22
  31. package/build/src/types/main.d.ts +13 -0
  32. package/build/src/utils.d.ts +88 -13
  33. package/build/src/virtual_file_system.d.ts +112 -0
  34. package/package.json +9 -3
  35. package/build/chunk-RR4HCA4M.js +0 -7
  36. package/build/src/ast_file_system.d.ts +0 -17
package/README.md CHANGED
@@ -420,83 +420,111 @@ export const policies = {
420
420
  }
421
421
  ```
422
422
 
423
- ### makeEntityIndex
424
- The method is used to create an index file for a collection of entities discovered from one or more root folders. We use this method to create an index file for controllers or generate types for Inertia pages.
423
+ ## Index generator
424
+
425
+ The `IndexGenerator` is a core concept in Assembler that is used to watch the filesystem and create barrel files or types from a source directory.
426
+
427
+ For example, the core of the framework uses the following config to generate controllers, events, and listeners barrel file.
425
428
 
426
429
  ```ts
427
- const transformer = new CodeTransformer(appRoot)
430
+ import hooks from '@adonisjs/assembler/hooks'
431
+
432
+ export default hooks.init((type, parent, indexGenerator) => {
433
+ indexGenerator.add('controllers', {
434
+ source: './app/controllers',
435
+ importAlias: '#controllers',
436
+ as: 'barrelFile',
437
+ exportName: 'controllers',
438
+ removeSuffix: 'controllers',
439
+ output: './.adonisjs/server/controllers.ts',
440
+ })
428
441
 
429
- const output = await transformer.makeEntityIndex({
430
- source: 'app/controllers',
431
- importAlias: '#controllers'
432
- }, {
433
- destination: '.adonisjs/backend/controllers',
434
- exportName: 'controllers'
435
- })
442
+ indexGenerator.add('events', {
443
+ source: './app/events',
444
+ importAlias: '#events',
445
+ as: 'barrelFile',
446
+ exportName: 'events',
447
+ output: './.adonisjs/server/events.ts',
448
+ })
436
449
 
437
- /**
438
- export const controllers = {
439
- SignupController: () => import('#controllers/auth/signup_controller'),
440
- PostsController: () => import('#controllers/posts_controller'),
441
- HomePage: () => import('#controllers/public/home_page'),
442
- UserPostsController: () => import('#controllers/user/posts_controller'),
443
- }
444
- */
450
+ indexGenerator.add('listeners', {
451
+ source: './app/listeners',
452
+ importAlias: '#listeners',
453
+ as: 'barrelFile',
454
+ exportName: 'listeners',
455
+ removeSuffix: 'listener',
456
+ output: './.adonisjs/server/listeners.ts',
457
+ })
458
+ })
445
459
  ```
446
460
 
447
- If you would like to remove the `Controller` suffix from the key (which we do in our official generator), then you can specify the `removeNameSuffix` option.
461
+ Once the configurations have been registered with the `IndexGenerator`, it will scan the needed directories and generate the output files. Additionally, the file watchers will re-trigger the index generation when a file is added or removed from the source directory.
448
462
 
449
- ```ts
450
- const output = await transformer.makeEntityIndex({
451
- source: 'app/controllers',
452
- importAlias: '#controllers'
453
- }, {
454
- destination: '.adonisjs/backend/controllers',
455
- exportName: 'controllers',
456
- removeNameSuffix: 'controller'
457
- })
463
+ ### Barrel file generation
464
+
465
+ Barrel files provide a single entry point by exporting a collection of lazily imported entities, recursively gathered from a source directory. The `IndexGenerator` automates this process by scanning nested directories and generating import mappings that mirror the file structure.
466
+
467
+ For example, given the following `controllers` directory structure:
468
+
469
+ ```sh
470
+ app/controllers/
471
+ ├── auth/
472
+ │ ├── login_controller.ts
473
+ │ └── register_controller.ts
474
+ ├── blog/
475
+ │ ├── posts_controller.ts
476
+ │ └── post_comments_controller.ts
477
+ └── users_controller.ts
458
478
  ```
459
479
 
460
- For more advanced use-cases, you can specify the `computeBaseName` method to self compute the key name for the collection.
480
+ When processed with the controllers configuration, the `IndexGenerator` produces a barrel file that reflects the directory hierarchy as nested objects, using capitalized file names as property keys.
461
481
 
462
482
  ```ts
463
- import StringBuilder from '@poppinss/utils/string_builder'
464
-
465
- const output = await transformer.makeEntityIndex({
466
- source: 'app/controllers',
467
- importAlias: '#controllers'
468
- }, {
469
- destination: '.adonisjs/backend/controllers',
470
- exportName: 'controllers',
471
- computeBaseName(filePath, sourcePath) {
472
- const baseName = relative(sourcePath, filePath)
473
- return new StringBuilder(baseName).toUnixSlash().removeExtension().removeSuffix('Controller').toString()
483
+ export const controllers = {
484
+ auth: {
485
+ Login: () => import('#controllers/auth/login_controller'),
486
+ Register: () => import('#controllers/auth/register_controller'),
474
487
  },
475
- })
488
+ blog: {
489
+ Posts: () => import('#controllers/blog/posts_controller'),
490
+ PostComments: () => import('#controllers/blog/post_comments_controller'),
491
+ },
492
+ Users: () => import('#controllers/users_controller'),
493
+ }
476
494
  ```
477
495
 
478
- #### Controlling the output
479
- The output is an object with key-value pair in which the value is a lazily imported module. However, you can customize the output to generate a TypeScript type using the `computeOutput` method.
496
+ ### Types generation
497
+
498
+ To generate a types file, register a custom callback that takes an instance of the `VirtualFileSystem` and updates the output string via the `buffer` object.
499
+
500
+ The collection is represented as key–value pairs:
501
+
502
+ - **Key** — the relative path (without extension) from the root of the source directory.
503
+ - **Value** — an object containing the file's `importPath`, `relativePath`, and `absolutePath`.
480
504
 
481
505
  ```ts
482
- const output = await transformer.makeEntityIndex(
483
- { source: './inertia/pages', allowedExtensions: ['.tsx'] },
484
- {
485
- destination: outputPath,
486
- computeOutput(entries) {
487
- return entries
488
- .reduce<string[]>(
489
- (result, entry) => {
490
- result.push(`${entry.name}: typeof import('${entry.importPath}')`)
491
- return result
492
- },
493
- [`declare module '@adonisjs/inertia' {`, 'export interface Pages {']
506
+ import hooks from '@adonisjs/assembler/hooks'
507
+
508
+ export default hooks.init((type, parent, indexGenerator) => {
509
+ indexGenerator.add('inertiaPages', {
510
+ source: './inertia/pages',
511
+ as: (vfs, buffer) => {
512
+ buffer.write(`declare module '@adonisjs/inertia' {`).indent()
513
+ buffer.write(`export interface Pages {`).indent()
514
+
515
+ const files = vfs.asList()
516
+ Object.keys(files).forEach((filePath) => {
517
+ buffer.write(
518
+ `'${filePath}': InferPageProps<typeof import('${file.importPath}').default>`
494
519
  )
495
- .concat('}', '}')
496
- .join('\n')
520
+ })
521
+
522
+ buffer.dedent().write('}')
523
+ buffer.dedent().write('}')
497
524
  },
498
- }
499
- )
525
+ output: './.adonisjs/server/inertia_pages.d.ts',
526
+ })
527
+ })
500
528
  ```
501
529
 
502
530
  ## Contributing
@@ -0,0 +1,392 @@
1
+ import {
2
+ VirtualFileSystem,
3
+ debug_default,
4
+ removeExtension,
5
+ throttle
6
+ } from "./chunk-PORDZS62.js";
7
+
8
+ // src/index_generator/source.ts
9
+ import string from "@poppinss/utils/string";
10
+ import { mkdir, writeFile } from "fs/promises";
11
+ import { dirname, join, relative } from "path/posix";
12
+ import StringBuilder from "@poppinss/utils/string_builder";
13
+
14
+ // src/file_buffer.ts
15
+ var FileBuffer = class _FileBuffer {
16
+ /**
17
+ * Collected lines
18
+ */
19
+ #buffer = [];
20
+ /**
21
+ * Current indentation size. Each call to indent will increment
22
+ * it by 2
23
+ */
24
+ #identationSize = 0;
25
+ /**
26
+ * Cached compiled output. Once this value is set, the `flush`
27
+ * method will become a noop
28
+ */
29
+ #compiledOutput;
30
+ /**
31
+ * Creates a new child buffer instance
32
+ *
33
+ * @returns A new FileBuffer instance
34
+ */
35
+ create() {
36
+ return new _FileBuffer();
37
+ }
38
+ /**
39
+ * Returns the size of buffer text
40
+ *
41
+ * @returns The number of lines in the buffer
42
+ */
43
+ get size() {
44
+ return this.#buffer.length;
45
+ }
46
+ /**
47
+ * Write a new line to the output with current indentation
48
+ *
49
+ * @param text - The text to write as a new line
50
+ * @returns This FileBuffer instance for method chaining
51
+ */
52
+ writeLine(text) {
53
+ this.#buffer.push(`${" ".repeat(this.#identationSize)}${text}
54
+ `);
55
+ return this;
56
+ }
57
+ /**
58
+ * Write text to the output without adding a new line
59
+ *
60
+ * @param text - The text to write without a newline
61
+ * @returns This FileBuffer instance for method chaining
62
+ */
63
+ write(text) {
64
+ this.#buffer.push(`${" ".repeat(this.#identationSize)}${text}`);
65
+ return this;
66
+ }
67
+ /**
68
+ * Increase indentation by 2 spaces
69
+ *
70
+ * @returns This FileBuffer instance for method chaining
71
+ */
72
+ indent() {
73
+ this.#identationSize += 2;
74
+ return this;
75
+ }
76
+ /**
77
+ * Decrease indentation by 2 spaces (minimum of 0)
78
+ *
79
+ * @returns This FileBuffer instance for method chaining
80
+ */
81
+ dedent() {
82
+ this.#identationSize -= 2;
83
+ if (this.#identationSize < 0) {
84
+ this.#identationSize = 0;
85
+ }
86
+ return this;
87
+ }
88
+ /**
89
+ * Return template as a string, joining all buffer lines
90
+ *
91
+ * Once called, the output is cached and subsequent calls return the same result.
92
+ * The flush method becomes a no-op after the first call.
93
+ *
94
+ * @returns The complete buffer content as a string
95
+ */
96
+ flush() {
97
+ if (this.#compiledOutput !== void 0) {
98
+ return this.#compiledOutput;
99
+ }
100
+ this.#compiledOutput = this.#buffer.join("\n");
101
+ return this.#compiledOutput;
102
+ }
103
+ };
104
+
105
+ // src/index_generator/source.ts
106
+ var IndexGeneratorSource = class {
107
+ /**
108
+ * The application root directory path
109
+ */
110
+ #appRoot;
111
+ /**
112
+ * The absolute path to the output file
113
+ */
114
+ #output;
115
+ /**
116
+ * The absolute path to the source directory
117
+ */
118
+ #source;
119
+ /**
120
+ * The directory containing the output file
121
+ */
122
+ #outputDirname;
123
+ /**
124
+ * Virtual file system for scanning source files
125
+ */
126
+ #vfs;
127
+ /**
128
+ * Configuration for this index generator source
129
+ */
130
+ #config;
131
+ /**
132
+ * CLI logger instance for output messages
133
+ */
134
+ #cliLogger;
135
+ /**
136
+ * Generate the output content and write it to the output file
137
+ *
138
+ * This method creates the file buffer, populates it with the generated
139
+ * content based on configuration, and writes it to disk.
140
+ */
141
+ #generateOutput = throttle(async () => {
142
+ const buffer = new FileBuffer();
143
+ if (this.#config.as === "barrelFile") {
144
+ this.#asBarrelFile(this.#vfs, buffer, this.#config.exportName);
145
+ } else {
146
+ this.#config.as(this.#vfs, buffer, this.#config);
147
+ }
148
+ await mkdir(dirname(this.#output), { recursive: true });
149
+ await writeFile(this.#output, buffer.flush());
150
+ });
151
+ /**
152
+ * Unique name for this index generator source
153
+ */
154
+ name;
155
+ /**
156
+ * Create a new IndexGeneratorSource instance
157
+ *
158
+ * @param name - Unique name for this index generator source
159
+ * @param appRoot - The application root directory path
160
+ * @param cliLogger - Logger instance for CLI output
161
+ * @param config - Configuration for this index generator source
162
+ */
163
+ constructor(name, appRoot, cliLogger, config) {
164
+ this.name = name;
165
+ this.#config = config;
166
+ this.#appRoot = appRoot;
167
+ this.#cliLogger = cliLogger;
168
+ this.#source = join(this.#appRoot, this.#config.source);
169
+ this.#output = join(this.#appRoot, this.#config.output);
170
+ this.#outputDirname = dirname(this.#output);
171
+ this.#vfs = new VirtualFileSystem(this.#source, {
172
+ glob: this.#config.glob
173
+ });
174
+ }
175
+ /**
176
+ * Converts a recursive file tree to a string representation
177
+ *
178
+ * This method recursively processes a file tree structure and writes
179
+ * it as JavaScript object notation to the provided buffer.
180
+ *
181
+ * @param input - The recursive file tree to convert
182
+ * @param buffer - The file buffer to write the output to
183
+ */
184
+ #treeToString(input, buffer) {
185
+ Object.keys(input).forEach((key) => {
186
+ const value = input[key];
187
+ if (typeof value === "string") {
188
+ buffer.write(`'${key}': ${value},`);
189
+ } else {
190
+ buffer.write(`'${key}': {`).indent();
191
+ this.#treeToString(value, buffer);
192
+ buffer.dedent().write(`},`);
193
+ }
194
+ });
195
+ }
196
+ /**
197
+ * Transforms the barrel file index key. Converts basename to PascalCase
198
+ * and all other paths to camelCase
199
+ *
200
+ * @param config - Configuration containing suffix removal options
201
+ * @returns Function that transforms file paths to appropriate keys
202
+ */
203
+ #createBarrelFileKeyGenerator(config) {
204
+ return function(key) {
205
+ const paths = key.split("/");
206
+ const baseName = new StringBuilder(paths.pop()).removeSuffix(config.removeSuffix ?? "").pascalCase().toString();
207
+ return [...paths.map((p) => string.camelCase(p)), baseName].join("/");
208
+ };
209
+ }
210
+ /**
211
+ * Converts the file path to a lazy import. In case of an alias, the source
212
+ * path is replaced with the alias, otherwise a relative import is created
213
+ * from the output dirname.
214
+ *
215
+ * @param source - The source directory path
216
+ * @param outputDirname - The output directory path
217
+ * @param config - Configuration containing import alias options
218
+ * @returns Function that converts file paths to import statements
219
+ */
220
+ #createBarrelFileImportGenerator(source, outputDirname, config) {
221
+ return function(filePath) {
222
+ if (config.importAlias) {
223
+ debug_default('converting "%s" to import alias, source "%s"', filePath, source);
224
+ return `() => import('${removeExtension(filePath.replace(source, config.importAlias))}')`;
225
+ }
226
+ debug_default('converting "%s" to relative import, source "%s"', filePath, outputDirname);
227
+ return `() => import('${relative(outputDirname, filePath)}')`;
228
+ };
229
+ }
230
+ /**
231
+ * Generate a barrel file export structure
232
+ *
233
+ * This method creates a nested object structure that represents all
234
+ * discovered files as lazy imports, organized by directory structure.
235
+ *
236
+ * @param vfs - Virtual file system containing the scanned files
237
+ * @param buffer - File buffer to write the barrel exports to
238
+ * @param exportName - Name for the main export object
239
+ */
240
+ #asBarrelFile(vfs, buffer, exportName) {
241
+ const keyGenerator = this.#createBarrelFileKeyGenerator(this.#config);
242
+ const importGenerator = this.#createBarrelFileImportGenerator(
243
+ this.#source,
244
+ this.#outputDirname,
245
+ this.#config
246
+ );
247
+ const tree = vfs.asTree({
248
+ transformKey: keyGenerator,
249
+ transformValue: importGenerator
250
+ });
251
+ buffer.write(`export const ${exportName} = {`).indent();
252
+ this.#treeToString(tree, buffer);
253
+ buffer.dedent().write(`}`);
254
+ }
255
+ /**
256
+ * Create a log action for tracking file generation progress
257
+ *
258
+ * @example
259
+ * const action = this.#createLogAction()
260
+ * // ... perform operations
261
+ * action.displayDuration().succeeded()
262
+ */
263
+ #createLogAction() {
264
+ return this.#cliLogger.action(`create ${this.#config.output}`);
265
+ }
266
+ /**
267
+ * Add a file to the virtual file system and regenerate index if needed
268
+ *
269
+ * If the file matches the configured glob patterns, it will be added
270
+ * to the virtual file system and the index file will be regenerated.
271
+ *
272
+ * @param filePath - Absolute path of the file to add
273
+ */
274
+ async addFile(filePath) {
275
+ const added = this.#vfs.add(filePath);
276
+ if (added) {
277
+ debug_default('file added, re-generating "%s" index', this.name);
278
+ const action = this.#createLogAction();
279
+ await this.#generateOutput();
280
+ action.displayDuration().succeeded();
281
+ }
282
+ }
283
+ /**
284
+ * Remove a file from the virtual file system and regenerate index if needed
285
+ *
286
+ * If the file was previously tracked, it will be removed from the
287
+ * virtual file system and the index file will be regenerated.
288
+ *
289
+ * @param filePath - Absolute path of the file to remove
290
+ */
291
+ async removeFile(filePath) {
292
+ const removed = this.#vfs.remove(filePath);
293
+ if (removed) {
294
+ debug_default('file removed, re-generating "%s" index', this.name);
295
+ const action = this.#createLogAction();
296
+ await this.#generateOutput();
297
+ action.displayDuration().succeeded();
298
+ }
299
+ }
300
+ /**
301
+ * Generate the index file
302
+ *
303
+ * This method scans the source directory, processes files according to
304
+ * the configuration, and writes the generated index file to disk.
305
+ */
306
+ async generate() {
307
+ const action = this.#createLogAction();
308
+ await this.#vfs.scan();
309
+ await this.#generateOutput();
310
+ action.displayDuration().succeeded();
311
+ }
312
+ };
313
+
314
+ // src/index_generator/main.ts
315
+ var IndexGenerator = class {
316
+ /**
317
+ * The application root directory path
318
+ */
319
+ #appRoot;
320
+ /**
321
+ * Collection of registered index generator sources
322
+ */
323
+ #sources = {};
324
+ #cliLogger;
325
+ /**
326
+ * Create a new IndexGenerator instance
327
+ *
328
+ * @param appRoot - The application root directory path
329
+ * @param cliLogger - Logger instance for CLI output
330
+ */
331
+ constructor(appRoot, cliLogger) {
332
+ this.#appRoot = appRoot;
333
+ this.#cliLogger = cliLogger;
334
+ }
335
+ /**
336
+ * Add a new index generator source
337
+ *
338
+ * @param name - Unique name for the source
339
+ * @param config - Configuration for the index generator source
340
+ * @returns This IndexGenerator instance for method chaining
341
+ */
342
+ add(name, config) {
343
+ this.#sources[name] = new IndexGeneratorSource(name, this.#appRoot, this.#cliLogger, config);
344
+ return this;
345
+ }
346
+ /**
347
+ * Add a file to all registered index generator sources
348
+ *
349
+ * This method propagates the file addition to all registered sources,
350
+ * allowing them to regenerate their index files if the new file matches
351
+ * their glob patterns.
352
+ *
353
+ * @param filePath - Absolute path of the file to add
354
+ */
355
+ async addFile(filePath) {
356
+ const sources = Object.values(this.#sources);
357
+ for (let source of sources) {
358
+ await source.addFile(filePath);
359
+ }
360
+ }
361
+ /**
362
+ * Remove a file from all registered index generator sources
363
+ *
364
+ * This method propagates the file removal to all registered sources,
365
+ * allowing them to regenerate their index files if the removed file
366
+ * was previously tracked.
367
+ *
368
+ * @param filePath - Absolute path of the file to remove
369
+ */
370
+ async removeFile(filePath) {
371
+ const sources = Object.values(this.#sources);
372
+ for (let source of sources) {
373
+ await source.removeFile(filePath);
374
+ }
375
+ }
376
+ /**
377
+ * Generate all registered index files
378
+ *
379
+ * Iterates through all registered sources and generates their
380
+ * corresponding index files.
381
+ */
382
+ async generate() {
383
+ const sources = Object.values(this.#sources);
384
+ for (let source of sources) {
385
+ await source.generate();
386
+ }
387
+ }
388
+ };
389
+
390
+ export {
391
+ IndexGenerator
392
+ };