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