@platformatic/basic 2.66.1 → 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 CHANGED
@@ -28,6 +28,10 @@ export const configCandidates = [
28
28
  'watt.tml'
29
29
  ]
30
30
 
31
+ function hasDependency (packageJson, dependency) {
32
+ return packageJson.dependencies?.[dependency] || packageJson.devDependencies?.[dependency]
33
+ }
34
+
31
35
  function isImportFailedError (error, pkg) {
32
36
  if (error.code !== 'ERR_MODULE_NOT_FOUND' && error.code !== 'MODULE_NOT_FOUND') {
33
37
  return false
@@ -60,15 +64,37 @@ async function importStackablePackage (directory, pkg) {
60
64
 
61
65
  const serviceDirectory = workerData ? relative(workerData.dirname, directory) : directory
62
66
  throw new Error(
63
- `Unable to import package '${pkg}'. Please add it as a dependency in the package.json file in the folder ${serviceDirectory}.`
67
+ `Unable to import package '${pkg}'. Please add it as a dependency in the package.json file in the folder ${serviceDirectory}.`
64
68
  )
65
69
  }
66
70
  }
67
71
 
68
- export async function importStackableAndConfig (root, config) {
69
- let moduleName = '@platformatic/node'
70
- let autodetectDescription = 'is using a generic Node.js application'
72
+ export function detectStackable (packageJson) {
73
+ let name = '@platformatic/node'
74
+ let label = 'Node.js'
75
+
76
+ if (hasDependency(packageJson, '@nestjs/core')) {
77
+ name = '@platformatic/nest'
78
+ label = 'NestJS'
79
+ } else if (hasDependency(packageJson, 'next')) {
80
+ name = '@platformatic/next'
81
+ label = 'Next.js'
82
+ } else if (hasDependency(packageJson, '@remix-run/dev')) {
83
+ name = '@platformatic/remix'
84
+ label = 'Remix'
85
+ } else if (hasDependency(packageJson, 'astro')) {
86
+ name = '@platformatic/astro'
87
+ label = 'Astro'
88
+ // Since Vite is often used with other frameworks, we must check for Vite last
89
+ } else if (hasDependency(packageJson, 'vite')) {
90
+ name = '@platformatic/vite'
91
+ label = 'Vite'
92
+ }
93
+
94
+ return { name, label }
95
+ }
71
96
 
97
+ export async function importStackableAndConfig (root, config) {
72
98
  let rootPackageJson
73
99
  try {
74
100
  rootPackageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf-8'))
@@ -87,25 +113,16 @@ export async function importStackableAndConfig (root, config) {
87
113
  }
88
114
  }
89
115
 
90
- const { dependencies, devDependencies } = rootPackageJson
91
-
92
- if (dependencies?.next || devDependencies?.next) {
93
- autodetectDescription = 'is using Next.js'
94
- moduleName = '@platformatic/next'
95
- } else if (dependencies?.['@remix-run/dev'] || devDependencies?.['@remix-run/dev']) {
96
- autodetectDescription = 'is using Remix'
97
- moduleName = '@platformatic/remix'
98
- } else if (dependencies?.astro || devDependencies?.astro) {
99
- autodetectDescription = 'is using Astro'
100
- moduleName = '@platformatic/astro'
101
- } else if (dependencies?.vite || devDependencies?.vite) {
102
- autodetectDescription = 'is using Vite'
103
- moduleName = '@platformatic/vite'
104
- }
105
-
116
+ const { label, name: moduleName } = detectStackable(rootPackageJson)
106
117
  const stackable = await importStackablePackage(root, moduleName)
107
118
 
108
- return { stackable, config, autodetectDescription, moduleName }
119
+ return {
120
+ stackable,
121
+ config,
122
+ autodetectDescription:
123
+ moduleName === '@platformatic/node' ? 'is a generic Node.js application' : `is using ${label}`,
124
+ moduleName
125
+ }
109
126
  }
110
127
 
111
128
  async function buildStackable (opts) {
package/lib/base.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { client, collectMetrics } from '@platformatic/metrics'
2
- import { deepmerge, executeWithTimeout, buildPinoOptions } from '@platformatic/utils'
2
+ import { buildPinoOptions, deepmerge, executeWithTimeout } from '@platformatic/utils'
3
3
  import { parseCommandString } from 'execa'
4
4
  import { spawn } from 'node:child_process'
5
- import { once } from 'node:events'
5
+ import EventEmitter, { once } from 'node:events'
6
6
  import { existsSync } from 'node:fs'
7
7
  import { platform } from 'node:os'
8
8
  import { pathToFileURL } from 'node:url'
@@ -14,13 +14,17 @@ import { ChildManager } from './worker/child-manager.js'
14
14
 
15
15
  const kITC = Symbol.for('plt.runtime.itc')
16
16
 
17
- export class BaseStackable {
17
+ export class BaseStackable extends EventEmitter {
18
18
  childManager
19
19
  subprocess
20
+ subprocessForceClose
21
+ subprocessTerminationSignal
20
22
  #subprocessStarted
21
23
  #metricsCollected
22
24
 
23
25
  constructor (type, version, options, root, configManager, standardStreams = {}) {
26
+ super()
27
+
24
28
  options.context.worker ??= { count: 1, index: 0 }
25
29
 
26
30
  this.type = type
@@ -48,9 +52,18 @@ export class BaseStackable {
48
52
  this.runtimeConfig = deepmerge(options.context?.runtimeConfig ?? {}, workerData?.config ?? {})
49
53
  this.stdout = standardStreams?.stdout ?? process.stdout
50
54
  this.stderr = standardStreams?.stderr ?? process.stderr
55
+ this.subprocessForceClose = false
56
+ this.subprocessTerminationSignal = 'SIGINT'
51
57
 
52
58
  const loggerOptions = deepmerge(this.runtimeConfig?.logger ?? {}, this.configManager.current?.logger ?? {})
53
- const pinoOptions = buildPinoOptions(loggerOptions, this.serverConfig?.logger, this.serviceId, this.workerId, options, this.root)
59
+ const pinoOptions = buildPinoOptions(
60
+ loggerOptions,
61
+ this.serverConfig?.logger,
62
+ this.serviceId,
63
+ this.workerId,
64
+ options,
65
+ this.root
66
+ )
54
67
  this.logger = pino(pinoOptions, standardStreams?.stdout)
55
68
 
56
69
  // Setup globals
@@ -69,6 +82,7 @@ export class BaseStackable {
69
82
  prometheus: { client, registry: this.metricsRegistry },
70
83
  setCustomHealthCheck: this.setCustomHealthCheck.bind(this),
71
84
  setCustomReadinessCheck: this.setCustomReadinessCheck.bind(this),
85
+ notifyConfig: this.notifyConfig.bind(this),
72
86
  logger: this.logger
73
87
  })
74
88
  }
@@ -114,6 +128,22 @@ export class BaseStackable {
114
128
  return this.getUrl() ?? this.getDispatchFunc()
115
129
  }
116
130
 
131
+ getMeta () {
132
+ return {
133
+ composer: {
134
+ wantsAbsoluteUrls: false
135
+ }
136
+ }
137
+ }
138
+
139
+ async getMetrics ({ format } = {}) {
140
+ if (this.childManager && this.clientWs) {
141
+ return this.childManager.send(this.clientWs, 'getMetrics', { format })
142
+ }
143
+
144
+ return format === 'json' ? await this.metricsRegistry.getMetricsAsJSON() : await this.metricsRegistry.metrics()
145
+ }
146
+
117
147
  async getOpenapiSchema () {
118
148
  return this.openapiSchema
119
149
  }
@@ -227,6 +257,7 @@ export class BaseStackable {
227
257
 
228
258
  this.childManager.on('config', config => {
229
259
  this.subprocessConfig = config
260
+ this.notifyConfig(config)
230
261
  })
231
262
 
232
263
  this.childManager.on('connectionString', connectionString => {
@@ -278,13 +309,18 @@ export class BaseStackable {
278
309
  const exitPromise = once(this.subprocess, 'exit')
279
310
 
280
311
  // Attempt graceful close on the process
281
- this.childManager.notify(this.clientWs, 'close')
312
+ const handled = await this.childManager.send(this.clientWs, 'close', this.subprocessTerminationSignal)
313
+
314
+ if (!handled && this.subprocessForceClose) {
315
+ this.subprocess.kill(this.subprocessTerminationSignal)
316
+ }
282
317
 
283
318
  // If the process hasn't exited in X seconds, kill it in the polite way
284
319
  /* c8 ignore next 10 */
285
320
  const res = await executeWithTimeout(exitPromise, exitTimeout)
321
+
286
322
  if (res === 'timeout') {
287
- this.subprocess.kill(this.subprocessTerminationSignal ?? 'SIGINT')
323
+ this.subprocess.kill(this.subprocessTerminationSignal)
288
324
 
289
325
  // If the process hasn't exited in X seconds, kill it the hard way
290
326
  const res = await executeWithTimeout(exitPromise, exitTimeout)
@@ -303,6 +339,28 @@ export class BaseStackable {
303
339
  return this.childManager
304
340
  }
305
341
 
342
+ async getChildManagerContext (basePath) {
343
+ const meta = await this.getMeta()
344
+
345
+ return {
346
+ id: this.id,
347
+ config: this.configManager.current,
348
+ serviceId: this.serviceId,
349
+ workerId: this.workerId,
350
+ // Always use URL to avoid serialization problem in Windows
351
+ root: pathToFileURL(this.root).toString(),
352
+ basePath,
353
+ logLevel: this.logger.level,
354
+ isEntrypoint: this.isEntrypoint,
355
+ runtimeBasePath: this.runtimeConfig?.basePath ?? null,
356
+ wantsAbsoluteUrls: meta.composer?.wantsAbsoluteUrls ?? false,
357
+ /* c8 ignore next 2 - Else branches */
358
+ port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
359
+ host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true,
360
+ telemetryConfig: this.telemetryConfig
361
+ }
362
+ }
363
+
306
364
  async spawn (command) {
307
365
  const [executable, ...args] = parseCommandString(command)
308
366
  const hasChainedCommands = command.includes('&&') || command.includes('||') || command.includes(';')
@@ -328,6 +386,10 @@ export class BaseStackable {
328
386
  return subprocess
329
387
  }
330
388
 
389
+ notifyConfig (config) {
390
+ this.emit('config', config)
391
+ }
392
+
331
393
  async _collectMetrics () {
332
394
  if (this.#metricsCollected) {
333
395
  return
@@ -391,45 +453,7 @@ export class BaseStackable {
391
453
  }
392
454
  }
393
455
 
394
- async getMetrics ({ format } = {}) {
395
- if (this.childManager && this.clientWs) {
396
- return this.childManager.send(this.clientWs, 'getMetrics', { format })
397
- }
398
-
399
- return format === 'json' ? await this.metricsRegistry.getMetricsAsJSON() : await this.metricsRegistry.metrics()
400
- }
401
-
402
456
  async #invalidateHttpCache (opts = {}) {
403
457
  await globalThis[kITC].send('invalidateHttpCache', opts)
404
458
  }
405
-
406
- getMeta () {
407
- return {
408
- composer: {
409
- wantsAbsoluteUrls: false
410
- }
411
- }
412
- }
413
-
414
- async getChildManagerContext (basePath) {
415
- const meta = await this.getMeta()
416
-
417
- return {
418
- id: this.id,
419
- config: this.configManager.current,
420
- serviceId: this.serviceId,
421
- workerId: this.workerId,
422
- // Always use URL to avoid serialization problem in Windows
423
- root: pathToFileURL(this.root).toString(),
424
- basePath,
425
- logLevel: this.logger.level,
426
- isEntrypoint: this.isEntrypoint,
427
- runtimeBasePath: this.runtimeConfig?.basePath ?? null,
428
- wantsAbsoluteUrls: meta.composer?.wantsAbsoluteUrls ?? false,
429
- /* c8 ignore next 2 */
430
- port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
431
- host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true,
432
- telemetryConfig: this.telemetryConfig
433
- }
434
- }
435
459
  }
@@ -1,6 +1,13 @@
1
1
  import { ITC } from '@platformatic/itc'
2
2
  import { client, collectMetrics } from '@platformatic/metrics'
3
- import { buildPinoFormatters, buildPinoTimestamp, disablePinoDirectWrite, ensureFlushedWorkerStdio, ensureLoggableError, features } from '@platformatic/utils'
3
+ import {
4
+ buildPinoFormatters,
5
+ buildPinoTimestamp,
6
+ disablePinoDirectWrite,
7
+ ensureFlushedWorkerStdio,
8
+ ensureLoggableError,
9
+ features
10
+ } from '@platformatic/utils'
4
11
  import diagnosticChannel, { tracingChannel } from 'node:diagnostics_channel'
5
12
  import { EventEmitter, once } from 'node:events'
6
13
  import { readFile } from 'node:fs/promises'
@@ -61,7 +68,6 @@ function createInterceptor () {
61
68
  export class ChildProcess extends ITC {
62
69
  #listener
63
70
  #socket
64
- #child
65
71
  #logger
66
72
  #metricsRegistry
67
73
  #pendingMessages
@@ -76,6 +82,25 @@ export class ChildProcess extends ITC {
76
82
  },
77
83
  getMetrics: (...args) => {
78
84
  return this.#getMetrics(...args)
85
+ },
86
+ close: (signal) => {
87
+ let handled = false
88
+
89
+ try {
90
+ handled = globalThis.platformatic.events.emit('close', signal)
91
+ } catch (error) {
92
+ this.#logger.error({ err: ensureLoggableError(error) }, 'Error while handling close event.')
93
+ process.exitCode = 1
94
+ }
95
+
96
+ if (!handled) {
97
+ // No user event, just exit without errors
98
+ setImmediate(() => {
99
+ process.exit(process.exitCode ?? 0)
100
+ })
101
+ }
102
+
103
+ return handled
79
104
  }
80
105
  }
81
106
  })
@@ -92,23 +117,37 @@ export class ChildProcess extends ITC {
92
117
  this.#setupServer()
93
118
  this.#setupInterceptors()
94
119
 
95
- this.on('close', () => {
96
- if (!globalThis.platformatic.events.emit('close')) {
97
- // No user event, just exit without errors
98
- process.exit(0)
99
- }
100
- })
101
-
102
120
  this.registerGlobals({
103
121
  logger: this.#logger,
104
122
  setOpenapiSchema: this.setOpenapiSchema.bind(this),
105
123
  setGraphqlSchema: this.setGraphqlSchema.bind(this),
106
124
  setConnectionString: this.setConnectionString.bind(this),
107
125
  setBasePath: this.setBasePath.bind(this),
108
- prometheus: { client, registry: this.#metricsRegistry }
126
+ prometheus: { client, registry: this.#metricsRegistry },
127
+ notifyConfig: this.#notifyConfig.bind(this)
109
128
  })
110
129
  }
111
130
 
131
+ registerGlobals (globals) {
132
+ globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, globals)
133
+ }
134
+
135
+ setOpenapiSchema (schema) {
136
+ this.notify('openapiSchema', schema)
137
+ }
138
+
139
+ setGraphqlSchema (schema) {
140
+ this.notify('graphqlSchema', schema)
141
+ }
142
+
143
+ setConnectionString (connectionString) {
144
+ this.notify('connectionString', connectionString)
145
+ }
146
+
147
+ setBasePath (basePath) {
148
+ this.notify('basePath', basePath)
149
+ }
150
+
112
151
  _setupListener (listener) {
113
152
  this.#listener = listener
114
153
 
@@ -229,7 +268,7 @@ export class ChildProcess extends ITC {
229
268
  #setupServer () {
230
269
  const subscribers = {
231
270
  asyncStart ({ options }) {
232
- // Unix socket, do nothing
271
+ // Unix socket, do nothing
233
272
  if (options.path) {
234
273
  return
235
274
  }
@@ -287,9 +326,9 @@ export class ChildProcess extends ITC {
287
326
 
288
327
  #setupHandlers () {
289
328
  const errorLabel =
290
- typeof globalThis.platformatic.workerId !== 'undefined'
291
- ? `worker ${globalThis.platformatic.workerId} of the service "${globalThis.platformatic.serviceId}"`
292
- : `service "${globalThis.platformatic.serviceId}"`
329
+ typeof globalThis.platformatic.workerId !== 'undefined'
330
+ ? `worker ${globalThis.platformatic.workerId} of the service "${globalThis.platformatic.serviceId}"`
331
+ : `service "${globalThis.platformatic.serviceId}"`
293
332
 
294
333
  function handleUnhandled (type, err) {
295
334
  this.#logger.error({ err: ensureLoggableError(err) }, `Child process for the ${errorLabel} threw an ${type}.`)
@@ -302,24 +341,8 @@ export class ChildProcess extends ITC {
302
341
  process.on('unhandledRejection', handleUnhandled.bind(this, 'unhandled rejection'))
303
342
  }
304
343
 
305
- registerGlobals (globals) {
306
- globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, globals)
307
- }
308
-
309
- setOpenapiSchema (schema) {
310
- this.notify('openapiSchema', schema)
311
- }
312
-
313
- setGraphqlSchema (schema) {
314
- this.notify('graphqlSchema', schema)
315
- }
316
-
317
- setConnectionString (connectionString) {
318
- this.notify('connectionString', connectionString)
319
- }
320
-
321
- setBasePath (basePath) {
322
- this.notify('basePath', basePath)
344
+ #notifyConfig (config) {
345
+ this.notify('config', config)
323
346
  }
324
347
  }
325
348
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/basic",
3
- "version": "2.66.1",
3
+ "version": "2.67.0-alpha.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -24,11 +24,11 @@
24
24
  "split2": "^4.2.0",
25
25
  "undici": "^7.0.0",
26
26
  "ws": "^8.18.0",
27
- "@platformatic/config": "2.66.1",
28
- "@platformatic/itc": "2.66.1",
29
- "@platformatic/telemetry": "2.66.1",
30
- "@platformatic/utils": "2.66.1",
31
- "@platformatic/metrics": "2.66.1"
27
+ "@platformatic/config": "2.67.0-alpha.1",
28
+ "@platformatic/itc": "2.67.0-alpha.1",
29
+ "@platformatic/telemetry": "2.67.0-alpha.1",
30
+ "@platformatic/metrics": "2.67.0-alpha.1",
31
+ "@platformatic/utils": "2.67.0-alpha.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "borp": "^0.20.0",
@@ -37,6 +37,7 @@
37
37
  "fastify": "^5.0.0",
38
38
  "get-port": "^7.1.0",
39
39
  "json-schema-to-typescript": "^15.0.0",
40
+ "minimatch": "^10.0.1",
40
41
  "neostandard": "^0.12.0",
41
42
  "next": "^15.0.0",
42
43
  "react": "^18.3.1",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/basic/2.66.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/basic/2.67.0-alpha.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Stackable",
5
5
  "type": "object",