@platformatic/nest 2.67.0-alpha.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/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +13 -0
- package/config.d.ts +437 -0
- package/eslint.config.js +3 -0
- package/index.js +312 -0
- package/lib/schema.js +76 -0
- package/package.json +47 -0
- package/schema.json +1413 -0
package/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseStackable,
|
|
3
|
+
cleanBasePath,
|
|
4
|
+
ensureTrailingSlash,
|
|
5
|
+
errors,
|
|
6
|
+
getServerUrl,
|
|
7
|
+
importFile,
|
|
8
|
+
resolvePackage,
|
|
9
|
+
schemaOptions,
|
|
10
|
+
transformConfig
|
|
11
|
+
} from '@platformatic/basic'
|
|
12
|
+
import { ConfigManager } from '@platformatic/config'
|
|
13
|
+
import { features } from '@platformatic/utils'
|
|
14
|
+
import inject from 'light-my-request'
|
|
15
|
+
import { readFile } from 'node:fs/promises'
|
|
16
|
+
import { dirname, resolve } from 'node:path'
|
|
17
|
+
import { pinoHttp } from 'pino-http'
|
|
18
|
+
import { satisfies } from 'semver'
|
|
19
|
+
import { packageJson, schema } from './lib/schema.js'
|
|
20
|
+
|
|
21
|
+
const supportedVersions = '^11.0.0'
|
|
22
|
+
|
|
23
|
+
export class NestStackable extends BaseStackable {
|
|
24
|
+
#basePath
|
|
25
|
+
#nestjsCore
|
|
26
|
+
#nestjsCli
|
|
27
|
+
#isFastify
|
|
28
|
+
#app
|
|
29
|
+
#server
|
|
30
|
+
#dispatcher
|
|
31
|
+
|
|
32
|
+
constructor (options, root, configManager) {
|
|
33
|
+
super('nest', packageJson.version, options, root, configManager)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async init () {
|
|
37
|
+
const config = this.configManager.current
|
|
38
|
+
|
|
39
|
+
this.#isFastify = config.nest.adapter === 'fastify'
|
|
40
|
+
this.#nestjsCore = resolve(resolvePackage(this.root, '@nestjs/core'))
|
|
41
|
+
// As @nest/cli is not exporting any file, we assume it's in the same folder of @nestjs/core.
|
|
42
|
+
this.#nestjsCli = resolve(this.#nestjsCore, '../../cli/bin/nest.js')
|
|
43
|
+
|
|
44
|
+
const nestPackage = JSON.parse(await readFile(resolve(dirname(this.#nestjsCore), 'package.json'), 'utf-8'))
|
|
45
|
+
|
|
46
|
+
if (!this.isProduction && !satisfies(nestPackage.version, supportedVersions)) {
|
|
47
|
+
throw new errors.UnsupportedVersion('@nestjs/core', nestPackage.version, supportedVersions)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.#basePath = config.application?.basePath
|
|
51
|
+
? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
|
|
52
|
+
: undefined
|
|
53
|
+
|
|
54
|
+
this.registerGlobals({ basePath: this.#basePath })
|
|
55
|
+
|
|
56
|
+
this.subprocessForceClose = true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async start ({ listen }) {
|
|
60
|
+
// Make this idempotent
|
|
61
|
+
if (this.url) {
|
|
62
|
+
return this.url
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const config = this.configManager.current
|
|
66
|
+
const command = config.application.commands[this.isProduction ? 'production' : 'development']
|
|
67
|
+
|
|
68
|
+
// In development mode, we use the Nest CLI in a children thread - Using build then start would result in a bad DX
|
|
69
|
+
this.on('config', config => {
|
|
70
|
+
this.#basePath = config.basePath
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (command || !this.isProduction) {
|
|
74
|
+
await this.startWithCommand(command || `node ${this.#nestjsCli} start --watch --preserveWatchOutput`)
|
|
75
|
+
} else {
|
|
76
|
+
return this.#startProduction(listen)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async stop () {
|
|
81
|
+
if (this.childManager) {
|
|
82
|
+
return this.stopCommand()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this.#isFastify) {
|
|
86
|
+
return this.#server.close()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* c8 ignore next 3 */
|
|
90
|
+
if (!this.#server?.listening) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
this.#server.close(error => {
|
|
96
|
+
/* c8 ignore next 3 */
|
|
97
|
+
if (error) {
|
|
98
|
+
return reject(error)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
resolve()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async build () {
|
|
107
|
+
if (!this.#nestjsCore) {
|
|
108
|
+
await this.init()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const config = this.configManager.current
|
|
112
|
+
this.#basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
|
|
113
|
+
|
|
114
|
+
return this.buildWithCommand(config.application.commands.build ?? `node ${this.#nestjsCli} build`, this.#basePath)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async inject (injectParams, onInject) {
|
|
118
|
+
let res
|
|
119
|
+
|
|
120
|
+
if (this.startHttpTimer && this.endHttpTimer) {
|
|
121
|
+
this.startHttpTimer({ request: injectParams })
|
|
122
|
+
if (onInject) {
|
|
123
|
+
const originalOnInject = onInject
|
|
124
|
+
onInject = (err, response) => {
|
|
125
|
+
this.endHttpTimer({ request: injectParams, response })
|
|
126
|
+
originalOnInject(err, response)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (this.#isFastify) {
|
|
132
|
+
res = await this.#server.inject(injectParams, onInject)
|
|
133
|
+
} else {
|
|
134
|
+
res = await inject(this.#dispatcher, injectParams, onInject)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* c8 ignore next 3 */
|
|
138
|
+
if (onInject) {
|
|
139
|
+
return
|
|
140
|
+
} else if (this.endHttpTimer) {
|
|
141
|
+
this.endHttpTimer({ request: injectParams, response: res })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Since inject might be called from the main thread directly via ITC, let's clean it up
|
|
145
|
+
const { statusCode, headers, body, payload, rawPayload } = res
|
|
146
|
+
return { statusCode, headers, body, payload, rawPayload }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getMeta () {
|
|
150
|
+
const hasBasePath = this.basePath || this.#basePath
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
composer: {
|
|
154
|
+
tcp: typeof this.url !== 'undefined',
|
|
155
|
+
url: this.url,
|
|
156
|
+
prefix: this.basePath ?? this.#basePath,
|
|
157
|
+
wantsAbsoluteUrls: !!hasBasePath,
|
|
158
|
+
needsRootRedirect: false
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* c8 ignore next 5 */
|
|
164
|
+
async getWatchConfig () {
|
|
165
|
+
return {
|
|
166
|
+
enabled: false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async #startProduction (listen) {
|
|
171
|
+
// Listen if entrypoint
|
|
172
|
+
if (this.#app && listen) {
|
|
173
|
+
await this.#listen()
|
|
174
|
+
return this.url
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const outputDirectory = this.configManager.current.application.outputDirectory
|
|
178
|
+
const { path, name } = this.configManager.current.nest.appModule
|
|
179
|
+
this.verifyOutputDirectory(resolve(this.root, outputDirectory))
|
|
180
|
+
|
|
181
|
+
// Import all the necessary modules
|
|
182
|
+
const { NestFactory } = await importFile(this.#nestjsCore)
|
|
183
|
+
const Adapter = await this.#importAdapter()
|
|
184
|
+
const appModuleExport = await importFile(resolve(this.root, `${outputDirectory}/${path}.js`))
|
|
185
|
+
const appModule = appModuleExport[name]
|
|
186
|
+
const setup = await this.#importSetup()
|
|
187
|
+
|
|
188
|
+
// Create the server
|
|
189
|
+
if (this.#isFastify) {
|
|
190
|
+
this.#app = await NestFactory.create(appModule, new Adapter({ loggerInstance: this.logger }))
|
|
191
|
+
|
|
192
|
+
setup?.(this.#app)
|
|
193
|
+
await this.#app.init()
|
|
194
|
+
|
|
195
|
+
this.#server = this.#app.getInstance()
|
|
196
|
+
} else {
|
|
197
|
+
this.#app = await NestFactory.create(appModule, new Adapter())
|
|
198
|
+
|
|
199
|
+
const instance = this.#app.getInstance()
|
|
200
|
+
instance.disable('x-powered-by')
|
|
201
|
+
instance.use(pinoHttp({ logger: this.logger }))
|
|
202
|
+
|
|
203
|
+
setup?.(this.#app)
|
|
204
|
+
await this.#app.init()
|
|
205
|
+
|
|
206
|
+
this.#server = this.#app.getHttpServer()
|
|
207
|
+
this.#dispatcher = this.#server.listeners('request')[0]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (listen) {
|
|
211
|
+
await this.#listen()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await this._collectMetrics()
|
|
215
|
+
return this.url
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async #listen () {
|
|
219
|
+
const serverOptions = this.serverConfig
|
|
220
|
+
const listenOptions = { host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 }
|
|
221
|
+
|
|
222
|
+
if (this.isProduction && features.node.reusePort) {
|
|
223
|
+
listenOptions.reusePort = true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await this.#app.listen(listenOptions)
|
|
227
|
+
this.url = getServerUrl(this.#isFastify ? this.#server.server : this.#server)
|
|
228
|
+
|
|
229
|
+
return this.url
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async #importAdapter () {
|
|
233
|
+
let adapter
|
|
234
|
+
const toImport = `@nestjs/platform-${this.configManager.current.nest.adapter}`
|
|
235
|
+
|
|
236
|
+
this.logger.debug(`Using NestJS adapter ${toImport}.`)
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
adapter = await importFile(resolvePackage(this.root, toImport))
|
|
240
|
+
return adapter[this.#isFastify ? 'FastifyAdapter' : 'ExpressAdapter']
|
|
241
|
+
} catch (e) {
|
|
242
|
+
throw new Error(`Cannot import the NestJS adapter. Please add ${toImport} to the dependencies and try again.`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async #importSetup () {
|
|
247
|
+
const config = this.configManager.current
|
|
248
|
+
|
|
249
|
+
if (!config.nest.setup.path) {
|
|
250
|
+
return undefined
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let setupModule
|
|
254
|
+
let setup
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
setupModule = await importFile(
|
|
258
|
+
resolve(this.root, `${config.application.outputDirectory}/${config.nest.setup.path}.js`)
|
|
259
|
+
)
|
|
260
|
+
} catch (e) {
|
|
261
|
+
throw new Error(`Cannot import the NestJS setup file: ${e.message}.`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// This is for improved compatibility
|
|
265
|
+
if (config.nest.setup.name) {
|
|
266
|
+
setup = setupModule[config.setup.name]
|
|
267
|
+
} else {
|
|
268
|
+
setup = setupModule.default
|
|
269
|
+
|
|
270
|
+
if (setup && typeof setup !== 'function' && typeof setup.default === 'function') {
|
|
271
|
+
setup = setup.default
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (typeof setup !== 'function') {
|
|
276
|
+
const name = config.setup.name ? ` named ${config.setup.name}` : ''
|
|
277
|
+
throw new Error(`The NestJS setup file must export a function named ${name}, but got ${typeof setup}.`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return setup
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function buildStackable (opts) {
|
|
285
|
+
const root = opts.context.directory
|
|
286
|
+
|
|
287
|
+
const configManager = new ConfigManager({
|
|
288
|
+
schema,
|
|
289
|
+
source: opts.config ?? {},
|
|
290
|
+
schemaOptions,
|
|
291
|
+
transformConfig,
|
|
292
|
+
dirname: root,
|
|
293
|
+
context: opts.context
|
|
294
|
+
})
|
|
295
|
+
await configManager.parseAndValidate()
|
|
296
|
+
|
|
297
|
+
return new NestStackable(opts, root, configManager)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export { schema, schemaComponents } from './lib/schema.js'
|
|
301
|
+
|
|
302
|
+
export default {
|
|
303
|
+
configType: 'nest',
|
|
304
|
+
configManagerConfig: {
|
|
305
|
+
schemaOptions,
|
|
306
|
+
transformConfig
|
|
307
|
+
},
|
|
308
|
+
buildStackable,
|
|
309
|
+
schema,
|
|
310
|
+
version: packageJson.version,
|
|
311
|
+
modulesToLoad: []
|
|
312
|
+
}
|
package/lib/schema.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { schemaComponents as basicSchemaComponents } from '@platformatic/basic'
|
|
2
|
+
import { schemaComponents as utilsSchemaComponents } from '@platformatic/utils'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
|
|
6
|
+
|
|
7
|
+
export const version = packageJson.version
|
|
8
|
+
|
|
9
|
+
const nest = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
adapter: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['express', 'fastify'],
|
|
15
|
+
// We would probably prefer 'fastify' as default, but NestJS uses express by default so we don't want to break existing setups
|
|
16
|
+
default: 'express'
|
|
17
|
+
},
|
|
18
|
+
appModule: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
path: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
default: 'app.module'
|
|
24
|
+
},
|
|
25
|
+
name: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
default: 'AppModule'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
default: {}
|
|
32
|
+
},
|
|
33
|
+
setup: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
path: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: ''
|
|
39
|
+
},
|
|
40
|
+
name: {
|
|
41
|
+
type: 'string'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
default: {}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
default: {},
|
|
49
|
+
additionalProperties: false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const schemaComponents = { node: nest }
|
|
53
|
+
|
|
54
|
+
export const schema = {
|
|
55
|
+
$id: `https://schemas.platformatic.dev/@platformatic/nest/${version}.json`,
|
|
56
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
57
|
+
title: 'Platformatic NestJS Stackable',
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
$schema: {
|
|
61
|
+
type: 'string'
|
|
62
|
+
},
|
|
63
|
+
logger: utilsSchemaComponents.logger,
|
|
64
|
+
server: utilsSchemaComponents.server,
|
|
65
|
+
watch: basicSchemaComponents.watch,
|
|
66
|
+
application: basicSchemaComponents.application,
|
|
67
|
+
runtime: utilsSchemaComponents.wrappedRuntime,
|
|
68
|
+
nest
|
|
69
|
+
},
|
|
70
|
+
additionalProperties: false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* c8 ignore next 3 */
|
|
74
|
+
if (process.argv[1] === import.meta.filename) {
|
|
75
|
+
console.log(JSON.stringify(schema, null, 2))
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@platformatic/nest",
|
|
3
|
+
"version": "2.67.0-alpha.1",
|
|
4
|
+
"description": "Platformatic Nest.js Stackable",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/platformatic/platformatic.git"
|
|
10
|
+
},
|
|
11
|
+
"author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/platformatic/platformatic/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/platformatic/platformatic#readme",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"light-my-request": "^6.0.0",
|
|
19
|
+
"pino-http": "^10.2.0",
|
|
20
|
+
"@platformatic/basic": "2.67.0-alpha.1",
|
|
21
|
+
"@platformatic/generators": "2.67.0-alpha.1",
|
|
22
|
+
"@platformatic/utils": "2.67.0-alpha.1",
|
|
23
|
+
"@platformatic/config": "2.67.0-alpha.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@nestjs/cli": "^11.0.7",
|
|
27
|
+
"@nestjs/core": "^11.1.2",
|
|
28
|
+
"@nestjs/platform-express": "^11.1.2",
|
|
29
|
+
"@nestjs/platform-fastify": "^11.1.2",
|
|
30
|
+
"borp": "^0.20.0",
|
|
31
|
+
"eslint": "9",
|
|
32
|
+
"json-schema-to-typescript": "^15.0.1",
|
|
33
|
+
"neostandard": "^0.12.0",
|
|
34
|
+
"tsx": "^4.19.0",
|
|
35
|
+
"typescript": "^5.5.4",
|
|
36
|
+
"@platformatic/service": "2.67.0-alpha.1",
|
|
37
|
+
"@platformatic/composer": "2.67.0-alpha.1"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "pnpm run lint && borp --concurrency=1 --no-timeout",
|
|
41
|
+
"coverage": "pnpm run lint && borp -C -X test -X test/fixtures --concurrency=1 --no-timeout",
|
|
42
|
+
"gen-schema": "node lib/schema.js > schema.json",
|
|
43
|
+
"gen-types": "json2ts > config.d.ts < schema.json",
|
|
44
|
+
"build": "pnpm run gen-schema && pnpm run gen-types",
|
|
45
|
+
"lint": "eslint"
|
|
46
|
+
}
|
|
47
|
+
}
|