@platformatic/node 3.4.1 → 3.5.0

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 CHANGED
@@ -1,408 +1,32 @@
1
- import {
2
- BaseStackable,
3
- cleanBasePath,
4
- createServerListener,
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
- const validFields = [
23
- 'main',
24
- 'exports',
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
- const validFilesBasenames = ['index', 'main', 'app', 'application', 'server', 'start', 'bundle', 'run', 'entrypoint']
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 buildStackable (opts) {
383
- const root = opts.context.directory
13
+ export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
14
+ const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
384
15
 
385
- const configManager = new ConfigManager({
386
- schema,
387
- source: opts.config ?? {},
388
- schemaOptions,
389
- transformConfig,
390
- dirname: root
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 { schema, schemaComponents } from './lib/schema.js'
398
-
399
- export default {
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'