@adonisjs/assembler 8.0.0-next.2 → 8.0.0-next.21

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 (35) hide show
  1. package/README.md +88 -84
  2. package/build/chunk-JFBQ4OEM.js +434 -0
  3. package/build/chunk-NAASGAFO.js +478 -0
  4. package/build/chunk-NR7VMFWO.js +468 -0
  5. package/build/chunk-TIKQQRMX.js +116 -0
  6. package/build/index.d.ts +3 -0
  7. package/build/index.js +800 -447
  8. package/build/src/bundler.d.ts +44 -3
  9. package/build/src/code_scanners/routes_scanner/main.d.ts +119 -0
  10. package/build/src/code_scanners/routes_scanner/main.js +8 -0
  11. package/build/src/code_scanners/routes_scanner/validator_extractor.d.ts +26 -0
  12. package/build/src/code_transformer/main.d.ts +44 -43
  13. package/build/src/code_transformer/main.js +123 -101
  14. package/build/src/code_transformer/rc_file_transformer.d.ts +56 -4
  15. package/build/src/debug.d.ts +12 -0
  16. package/build/src/dev_server.d.ts +92 -17
  17. package/build/src/file_buffer.d.ts +87 -0
  18. package/build/src/file_system.d.ts +46 -8
  19. package/build/src/helpers.d.ts +115 -0
  20. package/build/src/helpers.js +16 -0
  21. package/build/src/index_generator/main.d.ts +68 -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 +41 -0
  25. package/build/src/shortcuts_manager.d.ts +43 -28
  26. package/build/src/test_runner.d.ts +57 -12
  27. package/build/src/types/code_scanners.d.ts +226 -0
  28. package/build/src/types/code_transformer.d.ts +61 -19
  29. package/build/src/types/common.d.ts +271 -51
  30. package/build/src/types/hooks.d.ts +235 -22
  31. package/build/src/types/main.d.ts +15 -1
  32. package/build/src/utils.d.ts +99 -15
  33. package/build/src/virtual_file_system.d.ts +112 -0
  34. package/package.json +33 -20
  35. package/build/chunk-RR4HCA4M.js +0 -7
package/README.md CHANGED
@@ -50,17 +50,7 @@ const devServer = new DevServer(appRoot, {
50
50
  pattern: 'resources/views/**/*.edge',
51
51
  reloadServer: false,
52
52
  }
53
- ],
54
-
55
- /**
56
- * The assets bundler process to start
57
- */
58
- assets: {
59
- enabled: true,
60
- name: 'vite',
61
- cmd: 'vite',
62
- args: []
63
- }
53
+ ]
64
54
  })
65
55
 
66
56
  devServer.onError((error) => {
@@ -87,9 +77,6 @@ await devServer.start()
87
77
  ## Test runner
88
78
  The `TestRunner` is used to execute the `bin/test.ts` file of your AdonisJS application. Like the `DevServer`, the `TestRunner` allows you to watch for file changes and re-run the tests. The following steps are taken to re-run tests in watch mode.
89
79
 
90
- > [!NOTE]
91
- > Read [Using a file watcher](#using-a-file-watcher) section to understand which files are watched by the file watcher.
92
-
93
80
  - If the changed file is a test file, only tests for that file will be re-run.
94
81
  - Otherwise, all tests will re-run with respect to the initial filters applied when running the `node ace test` command.
95
82
 
@@ -176,17 +163,6 @@ const bundler = new Bundler(appRoot, ts, {
176
163
  reloadServer: false,
177
164
  }
178
165
  ],
179
-
180
- /**
181
- * The assets bundler to use to bundle the frontend
182
- * assets
183
- */
184
- assets: {
185
- enabled: true,
186
- name: 'vite',
187
- cmd: 'vite',
188
- args: ['build']
189
- }
190
166
  })
191
167
  ```
192
168
 
@@ -444,83 +420,111 @@ export const policies = {
444
420
  }
445
421
  ```
446
422
 
447
- ### makeEntityIndex
448
- 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.
449
428
 
450
429
  ```ts
451
- 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
+ })
452
441
 
453
- const output = await transformer.makeEntityIndex({
454
- source: 'app/controllers',
455
- importAlias: '#controllers'
456
- }, {
457
- destination: '.adonisjs/backend/controllers',
458
- exportName: 'controllers'
459
- })
442
+ indexGenerator.add('events', {
443
+ source: './app/events',
444
+ importAlias: '#events',
445
+ as: 'barrelFile',
446
+ exportName: 'events',
447
+ output: './.adonisjs/server/events.ts',
448
+ })
460
449
 
461
- /**
462
- export const controllers = {
463
- SignupController: () => import('#controllers/auth/signup_controller'),
464
- PostsController: () => import('#controllers/posts_controller'),
465
- HomePage: () => import('#controllers/public/home_page'),
466
- UserPostsController: () => import('#controllers/user/posts_controller'),
467
- }
468
- */
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
+ })
469
459
  ```
470
460
 
471
- 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.
472
462
 
473
- ```ts
474
- const output = await transformer.makeEntityIndex({
475
- source: 'app/controllers',
476
- importAlias: '#controllers'
477
- }, {
478
- destination: '.adonisjs/backend/controllers',
479
- exportName: 'controllers',
480
- removeNameSuffix: 'controller'
481
- })
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
482
478
  ```
483
479
 
484
- 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.
485
481
 
486
482
  ```ts
487
- import StringBuilder from '@poppinss/utils/string_builder'
488
-
489
- const output = await transformer.makeEntityIndex({
490
- source: 'app/controllers',
491
- importAlias: '#controllers'
492
- }, {
493
- destination: '.adonisjs/backend/controllers',
494
- exportName: 'controllers',
495
- computeBaseName(filePath, sourcePath) {
496
- const baseName = relative(sourcePath, filePath)
497
- 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'),
498
487
  },
499
- })
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
+ }
500
494
  ```
501
495
 
502
- #### Controlling the output
503
- 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`.
504
504
 
505
505
  ```ts
506
- const output = await transformer.makeEntityIndex(
507
- { source: './inertia/pages', allowedExtensions: ['.tsx'] },
508
- {
509
- destination: outputPath,
510
- computeOutput(entries) {
511
- return entries
512
- .reduce<string[]>(
513
- (result, entry) => {
514
- result.push(`${entry.name}: typeof import('${entry.importPath}')`)
515
- return result
516
- },
517
- [`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>`
518
519
  )
519
- .concat('}', '}')
520
- .join('\n')
520
+ })
521
+
522
+ buffer.dedent().write('}')
523
+ buffer.dedent().write('}')
521
524
  },
522
- }
523
- )
525
+ output: './.adonisjs/server/inertia_pages.d.ts',
526
+ })
527
+ })
524
528
  ```
525
529
 
526
530
  ## Contributing
@@ -0,0 +1,434 @@
1
+ // src/debug.ts
2
+ import { debuglog } from "util";
3
+ var debug_default = debuglog("adonisjs:assembler");
4
+
5
+ // src/virtual_file_system.ts
6
+ import { fdir } from "fdir";
7
+ import Cache2 from "tmp-cache";
8
+ import { relative as relative2 } from "path/posix";
9
+ import lodash from "@poppinss/utils/lodash";
10
+ import string from "@poppinss/utils/string";
11
+ import { readFile } from "fs/promises";
12
+ import { naturalSort } from "@poppinss/utils";
13
+ import { Lang, parse } from "@ast-grep/napi";
14
+ import picomatch from "picomatch";
15
+
16
+ // src/utils.ts
17
+ import Cache from "tmp-cache";
18
+ import { isJunk } from "junk";
19
+ import fastGlob from "fast-glob";
20
+ import Hooks from "@poppinss/hooks";
21
+ import { existsSync } from "fs";
22
+ import getRandomPort from "get-port";
23
+ import { fileURLToPath } from "url";
24
+ import { execaNode, execa } from "execa";
25
+ import { importDefault } from "@poppinss/utils";
26
+ import { copyFile, mkdir } from "fs/promises";
27
+ import { EnvLoader, EnvParser } from "@adonisjs/env";
28
+ import chokidar from "chokidar";
29
+ import { parseTsconfig } from "get-tsconfig";
30
+ import { basename, dirname, isAbsolute, join, relative } from "path";
31
+ var DEFAULT_NODE_ARGS = ["--import=@poppinss/ts-exec", "--enable-source-maps"];
32
+ function parseConfig(cwd, ts) {
33
+ const cwdPath = typeof cwd === "string" ? cwd : fileURLToPath(cwd);
34
+ const configFile = join(cwdPath, "tsconfig.json");
35
+ debug_default('parsing config file "%s"', configFile);
36
+ let hardException = null;
37
+ const parsedConfig = ts.getParsedCommandLineOfConfigFile(
38
+ configFile,
39
+ {},
40
+ {
41
+ ...ts.sys,
42
+ useCaseSensitiveFileNames: true,
43
+ getCurrentDirectory: () => cwdPath,
44
+ onUnRecoverableConfigFileDiagnostic: (error) => hardException = error
45
+ }
46
+ );
47
+ if (hardException) {
48
+ const compilerHost = ts.createCompilerHost({});
49
+ console.log(ts.formatDiagnosticsWithColorAndContext([hardException], compilerHost));
50
+ return;
51
+ }
52
+ if (parsedConfig.errors.length) {
53
+ const compilerHost = ts.createCompilerHost({});
54
+ console.log(ts.formatDiagnosticsWithColorAndContext(parsedConfig.errors, compilerHost));
55
+ return;
56
+ }
57
+ if (parsedConfig.raw.include) {
58
+ parsedConfig.raw.include = parsedConfig.raw.include.map((includePath) => {
59
+ return includePath.replace("${configDir}/", "");
60
+ });
61
+ }
62
+ if (parsedConfig.raw.exclude) {
63
+ parsedConfig.raw.exclude = parsedConfig.raw.exclude.map((excludePath) => {
64
+ return excludePath.replace("${configDir}/", "");
65
+ });
66
+ }
67
+ return parsedConfig;
68
+ }
69
+ function readTsConfig(cwd) {
70
+ const tsConfigPath = join(cwd, "tsconfig.json");
71
+ debug_default('reading config file from location "%s"', tsConfigPath);
72
+ try {
73
+ const tsConfig = parseTsconfig(tsConfigPath);
74
+ if (tsConfig.include) {
75
+ tsConfig.include = tsConfig.include.map((resolvedPath) => {
76
+ return resolvedPath.replace(cwd, "");
77
+ });
78
+ }
79
+ if (tsConfig.exclude) {
80
+ tsConfig.exclude = tsConfig.exclude.map((resolvedPath) => {
81
+ return resolvedPath.replace(cwd, "");
82
+ });
83
+ }
84
+ debug_default("read tsconfig %O", tsConfig);
85
+ return {
86
+ path: tsConfigPath,
87
+ config: tsConfig
88
+ };
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ function runNode(cwd, options) {
94
+ const childProcess = execaNode(options.script, options.scriptArgs, {
95
+ nodeOptions: DEFAULT_NODE_ARGS.concat(options.nodeArgs),
96
+ preferLocal: true,
97
+ windowsHide: false,
98
+ localDir: cwd,
99
+ cwd,
100
+ reject: options.reject ?? false,
101
+ buffer: false,
102
+ stdio: options.stdio || "inherit",
103
+ env: {
104
+ ...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
105
+ ...options.env
106
+ }
107
+ });
108
+ return childProcess;
109
+ }
110
+ function run(cwd, options) {
111
+ const childProcess = execa(options.script, options.scriptArgs, {
112
+ preferLocal: true,
113
+ windowsHide: false,
114
+ localDir: cwd,
115
+ cwd,
116
+ buffer: false,
117
+ stdio: options.stdio || "inherit",
118
+ env: {
119
+ ...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
120
+ ...options.env
121
+ }
122
+ });
123
+ return childProcess;
124
+ }
125
+ function watch(options) {
126
+ return chokidar.watch(["."], options);
127
+ }
128
+ async function getPort(cwd) {
129
+ if (process.env.PORT) {
130
+ return getRandomPort({ port: Number(process.env.PORT) });
131
+ }
132
+ const files = await new EnvLoader(cwd).load();
133
+ for (let file of files) {
134
+ const envVariables = await new EnvParser(file.contents, cwd).parse();
135
+ if (envVariables.PORT) {
136
+ return getRandomPort({ port: Number(envVariables.PORT) });
137
+ }
138
+ }
139
+ return getRandomPort({ port: 3333 });
140
+ }
141
+ async function copyFiles(files, cwd, outDir) {
142
+ const { paths, patterns } = files.reduce(
143
+ (result, file) => {
144
+ if (fastGlob.isDynamicPattern(file)) {
145
+ result.patterns.push(file);
146
+ return result;
147
+ }
148
+ if (existsSync(join(cwd, file))) {
149
+ result.paths.push(file);
150
+ }
151
+ return result;
152
+ },
153
+ { patterns: [], paths: [] }
154
+ );
155
+ debug_default("copyFiles inputs: %O, paths: %O, patterns: %O", files, paths, patterns);
156
+ const filePaths = paths.concat(await fastGlob(patterns, { cwd, dot: true })).filter((file) => {
157
+ return !isJunk(basename(file));
158
+ });
159
+ debug_default('copying files %O to destination "%s"', filePaths, outDir);
160
+ const copyPromises = filePaths.map(async (file) => {
161
+ const src = isAbsolute(file) ? file : join(cwd, file);
162
+ const dest = join(outDir, relative(cwd, src));
163
+ await mkdir(dirname(dest), { recursive: true });
164
+ return copyFile(src, dest);
165
+ });
166
+ return await Promise.all(copyPromises);
167
+ }
168
+ function memoize(fn, maxKeys) {
169
+ const cache = new Cache({ max: maxKeys });
170
+ return (input, ...args) => {
171
+ if (cache.has(input)) {
172
+ return cache.get(input);
173
+ }
174
+ return fn(input, ...args);
175
+ };
176
+ }
177
+ function isRelative(pathValue) {
178
+ return pathValue.startsWith("./") || pathValue.startsWith("../");
179
+ }
180
+ async function loadHooks(rcFileHooks, names) {
181
+ const groups = names.map((name) => {
182
+ return {
183
+ group: name,
184
+ hooks: rcFileHooks?.[name] ?? []
185
+ };
186
+ });
187
+ const hooks = new Hooks();
188
+ for (const { group, hooks: collection } of groups) {
189
+ for (const item of collection) {
190
+ if ("run" in item) {
191
+ hooks.add(group, item.run);
192
+ } else {
193
+ hooks.add(group, await importDefault(item));
194
+ }
195
+ }
196
+ }
197
+ return hooks;
198
+ }
199
+ function throttle(fn, name) {
200
+ name = name || "throttled";
201
+ let isBusy = false;
202
+ let hasQueuedCalls = false;
203
+ let lastCallArgs;
204
+ async function throttled(...args) {
205
+ if (isBusy) {
206
+ debug_default('ignoring "%s" invocation as current execution is in progress', name);
207
+ hasQueuedCalls = true;
208
+ lastCallArgs = args;
209
+ return;
210
+ }
211
+ isBusy = true;
212
+ debug_default('executing throttled function "%s"', name);
213
+ await fn(...args);
214
+ debug_default('executed throttled function "%s"', name);
215
+ isBusy = false;
216
+ if (hasQueuedCalls) {
217
+ hasQueuedCalls = false;
218
+ debug_default('resuming and running latest "%s" invocation', name);
219
+ await throttled(...lastCallArgs);
220
+ }
221
+ }
222
+ return throttled;
223
+ }
224
+ function removeExtension(filePath) {
225
+ return filePath.substring(0, filePath.lastIndexOf("."));
226
+ }
227
+
228
+ // src/virtual_file_system.ts
229
+ var BYPASS_FN = (input) => input;
230
+ var DEFAULT_GLOB = ["**/!(*.d).ts", "**/*.tsx", "**/*.js"];
231
+ var VirtualFileSystem = class {
232
+ /**
233
+ * Absolute path to the source directory from where to read the files.
234
+ * Additionally, a glob pattern or a filter could be specified to
235
+ * narrow down the scanned files list.
236
+ */
237
+ #source;
238
+ /**
239
+ * Filesystem options
240
+ */
241
+ #options;
242
+ /**
243
+ * Files collected from the initial scan with pre-computed relative paths
244
+ */
245
+ #files = /* @__PURE__ */ new Map();
246
+ /**
247
+ * LRU cache storing parsed AST nodes by file path with size limit
248
+ */
249
+ #astCache = new Cache2({ max: 60 });
250
+ /**
251
+ * Matcher is defined when glob is defined via the options
252
+ */
253
+ #matcher;
254
+ /**
255
+ * Picomatch options used for file pattern matching
256
+ */
257
+ #picoMatchOptions;
258
+ /**
259
+ * Create a new VirtualFileSystem instance
260
+ *
261
+ * @param source - Absolute path to the source directory
262
+ * @param options - Optional configuration for file filtering and processing
263
+ */
264
+ constructor(source, options) {
265
+ this.#source = source;
266
+ this.#options = options ?? {};
267
+ this.#picoMatchOptions = {
268
+ cwd: this.#source
269
+ };
270
+ this.#matcher = picomatch(this.#options.glob ?? DEFAULT_GLOB, this.#picoMatchOptions);
271
+ }
272
+ /**
273
+ * Scans the filesystem to collect the files. Newly files must
274
+ * be added via the ".add" method.
275
+ *
276
+ * This method performs an initial scan of the source directory using
277
+ * the configured glob patterns and populates the internal file list.
278
+ */
279
+ async scan() {
280
+ debug_default('fetching entities from source "%s"', this.#source);
281
+ const filesList = await new fdir().globWithOptions(this.#options.glob ?? DEFAULT_GLOB, this.#picoMatchOptions).withFullPaths().crawl(this.#source).withPromise();
282
+ debug_default("scanned files %O", filesList);
283
+ const sortedFiles = filesList.sort(naturalSort);
284
+ this.#files.clear();
285
+ for (let filePath of sortedFiles) {
286
+ filePath = string.toUnixSlash(filePath);
287
+ const relativePath = removeExtension(relative2(this.#source, filePath));
288
+ this.#files.set(filePath, relativePath);
289
+ }
290
+ }
291
+ /**
292
+ * Check if a given file is part of the virtual file system. The method
293
+ * checks for the scanned files as well as glob pattern matches.
294
+ *
295
+ * @param filePath - Absolute file path to check
296
+ * @returns True if the file is tracked or matches the configured patterns
297
+ */
298
+ has(filePath) {
299
+ if (this.#files.has(filePath)) {
300
+ return true;
301
+ }
302
+ if (!filePath.startsWith(this.#source)) {
303
+ return false;
304
+ }
305
+ return this.#matcher(filePath);
306
+ }
307
+ /**
308
+ * Returns the files as a flat list of key-value pairs
309
+ *
310
+ * Converts the tracked files into a flat object where keys are relative
311
+ * paths (without extensions) and values are absolute file paths.
312
+ *
313
+ * @param options - Optional transformation functions for keys and values
314
+ * @returns Object with file mappings
315
+ */
316
+ asList(options) {
317
+ const list = {};
318
+ const transformKey = options?.transformKey ?? BYPASS_FN;
319
+ const transformValue = options?.transformValue ?? BYPASS_FN;
320
+ for (const [filePath, relativePath] of this.#files) {
321
+ list[transformKey(relativePath)] = transformValue(filePath);
322
+ }
323
+ return list;
324
+ }
325
+ /**
326
+ * Returns the files as a nested tree structure
327
+ *
328
+ * Converts the tracked files into a hierarchical object structure that
329
+ * mirrors the directory structure of the source files.
330
+ *
331
+ * @param options - Optional transformation functions for keys and values
332
+ * @returns Nested object representing the file tree
333
+ */
334
+ asTree(options) {
335
+ const list = {};
336
+ const transformKey = options?.transformKey ?? BYPASS_FN;
337
+ const transformValue = options?.transformValue ?? BYPASS_FN;
338
+ for (const [filePath, relativePath] of this.#files) {
339
+ const key = transformKey(relativePath);
340
+ lodash.set(list, key.split("/"), transformValue(filePath, key));
341
+ }
342
+ return list;
343
+ }
344
+ /**
345
+ * Add a new file to the virtual file system. File is only added when it
346
+ * matches the pre-defined filters.
347
+ *
348
+ * @param filePath - Absolute path of the file to add
349
+ * @returns True if the file was added, false if it doesn't match filters
350
+ */
351
+ add(filePath) {
352
+ if (this.has(filePath)) {
353
+ debug_default('adding new "%s" file to the virtual file system', filePath);
354
+ const relativePath = removeExtension(relative2(this.#source, filePath));
355
+ this.#files.set(filePath, relativePath);
356
+ return true;
357
+ }
358
+ return false;
359
+ }
360
+ /**
361
+ * Remove a file from the virtual file system
362
+ *
363
+ * @param filePath - Absolute path of the file to remove
364
+ * @returns True if the file was removed, false if it wasn't tracked
365
+ */
366
+ remove(filePath) {
367
+ debug_default('removing "%s" file from virtual file system', filePath);
368
+ return this.#files.delete(filePath);
369
+ }
370
+ /**
371
+ * Returns the file contents as AST-grep node and caches it
372
+ * forever. Use the "invalidate" method to remove it from the cache.
373
+ *
374
+ * This method reads the file content, parses it into an AST using ast-grep,
375
+ * and caches the result for future requests to improve performance.
376
+ *
377
+ * @param filePath - The absolute path to the file to parse
378
+ * @returns Promise resolving to the AST-grep node
379
+ */
380
+ async get(filePath) {
381
+ const cached = this.#astCache.get(filePath);
382
+ if (cached) {
383
+ debug_default('returning AST nodes from cache "%s"', filePath);
384
+ return cached;
385
+ }
386
+ const fileContents = await readFile(filePath, "utf-8");
387
+ debug_default('parsing "%s" file to AST', filePath);
388
+ this.#astCache.set(filePath, parse(Lang.TypeScript, fileContents).root());
389
+ return this.#astCache.get(filePath);
390
+ }
391
+ /**
392
+ * Invalidates AST cache for a single file or all files
393
+ *
394
+ * Use this method when files have been modified to ensure fresh
395
+ * AST parsing on subsequent get() calls.
396
+ *
397
+ * @param filePath - Optional file path to clear. If omitted, clears entire cache
398
+ */
399
+ invalidate(filePath) {
400
+ if (filePath) {
401
+ debug_default('invalidate AST cache "%s"', filePath);
402
+ this.#astCache.delete(filePath);
403
+ } else {
404
+ debug_default("clear AST cache");
405
+ this.#astCache.clear();
406
+ }
407
+ }
408
+ /**
409
+ * Clear all scanned files from memory
410
+ *
411
+ * Removes all tracked files from the internal file list, effectively
412
+ * resetting the virtual file system.
413
+ */
414
+ clear() {
415
+ this.#files.clear();
416
+ }
417
+ };
418
+
419
+ export {
420
+ debug_default,
421
+ parseConfig,
422
+ readTsConfig,
423
+ runNode,
424
+ run,
425
+ watch,
426
+ getPort,
427
+ copyFiles,
428
+ memoize,
429
+ isRelative,
430
+ loadHooks,
431
+ throttle,
432
+ removeExtension,
433
+ VirtualFileSystem
434
+ };