@adonisjs/assembler 6.1.3-3 → 6.1.3-4

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/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # The MIT License
2
2
 
3
- Copyright (c) 2023 AdonisJS Framework
3
+ Copyright (c) 2023 Harminder Virk
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <br />
4
4
 
5
- [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url]
5
+ [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![snyk-image]][snyk-url]
6
6
 
7
7
  ## Introduction
8
8
  Assembler exports the API for starting the **AdonisJS development server**, **building project for production** and **running tests** in watch mode. Assembler must be used during development only.
@@ -32,5 +32,5 @@ AdonisJS Assembler is open-sourced software licensed under the [MIT license](LIC
32
32
  [license-url]: LICENSE.md
33
33
  [license-image]: https://img.shields.io/github/license/adonisjs/ace?style=for-the-badge
34
34
 
35
- [synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/assembler?label=Synk%20Vulnerabilities&style=for-the-badge
36
- [synk-url]: https://snyk.io/test/github/adonisjs/assembler?targetFile=package.json "synk"
35
+ [snyk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/assembler?label=snyk%20Vulnerabilities&style=for-the-badge
36
+ [snyk-url]: https://snyk.io/test/github/adonisjs/assembler?targetFile=package.json "snyk"
package/build/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { Bundler } from './src/bundler.js';
2
2
  export { DevServer } from './src/dev_server.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA"}
@@ -8,3 +8,4 @@ export declare class Bundler {
8
8
  setLogger(logger: Logger): this;
9
9
  bundle(stopOnError?: boolean, client?: 'npm' | 'yarn' | 'pnpm'): Promise<boolean>;
10
10
  }
11
+ //# sourceMappingURL=bundler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../../src/bundler.ts"],"names":[],"mappings":";AAYA,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AAGtC,OAAO,EAAS,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAIpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAUhD,qBAAa,OAAO;;gBAcN,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,QAAQ,EAAE,OAAO,EAAE,cAAc;IAkIlE,SAAS,CAAC,MAAM,EAAE,MAAM;IAQlB,MAAM,CACV,WAAW,GAAE,OAAc,EAC3B,MAAM,GAAE,KAAK,GAAG,MAAM,GAAG,MAAc,GACtC,OAAO,CAAC,OAAO,CAAC;CAmFpB"}
@@ -13,3 +13,4 @@ export declare class DevServer {
13
13
  poll: boolean;
14
14
  }): Promise<void>;
15
15
  }
16
+ //# sourceMappingURL=dev_server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev_server.d.ts","sourceRoot":"","sources":["../../src/dev_server.ts"],"names":[],"mappings":";AAWA,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AAGtC,OAAO,EAAS,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAKpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAkBlD,qBAAa,SAAS;;gBAqBR,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB;IAyP/C,SAAS,CAAC,MAAM,EAAE,MAAM;IASxB,OAAO,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,GAAG,IAAI;IASlD,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,GAAG,IAAI;IAQtC,KAAK;IAWL,aAAa,CAAC,EAAE,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE;CA6HrE"}
@@ -1,3 +1,4 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import type tsStatic from 'typescript';
3
3
  export declare function parseConfig(cwd: string | URL, ts: typeof tsStatic): tsStatic.ParsedCommandLine | undefined;
4
+ //# sourceMappingURL=parse_config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse_config.d.ts","sourceRoot":"","sources":["../../src/parse_config.ts"],"names":[],"mappings":";AASA,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AAOtC,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,EAAE,EAAE,OAAO,QAAQ,0CAejE"}
@@ -2,3 +2,4 @@
2
2
  import type { RunOptions } from './types.js';
3
3
  export declare function runNode(cwd: string | URL, options: RunOptions): import("execa").ExecaChildProcess<string>;
4
4
  export declare function run(cwd: string | URL, options: Omit<RunOptions, 'nodeArgs'>): import("execa").ExecaChildProcess<string>;
5
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":";AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAkB5C,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,UAAU,6CAgB7D;AAKD,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,6CAe3E"}
@@ -34,3 +34,4 @@ export type BundlerOptions = {
34
34
  metaFiles?: MetaFile[];
35
35
  assets?: AssetsBundlerOptions;
36
36
  };
37
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":";AAYA,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CACxB,CAAA;AAKD,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AAKD,MAAM,MAAM,QAAQ,GAAG;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,OAAO,CAAA;CACtB,CAAA;AAKD,MAAM,MAAM,oBAAoB,GAC5B;IACE,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,GACD;IACE,KAAK,EAAE,IAAI,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAKL,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;IACtB,MAAM,CAAC,EAAE,oBAAoB,CAAA;CAC9B,CAAA;AAKD,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;IACtB,MAAM,CAAC,EAAE,oBAAoB,CAAA;CAC9B,CAAA"}
@@ -6,3 +6,4 @@ export declare function watch(cwd: string | URL, ts: typeof tsStatic, options: W
6
6
  watcher: Watcher;
7
7
  chokidar: import("chokidar").FSWatcher;
8
8
  } | undefined;
9
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/watch.ts"],"names":[],"mappings":";AASA,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAA;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAE/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAM9C,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,EAAE,EAAE,OAAO,QAAQ,EAAE,OAAO,EAAE,YAAY;;;cASlF"}
package/index.ts ADDED
@@ -0,0 +1,11 @@
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
+
10
+ export { Bundler } from './src/bundler.js'
11
+ export { DevServer } from './src/dev_server.js'
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@adonisjs/assembler",
3
- "version": "6.1.3-3",
3
+ "version": "6.1.3-4",
4
4
  "description": "Provides utilities to run AdonisJS development server and build project for production",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
7
7
  "files": [
8
+ "src",
9
+ "index.ts",
8
10
  "build/src",
9
11
  "build/index.d.ts",
12
+ "build/index.d.ts.map",
10
13
  "build/index.js"
11
14
  ],
12
15
  "exports": {
@@ -35,34 +38,34 @@
35
38
  "author": "virk,adonisjs",
36
39
  "license": "MIT",
37
40
  "devDependencies": {
38
- "@commitlint/cli": "^17.4.4",
41
+ "@commitlint/cli": "^17.5.0",
39
42
  "@commitlint/config-conventional": "^17.4.4",
40
43
  "@japa/assert": "^1.4.1",
41
44
  "@japa/file-system": "^1.0.1",
42
45
  "@japa/run-failed-tests": "^1.1.1",
43
46
  "@japa/runner": "^2.5.1",
44
47
  "@japa/spec-reporter": "^1.3.3",
45
- "@swc/core": "^1.3.39",
46
- "@types/node": "^18.15.0",
48
+ "@swc/core": "^1.3.42",
49
+ "@types/node": "^18.15.10",
47
50
  "c8": "^7.13.0",
48
51
  "cross-env": "^7.0.3",
49
52
  "del-cli": "^5.0.0",
50
53
  "eslint": "^8.36.0",
51
- "eslint-config-prettier": "^8.7.0",
54
+ "eslint-config-prettier": "^8.8.0",
52
55
  "eslint-plugin-adonis": "^3.0.3",
53
56
  "eslint-plugin-prettier": "^4.2.1",
54
57
  "github-label-sync": "^2.3.1",
55
58
  "husky": "^8.0.3",
56
- "np": "^7.6.3",
59
+ "np": "^7.6.4",
57
60
  "p-event": "^5.0.1",
58
- "prettier": "^2.8.4",
61
+ "prettier": "^2.8.7",
59
62
  "ts-node": "^10.9.1",
60
- "typescript": "^4.9.5"
63
+ "typescript": "^5.0.2"
61
64
  },
62
65
  "dependencies": {
63
- "@adonisjs/env": "^4.2.0-1",
64
- "@poppinss/chokidar-ts": "^4.1.0-1",
65
- "@poppinss/cliui": "^6.1.1-1",
66
+ "@adonisjs/env": "^4.2.0-2",
67
+ "@poppinss/chokidar-ts": "^4.1.0-2",
68
+ "@poppinss/cliui": "^6.1.1-2",
66
69
  "@types/picomatch": "^2.3.0",
67
70
  "cpy": "^9.0.1",
68
71
  "execa": "^7.0.0",
package/src/bundler.ts ADDED
@@ -0,0 +1,268 @@
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
+
10
+ import slash from 'slash'
11
+ import copyfiles from 'cpy'
12
+ import fs from 'node:fs/promises'
13
+ import type tsStatic from 'typescript'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { join, relative } from 'node:path'
16
+ import { cliui, type Logger } from '@poppinss/cliui'
17
+
18
+ import { run } from './run.js'
19
+ import { parseConfig } from './parse_config.js'
20
+ import type { BundlerOptions } from './types.js'
21
+
22
+ /**
23
+ * Instance of CLIUI
24
+ */
25
+ const ui = cliui()
26
+
27
+ /**
28
+ * The bundler class exposes the API to build an AdonisJS project.
29
+ */
30
+ export class Bundler {
31
+ #cwd: URL
32
+ #cwdPath: string
33
+ #ts: typeof tsStatic
34
+ #logger = ui.logger
35
+ #options: BundlerOptions
36
+
37
+ /**
38
+ * Getting reference to colors library from logger
39
+ */
40
+ get #colors() {
41
+ return this.#logger.getColors()
42
+ }
43
+
44
+ constructor(cwd: URL, ts: typeof tsStatic, options: BundlerOptions) {
45
+ this.#cwd = cwd
46
+ this.#cwdPath = fileURLToPath(this.#cwd)
47
+ this.#ts = ts
48
+ this.#options = options
49
+ }
50
+
51
+ #getRelativeName(filePath: string) {
52
+ return slash(relative(this.#cwdPath, filePath))
53
+ }
54
+
55
+ /**
56
+ * Cleans up the build directory
57
+ */
58
+ async #cleanupBuildDirectory(outDir: string) {
59
+ await fs.rm(outDir, { recursive: true, force: true, maxRetries: 5 })
60
+ }
61
+
62
+ /**
63
+ * Runs assets bundler command to build assets.
64
+ */
65
+ async #buildAssets(): Promise<boolean> {
66
+ const assetsBundler = this.#options.assets
67
+ if (!assetsBundler?.serve) {
68
+ return true
69
+ }
70
+
71
+ try {
72
+ this.#logger.info('compiling frontend assets', { suffix: assetsBundler.cmd })
73
+ await run(this.#cwd, {
74
+ stdio: 'inherit',
75
+ script: assetsBundler.cmd,
76
+ scriptArgs: [],
77
+ })
78
+ return true
79
+ } catch {
80
+ return false
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Runs tsc command to build the source.
86
+ */
87
+ async #runTsc(outDir: string): Promise<boolean> {
88
+ try {
89
+ await run(this.#cwd, {
90
+ stdio: 'inherit',
91
+ script: 'tsc',
92
+ scriptArgs: ['--outDir', outDir],
93
+ })
94
+ return true
95
+ } catch {
96
+ return false
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Copy files to destination directory
102
+ */
103
+ async #copyFiles(files: string[], outDir: string) {
104
+ try {
105
+ await copyfiles(files, outDir, { cwd: this.#cwdPath })
106
+ } catch (error) {
107
+ if (!error.message.includes("the file doesn't exist")) {
108
+ throw error
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Copy meta files to the output directory
115
+ */
116
+ async #copyMetaFiles(outDir: string, additionalFilesToCopy: string[]) {
117
+ const metaFiles = (this.#options.metaFiles || [])
118
+ .map((file) => file.pattern)
119
+ .concat(additionalFilesToCopy)
120
+
121
+ await this.#copyFiles(metaFiles, outDir)
122
+ }
123
+
124
+ /**
125
+ * Copies .adonisrc.json file to the destination
126
+ */
127
+ async #copyAdonisRcFile(outDir: string) {
128
+ const existingContents = JSON.parse(
129
+ await fs.readFile(join(this.#cwdPath, '.adonisrc.json'), 'utf-8')
130
+ )
131
+ const compiledContents = Object.assign({}, existingContents, {
132
+ typescript: false,
133
+ lastCompiledAt: new Date().toISOString(),
134
+ })
135
+
136
+ await fs.mkdir(outDir, { recursive: true })
137
+ await fs.writeFile(
138
+ join(outDir, '.adonisrc.json'),
139
+ JSON.stringify(compiledContents, null, 2) + '\n'
140
+ )
141
+ }
142
+
143
+ /**
144
+ * Returns the lock file name for a given packages client
145
+ */
146
+ #getClientLockFile(client: 'npm' | 'yarn' | 'pnpm') {
147
+ switch (client) {
148
+ case 'npm':
149
+ return 'package-lock.json'
150
+ case 'yarn':
151
+ return 'yarn.lock'
152
+ case 'pnpm':
153
+ return 'pnpm-lock.yaml'
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Returns the installation command for a given packages client
159
+ */
160
+ #getClientInstallCommand(client: 'npm' | 'yarn' | 'pnpm') {
161
+ switch (client) {
162
+ case 'npm':
163
+ return 'npm ci --omit="dev"'
164
+ case 'yarn':
165
+ return 'yarn install --production'
166
+ case 'pnpm':
167
+ return 'pnpm i --prod'
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Set a custom CLI UI logger
173
+ */
174
+ setLogger(logger: Logger) {
175
+ this.#logger = logger
176
+ return this
177
+ }
178
+
179
+ /**
180
+ * Bundles the application to be run in production
181
+ */
182
+ async bundle(
183
+ stopOnError: boolean = true,
184
+ client: 'npm' | 'yarn' | 'pnpm' = 'npm'
185
+ ): Promise<boolean> {
186
+ /**
187
+ * Step 1: Parse config file to get the build output directory
188
+ */
189
+ const config = parseConfig(this.#cwd, this.#ts)
190
+ if (!config) {
191
+ return false
192
+ }
193
+
194
+ /**
195
+ * Step 2: Cleanup existing build directory (if any)
196
+ */
197
+ const outDir = config.options.outDir || fileURLToPath(new URL('build/', this.#cwd))
198
+ this.#logger.info('cleaning up output directory', { suffix: this.#getRelativeName(outDir) })
199
+ await this.#cleanupBuildDirectory(outDir)
200
+
201
+ /**
202
+ * Step 3: Build frontend assets
203
+ */
204
+ if (!(await this.#buildAssets())) {
205
+ return false
206
+ }
207
+
208
+ /**
209
+ * Step 4: Build typescript source code
210
+ */
211
+ this.#logger.info('compiling typescript source', { suffix: 'tsc' })
212
+ const buildCompleted = await this.#runTsc(outDir)
213
+ await this.#copyFiles(['ace.js'], outDir)
214
+
215
+ /**
216
+ * Remove incomplete build directory when tsc build
217
+ * failed and stopOnError is set to true.
218
+ */
219
+ if (!buildCompleted && stopOnError) {
220
+ await this.#cleanupBuildDirectory(outDir)
221
+ const instructions = ui
222
+ .sticker()
223
+ .fullScreen()
224
+ .drawBorder((borderChar, colors) => colors.red(borderChar))
225
+
226
+ instructions.add(
227
+ this.#colors.red('Cannot complete the build process as there are TypeScript errors.')
228
+ )
229
+ instructions.add(
230
+ this.#colors.red(
231
+ 'Use "--ignore-ts-errors" flag to ignore TypeScript errors and continue the build.'
232
+ )
233
+ )
234
+
235
+ this.#logger.logError(instructions.prepare())
236
+ return false
237
+ }
238
+
239
+ /**
240
+ * Step 5: Copy meta files to the build directory
241
+ */
242
+ const pkgFiles = ['package.json', this.#getClientLockFile(client)]
243
+ this.#logger.info('copying meta files to the output directory')
244
+ await this.#copyMetaFiles(outDir, pkgFiles)
245
+
246
+ /**
247
+ * Step 6: Copy .adonisrc.json file to the build directory
248
+ */
249
+ this.#logger.info('copying .adonisrc.json file to the output directory')
250
+ await this.#copyAdonisRcFile(outDir)
251
+
252
+ this.#logger.success('build completed')
253
+ this.#logger.log('')
254
+
255
+ /**
256
+ * Next steps
257
+ */
258
+ ui.instructions()
259
+ .useRenderer(this.#logger.getRenderer())
260
+ .heading('Run the following commands to start the server in production')
261
+ .add(this.#colors.cyan(`cd ${this.#getRelativeName(outDir)}`))
262
+ .add(this.#colors.cyan(this.#getClientInstallCommand(client)))
263
+ .add(this.#colors.cyan('node bin/server.js'))
264
+ .render()
265
+
266
+ return true
267
+ }
268
+ }
@@ -0,0 +1,470 @@
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
+
10
+ import getPort from 'get-port'
11
+ import picomatch from 'picomatch'
12
+ import type tsStatic from 'typescript'
13
+ import { type ExecaChildProcess } from 'execa'
14
+ import type { Watcher } from '@poppinss/chokidar-ts'
15
+ import { cliui, type Logger } from '@poppinss/cliui'
16
+ import { EnvLoader, EnvParser } from '@adonisjs/env'
17
+
18
+ import { watch } from './watch.js'
19
+ import { run, runNode } from './run.js'
20
+ import type { DevServerOptions } from './types.js'
21
+
22
+ /**
23
+ * Instance of CLIUI
24
+ */
25
+ const ui = cliui()
26
+
27
+ /**
28
+ * Exposes the API to start the development. Optionally, the watch API can be
29
+ * used to watch for file changes and restart the development server.
30
+ *
31
+ * The Dev server performs the following actions
32
+ *
33
+ * - Assigns a random PORT, when PORT inside .env file is in use
34
+ * - Uses tsconfig.json file to collect a list of files to watch.
35
+ * - Uses metaFiles from .adonisrc.json file to collect a list of files to watch.
36
+ * - Restart HTTP server on every file change.
37
+ */
38
+ export class DevServer {
39
+ #cwd: URL
40
+ #logger = ui.logger
41
+ #options: DevServerOptions
42
+ #isWatching: boolean = false
43
+ #scriptFile: string = 'bin/server.js'
44
+ #httpServerProcess?: ExecaChildProcess<string>
45
+ #isMetaFileWithReloadsEnabled: picomatch.Matcher
46
+ #isMetaFileWithReloadsDisabled: picomatch.Matcher
47
+ #watcher?: ReturnType<Watcher['watch']>
48
+ #assetsServerProcess?: ExecaChildProcess<string>
49
+ #onError?: (error: any) => any
50
+ #onClose?: (exitCode: number) => any
51
+
52
+ /**
53
+ * Getting reference to colors library from logger
54
+ */
55
+ get #colors() {
56
+ return this.#logger.getColors()
57
+ }
58
+
59
+ constructor(cwd: URL, options: DevServerOptions) {
60
+ this.#cwd = cwd
61
+ this.#options = options
62
+
63
+ this.#isMetaFileWithReloadsEnabled = picomatch(
64
+ (this.#options.metaFiles || [])
65
+ .filter(({ reloadServer }) => reloadServer === true)
66
+ .map(({ pattern }) => pattern)
67
+ )
68
+
69
+ this.#isMetaFileWithReloadsDisabled = picomatch(
70
+ (this.#options.metaFiles || [])
71
+ .filter(({ reloadServer }) => reloadServer !== true)
72
+ .map(({ pattern }) => pattern)
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Check if file is an .env file
78
+ */
79
+ #isDotEnvFile(filePath: string) {
80
+ if (filePath === '.env') {
81
+ return true
82
+ }
83
+
84
+ return filePath.includes('.env.')
85
+ }
86
+
87
+ /**
88
+ * Check if file is .adonisrc.json file
89
+ */
90
+ #isRcFile(filePath: string) {
91
+ return filePath === '.adonisrc.json'
92
+ }
93
+
94
+ /**
95
+ * Inspect if child process message is from AdonisJS HTTP server
96
+ */
97
+ #isAdonisJSReadyMessage(
98
+ message: unknown
99
+ ): message is { isAdonisJS: true; environment: 'web'; port: number; host: string } {
100
+ return (
101
+ message !== null &&
102
+ typeof message === 'object' &&
103
+ 'isAdonisJS' in message &&
104
+ 'environment' in message &&
105
+ message.environment === 'web'
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Conditionally clear the terminal screen
111
+ */
112
+ #clearScreen() {
113
+ if (this.#options.clearScreen) {
114
+ process.stdout.write('\u001Bc')
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Logs messages from vite dev server stdout and stderr
120
+ */
121
+ #logViteDevServerMessage(data: Buffer) {
122
+ const dataString = data.toString()
123
+ const lines = dataString.split('\n')
124
+
125
+ /**
126
+ * Logging VITE ready in message with proper
127
+ * spaces and newlines
128
+ */
129
+ if (dataString.includes('ready in')) {
130
+ console.log('')
131
+ console.log(dataString.trim())
132
+ return
133
+ }
134
+
135
+ /**
136
+ * Put a wrapper around vite network address log
137
+ */
138
+ if (dataString.includes('Local') && dataString.includes('Network')) {
139
+ const sticker = ui.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer())
140
+
141
+ lines.forEach((line: string) => {
142
+ if (line.trim()) {
143
+ sticker.add(line)
144
+ }
145
+ })
146
+
147
+ sticker.render()
148
+ return
149
+ }
150
+
151
+ /**
152
+ * Log rest of the lines
153
+ */
154
+ lines.forEach((line: string) => {
155
+ if (line.trim()) {
156
+ console.log(line)
157
+ }
158
+ })
159
+ }
160
+
161
+ /**
162
+ * Logs messages from assets dev server stdout and stderr
163
+ */
164
+ #logAssetsDevServerMessage(data: Buffer) {
165
+ const dataString = data.toString()
166
+ const lines = dataString.split('\n')
167
+ lines.forEach((line: string) => {
168
+ if (line.trim()) {
169
+ console.log(line)
170
+ }
171
+ })
172
+ }
173
+
174
+ /**
175
+ * Returns PORT for starting the HTTP server with option to use
176
+ * a random PORT if main PORT is in use.
177
+ */
178
+ async #getPort(): Promise<number> {
179
+ /**
180
+ * Use existing port if exists
181
+ */
182
+ if (process.env.PORT) {
183
+ return getPort({ port: Number(process.env.PORT) })
184
+ }
185
+
186
+ const files = await new EnvLoader(this.#cwd).load()
187
+ for (let file of files) {
188
+ const envVariables = new EnvParser(file.contents).parse()
189
+ if (envVariables.PORT) {
190
+ return getPort({ port: Number(envVariables.PORT) })
191
+ }
192
+ }
193
+
194
+ return getPort({ port: 3333 })
195
+ }
196
+
197
+ /**
198
+ * Starts the HTTP server
199
+ */
200
+ #startHTTPServer(port: string, mode: 'blocking' | 'nonblocking') {
201
+ this.#httpServerProcess = runNode(this.#cwd, {
202
+ script: this.#scriptFile,
203
+ env: { PORT: port, ...this.#options.env },
204
+ nodeArgs: this.#options.nodeArgs,
205
+ scriptArgs: this.#options.scriptArgs,
206
+ })
207
+
208
+ this.#httpServerProcess.on('message', (message) => {
209
+ if (this.#isAdonisJSReadyMessage(message)) {
210
+ ui.sticker()
211
+ .useColors(this.#colors)
212
+ .useRenderer(this.#logger.getRenderer())
213
+ .add(`Server address: ${this.#colors.cyan(`http://${message.host}:${message.port}`)}`)
214
+ .add(
215
+ `File system watcher: ${this.#colors.cyan(
216
+ `${this.#isWatching ? 'enabled' : 'disabled'}`
217
+ )}`
218
+ )
219
+ .render()
220
+ }
221
+ })
222
+
223
+ this.#httpServerProcess
224
+ .then((result) => {
225
+ this.#logger.warning(`underlying HTTP server closed with status code "${result.exitCode}"`)
226
+ if (mode === 'nonblocking') {
227
+ this.#onClose?.(result.exitCode)
228
+ this.#watcher?.close()
229
+ }
230
+ })
231
+ .catch((error) => {
232
+ this.#logger.warning('unable to connect to underlying HTTP server process')
233
+ this.#logger.fatal(error)
234
+ this.#onError?.(error)
235
+ this.#watcher?.close()
236
+ })
237
+ }
238
+
239
+ /**
240
+ * Starts the assets bundler server. The assets bundler server process is
241
+ * considered as the secondary process and therefore we do not perform
242
+ * any cleanup if it dies.
243
+ */
244
+ #startAssetsServer() {
245
+ const assetsBundler = this.#options.assets
246
+ if (!assetsBundler?.serve) {
247
+ return
248
+ }
249
+
250
+ this.#logger.info(`starting "${assetsBundler.driver}" dev server...`)
251
+ this.#assetsServerProcess = run(this.#cwd, {
252
+ script: assetsBundler.cmd,
253
+
254
+ /**
255
+ * We do not inherit the stdio for vite and encore, because they then
256
+ * own the stdin and interrupts the `Ctrl + C`.
257
+ */
258
+ stdio: 'pipe',
259
+ scriptArgs: this.#options.scriptArgs,
260
+ })
261
+
262
+ /**
263
+ * Log child process messages
264
+ */
265
+ this.#assetsServerProcess.stdout?.on('data', (data) => {
266
+ if (assetsBundler.driver === 'vite') {
267
+ this.#logViteDevServerMessage(data)
268
+ } else {
269
+ this.#logAssetsDevServerMessage(data)
270
+ }
271
+ })
272
+
273
+ this.#assetsServerProcess.stderr?.on('data', (data) => {
274
+ if (assetsBundler.driver === 'vite') {
275
+ this.#logViteDevServerMessage(data)
276
+ } else {
277
+ this.#logAssetsDevServerMessage(data)
278
+ }
279
+ })
280
+
281
+ this.#assetsServerProcess
282
+ .then((result) => {
283
+ this.#logger.warning(
284
+ `"${assetsBundler.driver}" dev server closed with status code "${result.exitCode}"`
285
+ )
286
+ })
287
+ .catch((error) => {
288
+ this.#logger.warning(`unable to connect to "${assetsBundler.driver}" dev server`)
289
+ this.#logger.fatal(error)
290
+ })
291
+ }
292
+
293
+ /**
294
+ * Restart the development server
295
+ */
296
+ #restart(port: string) {
297
+ if (this.#httpServerProcess) {
298
+ this.#httpServerProcess.removeAllListeners()
299
+ this.#httpServerProcess.kill('SIGKILL')
300
+ }
301
+
302
+ this.#startHTTPServer(port, 'blocking')
303
+ }
304
+
305
+ /**
306
+ * Set a custom CLI UI logger
307
+ */
308
+ setLogger(logger: Logger) {
309
+ this.#logger = logger
310
+ return this
311
+ }
312
+
313
+ /**
314
+ * Add listener to get notified when dev server is
315
+ * closed
316
+ */
317
+ onClose(callback: (exitCode: number) => any): this {
318
+ this.#onClose = callback
319
+ return this
320
+ }
321
+
322
+ /**
323
+ * Add listener to get notified when dev server exists
324
+ * with an error
325
+ */
326
+ onError(callback: (error: any) => any): this {
327
+ this.#onError = callback
328
+ return this
329
+ }
330
+
331
+ /**
332
+ * Start the development server
333
+ */
334
+ async start() {
335
+ this.#clearScreen()
336
+ this.#logger.info('starting HTTP server...')
337
+ this.#startHTTPServer(String(await this.#getPort()), 'nonblocking')
338
+
339
+ this.#startAssetsServer()
340
+ }
341
+
342
+ /**
343
+ * Start the development server in watch mode
344
+ */
345
+ async startAndWatch(ts: typeof tsStatic, options?: { poll: boolean }) {
346
+ const port = String(await this.#getPort())
347
+ this.#isWatching = true
348
+
349
+ this.#clearScreen()
350
+ this.#logger.info('starting HTTP server...')
351
+
352
+ this.#startHTTPServer(port, 'blocking')
353
+ this.#startAssetsServer()
354
+
355
+ /**
356
+ * Create watcher using tsconfig.json file
357
+ */
358
+ const output = watch(this.#cwd, ts, options || {})
359
+ if (!output) {
360
+ this.#onClose?.(1)
361
+ return
362
+ }
363
+
364
+ /**
365
+ * Storing reference to watcher, so that we can close it
366
+ * when HTTP server exists with error
367
+ */
368
+ this.#watcher = output.chokidar
369
+
370
+ /**
371
+ * Notify the watcher is ready
372
+ */
373
+ output.watcher.on('watcher:ready', () => {
374
+ this.#logger.info('watching file system for changes...')
375
+ })
376
+
377
+ /**
378
+ * Cleanup when watcher dies
379
+ */
380
+ output.chokidar.on('error', (error) => {
381
+ this.#logger.warning('file system watcher failure')
382
+ this.#logger.fatal(error)
383
+ this.#onError?.(error)
384
+ output.chokidar.close()
385
+ })
386
+
387
+ /**
388
+ * Changes in TypeScript source file
389
+ */
390
+ output.watcher.on('source:add', ({ relativePath }) => {
391
+ this.#clearScreen()
392
+ this.#logger.log(`${this.#colors.green('add')} ${relativePath}`)
393
+ this.#restart(port)
394
+ })
395
+ output.watcher.on('source:change', ({ relativePath }) => {
396
+ this.#clearScreen()
397
+ this.#logger.log(`${this.#colors.green('update')} ${relativePath}`)
398
+ this.#restart(port)
399
+ })
400
+ output.watcher.on('source:unlink', ({ relativePath }) => {
401
+ this.#clearScreen()
402
+ this.#logger.log(`${this.#colors.green('delete')} ${relativePath}`)
403
+ this.#restart(port)
404
+ })
405
+
406
+ /**
407
+ * Changes in other files
408
+ */
409
+ output.watcher.on('add', ({ relativePath }) => {
410
+ if (this.#isDotEnvFile(relativePath) || this.#isRcFile(relativePath)) {
411
+ this.#clearScreen()
412
+ this.#logger.log(`${this.#colors.green('add')} ${relativePath}`)
413
+ this.#restart(port)
414
+ return
415
+ }
416
+
417
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
418
+ this.#clearScreen()
419
+ this.#logger.log(`${this.#colors.green('add')} ${relativePath}`)
420
+ this.#restart(port)
421
+ return
422
+ }
423
+
424
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
425
+ this.#clearScreen()
426
+ this.#logger.log(`${this.#colors.green('add')} ${relativePath}`)
427
+ }
428
+ })
429
+ output.watcher.on('change', ({ relativePath }) => {
430
+ if (this.#isDotEnvFile(relativePath) || this.#isRcFile(relativePath)) {
431
+ this.#clearScreen()
432
+ this.#logger.log(`${this.#colors.green('update')} ${relativePath}`)
433
+ this.#restart(port)
434
+ return
435
+ }
436
+
437
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
438
+ this.#clearScreen()
439
+ this.#logger.log(`${this.#colors.green('update')} ${relativePath}`)
440
+ this.#restart(port)
441
+ return
442
+ }
443
+
444
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
445
+ this.#clearScreen()
446
+ this.#logger.log(`${this.#colors.green('update')} ${relativePath}`)
447
+ }
448
+ })
449
+ output.watcher.on('unlink', ({ relativePath }) => {
450
+ if (this.#isDotEnvFile(relativePath) || this.#isRcFile(relativePath)) {
451
+ this.#clearScreen()
452
+ this.#logger.log(`${this.#colors.green('delete')} ${relativePath}`)
453
+ this.#restart(port)
454
+ return
455
+ }
456
+
457
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
458
+ this.#clearScreen()
459
+ this.#logger.log(`${this.#colors.green('delete')} ${relativePath}`)
460
+ this.#restart(port)
461
+ return
462
+ }
463
+
464
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
465
+ this.#clearScreen()
466
+ this.#logger.log(`${this.#colors.green('delete')} ${relativePath}`)
467
+ }
468
+ })
469
+ }
470
+ }
@@ -0,0 +1,32 @@
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
+
10
+ import type tsStatic from 'typescript'
11
+ import { ConfigParser } from '@poppinss/chokidar-ts'
12
+
13
+ /**
14
+ * Parses tsconfig.json and prints errors using typescript compiler
15
+ * host
16
+ */
17
+ export function parseConfig(cwd: string | URL, ts: typeof tsStatic) {
18
+ const { config, error } = new ConfigParser(cwd, 'tsconfig.json', ts).parse()
19
+ if (error) {
20
+ const compilerHost = ts.createCompilerHost({})
21
+ console.log(ts.formatDiagnosticsWithColorAndContext([error], compilerHost))
22
+ return
23
+ }
24
+
25
+ if (config!.errors.length) {
26
+ const compilerHost = ts.createCompilerHost({})
27
+ console.log(ts.formatDiagnosticsWithColorAndContext(config!.errors, compilerHost))
28
+ return
29
+ }
30
+
31
+ return config
32
+ }
package/src/run.ts ADDED
@@ -0,0 +1,65 @@
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
+
10
+ import { execaNode, execa } from 'execa'
11
+ import type { RunOptions } from './types.js'
12
+
13
+ /**
14
+ * Default set of args to pass in order to run TypeScript
15
+ * source
16
+ */
17
+ const DEFAULT_NODE_ARGS = [
18
+ // Use ts-node/esm loader. The project must install it
19
+ '--loader=ts-node/esm',
20
+ // Disable annonying warnings
21
+ '--no-warnings',
22
+ // Enable expiremental meta resolve for cases where someone uses magic import string
23
+ '--experimental-import-meta-resolve',
24
+ ]
25
+
26
+ /**
27
+ * Runs a Node.js script as a child process and inherits the stdio streams
28
+ */
29
+ export function runNode(cwd: string | URL, options: RunOptions) {
30
+ const childProcess = execaNode(options.script, options.scriptArgs, {
31
+ nodeOptions: DEFAULT_NODE_ARGS.concat(options.nodeArgs),
32
+ preferLocal: true,
33
+ windowsHide: false,
34
+ localDir: cwd,
35
+ cwd,
36
+ buffer: false,
37
+ stdio: options.stdio || 'inherit',
38
+ env: {
39
+ ...(options.stdio === 'pipe' ? { FORCE_COLOR: 'true' } : {}),
40
+ ...options.env,
41
+ },
42
+ })
43
+
44
+ return childProcess
45
+ }
46
+
47
+ /**
48
+ * Runs a script as a child process and inherits the stdio streams
49
+ */
50
+ export function run(cwd: string | URL, options: Omit<RunOptions, 'nodeArgs'>) {
51
+ const childProcess = execa(options.script, options.scriptArgs, {
52
+ preferLocal: true,
53
+ windowsHide: false,
54
+ localDir: cwd,
55
+ cwd,
56
+ buffer: false,
57
+ stdio: options.stdio || 'inherit',
58
+ env: {
59
+ ...(options.stdio === 'pipe' ? { FORCE_COLOR: 'true' } : {}),
60
+ ...options.env,
61
+ },
62
+ })
63
+
64
+ return childProcess
65
+ }
package/src/types.ts ADDED
@@ -0,0 +1,69 @@
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
+
10
+ /**
11
+ * Options needed to run a script file
12
+ */
13
+ export type RunOptions = {
14
+ script: string
15
+ scriptArgs: string[]
16
+ nodeArgs: string[]
17
+ stdio?: 'pipe' | 'inherit'
18
+ env?: NodeJS.ProcessEnv
19
+ }
20
+
21
+ /**
22
+ * Watcher options
23
+ */
24
+ export type WatchOptions = {
25
+ poll?: boolean
26
+ }
27
+
28
+ /**
29
+ * Meta file config defined in ".adonisrc.json" file
30
+ */
31
+ export type MetaFile = {
32
+ pattern: string
33
+ reloadServer: boolean
34
+ }
35
+
36
+ /**
37
+ * Options accepted by assets bundler
38
+ */
39
+ export type AssetsBundlerOptions =
40
+ | {
41
+ serve: false
42
+ driver?: string
43
+ cmd?: string
44
+ }
45
+ | {
46
+ serve: true
47
+ driver: string
48
+ cmd: string
49
+ }
50
+
51
+ /**
52
+ * Options accepted by the dev server
53
+ */
54
+ export type DevServerOptions = {
55
+ scriptArgs: string[]
56
+ nodeArgs: string[]
57
+ clearScreen?: boolean
58
+ env?: NodeJS.ProcessEnv
59
+ metaFiles?: MetaFile[]
60
+ assets?: AssetsBundlerOptions
61
+ }
62
+
63
+ /**
64
+ * Options accepted by the project bundler
65
+ */
66
+ export type BundlerOptions = {
67
+ metaFiles?: MetaFile[]
68
+ assets?: AssetsBundlerOptions
69
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,29 @@
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
+
10
+ import type tsStatic from 'typescript'
11
+ import { fileURLToPath } from 'node:url'
12
+ import { Watcher } from '@poppinss/chokidar-ts'
13
+
14
+ import type { WatchOptions } from './types.js'
15
+ import { parseConfig } from './parse_config.js'
16
+
17
+ /**
18
+ * Watches the file system using tsconfig file
19
+ */
20
+ export function watch(cwd: string | URL, ts: typeof tsStatic, options: WatchOptions) {
21
+ const config = parseConfig(cwd, ts)
22
+ if (!config) {
23
+ return
24
+ }
25
+
26
+ const watcher = new Watcher(typeof cwd === 'string' ? cwd : fileURLToPath(cwd), config!)
27
+ const chokidar = watcher.watch(['.'], { usePolling: options.poll })
28
+ return { watcher, chokidar }
29
+ }