@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.
- package/README.md +260 -0
- package/build/chunk-DF48asd8.js +9 -0
- package/build/codemod_exception-BMNJZ0i1.js +280 -0
- package/build/index.d.ts +1 -1
- package/build/index.js +1854 -1724
- package/build/main-Cpfvmdw6.js +562 -0
- package/build/main-INOi9swJ.js +471 -0
- package/build/src/bundler.d.ts +2 -0
- package/build/src/code_scanners/routes_scanner/main.d.ts +16 -2
- package/build/src/code_scanners/routes_scanner/main.js +4 -445
- package/build/src/code_transformer/main.d.ts +14 -1
- package/build/src/code_transformer/main.js +981 -622
- package/build/src/code_transformer/rc_file_transformer.d.ts +28 -2
- package/build/src/debug.d.ts +1 -1
- package/build/src/dev_server.d.ts +60 -12
- package/build/src/exceptions/codemod_exception.d.ts +178 -0
- package/build/src/file_buffer.d.ts +19 -0
- package/build/src/file_system.d.ts +3 -3
- package/build/src/helpers.js +205 -16
- package/build/src/index_generator/main.js +4 -7
- package/build/src/paths_resolver.d.ts +2 -1
- package/build/src/test_runner.d.ts +3 -2
- package/build/src/types/code_scanners.d.ts +29 -13
- package/build/src/types/code_transformer.d.ts +127 -0
- package/build/src/types/common.d.ts +98 -2
- package/build/src/types/hooks.d.ts +4 -1
- package/build/src/types/main.js +2 -0
- package/build/src/utils.d.ts +7 -3
- package/build/src/virtual_file_system.d.ts +1 -1
- package/build/virtual_file_system-dzfXNwEp.js +572 -0
- package/package.json +41 -39
- package/build/chunk-7XU453QB.js +0 -418
- package/build/chunk-PORDZS62.js +0 -391
- package/build/chunk-TIKQQRMX.js +0 -116
- package/build/src/hooks.d.ts +0 -224
|
@@ -1,634 +1,993 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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 };
|