@platformatic/basic 2.67.0 → 2.67.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
@@ -1,3 +1,4 @@
1
+ import { ConfigManager } from '@platformatic/config'
1
2
  import { createRequire } from '@platformatic/utils'
2
3
  import jsonPatch from 'fast-json-patch'
3
4
  import { existsSync } from 'node:fs'
@@ -28,6 +29,10 @@ export const configCandidates = [
28
29
  'watt.tml'
29
30
  ]
30
31
 
32
+ function hasDependency (packageJson, dependency) {
33
+ return packageJson.dependencies?.[dependency] || packageJson.devDependencies?.[dependency]
34
+ }
35
+
31
36
  function isImportFailedError (error, pkg) {
32
37
  if (error.code !== 'ERR_MODULE_NOT_FOUND' && error.code !== 'MODULE_NOT_FOUND') {
33
38
  return false
@@ -60,15 +65,37 @@ async function importStackablePackage (directory, pkg) {
60
65
 
61
66
  const serviceDirectory = workerData ? relative(workerData.dirname, directory) : directory
62
67
  throw new Error(
63
- `Unable to import package '${pkg}'. Please add it as a dependency in the package.json file in the folder ${serviceDirectory}.`
68
+ `Unable to import package '${pkg}'. Please add it as a dependency in the package.json file in the folder ${serviceDirectory}.`
64
69
  )
65
70
  }
66
71
  }
67
72
 
68
- export async function importStackableAndConfig (root, config) {
69
- let moduleName = '@platformatic/node'
70
- let autodetectDescription = 'is using a generic Node.js application'
73
+ export async function detectStackable (root, packageJson) {
74
+ let name = '@platformatic/node'
75
+ let label = 'Node.js'
76
+
77
+ if (hasDependency(packageJson, '@nestjs/core')) {
78
+ name = '@platformatic/nest'
79
+ label = 'NestJS'
80
+ } else if (hasDependency(packageJson, 'next')) {
81
+ name = '@platformatic/next'
82
+ label = 'Next.js'
83
+ } else if (hasDependency(packageJson, '@remix-run/dev')) {
84
+ name = '@platformatic/remix'
85
+ label = 'Remix'
86
+ } else if (hasDependency(packageJson, 'astro')) {
87
+ name = '@platformatic/astro'
88
+ label = 'Astro'
89
+ // Since Vite is often used with other frameworks, we must check for Vite last
90
+ } else if (hasDependency(packageJson, 'vite')) {
91
+ name = '@platformatic/vite'
92
+ label = 'Vite'
93
+ }
71
94
 
95
+ return { name, label }
96
+ }
97
+
98
+ export async function importStackableAndConfig (root, config, context) {
72
99
  let rootPackageJson
73
100
  try {
74
101
  rootPackageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf-8'))
@@ -76,6 +103,8 @@ export async function importStackableAndConfig (root, config) {
76
103
  rootPackageJson = {}
77
104
  }
78
105
 
106
+ const hadConfig = !!config
107
+
79
108
  if (!config) {
80
109
  for (const candidate of configCandidates) {
81
110
  const candidatePath = resolve(root, candidate)
@@ -87,53 +116,44 @@ export async function importStackableAndConfig (root, config) {
87
116
  }
88
117
  }
89
118
 
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'
119
+ const { label, name: moduleName } = await detectStackable(root, rootPackageJson)
120
+
121
+ if (context) {
122
+ const serviceRoot = relative(process.cwd(), root)
123
+
124
+ if (!hadConfig && context.serviceId && !(await ConfigManager.findConfigFile(root)) && context.worker?.index === 0) {
125
+ const autodetectDescription =
126
+ moduleName === '@platformatic/node' ? 'is a generic Node.js application' : `is using ${label}`
127
+
128
+ const logger = pino({ level: context.serverConfig?.logger?.level ?? 'warn', name: context.serviceId })
129
+
130
+ logger.warn(`We have auto-detected that service "${context.serviceId}" ${autodetectDescription}.`)
131
+ logger.warn(
132
+ `We suggest you create a watt.json or a platformatic.json file in the folder ${serviceRoot} with the "$schema" property set to "https://schemas.platformatic.dev/${moduleName}/${packageJson.version}.json".`
133
+ )
134
+ logger.warn(`Also don't forget to add "${moduleName}" to the service dependencies.`)
135
+ logger.warn('You can also run "wattpm import" to do this automatically.\n')
136
+ }
104
137
  }
105
138
 
106
139
  const stackable = await importStackablePackage(root, moduleName)
107
140
 
108
- return { stackable, config, autodetectDescription, moduleName }
141
+ return {
142
+ stackable,
143
+ config,
144
+ autodetectDescription:
145
+ moduleName === '@platformatic/node' ? 'is a generic Node.js application' : `is using ${label}`,
146
+ moduleName
147
+ }
109
148
  }
110
149
 
111
150
  async function buildStackable (opts) {
112
151
  const hadConfig = !!opts.config
113
- const { stackable, config, autodetectDescription, moduleName } = await importStackableAndConfig(
114
- opts.context.directory,
115
- opts.config
116
- )
152
+ const { stackable, config } = await importStackableAndConfig(opts.context.directory, opts.config, opts.context)
117
153
  opts.config = config
118
154
 
119
- const serviceRoot = relative(process.cwd(), opts.context.directory)
120
- if (
121
- !hadConfig &&
122
- !existsSync(resolve(serviceRoot, 'platformatic.json') || existsSync(resolve(serviceRoot, 'watt.json'))) &&
123
- opts.context.worker?.count > 1
124
- ) {
125
- const logger = pino({
126
- level: opts.context.serverConfig?.logger?.level ?? 'warn',
127
- name: opts.context.serviceId
128
- })
129
-
130
- logger.warn(
131
- [
132
- `Platformatic has auto-detected that service "${opts.context.serviceId}" ${autodetectDescription}.\n`,
133
- `We suggest you create a platformatic.json or watt.json file in the folder ${serviceRoot} with the "$schema" `,
134
- `property set to "https://schemas.platformatic.dev/${moduleName}/${packageJson.version}.json".`
135
- ].join('')
136
- )
155
+ if (!hadConfig && typeof stackable.createDefaultConfig === 'function') {
156
+ opts.config = await stackable.createDefaultConfig?.(opts)
137
157
  }
138
158
 
139
159
  return stackable.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.67.0",
3
+ "version": "2.67.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.67.0",
28
- "@platformatic/itc": "2.67.0",
29
- "@platformatic/telemetry": "2.67.0",
30
- "@platformatic/metrics": "2.67.0",
31
- "@platformatic/utils": "2.67.0"
27
+ "@platformatic/itc": "2.67.1",
28
+ "@platformatic/config": "2.67.1",
29
+ "@platformatic/metrics": "2.67.1",
30
+ "@platformatic/telemetry": "2.67.1",
31
+ "@platformatic/utils": "2.67.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.67.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/basic/2.67.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Stackable",
5
5
  "type": "object",