@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/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
+ }