@adonisjs/assembler 6.1.3-17 → 6.1.3-18

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.
@@ -0,0 +1,28 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { RcFileTransformer } from './rc_file_transformer.js';
3
+ import type { AddMiddlewareEntry, EnvValidationDefinition } from '../types.js';
4
+ /**
5
+ * This class is responsible for updating
6
+ */
7
+ export declare class CodeTransformer {
8
+ #private;
9
+ constructor(cwd: URL);
10
+ /**
11
+ * Update the `adonisrc.ts` file
12
+ */
13
+ updateRcFile(callback: (transformer: RcFileTransformer) => void): Promise<void>;
14
+ /**
15
+ * Define new middlewares inside the `start/kernel.ts`
16
+ * file
17
+ *
18
+ * This function is highly based on some assumptions
19
+ * and will not work if you significantly tweaked
20
+ * your `start/kernel.ts` file.
21
+ */
22
+ addMiddlewareToStack(stack: 'server' | 'router' | 'named', middleware: AddMiddlewareEntry[]): Promise<void>;
23
+ /**
24
+ * Add new env variable validation in the
25
+ * `env.ts` file
26
+ */
27
+ defineEnvValidations(definition: EnvValidationDefinition): Promise<void>;
28
+ }
@@ -0,0 +1,166 @@
1
+ /*
2
+ * @adonisjs/assembler
3
+ *
4
+ * (c) AdonisJS
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+ import { join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { Node, Project, SyntaxKind } from 'ts-morph';
12
+ import { RcFileTransformer } from './rc_file_transformer.js';
13
+ /**
14
+ * This class is responsible for updating
15
+ */
16
+ export class CodeTransformer {
17
+ /**
18
+ * Directory of the adonisjs project
19
+ */
20
+ #cwd;
21
+ /**
22
+ * The TsMorph project
23
+ */
24
+ #project;
25
+ constructor(cwd) {
26
+ this.#cwd = cwd;
27
+ this.#project = new Project({
28
+ tsConfigFilePath: join(fileURLToPath(this.#cwd), 'tsconfig.json'),
29
+ });
30
+ }
31
+ /**
32
+ * Update the `adonisrc.ts` file
33
+ */
34
+ async updateRcFile(callback) {
35
+ const rcFileTransformer = new RcFileTransformer(this.#cwd, this.#project);
36
+ callback(rcFileTransformer);
37
+ await rcFileTransformer.save();
38
+ }
39
+ /**
40
+ * Add a new middleware to the middleware array of the
41
+ * given file
42
+ */
43
+ #addToMiddlewareArray(file, target, middlewareEntry) {
44
+ const callExpressions = file
45
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
46
+ .filter((statement) => statement.getExpression().getText() === target);
47
+ if (!callExpressions.length) {
48
+ throw new Error(`Cannot find ${target} statement in the file.`);
49
+ }
50
+ const arrayLiteralExpression = callExpressions[0].getArguments()[0];
51
+ if (!arrayLiteralExpression || !Node.isArrayLiteralExpression(arrayLiteralExpression)) {
52
+ throw new Error(`Cannot find middleware array in ${target} statement.`);
53
+ }
54
+ const middleware = `() => import('${middlewareEntry.path}')`;
55
+ if (middlewareEntry.position === 'before') {
56
+ arrayLiteralExpression.insertElement(0, middleware);
57
+ }
58
+ else {
59
+ arrayLiteralExpression.addElement(middleware);
60
+ }
61
+ }
62
+ /**
63
+ * Add a new middleware to the named middleware of the given file
64
+ */
65
+ #addToNamedMiddleware(file, middlewareEntry) {
66
+ if (!middlewareEntry.name)
67
+ throw new Error('Named middleware requires a name.');
68
+ const callArguments = file
69
+ .getVariableDeclarationOrThrow('middleware')
70
+ .getInitializerIfKindOrThrow(SyntaxKind.CallExpression)
71
+ .getArguments();
72
+ if (callArguments.length === 0) {
73
+ throw new Error('Named middleware call has no arguments.');
74
+ }
75
+ const namedMiddlewareObject = callArguments[0];
76
+ if (!Node.isObjectLiteralExpression(namedMiddlewareObject)) {
77
+ throw new Error('The argument of the named middleware call is not an object literal.');
78
+ }
79
+ const middleware = `${middlewareEntry.name}: () => import('${middlewareEntry.path}')`;
80
+ namedMiddlewareObject.insertProperty(0, middleware);
81
+ }
82
+ /**
83
+ * Write a leading comment
84
+ */
85
+ #addLeadingComment(writer, comment) {
86
+ if (!comment)
87
+ return writer.blankLine();
88
+ return writer
89
+ .blankLine()
90
+ .writeLine('/*')
91
+ .writeLine(`|----------------------------------------------------------`)
92
+ .writeLine(`| ${comment}`)
93
+ .writeLine(`|----------------------------------------------------------`)
94
+ .writeLine(`*/`);
95
+ }
96
+ /**
97
+ * Define new middlewares inside the `start/kernel.ts`
98
+ * file
99
+ *
100
+ * This function is highly based on some assumptions
101
+ * and will not work if you significantly tweaked
102
+ * your `start/kernel.ts` file.
103
+ */
104
+ async addMiddlewareToStack(stack, middleware) {
105
+ /**
106
+ * Get the `start/kernel.ts` source file
107
+ */
108
+ const kernelUrl = fileURLToPath(new URL('./start/kernel.ts', this.#cwd));
109
+ const file = this.#project.getSourceFileOrThrow(kernelUrl);
110
+ /**
111
+ * Process each middleware entry
112
+ */
113
+ for (const middlewareEntry of middleware) {
114
+ if (stack === 'named') {
115
+ this.#addToNamedMiddleware(file, middlewareEntry);
116
+ }
117
+ else {
118
+ this.#addToMiddlewareArray(file, `${stack}.use`, middlewareEntry);
119
+ }
120
+ }
121
+ file.formatText();
122
+ await file.save();
123
+ }
124
+ /**
125
+ * Add new env variable validation in the
126
+ * `env.ts` file
127
+ */
128
+ async defineEnvValidations(definition) {
129
+ /**
130
+ * Get the `start/env.ts` source file
131
+ */
132
+ const kernelUrl = fileURLToPath(new URL('./start/env.ts', this.#cwd));
133
+ const file = this.#project.getSourceFileOrThrow(kernelUrl);
134
+ /**
135
+ * Get the `Env.create` call expression
136
+ */
137
+ const callExpressions = file
138
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
139
+ .filter((statement) => statement.getExpression().getText() === 'Env.create');
140
+ if (!callExpressions.length) {
141
+ throw new Error(`Cannot find Env.create statement in the file.`);
142
+ }
143
+ const objectLiteralExpression = callExpressions[0].getArguments()[1];
144
+ if (!Node.isObjectLiteralExpression(objectLiteralExpression)) {
145
+ throw new Error(`The second argument of Env.create is not an object literal.`);
146
+ }
147
+ let firstAdded = false;
148
+ /**
149
+ * Add each variable validation
150
+ */
151
+ for (const [variable, validation] of Object.entries(definition.variables)) {
152
+ objectLiteralExpression.addPropertyAssignment({
153
+ name: variable,
154
+ initializer: validation,
155
+ leadingTrivia: (writer) => {
156
+ if (firstAdded)
157
+ return;
158
+ firstAdded = true;
159
+ return this.#addLeadingComment(writer, definition.leadingComment);
160
+ },
161
+ });
162
+ }
163
+ file.formatText();
164
+ await file.save();
165
+ }
166
+ }
@@ -0,0 +1,43 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { Project } from 'ts-morph';
3
+ import type { AppEnvironments } from '@adonisjs/application/types';
4
+ /**
5
+ * RcFileTransformer is used to transform the `adonisrc.ts` file
6
+ * for adding new commands, providers, meta files etc
7
+ */
8
+ export declare class RcFileTransformer {
9
+ #private;
10
+ constructor(cwd: URL, project: Project);
11
+ /**
12
+ * Add a new command to the rcFile
13
+ */
14
+ addCommand(commandPath: string): this;
15
+ /**
16
+ * Add a new preloaded file to the rcFile
17
+ */
18
+ addPreloadFile(modulePath: string, environments?: AppEnvironments[]): this;
19
+ /**
20
+ * Add a new provider to the rcFile
21
+ */
22
+ addProvider(providerPath: string, environments?: AppEnvironments[]): this;
23
+ /**
24
+ * Add a new meta file to the rcFile
25
+ */
26
+ addMetaFile(globPattern: string, reloadServer?: boolean): this;
27
+ /**
28
+ * Set directory name and path
29
+ */
30
+ setDirectory(key: string, value: string): this;
31
+ /**
32
+ * Set command alias
33
+ */
34
+ setCommandAlias(alias: string, command: string): this;
35
+ /**
36
+ * Add a new test suite to the rcFile
37
+ */
38
+ addSuite(suiteName: string, files: string | string[], timeout?: number): this;
39
+ /**
40
+ * Save the adonisrc.ts file
41
+ */
42
+ save(): Promise<void>;
43
+ }
@@ -0,0 +1,264 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { Node, SyntaxKind, } from 'ts-morph';
3
+ /**
4
+ * RcFileTransformer is used to transform the `adonisrc.ts` file
5
+ * for adding new commands, providers, meta files etc
6
+ */
7
+ export class RcFileTransformer {
8
+ #cwd;
9
+ #project;
10
+ constructor(cwd, project) {
11
+ this.#cwd = cwd;
12
+ this.#project = project;
13
+ }
14
+ /**
15
+ * Get the `adonisrc.ts` source file
16
+ */
17
+ #getRcFileOrThrow() {
18
+ const kernelUrl = fileURLToPath(new URL('./adonisrc.ts', this.#cwd));
19
+ return this.#project.getSourceFileOrThrow(kernelUrl);
20
+ }
21
+ /**
22
+ * Check if environments array has a subset of available environments
23
+ */
24
+ #isInSpecificEnvironment(environments) {
25
+ if (!environments)
26
+ return false;
27
+ return !!['web', 'console', 'test', 'repl'].find((env) => !environments.includes(env));
28
+ }
29
+ /**
30
+ * Locate the `defineConfig` call inside the `adonisrc.ts` file
31
+ */
32
+ #locateDefineConfigCallOrThrow(file) {
33
+ const call = file
34
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
35
+ .find((statement) => statement.getExpression().getText() === 'defineConfig');
36
+ if (!call) {
37
+ throw new Error('Could not locate the defineConfig call.');
38
+ }
39
+ return call;
40
+ }
41
+ /**
42
+ * Return the ObjectLiteralExpression of the defineConfig call
43
+ */
44
+ #getDefineConfigObjectOrThrow(defineConfigCall) {
45
+ const configObject = defineConfigCall
46
+ .getArguments()[0]
47
+ .asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
48
+ return configObject;
49
+ }
50
+ /**
51
+ * Check if the defineConfig() call has the property assignment
52
+ * inside it or not. If not, it will create one and return it.
53
+ */
54
+ #getPropertyAssignmentInDefineConfigCall(propertyName, initializer) {
55
+ const file = this.#getRcFileOrThrow();
56
+ const defineConfigCall = this.#locateDefineConfigCallOrThrow(file);
57
+ const configObject = this.#getDefineConfigObjectOrThrow(defineConfigCall);
58
+ let property = configObject.getProperty(propertyName);
59
+ if (!property) {
60
+ configObject.addPropertyAssignment({ name: propertyName, initializer });
61
+ property = configObject.getProperty(propertyName);
62
+ }
63
+ return property;
64
+ }
65
+ /**
66
+ * Extract list of imported modules from an ArrayLiteralExpression
67
+ *
68
+ * It assumes that the array can have two types of elements:
69
+ *
70
+ * - Simple lazy imported modules: [() => import('path/to/file')]
71
+ * - Or an object entry: [{ file: () => import('path/to/file'), environment: ['web', 'console'] }]
72
+ * where the `file` property is a lazy imported module.
73
+ */
74
+ #extractModulesFromArray(array) {
75
+ const modules = array.getElements().map((element) => {
76
+ /**
77
+ * Simple lazy imported module
78
+ */
79
+ if (Node.isArrowFunction(element)) {
80
+ const importExp = element.getFirstDescendantByKindOrThrow(SyntaxKind.CallExpression);
81
+ const literal = importExp.getFirstDescendantByKindOrThrow(SyntaxKind.StringLiteral);
82
+ return literal.getLiteralValue();
83
+ }
84
+ /**
85
+ * Object entry
86
+ */
87
+ if (Node.isObjectLiteralExpression(element)) {
88
+ const fileProp = element.getPropertyOrThrow('file');
89
+ const arrowFn = fileProp.getFirstDescendantByKindOrThrow(SyntaxKind.ArrowFunction);
90
+ const importExp = arrowFn.getFirstDescendantByKindOrThrow(SyntaxKind.CallExpression);
91
+ const literal = importExp.getFirstDescendantByKindOrThrow(SyntaxKind.StringLiteral);
92
+ return literal.getLiteralValue();
93
+ }
94
+ });
95
+ return modules.filter(Boolean);
96
+ }
97
+ /**
98
+ * Extract a specific property from an ArrayLiteralExpression
99
+ * that contains object entries.
100
+ *
101
+ * This function is mainly used for extractring the `pattern` property
102
+ * when adding a new meta files entry, or the `name` property when
103
+ * adding a new test suite.
104
+ */
105
+ #extractPropertyFromArray(array, propertyName) {
106
+ const property = array.getElements().map((el) => {
107
+ if (!Node.isObjectLiteralExpression(el))
108
+ return;
109
+ const nameProp = el.getPropertyOrThrow(propertyName);
110
+ if (!Node.isPropertyAssignment(nameProp))
111
+ return;
112
+ const name = nameProp.getInitializerIfKindOrThrow(SyntaxKind.StringLiteral);
113
+ return name.getLiteralValue();
114
+ });
115
+ return property.filter(Boolean);
116
+ }
117
+ /**
118
+ * Build a new module entry for the preloads and providers array
119
+ * based upon the environments specified
120
+ */
121
+ #buildNewModuleEntry(modulePath, environments) {
122
+ if (!this.#isInSpecificEnvironment(environments)) {
123
+ return `() => import('${modulePath}')`;
124
+ }
125
+ return `{
126
+ file: () => import('${modulePath}'),
127
+ environment: [${environments?.map((env) => `'${env}'`).join(', ')}],
128
+ }`;
129
+ }
130
+ /**
131
+ * Add a new command to the rcFile
132
+ */
133
+ addCommand(commandPath) {
134
+ const commandsProperty = this.#getPropertyAssignmentInDefineConfigCall('providers', '[]');
135
+ const commandsArray = commandsProperty.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
136
+ const commandString = `() => import('${commandPath}')`;
137
+ /**
138
+ * If the command already exists, do nothing
139
+ */
140
+ if (commandsArray.getElements().some((el) => el.getText() === commandString)) {
141
+ return this;
142
+ }
143
+ /**
144
+ * Add the command to the array
145
+ */
146
+ commandsArray.addElement(commandString);
147
+ return this;
148
+ }
149
+ /**
150
+ * Add a new preloaded file to the rcFile
151
+ */
152
+ addPreloadFile(modulePath, environments) {
153
+ const preloadsProperty = this.#getPropertyAssignmentInDefineConfigCall('preloads', '[]');
154
+ const preloadsArray = preloadsProperty.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
155
+ /**
156
+ * Check for duplicates
157
+ */
158
+ const existingPreloadedFiles = this.#extractModulesFromArray(preloadsArray);
159
+ const isDuplicate = existingPreloadedFiles.includes(modulePath);
160
+ if (isDuplicate) {
161
+ return this;
162
+ }
163
+ /**
164
+ * Add the preloaded file to the array
165
+ */
166
+ preloadsArray.addElement(this.#buildNewModuleEntry(modulePath, environments));
167
+ return this;
168
+ }
169
+ /**
170
+ * Add a new provider to the rcFile
171
+ */
172
+ addProvider(providerPath, environments) {
173
+ const property = this.#getPropertyAssignmentInDefineConfigCall('providers', '[]');
174
+ const providersArray = property.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
175
+ /**
176
+ * Check for duplicates
177
+ */
178
+ const existingProviderPaths = this.#extractModulesFromArray(providersArray);
179
+ const isDuplicate = existingProviderPaths.includes(providerPath);
180
+ if (isDuplicate) {
181
+ return this;
182
+ }
183
+ /**
184
+ * Add the provider to the array
185
+ */
186
+ providersArray.addElement(this.#buildNewModuleEntry(providerPath, environments));
187
+ return this;
188
+ }
189
+ /**
190
+ * Add a new meta file to the rcFile
191
+ */
192
+ addMetaFile(globPattern, reloadServer = false) {
193
+ const property = this.#getPropertyAssignmentInDefineConfigCall('metaFiles', '[]');
194
+ const metaFilesArray = property.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
195
+ /**
196
+ * Check for duplicates
197
+ */
198
+ const alreadyDefinedPatterns = this.#extractPropertyFromArray(metaFilesArray, 'pattern');
199
+ if (alreadyDefinedPatterns.includes(globPattern)) {
200
+ return this;
201
+ }
202
+ /**
203
+ * Add the meta file to the array
204
+ */
205
+ metaFilesArray.addElement(`{
206
+ pattern: '${globPattern}',
207
+ reloadServer: ${reloadServer},
208
+ }`);
209
+ return this;
210
+ }
211
+ /**
212
+ * Set directory name and path
213
+ */
214
+ setDirectory(key, value) {
215
+ const property = this.#getPropertyAssignmentInDefineConfigCall('directories', '{}');
216
+ const directories = property.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
217
+ directories.addPropertyAssignment({ name: key, initializer: `'${value}'` });
218
+ return this;
219
+ }
220
+ /**
221
+ * Set command alias
222
+ */
223
+ setCommandAlias(alias, command) {
224
+ const aliasProperty = this.#getPropertyAssignmentInDefineConfigCall('commandsAliases', '{}');
225
+ const aliases = aliasProperty.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
226
+ aliases.addPropertyAssignment({ name: alias, initializer: `'${command}'` });
227
+ return this;
228
+ }
229
+ /**
230
+ * Add a new test suite to the rcFile
231
+ */
232
+ addSuite(suiteName, files, timeout) {
233
+ const testProperty = this.#getPropertyAssignmentInDefineConfigCall('tests', `{ suites: [], forceExit: true, timeout: 2000 }`);
234
+ const property = testProperty
235
+ .getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
236
+ .getPropertyOrThrow('suites');
237
+ const suitesArray = property.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
238
+ /**
239
+ * Check for duplicates
240
+ */
241
+ const existingSuitesNames = this.#extractPropertyFromArray(suitesArray, 'name');
242
+ if (existingSuitesNames.includes(suiteName)) {
243
+ return this;
244
+ }
245
+ /**
246
+ * Add the suite to the array
247
+ */
248
+ const filesArray = Array.isArray(files) ? files : [files];
249
+ suitesArray.addElement(`{
250
+ name: '${suiteName}',
251
+ files: [${filesArray.map((file) => `'${file}'`).join(', ')}],
252
+ timeout: ${timeout ?? 2000},
253
+ }`);
254
+ return this;
255
+ }
256
+ /**
257
+ * Save the adonisrc.ts file
258
+ */
259
+ save() {
260
+ const file = this.#getRcFileOrThrow();
261
+ file.formatText();
262
+ return file.save();
263
+ }
264
+ }
@@ -92,3 +92,47 @@ export type BundlerOptions = {
92
92
  metaFiles?: MetaFile[];
93
93
  assets?: AssetsBundlerOptions;
94
94
  };
95
+ /**
96
+ * Entry to add a middleware to a given middleware stack
97
+ * via the CodeTransformer
98
+ */
99
+ export type AddMiddlewareEntry = {
100
+ /**
101
+ * If you are adding a named middleware, then you must
102
+ * define the name.
103
+ */
104
+ name?: string;
105
+ /**
106
+ * The path to the middleware file
107
+ *
108
+ * @example
109
+ * `@adonisjs/static/static_middleware`
110
+ * `#middlewares/silent_auth.js`
111
+ */
112
+ path: string;
113
+ /**
114
+ * The position to add the middleware. If `before`
115
+ * middleware will be added at the first position and
116
+ * therefore will be run before all others
117
+ *
118
+ * @default 'after'
119
+ */
120
+ position?: 'before' | 'after';
121
+ };
122
+ /**
123
+ * Defines the structure of an environment variable validation
124
+ * definition
125
+ */
126
+ export type EnvValidationDefinition = {
127
+ /**
128
+ * Write a leading comment on top of your variables
129
+ */
130
+ leadingComment?: string;
131
+ /**
132
+ * A key-value pair of env variables and their validation
133
+ *
134
+ * @example
135
+ * MY_VAR: 'Env.schema.string.optional()'
136
+ */
137
+ variables: Record<string, string>;
138
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adonisjs/assembler",
3
3
  "description": "Provides utilities to run AdonisJS development server and build project for production",
4
- "version": "6.1.3-17",
4
+ "version": "6.1.3-18",
5
5
  "engines": {
6
6
  "node": ">=18.16.0"
7
7
  },
@@ -14,6 +14,7 @@
14
14
  ],
15
15
  "exports": {
16
16
  ".": "./build/index.js",
17
+ "./code_transformer": "./build/src/code_transformer/main.js",
17
18
  "./types": "./build/src/types.js"
18
19
  },
19
20
  "scripts": {
@@ -29,9 +30,10 @@
29
30
  "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/assembler",
30
31
  "format": "prettier --write .",
31
32
  "prepublishOnly": "npm run build",
32
- "quick:test": "node --loader=ts-node/esm bin/test.ts"
33
+ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts"
33
34
  },
34
35
  "devDependencies": {
36
+ "@adonisjs/application": "7.1.2-9",
35
37
  "@adonisjs/eslint-config": "^1.1.8",
36
38
  "@adonisjs/prettier-config": "^1.1.8",
37
39
  "@adonisjs/tsconfig": "^1.1.8",
@@ -40,11 +42,13 @@
40
42
  "@japa/assert": "^2.0.0-1",
41
43
  "@japa/file-system": "^2.0.0-1",
42
44
  "@japa/runner": "^3.0.0-6",
45
+ "@japa/snapshot": "2.0.0-1",
43
46
  "@swc/core": "^1.3.71",
44
47
  "@types/node": "^20.4.5",
45
48
  "@types/picomatch": "^2.3.0",
46
49
  "c8": "^8.0.1",
47
50
  "cross-env": "^7.0.3",
51
+ "dedent": "^1.5.1",
48
52
  "del-cli": "^5.0.0",
49
53
  "eslint": "^8.45.0",
50
54
  "github-label-sync": "^2.3.1",
@@ -65,7 +69,8 @@
65
69
  "get-port": "^7.0.0",
66
70
  "junk": "^4.0.1",
67
71
  "picomatch": "^2.3.1",
68
- "slash": "^5.1.0"
72
+ "slash": "^5.1.0",
73
+ "ts-morph": "^19.0.0"
69
74
  },
70
75
  "peerDependencies": {
71
76
  "typescript": "^4.0.0 || ^5.0.0"