@adonisjs/assembler 8.0.0 → 8.0.1

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.
@@ -1,18 +1,43 @@
1
- import { t as CodemodException } from "../../codemod_exception-CzQgXAAf.js";
1
+ import "../../chunk-DF48asd8.js";
2
+ import { t as CodemodException } from "../../codemod_exception-BMNJZ0i1.js";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { detectPackageManager, installPackage } from "@antfu/install-pkg";
4
5
  import { ImportsBag } from "@poppinss/utils";
5
6
  import { join } from "node:path";
6
7
  import { Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
8
+ //#region src/code_transformer/rc_file_transformer.ts
7
9
  const ALLOWED_ENVIRONMENTS = [
8
10
  "web",
9
11
  "console",
10
12
  "test",
11
13
  "repl"
12
14
  ];
15
+ /**
16
+ * RcFileTransformer is used to transform the `adonisrc.ts` file
17
+ * for adding new commands, providers, meta files etc.
18
+ *
19
+ * This class provides a fluent API for modifying the AdonisJS configuration
20
+ * file (adonisrc.ts) by adding various types of entries like providers,
21
+ * commands, preloaded files, meta files, and test suites.
22
+ *
23
+ * @example
24
+ * const transformer = new RcFileTransformer(cwd, project)
25
+ * transformer.addProvider('#providers/app_provider')
26
+ * transformer.addCommand('#commands/make_controller')
27
+ * await transformer.save()
28
+ */
13
29
  var RcFileTransformer = class {
30
+ /**
31
+ * The current working directory URL
32
+ */
14
33
  #cwd;
34
+ /**
35
+ * The TsMorph project instance
36
+ */
15
37
  #project;
38
+ /**
39
+ * Settings to use when persisting files
40
+ */
16
41
  #editorSettings = {
17
42
  indentSize: 2,
18
43
  convertTabsToSpaces: true,
@@ -21,10 +46,22 @@ var RcFileTransformer = class {
21
46
  indentStyle: 2,
22
47
  semicolons: "remove"
23
48
  };
49
+ /**
50
+ * Create a new RcFileTransformer instance
51
+ *
52
+ * @param cwd - The current working directory URL
53
+ * @param project - The TsMorph project instance
54
+ */
24
55
  constructor(cwd, project) {
25
56
  this.#cwd = cwd;
26
57
  this.#project = project;
27
58
  }
59
+ /**
60
+ * Get the `adonisrc.ts` source file
61
+ *
62
+ * @returns The adonisrc.ts source file
63
+ * @throws CodemodException if the file cannot be found
64
+ */
28
65
  #getRcFileOrThrow() {
29
66
  const filePath = "adonisrc.ts";
30
67
  const rcFileUrl = fileURLToPath(new URL(`./${filePath}`, this.#cwd));
@@ -36,10 +73,23 @@ export default defineConfig({
36
73
  })`);
37
74
  return file;
38
75
  }
76
+ /**
77
+ * Check if environments array has a subset of available environments
78
+ *
79
+ * @param environments - Optional array of environment names
80
+ * @returns True if the provided environments are a subset of available ones
81
+ */
39
82
  #isInSpecificEnvironment(environments) {
40
83
  if (!environments) return false;
41
84
  return !!ALLOWED_ENVIRONMENTS.find((env) => !environments.includes(env));
42
85
  }
86
+ /**
87
+ * Locate the `defineConfig` call inside the `adonisrc.ts` file
88
+ *
89
+ * @param file - The source file to search in
90
+ * @returns The defineConfig call expression
91
+ * @throws CodemodException if defineConfig call cannot be found
92
+ */
43
93
  #locateDefineConfigCallOrThrow(file) {
44
94
  const call = file.getDescendantsOfKind(SyntaxKind.CallExpression).find((statement) => statement.getExpression().getText() === "defineConfig");
45
95
  if (!call) throw CodemodException.invalidRcFile("adonisrc.ts", `import { defineConfig } from '@adonisjs/core/app'
@@ -49,9 +99,24 @@ export default defineConfig({
49
99
  })`, "Could not locate the defineConfig call.");
50
100
  return call;
51
101
  }
102
+ /**
103
+ * Return the ObjectLiteralExpression of the defineConfig call
104
+ *
105
+ * @param defineConfigCall - The defineConfig call expression
106
+ * @returns The configuration object literal expression
107
+ * @throws Error if the object literal cannot be found
108
+ */
52
109
  #getDefineConfigObjectOrThrow(defineConfigCall) {
53
110
  return defineConfigCall.getArguments()[0].asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
54
111
  }
112
+ /**
113
+ * Check if the defineConfig() call has the property assignment
114
+ * inside it or not. If not, it will create one and return it.
115
+ *
116
+ * @param propertyName - The name of the property to find or create
117
+ * @param initializer - The initial value if the property needs to be created
118
+ * @returns The property assignment node
119
+ */
55
120
  #getPropertyAssignmentInDefineConfigCall(propertyName, initializer) {
56
121
  const file = this.#getRcFileOrThrow();
57
122
  const defineConfigCall = this.#locateDefineConfigCallOrThrow(file);
@@ -66,12 +131,35 @@ export default defineConfig({
66
131
  }
67
132
  return property;
68
133
  }
134
+ /**
135
+ * Extract list of imported modules from an ArrayLiteralExpression
136
+ *
137
+ * It assumes that the array can have two types of elements:
138
+ *
139
+ * - Simple lazy imported modules: [() => import('path/to/file')]
140
+ * - Or an object entry: [{ file: () => import('path/to/file'), environment: ['web', 'console'] }]
141
+ * where the `file` property is a lazy imported module.
142
+ */
69
143
  #extractModulesFromArray(array) {
70
144
  return array.getElements().map((element) => {
145
+ /**
146
+ * Simple lazy imported module
147
+ */
71
148
  if (Node.isArrowFunction(element)) return element.getFirstDescendantByKindOrThrow(SyntaxKind.CallExpression).getFirstDescendantByKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
149
+ /**
150
+ * Object entry
151
+ */
72
152
  if (Node.isObjectLiteralExpression(element)) return element.getPropertyOrThrow("file").getFirstDescendantByKindOrThrow(SyntaxKind.ArrowFunction).getFirstDescendantByKindOrThrow(SyntaxKind.CallExpression).getFirstDescendantByKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
73
153
  }).filter(Boolean);
74
154
  }
155
+ /**
156
+ * Extract a specific property from an ArrayLiteralExpression
157
+ * that contains object entries.
158
+ *
159
+ * This function is mainly used for extractring the `pattern` property
160
+ * when adding a new meta files entry, or the `name` property when
161
+ * adding a new test suite.
162
+ */
75
163
  #extractPropertyFromArray(array, propertyName) {
76
164
  return array.getElements().map((el) => {
77
165
  if (!Node.isObjectLiteralExpression(el)) return;
@@ -80,6 +168,10 @@ export default defineConfig({
80
168
  return nameProp.getInitializerIfKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
81
169
  }).filter(Boolean);
82
170
  }
171
+ /**
172
+ * Build a new module entry for the preloads and providers array
173
+ * based upon the environments specified
174
+ */
83
175
  #buildNewModuleEntry(modulePath, environments) {
84
176
  if (!this.#isInSpecificEnvironment(environments)) return `() => import('${modulePath}')`;
85
177
  return `{
@@ -87,34 +179,83 @@ export default defineConfig({
87
179
  environment: [${environments?.map((env) => `'${env}'`).join(", ")}],
88
180
  }`;
89
181
  }
182
+ /**
183
+ * Add a new command to the rcFile
184
+ *
185
+ * @param commandPath - The path to the command file
186
+ * @returns This RcFileTransformer instance for method chaining
187
+ */
90
188
  addCommand(commandPath) {
91
189
  const commandsArray = this.#getPropertyAssignmentInDefineConfigCall("commands", "[]").getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
92
190
  const commandString = `() => import('${commandPath}')`;
191
+ /**
192
+ * If the command already exists, do nothing
193
+ */
93
194
  if (commandsArray.getElements().some((el) => el.getText() === commandString)) return this;
195
+ /**
196
+ * Add the command to the array
197
+ */
94
198
  commandsArray.addElement(commandString);
95
199
  return this;
96
200
  }
201
+ /**
202
+ * Add a new preloaded file to the rcFile
203
+ *
204
+ * @param modulePath - The path to the preload file
205
+ * @param environments - Optional array of environments where this preload should run
206
+ * @returns This RcFileTransformer instance for method chaining
207
+ */
97
208
  addPreloadFile(modulePath, environments) {
98
209
  const preloadsArray = this.#getPropertyAssignmentInDefineConfigCall("preloads", "[]").getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
99
210
  if (this.#extractModulesFromArray(preloadsArray).includes(modulePath)) return this;
211
+ /**
212
+ * Add the preloaded file to the array
213
+ */
100
214
  preloadsArray.addElement(this.#buildNewModuleEntry(modulePath, environments));
101
215
  return this;
102
216
  }
217
+ /**
218
+ * Add a new provider to the rcFile
219
+ *
220
+ * @param providerPath - The path to the provider file
221
+ * @param environments - Optional array of environments where this provider should run
222
+ * @returns This RcFileTransformer instance for method chaining
223
+ */
103
224
  addProvider(providerPath, environments) {
104
225
  const providersArray = this.#getPropertyAssignmentInDefineConfigCall("providers", "[]").getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
105
226
  if (this.#extractModulesFromArray(providersArray).includes(providerPath)) return this;
227
+ /**
228
+ * Add the provider to the array
229
+ */
106
230
  providersArray.addElement(this.#buildNewModuleEntry(providerPath, environments));
107
231
  return this;
108
232
  }
233
+ /**
234
+ * Add a new meta file to the rcFile
235
+ *
236
+ * @param globPattern - The glob pattern for the meta file
237
+ * @param reloadServer - Whether the server should reload when this file changes
238
+ * @returns This RcFileTransformer instance for method chaining
239
+ */
109
240
  addMetaFile(globPattern, reloadServer = false) {
110
241
  const metaFilesArray = this.#getPropertyAssignmentInDefineConfigCall("metaFiles", "[]").getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
111
242
  if (this.#extractPropertyFromArray(metaFilesArray, "pattern").includes(globPattern)) return this;
243
+ /**
244
+ * Add the meta file to the array
245
+ */
112
246
  metaFilesArray.addElement(`{
113
247
  pattern: '${globPattern}',
114
248
  reloadServer: ${reloadServer},
115
249
  }`);
116
250
  return this;
117
251
  }
252
+ /**
253
+ * Set directory name and path in the directories configuration
254
+ *
255
+ * @param key - The directory key (e.g., 'controllers', 'models')
256
+ * @param value - The directory path
257
+ * @returns This RcFileTransformer instance for method chaining
258
+ */
118
259
  setDirectory(key, value) {
119
260
  this.#getPropertyAssignmentInDefineConfigCall("directories", "{}").getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression).addPropertyAssignment({
120
261
  name: key,
@@ -122,6 +263,13 @@ export default defineConfig({
122
263
  });
123
264
  return this;
124
265
  }
266
+ /**
267
+ * Set command alias in the command aliases configuration
268
+ *
269
+ * @param alias - The alias name
270
+ * @param command - The full command name
271
+ * @returns This RcFileTransformer instance for method chaining
272
+ */
125
273
  setCommandAlias(alias, command) {
126
274
  this.#getPropertyAssignmentInDefineConfigCall("commandsAliases", "{}").getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression).addPropertyAssignment({
127
275
  name: alias,
@@ -129,9 +277,20 @@ export default defineConfig({
129
277
  });
130
278
  return this;
131
279
  }
280
+ /**
281
+ * Add a new test suite to the rcFile
282
+ *
283
+ * @param suiteName - The name of the test suite
284
+ * @param files - File patterns for the test suite (string or array)
285
+ * @param timeout - Optional timeout in milliseconds (defaults to 2000)
286
+ * @returns This RcFileTransformer instance for method chaining
287
+ */
132
288
  addSuite(suiteName, files, timeout) {
133
289
  const suitesArray = this.#getPropertyAssignmentInDefineConfigCall("tests", `{ suites: [], forceExit: true, timeout: 2000 }`).getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression).getPropertyOrThrow("suites").getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
134
290
  if (this.#extractPropertyFromArray(suitesArray, "name").includes(suiteName)) return this;
291
+ /**
292
+ * Add the suite to the array
293
+ */
135
294
  const filesArray = Array.isArray(files) ? files : [files];
136
295
  suitesArray.addElement(`{
137
296
  name: '${suiteName}',
@@ -140,6 +299,15 @@ export default defineConfig({
140
299
  }`);
141
300
  return this;
142
301
  }
302
+ /**
303
+ * Add a new assembler hook
304
+ * The format `thunk` write `() => import(path)`.
305
+ *
306
+ * @param type - The type of hook to add
307
+ * @param value - The path to the hook file or value to write
308
+ * @param raw - Wether to write a thunk import or as raw value
309
+ * @returns This RcFileTransformer instance for method chaining
310
+ */
143
311
  addAssemblerHook(type, value, raw = false) {
144
312
  const hooks = this.#getPropertyAssignmentInDefineConfigCall("hooks", "{}").getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
145
313
  let hookArray = hooks.getProperty(type);
@@ -158,6 +326,13 @@ export default defineConfig({
158
326
  }
159
327
  return this;
160
328
  }
329
+ /**
330
+ * Add a named import
331
+ *
332
+ * @param specifier - The module specifier to import
333
+ * @param names - Names to import from the module
334
+ * @returns This RcFileTransformer instance for method chaining
335
+ */
161
336
  addNamedImport(specifier, names) {
162
337
  this.#getRcFileOrThrow().addImportDeclaration({
163
338
  moduleSpecifier: specifier,
@@ -165,6 +340,13 @@ export default defineConfig({
165
340
  });
166
341
  return this;
167
342
  }
343
+ /**
344
+ * Add a default import
345
+ *
346
+ * @param specifier - The module specifier to import
347
+ * @param name - Name of the default import
348
+ * @returns This RcFileTransformer instance for method chaining
349
+ */
168
350
  addDefaultImport(specifier, name) {
169
351
  this.#getRcFileOrThrow().addImportDeclaration({
170
352
  moduleSpecifier: specifier,
@@ -172,6 +354,13 @@ export default defineConfig({
172
354
  });
173
355
  return this;
174
356
  }
357
+ /**
358
+ * Get a directory value from the directories configuration.
359
+ *
360
+ * @param key - The directory key to retrieve
361
+ * @param defaultValue - The default value if not configured
362
+ * @returns The configured directory path or the default value
363
+ */
175
364
  getDirectory(key, defaultValue) {
176
365
  try {
177
366
  const file = this.#getRcFileOrThrow();
@@ -189,18 +378,62 @@ export default defineConfig({
189
378
  return defaultValue;
190
379
  }
191
380
  }
381
+ /**
382
+ * Save the adonisrc.ts file with all applied transformations
383
+ *
384
+ * Formats the file according to editor settings and saves it to disk.
385
+ *
386
+ * @returns Promise that resolves when the file is saved
387
+ */
192
388
  save() {
193
389
  const file = this.#getRcFileOrThrow();
194
390
  file.formatText(this.#editorSettings);
195
391
  return file.save();
196
392
  }
197
393
  };
394
+ //#endregion
395
+ //#region src/code_transformer/main.ts
396
+ /**
397
+ * This class is responsible for transforming AdonisJS project code,
398
+ * including updating middleware, environment validations, and other
399
+ * code generation tasks.
400
+ *
401
+ * The CodeTransformer provides methods for modifying various AdonisJS
402
+ * configuration files and code structures using AST manipulation through
403
+ * ts-morph. It can update middleware stacks, add environment validations,
404
+ * register plugins, and modify RC file configurations.
405
+ *
406
+ * @example
407
+ * const transformer = new CodeTransformer(cwd)
408
+ * await transformer.addMiddlewareToStack('server', [{
409
+ * path: '#middleware/cors_middleware',
410
+ * position: 'before'
411
+ * }])
412
+ */
198
413
  var CodeTransformer = class {
414
+ /**
415
+ * Utility function for installing packages
416
+ */
199
417
  installPackage = installPackage;
418
+ /**
419
+ * Utility function for detecting the package manager
420
+ */
200
421
  detectPackageManager = detectPackageManager;
422
+ /**
423
+ * Directory of the adonisjs project
424
+ */
201
425
  #cwd;
426
+ /**
427
+ * String path version of the current working directory
428
+ */
202
429
  #cwdPath;
430
+ /**
431
+ * The TsMorph project instance for AST manipulation
432
+ */
203
433
  project;
434
+ /**
435
+ * Settings to use when persisting files
436
+ */
204
437
  #editorSettings = {
205
438
  indentSize: 2,
206
439
  convertTabsToSpaces: true,
@@ -209,6 +442,11 @@ var CodeTransformer = class {
209
442
  indentStyle: 2,
210
443
  semicolons: "remove"
211
444
  };
445
+ /**
446
+ * Create a new CodeTransformer instance
447
+ *
448
+ * @param cwd - The current working directory URL
449
+ */
212
450
  constructor(cwd) {
213
451
  this.#cwd = cwd;
214
452
  this.#cwdPath = fileURLToPath(this.#cwd);
@@ -217,6 +455,14 @@ var CodeTransformer = class {
217
455
  manipulationSettings: { quoteKind: QuoteKind.Single }
218
456
  });
219
457
  }
458
+ /**
459
+ * Get directories configured in adonisrc.ts, with defaults fallback.
460
+ *
461
+ * This method reads the adonisrc.ts file and extracts the directories
462
+ * configuration. If a directory is not configured, the default value is used.
463
+ *
464
+ * @returns Object containing directory paths
465
+ */
220
466
  getDirectories() {
221
467
  const rcFileTransformer = new RcFileTransformer(this.#cwd, this.project);
222
468
  return {
@@ -228,15 +474,38 @@ var CodeTransformer = class {
228
474
  controllers: rcFileTransformer.getDirectory("controllers", "app/controllers")
229
475
  };
230
476
  }
477
+ /**
478
+ * Add a new middleware to the middleware array of the given file
479
+ *
480
+ * This method locates middleware stack calls (like server.use or router.use)
481
+ * and adds the middleware entry to the appropriate position in the array.
482
+ *
483
+ * @param file - The source file to modify
484
+ * @param target - The target method call (e.g., 'server.use', 'router.use')
485
+ * @param middlewareEntry - The middleware entry to add
486
+ */
231
487
  #addToMiddlewareArray(file, target, middlewareEntry) {
232
488
  const callExpressions = file.getDescendantsOfKind(SyntaxKind.CallExpression).filter((statement) => statement.getExpression().getText() === target);
233
489
  if (!callExpressions.length) throw new Error(`Cannot find ${target} statement in the file.`);
234
490
  const arrayLiteralExpression = callExpressions[0].getArguments()[0];
235
491
  if (!arrayLiteralExpression || !Node.isArrayLiteralExpression(arrayLiteralExpression)) throw new Error(`Cannot find middleware array in ${target} statement.`);
236
492
  const middleware = `() => import('${middlewareEntry.path}')`;
237
- if (arrayLiteralExpression.getElements().findIndex((element) => element.getText() === middleware) === -1) if (middlewareEntry.position === "before") arrayLiteralExpression.insertElement(0, middleware);
493
+ if (arrayLiteralExpression.getElements().findIndex((element) => element.getText() === middleware) === -1)
494
+ /**
495
+ * Add the middleware to the top or bottom of the array
496
+ */
497
+ if (middlewareEntry.position === "before") arrayLiteralExpression.insertElement(0, middleware);
238
498
  else arrayLiteralExpression.addElement(middleware);
239
499
  }
500
+ /**
501
+ * Add a new middleware to the named middleware of the given file
502
+ *
503
+ * This method adds middleware entries to the named middleware object,
504
+ * typically used for route-specific middleware registration.
505
+ *
506
+ * @param file - The source file to modify
507
+ * @param middlewareEntry - The middleware entry to add (must have a name)
508
+ */
240
509
  #addToNamedMiddleware(file, middlewareEntry) {
241
510
  if (!middlewareEntry.name) throw new Error("Named middleware requires a name.");
242
511
  const callArguments = file.getVariableDeclarationOrThrow("middleware").getInitializerIfKindOrThrow(SyntaxKind.CallExpression).getArguments();
@@ -244,10 +513,22 @@ var CodeTransformer = class {
244
513
  const namedMiddlewareObject = callArguments[0];
245
514
  if (!Node.isObjectLiteralExpression(namedMiddlewareObject)) throw new Error("The argument of the named middleware call is not an object literal.");
246
515
  if (!namedMiddlewareObject.getProperty(middlewareEntry.name)) {
516
+ /**
517
+ * Add the named middleware
518
+ */
247
519
  const middleware = `${middlewareEntry.name}: () => import('${middlewareEntry.path}')`;
248
520
  namedMiddlewareObject.insertProperty(0, middleware);
249
521
  }
250
522
  }
523
+ /**
524
+ * Add a policy to the list of pre-registered policies
525
+ *
526
+ * This method adds bouncer policy entries to the policies object,
527
+ * allowing them to be used in route authorization.
528
+ *
529
+ * @param file - The source file to modify
530
+ * @param policyEntry - The policy entry to add
531
+ */
251
532
  #addToPoliciesList(file, policyEntry) {
252
533
  const policiesObject = file.getVariableDeclarationOrThrow("policies").getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
253
534
  if (!policiesObject.getProperty(policyEntry.name)) {
@@ -255,13 +536,26 @@ var CodeTransformer = class {
255
536
  policiesObject.insertProperty(0, policy);
256
537
  }
257
538
  }
539
+ /**
540
+ * Add the given import declarations to the source file
541
+ * and merge named imports with the existing import
542
+ */
258
543
  #addImportDeclarations(file, importDeclarations) {
259
544
  importDeclarations.forEach((importDeclaration) => {
260
545
  const existingImport = file.getImportDeclarations().find((mod) => mod.getModuleSpecifierValue() === importDeclaration.module);
546
+ /**
547
+ * Add a new named import to existing import for the
548
+ * same module
549
+ */
261
550
  if (existingImport && importDeclaration.isNamed) {
262
551
  if (!existingImport.getNamedImports().find((namedImport) => namedImport.getName() === importDeclaration.identifier)) existingImport.addNamedImport(importDeclaration.identifier);
263
552
  return;
264
553
  }
554
+ /**
555
+ * Ignore default import when the same module is already imported.
556
+ * The chances are the existing default import and the importDeclaration
557
+ * identifiers are not the same. But we should not modify existing source
558
+ */
265
559
  if (existingImport) return;
266
560
  file.addImportDeclaration({
267
561
  ...importDeclaration.isNamed ? { namedImports: [importDeclaration.identifier] } : { defaultImport: importDeclaration.identifier },
@@ -269,6 +563,9 @@ var CodeTransformer = class {
269
563
  });
270
564
  });
271
565
  }
566
+ /**
567
+ * Convert ImportInfo array to import declarations and add them to the file
568
+ */
272
569
  #addImportsFromImportInfo(file, imports) {
273
570
  const importsBag = new ImportsBag();
274
571
  for (const importInfo of imports) importsBag.add(importInfo);
@@ -287,23 +584,50 @@ var CodeTransformer = class {
287
584
  });
288
585
  this.#addImportDeclarations(file, importDeclarations);
289
586
  }
587
+ /**
588
+ * Write a leading comment
589
+ */
290
590
  #addLeadingComment(writer, comment) {
291
591
  if (!comment) return writer.blankLine();
292
592
  return writer.blankLine().writeLine("/*").writeLine(`|----------------------------------------------------------`).writeLine(`| ${comment}`).writeLine(`|----------------------------------------------------------`).writeLine(`*/`);
293
593
  }
594
+ /**
595
+ * Add new env variable validation in the `env.ts` file
596
+ *
597
+ * @param definition - Environment validation definition containing variables and comment
598
+ */
294
599
  async defineEnvValidations(definition) {
295
600
  const filePath = `${this.getDirectories().start}/env.ts`;
601
+ /**
602
+ * Get the env.ts source file
603
+ */
296
604
  const envUrl = join(this.#cwdPath, `./${filePath}`);
297
605
  const file = this.project.getSourceFile(envUrl);
298
606
  if (!file) throw CodemodException.missingEnvFile(filePath, definition);
607
+ /**
608
+ * Get the `Env.create` call expression
609
+ */
299
610
  const callExpressions = file.getDescendantsOfKind(SyntaxKind.CallExpression).filter((statement) => statement.getExpression().getText() === "Env.create");
300
611
  if (!callExpressions.length) throw CodemodException.missingEnvCreate(filePath, definition);
301
612
  const objectLiteralExpression = callExpressions[0].getArguments()[1];
302
613
  if (!Node.isObjectLiteralExpression(objectLiteralExpression)) throw CodemodException.invalidEnvCreate(filePath, definition);
303
614
  let shouldAddComment = true;
615
+ /**
616
+ * Add each variable validation
617
+ */
304
618
  for (const [variable, validation] of Object.entries(definition.variables)) {
619
+ /**
620
+ * Check if the variable is already defined. If so, remove it
621
+ */
305
622
  const existingProperty = objectLiteralExpression.getProperty(variable);
623
+ /**
624
+ * Do not add leading comment if one or more properties
625
+ * already exists
626
+ */
306
627
  if (existingProperty) shouldAddComment = false;
628
+ /**
629
+ * Add property only when the property does not exist
630
+ */
307
631
  if (!existingProperty) objectLiteralExpression.addPropertyAssignment({
308
632
  name: variable,
309
633
  initializer: validation,
@@ -317,11 +641,27 @@ var CodeTransformer = class {
317
641
  file.formatText(this.#editorSettings);
318
642
  await file.save();
319
643
  }
644
+ /**
645
+ * Define new middlewares inside the `start/kernel.ts` file
646
+ *
647
+ * This function is highly based on some assumptions
648
+ * and will not work if you significantly tweaked
649
+ * your `start/kernel.ts` file.
650
+ *
651
+ * @param stack - The middleware stack to add to ('server', 'router', or 'named')
652
+ * @param middleware - Array of middleware entries to add
653
+ */
320
654
  async addMiddlewareToStack(stack, middleware) {
321
655
  const filePath = `${this.getDirectories().start}/kernel.ts`;
656
+ /**
657
+ * Get the kernel.ts source file
658
+ */
322
659
  const kernelUrl = join(this.#cwdPath, `./${filePath}`);
323
660
  const file = this.project.getSourceFile(kernelUrl);
324
661
  if (!file) throw CodemodException.missingKernelFile(filePath, stack, middleware);
662
+ /**
663
+ * Process each middleware entry
664
+ */
325
665
  try {
326
666
  for (const middlewareEntry of middleware) if (stack === "named") this.#addToNamedMiddleware(file, middlewareEntry);
327
667
  else this.#addToMiddlewareArray(file, `${stack}.use`, middlewareEntry);
@@ -332,35 +672,81 @@ var CodeTransformer = class {
332
672
  file.formatText(this.#editorSettings);
333
673
  await file.save();
334
674
  }
675
+ /**
676
+ * Update the `adonisrc.ts` file using the provided callback
677
+ *
678
+ * @param callback - Function that receives the RcFileTransformer for modifications
679
+ */
335
680
  async updateRcFile(callback) {
336
681
  const rcFileTransformer = new RcFileTransformer(this.#cwd, this.project);
337
682
  callback(rcFileTransformer);
338
683
  await rcFileTransformer.save();
339
684
  }
685
+ /**
686
+ * Add a new Japa plugin in the `tests/bootstrap.ts` file
687
+ *
688
+ * @param pluginCall - The plugin function call to add
689
+ * @param importDeclarations - Import declarations needed for the plugin
690
+ */
340
691
  async addJapaPlugin(pluginCall, importDeclarations) {
341
692
  const filePath = `${this.getDirectories().tests}/bootstrap.ts`;
693
+ /**
694
+ * Get the bootstrap.ts source file
695
+ */
342
696
  const testBootstrapUrl = join(this.#cwdPath, `./${filePath}`);
343
697
  const file = this.project.getSourceFile(testBootstrapUrl);
344
698
  if (!file) throw CodemodException.missingJapaBootstrap(filePath, pluginCall, importDeclarations);
699
+ /**
700
+ * Add the import declarations
701
+ */
345
702
  this.#addImportDeclarations(file, importDeclarations);
703
+ /**
704
+ * Insert the plugin call in the `plugins` array
705
+ */
346
706
  const pluginsArray = file.getVariableDeclaration("plugins")?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
707
+ /**
708
+ * Add plugin call to the plugins array
709
+ */
347
710
  if (pluginsArray) {
348
711
  if (!pluginsArray.getElements().find((element) => element.getText() === pluginCall)) pluginsArray.addElement(pluginCall);
349
712
  }
350
713
  file.formatText(this.#editorSettings);
351
714
  await file.save();
352
715
  }
716
+ /**
717
+ * Add a new Vite plugin to the `vite.config.ts` file
718
+ *
719
+ * @param pluginCall - The plugin function call to add
720
+ * @param importDeclarations - Import declarations needed for the plugin
721
+ */
353
722
  async addVitePlugin(pluginCall, importDeclarations) {
354
723
  const filePath = "vite.config.ts";
724
+ /**
725
+ * Get the `vite.config.ts` source file
726
+ */
355
727
  const viteConfigTsUrl = join(this.#cwdPath, `./${filePath}`);
356
728
  const file = this.project.getSourceFile(viteConfigTsUrl);
357
729
  if (!file) throw CodemodException.missingViteConfig(filePath, pluginCall, importDeclarations);
358
730
  try {
731
+ /**
732
+ * Add the import declarations
733
+ */
359
734
  this.#addImportDeclarations(file, importDeclarations);
735
+ /**
736
+ * Get the default export options
737
+ */
360
738
  const defaultExport = file.getDefaultExportSymbol();
361
739
  if (!defaultExport) throw new Error("Cannot find the default export in vite.config.ts");
740
+ /**
741
+ * Get the options object
742
+ * - Either the first argument of `defineConfig` call : `export default defineConfig({})`
743
+ * - Or child literal expression of the default export : `export default {}`
744
+ */
362
745
  const declaration = defaultExport.getDeclarations()[0];
363
746
  const pluginsArray = (declaration.getChildrenOfKind(SyntaxKind.ObjectLiteralExpression)[0] || declaration.getChildrenOfKind(SyntaxKind.CallExpression)[0].getArguments()[0]).getPropertyOrThrow("plugins").getFirstChildByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
747
+ /**
748
+ * Add plugin call to the plugins array
749
+ */
364
750
  if (!pluginsArray.getElements().find((element) => element.getText() === pluginCall)) pluginsArray.addElement(pluginCall);
365
751
  } catch (error) {
366
752
  if (error instanceof CodemodException) throw error;
@@ -370,11 +756,23 @@ var CodeTransformer = class {
370
756
  file.formatText(this.#editorSettings);
371
757
  await file.save();
372
758
  }
759
+ /**
760
+ * Adds a policy to the list of `policies` object configured
761
+ * inside the `app/policies/main.ts` file.
762
+ *
763
+ * @param policies - Array of bouncer policy entries to add
764
+ */
373
765
  async addPolicies(policies) {
374
766
  const filePath = `${this.getDirectories().policies}/main.ts`;
767
+ /**
768
+ * Get the policies/main.ts source file
769
+ */
375
770
  const policiesUrl = join(this.#cwdPath, `./${filePath}`);
376
771
  const file = this.project.getSourceFile(policiesUrl);
377
772
  if (!file) throw CodemodException.missingPoliciesFile(filePath, policies);
773
+ /**
774
+ * Process each policy entry
775
+ */
378
776
  try {
379
777
  for (const policy of policies) this.#addToPoliciesList(file, policy);
380
778
  } catch (error) {
@@ -386,11 +784,20 @@ var CodeTransformer = class {
386
784
  }
387
785
  async addValidator(definition) {
388
786
  const filePath = `${this.getDirectories().validators}/${definition.validatorFileName}`;
787
+ /**
788
+ * Get the validator file URL
789
+ */
389
790
  const validatorFileUrl = join(this.#cwdPath, `./${filePath}`);
390
791
  let file = this.project.getSourceFile(validatorFileUrl);
792
+ /**
793
+ * Try to load the file from disk if not already in the project
794
+ */
391
795
  if (!file) try {
392
796
  file = this.project.addSourceFileAtPath(validatorFileUrl);
393
797
  } catch {}
798
+ /**
799
+ * If the file does not exist, create it
800
+ */
394
801
  if (!file) {
395
802
  file = this.project.createSourceFile(validatorFileUrl, definition.contents);
396
803
  file.formatText(this.#editorSettings);
@@ -398,17 +805,29 @@ var CodeTransformer = class {
398
805
  return;
399
806
  }
400
807
  if (file.getVariableDeclaration(definition.exportName)) return;
808
+ /**
809
+ * Add the validator to the existing file
810
+ */
401
811
  file.addStatements(`\n${definition.contents}`);
402
812
  file.formatText(this.#editorSettings);
403
813
  await file.save();
404
814
  }
405
815
  async addLimiter(definition) {
406
816
  const filePath = `${this.getDirectories().start}/${definition.limiterFileName}`;
817
+ /**
818
+ * Get the limiter file URL
819
+ */
407
820
  const limiterFileUrl = join(this.#cwdPath, `./${filePath}`);
408
821
  let file = this.project.getSourceFile(limiterFileUrl);
822
+ /**
823
+ * Try to load the file from disk if not already in the project
824
+ */
409
825
  if (!file) try {
410
826
  file = this.project.addSourceFileAtPath(limiterFileUrl);
411
827
  } catch {}
828
+ /**
829
+ * If the file does not exist, create it
830
+ */
412
831
  if (!file) {
413
832
  file = this.project.createSourceFile(limiterFileUrl, definition.contents);
414
833
  file.formatText(this.#editorSettings);
@@ -416,25 +835,40 @@ var CodeTransformer = class {
416
835
  return;
417
836
  }
418
837
  if (file.getVariableDeclaration(definition.exportName)) return;
838
+ /**
839
+ * Add the limiter to the existing file
840
+ */
419
841
  file.addStatements(`\n${definition.contents}`);
420
842
  file.formatText(this.#editorSettings);
421
843
  await file.save();
422
844
  }
423
845
  async addModelMixins(modelFileName, mixins) {
424
846
  const filePath = `${this.getDirectories().models}/${modelFileName}`;
847
+ /**
848
+ * Get the model file URL
849
+ */
425
850
  const modelFileUrl = join(this.#cwdPath, `./${filePath}`);
426
851
  let file = this.project.getSourceFile(modelFileUrl);
852
+ /**
853
+ * Try to load the file from disk if not already in the project
854
+ */
427
855
  if (!file) try {
428
856
  file = this.project.addSourceFileAtPath(modelFileUrl);
429
857
  } catch {
430
858
  throw new Error(`Could not find source file at path: "${filePath}"`);
431
859
  }
860
+ /**
861
+ * Get the default export class declaration
862
+ */
432
863
  const defaultExportSymbol = file.getDefaultExportSymbol();
433
864
  if (!defaultExportSymbol) throw new Error(`Could not find default export in "${filePath}". The model must have a default export class.`);
434
865
  const declarations = defaultExportSymbol.getDeclarations();
435
866
  if (declarations.length === 0) throw new Error(`Could not find default export declaration in "${filePath}".`);
436
867
  const declaration = declarations[0];
437
868
  if (!Node.isClassDeclaration(declaration)) throw new Error(`Default export in "${filePath}" is not a class. The model must be exported as a class.`);
869
+ /**
870
+ * Add import declarations for the mixins
871
+ */
438
872
  const mixinImports = mixins.map((mixin) => {
439
873
  if (mixin.importType === "named") return {
440
874
  source: mixin.importPath,
@@ -446,21 +880,40 @@ var CodeTransformer = class {
446
880
  };
447
881
  });
448
882
  this.#addImportsFromImportInfo(file, mixinImports);
883
+ /**
884
+ * Get the heritage clause (extends clause)
885
+ */
449
886
  const heritageClause = declaration.getHeritageClauseByKind(SyntaxKind.ExtendsKeyword);
450
887
  if (!heritageClause) throw new Error(`Could not find extends clause in "${filePath}".`);
451
888
  const extendsExpression = heritageClause.getTypeNodes()[0];
452
889
  if (!extendsExpression) throw new Error(`Could not find extends expression in "${filePath}".`);
890
+ /**
891
+ * Get the expression that the class extends
892
+ */
453
893
  const extendsExpressionNode = extendsExpression.getExpression();
894
+ /**
895
+ * Check if the class already uses compose
896
+ */
454
897
  let composeCall;
455
898
  if (Node.isCallExpression(extendsExpressionNode)) {
456
899
  if (extendsExpressionNode.getExpression().getText() === "compose") composeCall = extendsExpressionNode;
457
900
  }
901
+ /**
902
+ * Build the mixin calls
903
+ */
458
904
  const mixinCalls = mixins.map((mixin) => {
459
905
  const args = mixin.args && mixin.args.length > 0 ? mixin.args.join(", ") : "";
460
906
  return `${mixin.name}(${args})`;
461
907
  });
908
+ /**
909
+ * If the class is already using compose, add the mixins to the compose call
910
+ */
462
911
  if (composeCall && Node.isCallExpression(composeCall)) {
463
912
  const existingArgsText = composeCall.getArguments().map((arg) => arg.getText());
913
+ /**
914
+ * Filter out mixins that are already applied by checking if a call
915
+ * to the mixin function already exists in the compose arguments
916
+ */
464
917
  const newMixinCalls = mixinCalls.filter((mixinCall) => {
465
918
  const mixinFunctionName = mixinCall.split("(")[0];
466
919
  return !existingArgsText.some((existingArg) => {
@@ -470,6 +923,10 @@ var CodeTransformer = class {
470
923
  const newArgs = [...existingArgsText, ...newMixinCalls];
471
924
  composeCall.replaceWithText(`compose(${newArgs.join(", ")})`);
472
925
  } else {
926
+ /**
927
+ * If the class is not using compose, wrap the extends expression in compose
928
+ * and add import for compose
929
+ */
473
930
  this.#addImportDeclarations(file, [{
474
931
  isNamed: true,
475
932
  module: "@adonisjs/core/helpers",
@@ -483,21 +940,36 @@ var CodeTransformer = class {
483
940
  }
484
941
  async addControllerMethod(definition) {
485
942
  const filePath = `${this.getDirectories().controllers}/${definition.controllerFileName}`;
943
+ /**
944
+ * Get the controller file URL
945
+ */
486
946
  const controllerFileUrl = join(this.#cwdPath, `./${filePath}`);
487
947
  let file = this.project.getSourceFile(controllerFileUrl);
948
+ /**
949
+ * Try to load the file from disk if not already in the project
950
+ */
488
951
  if (!file) try {
489
952
  file = this.project.addSourceFileAtPath(controllerFileUrl);
490
953
  } catch {}
954
+ /**
955
+ * If the file does not exist, create it with the controller class and method
956
+ */
491
957
  if (!file) {
492
958
  const contents = `export default class ${definition.className} {
493
959
  ${definition.contents}
494
960
  }`;
495
961
  file = this.project.createSourceFile(controllerFileUrl, contents);
962
+ /**
963
+ * Add imports if specified
964
+ */
496
965
  if (definition.imports) this.#addImportsFromImportInfo(file, definition.imports);
497
966
  file.formatText(this.#editorSettings);
498
967
  await file.save();
499
968
  return;
500
969
  }
970
+ /**
971
+ * Get the default export class declaration
972
+ */
501
973
  const defaultExportSymbol = file.getDefaultExportSymbol();
502
974
  if (!defaultExportSymbol) throw new Error(`Could not find default export in "${filePath}". The controller must have a default export class.`);
503
975
  const declarations = defaultExportSymbol.getDeclarations();
@@ -505,10 +977,17 @@ var CodeTransformer = class {
505
977
  const declaration = declarations[0];
506
978
  if (!Node.isClassDeclaration(declaration)) throw new Error(`Default export in "${filePath}" is not a class. The controller must be exported as a class.`);
507
979
  if (declaration.getMethod(definition.name)) return;
980
+ /**
981
+ * Add imports if specified
982
+ */
508
983
  if (definition.imports) this.#addImportsFromImportInfo(file, definition.imports);
984
+ /**
985
+ * Add the method to the class by inserting the raw method text
986
+ */
509
987
  declaration.addMember(definition.contents);
510
988
  file.formatText(this.#editorSettings);
511
989
  await file.save();
512
990
  }
513
991
  };
992
+ //#endregion
514
993
  export { CodeTransformer };