@platformatic/node 2.74.3 → 3.0.0-alpha.2

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export interface PlatformaticNodeJsStackable {
8
+ export interface PlatformaticNodeJsConfig {
9
9
  $schema?: string;
10
10
  logger?: {
11
11
  level: (
package/index.js CHANGED
@@ -1,435 +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 { features } from '@platformatic/utils'
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 { packageJson, schema } from './lib/schema.js'
20
- import { getTsconfig, ignoreDirs, isServiceBuildable } from './lib/utils.js'
1
+ import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
2
+ import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
3
+ import { schema } from './lib/schema.js'
4
+ import { NodeStackable } from './lib/stackable.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
- #useHttpForDispatch
56
-
57
- constructor (options, root, configManager) {
58
- super('nodejs', packageJson.version, options, root, configManager)
59
- }
60
-
61
- async start ({ listen }) {
62
- // Make this idempotent
63
- if (this.url) {
64
- return this.url
65
- }
66
-
67
- // Listen if entrypoint
68
- if (this.#app && listen) {
69
- await this._listen()
70
- return this.url
71
- }
72
-
73
- const config = this.configManager.current
74
-
75
- if (!this.isProduction && (await isServiceBuildable(this.root, config))) {
76
- this.logger.info(`Building service "${this.serviceId}" before starting in development mode ...`)
77
- try {
78
- await this.build()
79
- this.childManager = null
80
- } catch (e) {
81
- this.logger.error(`Error while building service "${this.serviceId}": ${e.message}`)
82
- }
83
- }
84
-
85
- const command = config.application.commands[this.isProduction ? 'production' : 'development']
86
-
87
- if (command) {
88
- return this.startWithCommand(command)
89
- }
90
-
91
- // Resolve the entrypoint
92
- // The priority is platformatic.application.json, then package.json and finally autodetect.
93
- // Only when autodetecting we eventually search in the dist folder when in production mode
94
- const finalEntrypoint = await this._findEntrypoint()
95
-
96
- // Require the application
97
- this.#basePath = config.application?.basePath
98
- ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
99
- : undefined
100
-
101
- this.registerGlobals({
102
- basePath: this.#basePath
103
- })
104
-
105
- // The server promise must be created before requiring the entrypoint even if it's not going to be used
106
- // at all. Otherwise there is chance we miss the listen event.
107
- const serverOptions = this.serverConfig
108
- const serverPromise = createServerListener(
109
- (this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
110
- (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
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 (config.node?.hasServer !== false && this.#module.hasServer !== false) {
119
- if (factory) {
120
- // We have build function, this Stackable will not use HTTP unless it is the entrypoint
121
- serverPromise.cancel()
122
-
123
- this.#app = await this.#module[factory]()
124
- this.#isFastify = isFastify(this.#app)
125
- this.#isKoa = isKoa(this.#app)
126
-
127
- if (this.#isFastify) {
128
- await this.#app.ready()
129
- } else if (this.#isKoa) {
130
- this.#dispatcher = this.#app.callback()
131
- } else if (this.#app instanceof Server) {
132
- this.#server = this.#app
133
- this.#dispatcher = this.#server.listeners('request')[0]
134
- }
135
-
136
- if (listen) {
137
- await this._listen()
138
- }
139
- } else {
140
- // User blackbox function, we wait for it to listen on a port
141
- this.#server = await serverPromise
142
- this.#dispatcher = this.#server.listeners('request')[0]
143
-
144
- this.url = getServerUrl(this.#server)
145
- }
146
- }
147
-
148
- await this._collectMetrics()
149
- return this.url
150
- }
151
-
152
- async stop () {
153
- if (this.childManager) {
154
- return this.stopCommand()
155
- }
156
-
157
- if (this.#isFastify) {
158
- return this.#app.close()
159
- }
160
-
161
- /* c8 ignore next 3 */
162
- if (!this.#server?.listening) {
163
- return
164
- }
165
-
166
- return new Promise((resolve, reject) => {
167
- this.#server.close(error => {
168
- /* c8 ignore next 3 */
169
- if (error) {
170
- return reject(error)
171
- }
172
-
173
- resolve()
174
- })
175
- })
176
- }
177
-
178
- async build () {
179
- const config = this.configManager.current
180
- const disableChildManager = config.node?.disablePlatformaticInBuild
181
- const command = config.application?.commands?.build
182
-
183
- if (command) {
184
- return this.buildWithCommand(command, null, { disableChildManager })
185
- }
186
-
187
- // If no command was specified, we try to see if there is a build script defined in package.json.
188
- const hasBuildScript = await this.#hasBuildScript()
189
-
190
- if (!hasBuildScript) {
191
- this.logger.debug(
192
- 'No "application.commands.build" configuration value specified and no build script found in package.json. Skipping build ...'
193
- )
194
- return
195
- }
196
-
197
- return this.buildWithCommand('npm run build', null, { disableChildManager })
198
- }
199
-
200
- async inject (injectParams, onInject) {
201
- let res
202
-
203
- if (this.#useHttpForDispatch) {
204
- this.logger.trace({ injectParams, url: this.url }, 'injecting via request')
205
- res = await injectViaRequest(this.url, injectParams, onInject)
206
- } else {
207
- if (this.#isFastify) {
208
- this.logger.trace({ injectParams }, 'injecting via fastify')
209
- res = await this.#app.inject(injectParams, onInject)
210
- } else {
211
- this.logger.trace({ injectParams }, 'injecting via light-my-request')
212
- res = await inject(this.#dispatcher ?? this.#app, injectParams, onInject)
213
- }
214
- }
215
-
216
- /* c8 ignore next 3 */
217
- if (onInject) {
218
- return
219
- }
220
-
221
- // Since inject might be called from the main thread directly via ITC, let's clean it up
222
- const { statusCode, headers, body, payload, rawPayload } = res
223
-
224
- return { statusCode, headers, body, payload, rawPayload }
225
- }
226
-
227
- _getWantsAbsoluteUrls () {
228
- const config = this.configManager.current
229
- return config.node.absoluteUrl
230
- }
231
-
232
- getMeta () {
233
- return {
234
- composer: {
235
- tcp: typeof this.url !== 'undefined',
236
- url: this.url,
237
- prefix: this.basePath ?? this.#basePath,
238
- wantsAbsoluteUrls: this._getWantsAbsoluteUrls(),
239
- needsRootTrailingSlash: true
240
- },
241
- connectionStrings: this.connectionString ? [this.connectionString] : []
242
- }
243
- }
244
-
245
- async getDispatchTarget () {
246
- this.#useHttpForDispatch =
247
- this.childManager || (this.url && this.configManager.current.node?.dispatchViaHttp === true)
248
-
249
- if (this.#useHttpForDispatch) {
250
- return this.getUrl()
251
- }
252
-
253
- return this.getDispatchFunc()
254
- }
255
-
256
- async _listen () {
257
- const serverOptions = this.serverConfig
258
- const listenOptions = { host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 }
259
-
260
- if (this.isProduction && features.node.reusePort) {
261
- listenOptions.reusePort = true
262
- }
263
-
264
- if (this.#isFastify) {
265
- await this.#app.listen(listenOptions)
266
- this.url = getServerUrl(this.#app.server)
267
- } else {
268
- // Express / Node / Koa
269
- this.#server = await new Promise((resolve, reject) => {
270
- return this.#app
271
- .listen(listenOptions, function () {
272
- resolve(this)
273
- })
274
- .on('error', reject)
275
- })
276
-
277
- this.url = getServerUrl(this.#server)
278
- }
279
-
280
- return this.url
281
- }
282
-
283
- _getApplication () {
284
- return this.#app
285
- }
286
-
287
- async _findEntrypoint () {
288
- const config = this.configManager.current
289
-
290
- if (config.node.main) {
291
- return pathResolve(this.root, config.node.main)
292
- }
293
-
294
- const { entrypoint, hadEntrypointField } = await getEntrypointInformation(this.root)
295
-
296
- if (typeof this.workerId === 'undefined' || this.workerId === 0) {
297
- if (!entrypoint) {
298
- this.logger.error(
299
- `The service "${this.serviceId}" had no valid entrypoint defined in the package.json file and no valid entrypoint file was found.`
300
- )
301
-
302
- process.exit(1)
303
- }
304
-
305
- if (!hadEntrypointField) {
306
- this.logger.warn(
307
- `The service "${this.serviceId}" had no valid entrypoint defined in the package.json file. Falling back to the file "${entrypoint}".`
308
- )
309
- }
310
- }
311
-
312
- return pathResolve(this.root, entrypoint)
313
- }
314
-
315
- async #hasBuildScript () {
316
- // If no command was specified, we try to see if there is a build script defined in package.json.
317
- let hasBuildScript
318
- try {
319
- const packageJson = JSON.parse(await readFile(resolve(this.root, 'package.json'), 'utf-8'))
320
- hasBuildScript = typeof packageJson.scripts.build === 'string' && packageJson.scripts.build
321
- } catch (e) {
322
- // No-op
323
- }
324
-
325
- return hasBuildScript
326
- }
327
-
328
- async getWatchConfig () {
329
- const config = this.configManager.current
330
-
331
- const enabled = config.watch?.enabled !== false
332
-
333
- if (!enabled) {
334
- return { enabled, path: this.root }
335
- }
336
-
337
- // ignore the outDir from tsconfig or service config if any
338
- let ignore = config.watch?.ignore
339
- if (!ignore) {
340
- const tsConfig = await getTsconfig(this.root, config)
341
- if (tsConfig) {
342
- ignore = ignoreDirs(tsConfig?.compilerOptions?.outDir, tsConfig?.watchOptions?.excludeDirectories)
343
- }
344
- }
345
-
346
- return {
347
- enabled,
348
- path: this.root,
349
- allow: config.watch?.allow,
350
- ignore
351
- }
352
- }
10
+ return config
353
11
  }
354
12
 
355
- async function getEntrypointInformation (root) {
356
- let entrypoint
357
- let packageJson
358
- let hadEntrypointField = false
359
-
360
- try {
361
- packageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf-8'))
362
- } catch {
363
- // No package.json, we only load the index.js file
364
- packageJson = {}
365
- }
366
-
367
- for (const field of validFields) {
368
- let current = packageJson
369
- const sequence = field.split('#')
370
-
371
- while (current && sequence.length && typeof current !== 'string') {
372
- current = current[sequence.shift()]
373
- }
374
-
375
- if (typeof current === 'string') {
376
- entrypoint = current
377
- hadEntrypointField = true
378
- break
379
- }
380
- }
13
+ export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
14
+ const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
381
15
 
382
- if (!entrypoint) {
383
- for (const basename of validFilesBasenames) {
384
- for (const ext of ['js', 'mjs', 'cjs']) {
385
- const file = `${basename}.${ext}`
386
-
387
- if (existsSync(resolve(root, file))) {
388
- entrypoint = file
389
- break
390
- }
391
- }
392
-
393
- if (entrypoint) {
394
- break
395
- }
396
- }
397
- }
398
-
399
- return { entrypoint, hadEntrypointField }
400
- }
401
-
402
- export async function buildStackable (opts) {
403
- const root = opts.context.directory
404
-
405
- const configManager = new ConfigManager({
406
- schema,
407
- source: opts.config ?? {},
408
- schemaOptions,
409
- transformConfig,
410
- dirname: root,
411
- context: opts.context
16
+ return utilsLoadConfiguration(source, context?.schema ?? schema, {
17
+ validationOptions,
18
+ transform,
19
+ replaceEnv: true,
20
+ root,
21
+ ...context
412
22
  })
413
- await configManager.parseAndValidate()
414
- const config = configManager.current
415
- // We need to update the config with the telemetry so the service name
416
- // used in telemetry can be retreived using the management API
417
- config.telemetry = opts.context.telemetryConfig
418
- configManager.update(config)
23
+ }
419
24
 
420
- return new NodeStackable(opts, root, configManager)
25
+ export async function create (configOrRoot, sourceOrConfig, context) {
26
+ const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
27
+ return new NodeStackable(config[kMetadata].root, config, context)
421
28
  }
422
29
 
423
30
  export { Generator } from './lib/generator.js'
424
- export { schema, schemaComponents } from './lib/schema.js'
425
-
426
- export default {
427
- configType: 'nodejs',
428
- configManagerConfig: {
429
- schemaOptions,
430
- transformConfig
431
- },
432
- buildStackable,
433
- schema,
434
- version: packageJson.version
435
- }
31
+ export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
32
+ export * from './lib/stackable.js'
package/lib/schema.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { schemaComponents as basicSchemaComponents } from '@platformatic/basic'
2
- import { schemaComponents as utilsSchemaComponents } from '@platformatic/utils'
2
+ import { schemaComponents as utilsSchemaComponents } from '@platformatic/foundation'
3
3
  import { readFileSync } from 'node:fs'
4
+ import { resolve } from 'node:path'
4
5
 
5
- export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
6
-
6
+ export const packageJson = JSON.parse(readFileSync(resolve(import.meta.dirname, '../package.json'), 'utf8'))
7
7
  export const version = packageJson.version
8
8
 
9
9
  const node = {
@@ -39,7 +39,7 @@ export const schemaComponents = { node }
39
39
  export const schema = {
40
40
  $id: `https://schemas.platformatic.dev/@platformatic/node/${version}.json`,
41
41
  $schema: 'http://json-schema.org/draft-07/schema#',
42
- title: 'Platformatic Node.js Stackable',
42
+ title: 'Platformatic Node.js Config',
43
43
  type: 'object',
44
44
  properties: {
45
45
  $schema: {
@@ -48,7 +48,7 @@ export const schema = {
48
48
  logger: utilsSchemaComponents.logger,
49
49
  server: utilsSchemaComponents.server,
50
50
  watch: basicSchemaComponents.watch,
51
- application: basicSchemaComponents.application,
51
+ application: basicSchemaComponents.buildableApplication,
52
52
  runtime: utilsSchemaComponents.wrappedRuntime,
53
53
  node
54
54
  },
@@ -0,0 +1,396 @@
1
+ import {
2
+ BaseStackable,
3
+ cleanBasePath,
4
+ createServerListener,
5
+ ensureTrailingSlash,
6
+ getServerUrl,
7
+ importFile,
8
+ injectViaRequest
9
+ } from '@platformatic/basic'
10
+ import { features } from '@platformatic/foundation'
11
+ import inject from 'light-my-request'
12
+ import { existsSync } from 'node:fs'
13
+ import { readFile } from 'node:fs/promises'
14
+ import { Server } from 'node:http'
15
+ import { resolve as resolvePath } from 'node:path'
16
+ import { version } from './schema.js'
17
+ import { getTsconfig, ignoreDirs, isServiceBuildable } from './utils.js'
18
+
19
+ const validFields = [
20
+ 'main',
21
+ 'exports',
22
+ 'exports',
23
+ 'exports#node',
24
+ 'exports#import',
25
+ 'exports#require',
26
+ 'exports#default',
27
+ 'exports#.#node',
28
+ 'exports#.#import',
29
+ 'exports#.#require',
30
+ 'exports#.#default'
31
+ ]
32
+
33
+ const validFilesBasenames = ['index', 'main', 'app', 'application', 'server', 'start', 'bundle', 'run', 'entrypoint']
34
+
35
+ // Paolo: This is kinda hackish but there is no better way. I apologize.
36
+ function isFastify (app) {
37
+ return Object.getOwnPropertySymbols(app).some(s => s.description === 'fastify.state')
38
+ }
39
+
40
+ function isKoa (app) {
41
+ return typeof app.callback === 'function'
42
+ }
43
+
44
+ async function getEntrypointInformation (root) {
45
+ let entrypoint
46
+ let packageJson
47
+ let hadEntrypointField = false
48
+
49
+ try {
50
+ packageJson = JSON.parse(await readFile(resolvePath(root, 'package.json'), 'utf-8'))
51
+ } catch {
52
+ // No package.json, we only load the index.js file
53
+ packageJson = {}
54
+ }
55
+
56
+ for (const field of validFields) {
57
+ let current = packageJson
58
+ const sequence = field.split('#')
59
+
60
+ while (current && sequence.length && typeof current !== 'string') {
61
+ current = current[sequence.shift()]
62
+ }
63
+
64
+ if (typeof current === 'string') {
65
+ entrypoint = current
66
+ hadEntrypointField = true
67
+ break
68
+ }
69
+ }
70
+
71
+ if (!entrypoint) {
72
+ for (const basename of validFilesBasenames) {
73
+ for (const ext of ['js', 'mjs', 'cjs']) {
74
+ const file = `${basename}.${ext}`
75
+
76
+ if (existsSync(resolvePath(root, file))) {
77
+ entrypoint = file
78
+ break
79
+ }
80
+ }
81
+
82
+ if (entrypoint) {
83
+ break
84
+ }
85
+ }
86
+ }
87
+
88
+ return { entrypoint, hadEntrypointField }
89
+ }
90
+
91
+ export class NodeStackable extends BaseStackable {
92
+ #module
93
+ #app
94
+ #server
95
+ #basePath
96
+ #dispatcher
97
+ #isFastify
98
+ #isKoa
99
+ #useHttpForDispatch
100
+
101
+ constructor (root, config, context) {
102
+ super('nodejs', version, root, config, context)
103
+ }
104
+
105
+ async start ({ listen }) {
106
+ // Make this idempotent
107
+ if (this.url) {
108
+ return this.url
109
+ }
110
+
111
+ // Listen if entrypoint
112
+ if (this.#app && listen) {
113
+ await this._listen()
114
+ return this.url
115
+ }
116
+
117
+ const config = this.config
118
+
119
+ if (!this.isProduction && (await isServiceBuildable(this.root, config))) {
120
+ this.logger.info(`Building service "${this.serviceId}" before starting in development mode ...`)
121
+ try {
122
+ await this.build()
123
+ this.childManager = null
124
+ } catch (e) {
125
+ this.logger.error(`Error while building service "${this.serviceId}": ${e.message}`)
126
+ }
127
+ }
128
+
129
+ const command = config.application.commands[this.isProduction ? 'production' : 'development']
130
+
131
+ if (command) {
132
+ return this.startWithCommand(command)
133
+ }
134
+
135
+ // Resolve the entrypoint
136
+ // The priority is platformatic.application.json, then package.json and finally autodetect.
137
+ // Only when autodetecting we eventually search in the dist folder when in production mode
138
+ const finalEntrypoint = await this._findEntrypoint()
139
+
140
+ // Require the application
141
+ this.#basePath = config.application?.basePath
142
+ ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
143
+ : undefined
144
+
145
+ this.registerGlobals({
146
+ basePath: this.#basePath
147
+ })
148
+
149
+ // The server promise must be created before requiring the entrypoint even if it's not going to be used
150
+ // at all. Otherwise there is chance we miss the listen event.
151
+ const serverOptions = this.serverConfig
152
+ const serverPromise = createServerListener(
153
+ (this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
154
+ (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
155
+ )
156
+ this.#module = await importFile(finalEntrypoint)
157
+ this.#module = this.#module.default || this.#module
158
+
159
+ // Deal with application
160
+ const factory = ['build', 'create'].find(f => typeof this.#module[f] === 'function')
161
+
162
+ if (config.node?.hasServer !== false && this.#module.hasServer !== false) {
163
+ if (factory) {
164
+ // We have build function, this Stackable will not use HTTP unless it is the entrypoint
165
+ serverPromise.cancel()
166
+
167
+ this.#app = await this.#module[factory]()
168
+ this.#isFastify = isFastify(this.#app)
169
+ this.#isKoa = isKoa(this.#app)
170
+
171
+ if (this.#isFastify) {
172
+ await this.#app.ready()
173
+ } else if (this.#isKoa) {
174
+ this.#dispatcher = this.#app.callback()
175
+ } else if (this.#app instanceof Server) {
176
+ this.#server = this.#app
177
+ this.#dispatcher = this.#server.listeners('request')[0]
178
+ }
179
+
180
+ if (listen) {
181
+ await this._listen()
182
+ }
183
+ } else {
184
+ // User blackbox function, we wait for it to listen on a port
185
+ this.#server = await serverPromise
186
+ this.#dispatcher = this.#server.listeners('request')[0]
187
+
188
+ this.url = getServerUrl(this.#server)
189
+ }
190
+ }
191
+
192
+ await this._collectMetrics()
193
+ return this.url
194
+ }
195
+
196
+ async stop () {
197
+ if (this.childManager) {
198
+ return this.stopCommand()
199
+ }
200
+
201
+ if (this.#isFastify) {
202
+ return this.#app.close()
203
+ }
204
+
205
+ /* c8 ignore next 3 */
206
+ if (!this.#server?.listening) {
207
+ return
208
+ }
209
+
210
+ return new Promise((resolve, reject) => {
211
+ this.#server.close(error => {
212
+ /* c8 ignore next 3 */
213
+ if (error) {
214
+ return reject(error)
215
+ }
216
+
217
+ resolve()
218
+ })
219
+ })
220
+ }
221
+
222
+ async build () {
223
+ const config = this.config
224
+ const disableChildManager = config.node?.disablePlatformaticInBuild
225
+ const command = config.application?.commands?.build
226
+
227
+ if (command) {
228
+ return this.buildWithCommand(command, null, { disableChildManager })
229
+ }
230
+
231
+ // If no command was specified, we try to see if there is a build script defined in package.json.
232
+ const hasBuildScript = await this.#hasBuildScript()
233
+
234
+ if (!hasBuildScript) {
235
+ this.logger.debug(
236
+ 'No "application.commands.build" configuration value specified and no build script found in package.json. Skipping build ...'
237
+ )
238
+ return
239
+ }
240
+
241
+ return this.buildWithCommand('npm run build', null, { disableChildManager })
242
+ }
243
+
244
+ async inject (injectParams, onInject) {
245
+ let res
246
+
247
+ if (this.#useHttpForDispatch) {
248
+ this.logger.trace({ injectParams, url: this.url }, 'injecting via request')
249
+ res = await injectViaRequest(this.url, injectParams, onInject)
250
+ } else {
251
+ if (this.#isFastify) {
252
+ this.logger.trace({ injectParams }, 'injecting via fastify')
253
+ res = await this.#app.inject(injectParams, onInject)
254
+ } else {
255
+ this.logger.trace({ injectParams }, 'injecting via light-my-request')
256
+ res = await inject(this.#dispatcher ?? this.#app, injectParams, onInject)
257
+ }
258
+ }
259
+
260
+ /* c8 ignore next 3 */
261
+ if (onInject) {
262
+ return
263
+ }
264
+
265
+ // Since inject might be called from the main thread directly via ITC, let's clean it up
266
+ const { statusCode, headers, body, payload, rawPayload } = res
267
+
268
+ return { statusCode, headers, body, payload, rawPayload }
269
+ }
270
+
271
+ _getWantsAbsoluteUrls () {
272
+ const config = this.config
273
+ return config.node.absoluteUrl
274
+ }
275
+
276
+ getMeta () {
277
+ return {
278
+ composer: {
279
+ tcp: typeof this.url !== 'undefined',
280
+ url: this.url,
281
+ prefix: this.basePath ?? this.#basePath,
282
+ wantsAbsoluteUrls: this._getWantsAbsoluteUrls(),
283
+ needsRootTrailingSlash: true
284
+ },
285
+ connectionStrings: this.connectionString ? [this.connectionString] : []
286
+ }
287
+ }
288
+
289
+ async getDispatchTarget () {
290
+ this.#useHttpForDispatch = this.childManager || (this.url && this.config.node?.dispatchViaHttp === true)
291
+
292
+ if (this.#useHttpForDispatch) {
293
+ return this.getUrl()
294
+ }
295
+
296
+ return this.getDispatchFunc()
297
+ }
298
+
299
+ async _listen () {
300
+ const serverOptions = this.serverConfig
301
+ const listenOptions = { host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 }
302
+
303
+ if (this.isProduction && features.node.reusePort) {
304
+ listenOptions.reusePort = true
305
+ }
306
+
307
+ if (this.#isFastify) {
308
+ await this.#app.listen(listenOptions)
309
+ this.url = getServerUrl(this.#app.server)
310
+ } else {
311
+ // Express / Node / Koa
312
+ this.#server = await new Promise((resolve, reject) => {
313
+ return this.#app
314
+ .listen(listenOptions, function () {
315
+ resolve(this)
316
+ })
317
+ .on('error', reject)
318
+ })
319
+
320
+ this.url = getServerUrl(this.#server)
321
+ }
322
+
323
+ return this.url
324
+ }
325
+
326
+ _getApplication () {
327
+ return this.#app
328
+ }
329
+
330
+ async _findEntrypoint () {
331
+ const config = this.config
332
+
333
+ if (config.node.main) {
334
+ return resolvePath(this.root, config.node.main)
335
+ }
336
+
337
+ const { entrypoint, hadEntrypointField } = await getEntrypointInformation(this.root)
338
+
339
+ if (typeof this.workerId === 'undefined' || this.workerId === 0) {
340
+ if (!entrypoint) {
341
+ this.logger.error(
342
+ `The service "${this.serviceId}" had no valid entrypoint defined in the package.json file and no valid entrypoint file was found.`
343
+ )
344
+
345
+ process.exit(1)
346
+ }
347
+
348
+ if (!hadEntrypointField) {
349
+ this.logger.warn(
350
+ `The service "${this.serviceId}" had no valid entrypoint defined in the package.json file. Falling back to the file "${entrypoint}".`
351
+ )
352
+ }
353
+ }
354
+
355
+ return resolvePath(this.root, entrypoint)
356
+ }
357
+
358
+ async #hasBuildScript () {
359
+ // If no command was specified, we try to see if there is a build script defined in package.json.
360
+ let hasBuildScript
361
+ try {
362
+ const packageJson = JSON.parse(await readFile(resolvePath(this.root, 'package.json'), 'utf-8'))
363
+ hasBuildScript = typeof packageJson.scripts.build === 'string' && packageJson.scripts.build
364
+ } catch (e) {
365
+ // No-op
366
+ }
367
+
368
+ return hasBuildScript
369
+ }
370
+
371
+ async getWatchConfig () {
372
+ const config = this.config
373
+
374
+ const enabled = config.watch?.enabled !== false
375
+
376
+ if (!enabled) {
377
+ return { enabled, path: this.root }
378
+ }
379
+
380
+ // ignore the outDir from tsconfig or service config if any
381
+ let ignore = config.watch?.ignore
382
+ if (!ignore) {
383
+ const tsConfig = await getTsconfig(this.root, config)
384
+ if (tsConfig) {
385
+ ignore = ignoreDirs(tsConfig?.compilerOptions?.outDir, tsConfig?.watchOptions?.excludeDirectories)
386
+ }
387
+ }
388
+
389
+ return {
390
+ enabled,
391
+ path: this.root,
392
+ allow: config.watch?.allow,
393
+ ignore
394
+ }
395
+ }
396
+ }
package/lib/utils.js CHANGED
@@ -1,6 +1,6 @@
1
- import path, { join } from 'node:path'
2
- import { readFile } from 'node:fs/promises'
3
1
  import json5 from 'json5'
2
+ import { readFile } from 'node:fs/promises'
3
+ import path, { join } from 'node:path'
4
4
 
5
5
  export async function isServiceBuildable (serviceRoot, config) {
6
6
  // skip vite as stackable as it has its own build command
@@ -56,12 +56,13 @@ export function ignoreDirs (outDir, watchOptionsExcludeDirectories) {
56
56
  if (outDir) {
57
57
  ignore.add(outDir)
58
58
  if (!outDir.endsWith('/**')) {
59
- ignore.add(`${outDir}/**`)
59
+ ignore.add(`${outDir}/*`)
60
+ ignore.add(`${outDir}/**/*`)
60
61
  }
61
62
  }
62
63
 
63
64
  if (ignore.size === 0) {
64
- return ['dist', 'dist/**']
65
+ return ['dist', 'dist/*', 'dist/**/*']
65
66
  }
66
67
 
67
68
  return Array.from(ignore)
package/package.json CHANGED
@@ -1,13 +1,9 @@
1
1
  {
2
2
  "name": "@platformatic/node",
3
- "version": "2.74.3",
3
+ "version": "3.0.0-alpha.2",
4
4
  "description": "Platformatic Node.js Stackable",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
- "bin": {
8
- "create-platformatic-node": "./bin/create.js",
9
- "start-platformatic-node": "./bin/start.js"
10
- },
11
7
  "repository": {
12
8
  "type": "git",
13
9
  "url": "git+https://github.com/platformatic/platformatic.git"
@@ -21,10 +17,9 @@
21
17
  "dependencies": {
22
18
  "json5": "^2.2.3",
23
19
  "light-my-request": "^6.0.0",
24
- "@platformatic/basic": "2.74.3",
25
- "@platformatic/generators": "2.74.3",
26
- "@platformatic/utils": "2.74.3",
27
- "@platformatic/config": "2.74.3"
20
+ "@platformatic/generators": "3.0.0-alpha.2",
21
+ "@platformatic/basic": "3.0.0-alpha.2",
22
+ "@platformatic/foundation": "3.0.0-alpha.2"
28
23
  },
29
24
  "devDependencies": {
30
25
  "borp": "^0.20.0",
@@ -36,12 +31,15 @@
36
31
  "neostandard": "^0.12.0",
37
32
  "tsx": "^4.19.0",
38
33
  "typescript": "^5.5.4",
39
- "@platformatic/composer": "2.74.3",
40
- "@platformatic/service": "2.74.3"
34
+ "@platformatic/composer": "3.0.0-alpha.2",
35
+ "@platformatic/service": "3.0.0-alpha.2"
36
+ },
37
+ "engines": {
38
+ "node": ">=22.18.0"
41
39
  },
42
40
  "scripts": {
43
- "test": "pnpm run lint && borp --concurrency=1 --no-timeout",
44
- "coverage": "pnpm run lint && borp -C -X test -X test/fixtures --concurrency=1 --no-timeout",
41
+ "test": "pnpm run lint && borp --concurrency=1 --timeout 1200000",
42
+ "coverage": "pnpm run lint && borp -C -X test -X test/fixtures --concurrency=1 --timeout 1200000",
45
43
  "gen-schema": "node lib/schema.js > schema.json",
46
44
  "gen-types": "json2ts > config.d.ts < schema.json",
47
45
  "build": "pnpm run gen-schema && pnpm run gen-types",
package/schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/node/2.74.3.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/node/3.0.0-alpha.2.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
- "title": "Platformatic Node.js Stackable",
4
+ "title": "Platformatic Node.js Config",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "$schema": {
@@ -362,6 +362,7 @@
362
362
  }
363
363
  },
364
364
  "additionalProperties": false,
365
+ "required": [],
365
366
  "default": {}
366
367
  },
367
368
  "runtime": {
package/bin/create.js DELETED
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { basename, join } from 'node:path'
4
- import { parseArgs } from 'node:util'
5
- import { Generator } from '../lib/generator.js'
6
-
7
- async function execute () {
8
- const args = parseArgs({
9
- args: process.argv.slice(2),
10
- options: {
11
- dir: {
12
- type: 'string',
13
- default: join(process.cwd(), 'plt-node')
14
- },
15
- main: { type: 'string', default: 'index.js' }
16
- }
17
- })
18
-
19
- const generator = new Generator()
20
-
21
- generator.setConfig({
22
- targetDirectory: args.values.dir,
23
- serviceName: basename(args.values.dir),
24
- main: args.values.main
25
- })
26
-
27
- await generator.run()
28
-
29
- console.log('Application created successfully! Run `npm run start` to start an application.')
30
- }
31
-
32
- execute()
package/bin/start.js DELETED
@@ -1,36 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { findConfigurationFile, loadConfigurationFile } from '@platformatic/config'
4
- import { ensureLoggableError } from '@platformatic/utils'
5
- import { buildStackable } from '../index.js'
6
-
7
- async function execute () {
8
- const root = process.cwd()
9
- const configurationFile = await findConfigurationFile(root)
10
-
11
- const stackable = await buildStackable({
12
- config: await loadConfigurationFile(configurationFile),
13
- context: {
14
- directory: process.cwd(),
15
- serverConfig: configurationFile?.runtime?.server ?? {},
16
- isEntrypoint: true,
17
- isProduction: true,
18
- runtimeConfig: {
19
- logger: {
20
- level: 'info',
21
- pretty: true
22
- }
23
- }
24
- }
25
- })
26
-
27
- try {
28
- // Set the location of the config
29
- const url = await stackable.start({ listen: true })
30
- stackable.logger.info('Server listening on %s', url)
31
- } catch (error) {
32
- stackable.logger.error({ error: ensureLoggableError(error) }, 'Error starting the application.')
33
- }
34
- }
35
-
36
- execute()