@platformatic/node 3.4.1 → 3.5.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/config.d.ts +343 -4
- package/index.js +23 -399
- package/lib/capability.js +407 -0
- package/lib/generator.js +134 -0
- package/lib/schema.js +23 -8
- package/lib/utils.js +69 -0
- package/package.json +17 -15
- package/schema.json +1376 -13
package/index.js
CHANGED
|
@@ -1,408 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ensureTrailingSlash,
|
|
6
|
-
getServerUrl,
|
|
7
|
-
importFile,
|
|
8
|
-
injectViaRequest,
|
|
9
|
-
schemaOptions,
|
|
10
|
-
transformConfig
|
|
11
|
-
} from '@platformatic/basic'
|
|
12
|
-
import { ConfigManager } from '@platformatic/config'
|
|
13
|
-
import { setupNodeHTTPTelemetry } from '@platformatic/telemetry'
|
|
14
|
-
import inject from 'light-my-request'
|
|
15
|
-
import { existsSync } from 'node:fs'
|
|
16
|
-
import { readFile } from 'node:fs/promises'
|
|
17
|
-
import { Server } from 'node:http'
|
|
18
|
-
import { resolve as pathResolve, resolve } from 'node:path'
|
|
19
|
-
import { pathToFileURL } from 'url'
|
|
20
|
-
import { packageJson, schema } from './lib/schema.js'
|
|
1
|
+
import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
|
|
2
|
+
import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
|
|
3
|
+
import { NodeCapability } from './lib/capability.js'
|
|
4
|
+
import { schema } from './lib/schema.js'
|
|
21
5
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'exports',
|
|
26
|
-
'exports#node',
|
|
27
|
-
'exports#import',
|
|
28
|
-
'exports#require',
|
|
29
|
-
'exports#default',
|
|
30
|
-
'exports#.#node',
|
|
31
|
-
'exports#.#import',
|
|
32
|
-
'exports#.#require',
|
|
33
|
-
'exports#.#default'
|
|
34
|
-
]
|
|
6
|
+
export async function transform (config, _schema, options) {
|
|
7
|
+
config = await basicTransform(config, schema, options)
|
|
8
|
+
config.telemetry = { ...options.telemetryConfig, ...config.telemetry }
|
|
35
9
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Paolo: This is kinda hackish but there is no better way. I apologize.
|
|
39
|
-
function isFastify (app) {
|
|
40
|
-
return Object.getOwnPropertySymbols(app).some(s => s.description === 'fastify.state')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isKoa (app) {
|
|
44
|
-
return typeof app.callback === 'function'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class NodeStackable extends BaseStackable {
|
|
48
|
-
#module
|
|
49
|
-
#app
|
|
50
|
-
#server
|
|
51
|
-
#basePath
|
|
52
|
-
#dispatcher
|
|
53
|
-
#isFastify
|
|
54
|
-
#isKoa
|
|
55
|
-
|
|
56
|
-
#startHttpTimer
|
|
57
|
-
#endHttpTimer
|
|
58
|
-
|
|
59
|
-
constructor (options, root, configManager) {
|
|
60
|
-
super('nodejs', packageJson.version, options, root, configManager)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async start ({ listen }) {
|
|
64
|
-
// Make this idempotent
|
|
65
|
-
if (this.url) {
|
|
66
|
-
return this.url
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Listen if entrypoint
|
|
70
|
-
if (this.#app && listen) {
|
|
71
|
-
await this._listen()
|
|
72
|
-
return this.url
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const config = this.configManager.current
|
|
76
|
-
const command = config.application.commands[this.isProduction ? 'production' : 'development']
|
|
77
|
-
|
|
78
|
-
if (command) {
|
|
79
|
-
return this.startWithCommand(command)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Resolve the entrypoint
|
|
83
|
-
// The priority is platformatic.application.json, then package.json and finally autodetect.
|
|
84
|
-
// Only when autodetecting we eventually search in the dist folder when in production mode
|
|
85
|
-
const finalEntrypoint = await this._findEntrypoint()
|
|
86
|
-
|
|
87
|
-
// Require the application
|
|
88
|
-
this.#basePath = config.application?.basePath
|
|
89
|
-
? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
|
|
90
|
-
: undefined
|
|
91
|
-
|
|
92
|
-
this.registerGlobals({
|
|
93
|
-
// Always use URL to avoid serialization problem in Windows
|
|
94
|
-
id: this.id,
|
|
95
|
-
root: pathToFileURL(this.root).toString(),
|
|
96
|
-
basePath: this.#basePath,
|
|
97
|
-
logLevel: this.logger.level
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
// The server promise must be created before requiring the entrypoint even if it's not going to be used
|
|
101
|
-
// at all. Otherwise there is chance we miss the listen event.
|
|
102
|
-
const serverOptions = this.serverConfig
|
|
103
|
-
const serverPromise = createServerListener(
|
|
104
|
-
(this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
|
|
105
|
-
(this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
|
|
106
|
-
)
|
|
107
|
-
// If telemetry is set, configure it
|
|
108
|
-
const telemetryConfig = this.telemetryConfig
|
|
109
|
-
if (telemetryConfig) {
|
|
110
|
-
setupNodeHTTPTelemetry(telemetryConfig, this.logger)
|
|
111
|
-
}
|
|
112
|
-
this.#module = await importFile(finalEntrypoint)
|
|
113
|
-
this.#module = this.#module.default || this.#module
|
|
114
|
-
|
|
115
|
-
// Deal with application
|
|
116
|
-
const factory = ['build', 'create'].find(f => typeof this.#module[f] === 'function')
|
|
117
|
-
|
|
118
|
-
if (factory) {
|
|
119
|
-
// We have build function, this Stackable will not use HTTP unless it is the entrypoint
|
|
120
|
-
serverPromise.cancel()
|
|
121
|
-
|
|
122
|
-
this.#app = await this.#module[factory]()
|
|
123
|
-
this.#isFastify = isFastify(this.#app)
|
|
124
|
-
this.#isKoa = isKoa(this.#app)
|
|
125
|
-
|
|
126
|
-
if (this.#isFastify) {
|
|
127
|
-
await this.#app.ready()
|
|
128
|
-
} else if (this.#isKoa) {
|
|
129
|
-
this.#dispatcher = this.#app.callback()
|
|
130
|
-
} else if (this.#app instanceof Server) {
|
|
131
|
-
this.#server = this.#app
|
|
132
|
-
this.#dispatcher = this.#server.listeners('request')[0]
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
// User blackbox function, we wait for it to listen on a port
|
|
136
|
-
this.#server = await serverPromise
|
|
137
|
-
this.url = getServerUrl(this.#server)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return this.url
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async stop () {
|
|
144
|
-
if (this.subprocess) {
|
|
145
|
-
return this.stopCommand()
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (this.#isFastify) {
|
|
149
|
-
return this.#app.close()
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/* c8 ignore next 3 */
|
|
153
|
-
if (!this.#server?.listening) {
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return new Promise((resolve, reject) => {
|
|
158
|
-
this.#server.close(error => {
|
|
159
|
-
/* c8 ignore next 3 */
|
|
160
|
-
if (error) {
|
|
161
|
-
return reject(error)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
resolve()
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async collectMetrics ({ startHttpTimer, endHttpTimer }) {
|
|
170
|
-
this.#startHttpTimer = startHttpTimer
|
|
171
|
-
this.#endHttpTimer = endHttpTimer
|
|
172
|
-
|
|
173
|
-
return { defaultMetrics: true, httpMetrics: true }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async build () {
|
|
177
|
-
const command = this.configManager.current.application.commands.build
|
|
178
|
-
|
|
179
|
-
if (command) {
|
|
180
|
-
return this.buildWithCommand(command, null)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// If no command was specified, we try to see if there is a build script defined in package.json.
|
|
184
|
-
const hasBuildScript = await this.#hasBuildScript()
|
|
185
|
-
|
|
186
|
-
if (!hasBuildScript) {
|
|
187
|
-
this.logger.debug(
|
|
188
|
-
'No "application.commands.build" configuration value specified and no build script found in package.json. Skipping build ...'
|
|
189
|
-
)
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return this.buildWithCommand('npm run build', null)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async inject (injectParams, onInject) {
|
|
197
|
-
let res
|
|
198
|
-
|
|
199
|
-
if (this.url) {
|
|
200
|
-
this.logger.trace({ injectParams, url: this.url }, 'injecting via request')
|
|
201
|
-
res = await injectViaRequest(this.url, injectParams, onInject)
|
|
202
|
-
} else {
|
|
203
|
-
if (this.#startHttpTimer && this.#endHttpTimer) {
|
|
204
|
-
this.#startHttpTimer({ request: injectParams })
|
|
205
|
-
|
|
206
|
-
if (onInject) {
|
|
207
|
-
const originalOnInject = onInject
|
|
208
|
-
onInject = (err, response) => {
|
|
209
|
-
this.#endHttpTimer({ request: injectParams, response })
|
|
210
|
-
originalOnInject(err, response)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (this.#isFastify) {
|
|
216
|
-
this.logger.trace({ injectParams }, 'injecting via fastify')
|
|
217
|
-
res = await this.#app.inject(injectParams, onInject)
|
|
218
|
-
} else {
|
|
219
|
-
this.logger.trace({ injectParams }, 'injecting via light-my-request')
|
|
220
|
-
res = await inject(this.#dispatcher ?? this.#app, injectParams, onInject)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (this.#endHttpTimer && !onInject) {
|
|
224
|
-
this.#endHttpTimer({ request: injectParams, response: res })
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/* c8 ignore next 3 */
|
|
229
|
-
if (onInject) {
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Since inject might be called from the main thread directly via ITC, let's clean it up
|
|
234
|
-
const { statusCode, headers, body, payload, rawPayload } = res
|
|
235
|
-
|
|
236
|
-
return { statusCode, headers, body, payload, rawPayload }
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
_getWantsAbsoluteUrls () {
|
|
240
|
-
const config = this.configManager.current
|
|
241
|
-
return config.node.absoluteUrl
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
getMeta () {
|
|
245
|
-
return {
|
|
246
|
-
composer: {
|
|
247
|
-
tcp: typeof this.url !== 'undefined',
|
|
248
|
-
url: this.url,
|
|
249
|
-
prefix: this.basePath ?? this.#basePath,
|
|
250
|
-
wantsAbsoluteUrls: this._getWantsAbsoluteUrls(),
|
|
251
|
-
needsRootRedirect: true
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async _listen () {
|
|
257
|
-
const serverOptions = this.serverConfig
|
|
258
|
-
|
|
259
|
-
if (this.#isFastify) {
|
|
260
|
-
await this.#app.listen({ host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 })
|
|
261
|
-
this.url = getServerUrl(this.#app.server)
|
|
262
|
-
} else {
|
|
263
|
-
// Express / Node / Koa
|
|
264
|
-
this.#server = await new Promise((resolve, reject) => {
|
|
265
|
-
return this.#app
|
|
266
|
-
.listen({ host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 }, function () {
|
|
267
|
-
resolve(this)
|
|
268
|
-
})
|
|
269
|
-
.on('error', reject)
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
this.url = getServerUrl(this.#server)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return this.url
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
_getApplication () {
|
|
279
|
-
return this.#app
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async _findEntrypoint () {
|
|
283
|
-
const config = this.configManager.current
|
|
284
|
-
const outputRoot = resolve(this.root, config.application.outputDirectory)
|
|
285
|
-
|
|
286
|
-
if (config.node.main) {
|
|
287
|
-
return pathResolve(this.root, config.node.main)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const { entrypoint, hadEntrypointField } = await getEntrypointInformation(this.root)
|
|
291
|
-
|
|
292
|
-
if (!entrypoint) {
|
|
293
|
-
this.logger.error(
|
|
294
|
-
`The service ${this.id} had no valid entrypoint defined in the package.json file and no valid entrypoint file was found.`
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
process.exit(1)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (!hadEntrypointField) {
|
|
301
|
-
this.logger.warn(
|
|
302
|
-
`The service ${this.id} had no valid entrypoint defined in the package.json file. Falling back to the file "${entrypoint}".`
|
|
303
|
-
)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let root = this.root
|
|
307
|
-
|
|
308
|
-
if (this.isProduction) {
|
|
309
|
-
const hasCommand = this.configManager.current.application.commands.build
|
|
310
|
-
const hasBuildScript = await this.#hasBuildScript()
|
|
311
|
-
|
|
312
|
-
if (hasCommand || hasBuildScript) {
|
|
313
|
-
this.verifyOutputDirectory(outputRoot)
|
|
314
|
-
root = outputRoot
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return pathResolve(root, entrypoint)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async #hasBuildScript () {
|
|
322
|
-
// If no command was specified, we try to see if there is a build script defined in package.json.
|
|
323
|
-
let hasBuildScript
|
|
324
|
-
try {
|
|
325
|
-
const packageJson = JSON.parse(await readFile(resolve(this.root, 'package.json'), 'utf-8'))
|
|
326
|
-
hasBuildScript = typeof packageJson.scripts.build === 'string' && packageJson.scripts.build
|
|
327
|
-
} catch (e) {
|
|
328
|
-
// No-op
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return hasBuildScript
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function getEntrypointInformation (root) {
|
|
336
|
-
let entrypoint
|
|
337
|
-
let packageJson
|
|
338
|
-
let hadEntrypointField = false
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
packageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf-8'))
|
|
342
|
-
} catch {
|
|
343
|
-
// No package.json, we only load the index.js file
|
|
344
|
-
packageJson = {}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
for (const field of validFields) {
|
|
348
|
-
let current = packageJson
|
|
349
|
-
const sequence = field.split('#')
|
|
350
|
-
|
|
351
|
-
while (current && sequence.length && typeof current !== 'string') {
|
|
352
|
-
current = current[sequence.shift()]
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (typeof current === 'string') {
|
|
356
|
-
entrypoint = current
|
|
357
|
-
hadEntrypointField = true
|
|
358
|
-
break
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (!entrypoint) {
|
|
363
|
-
for (const basename of validFilesBasenames) {
|
|
364
|
-
for (const ext of ['js', 'mjs', 'cjs']) {
|
|
365
|
-
const file = `${basename}.${ext}`
|
|
366
|
-
|
|
367
|
-
if (existsSync(resolve(root, file))) {
|
|
368
|
-
entrypoint = file
|
|
369
|
-
break
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (entrypoint) {
|
|
374
|
-
break
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return { entrypoint, hadEntrypointField }
|
|
10
|
+
return config
|
|
380
11
|
}
|
|
381
12
|
|
|
382
|
-
export async function
|
|
383
|
-
const root =
|
|
13
|
+
export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
|
|
14
|
+
const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
|
|
384
15
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
16
|
+
return utilsLoadConfiguration(source, context?.schema ?? schema, {
|
|
17
|
+
validationOptions,
|
|
18
|
+
transform,
|
|
19
|
+
replaceEnv: true,
|
|
20
|
+
root,
|
|
21
|
+
...context
|
|
391
22
|
})
|
|
392
|
-
await configManager.parseAndValidate()
|
|
393
|
-
|
|
394
|
-
return new NodeStackable(opts, root, configManager)
|
|
395
23
|
}
|
|
396
24
|
|
|
397
|
-
export
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
configType: 'nodejs',
|
|
401
|
-
configManagerConfig: {
|
|
402
|
-
schemaOptions,
|
|
403
|
-
transformConfig
|
|
404
|
-
},
|
|
405
|
-
buildStackable,
|
|
406
|
-
schema,
|
|
407
|
-
version: packageJson.version
|
|
25
|
+
export async function create (configOrRoot, sourceOrConfig, context) {
|
|
26
|
+
const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
|
|
27
|
+
return new NodeCapability(config[kMetadata].root, config, context)
|
|
408
28
|
}
|
|
29
|
+
|
|
30
|
+
export * from './lib/capability.js'
|
|
31
|
+
export { Generator } from './lib/generator.js'
|
|
32
|
+
export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
|