@platformatic/basic 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 PlatformaticStackable {
8
+ export interface PlatformaticBasicConfig {
9
9
  $schema?: string;
10
10
  runtime?: {
11
11
  preload?: string | string[];
package/index.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ export interface StartOptions {
2
+ listen?: boolean
3
+ }
4
+
5
+ export interface Dependency {
6
+ id: string
7
+ url?: string
8
+ local: boolean
9
+ }
10
+
11
+ export type BaseContext = Partial<{
12
+ serviceId: string
13
+ isEntrypoint: boolean
14
+ isProduction: boolean
15
+ isStandalone: boolean
16
+ directory: string
17
+ telemetryConfig: object
18
+ metricsConfig: object
19
+ serverConfig: object
20
+ hasManagementApi: boolean
21
+ localServiceEnvVars: Map<string, string>
22
+ }>
23
+
24
+ export interface BaseOptions<Context = BaseContext> {
25
+ context: Context
26
+ }
27
+
28
+ export declare const schemaOptions: Partial<Record<string, unknown>>
29
+
30
+ export class BaseStackable<Config = Record<string, any>, Options = BaseOptions> {
31
+ basePath: string
32
+ constructor (
33
+ type: string,
34
+ version: string,
35
+ root: string,
36
+ config: object,
37
+ standardStreams?: Record<string, NodeJS.WritableStream>
38
+ )
39
+
40
+ init (): Promise<void>
41
+ start (options: StartOptions): Promise<void>
42
+ stop (): Promise<void>
43
+ build (): Promise<void>
44
+ getUrl (): string
45
+ updateContext (context: Partial<BaseContext>): Promise<void>
46
+ getConfig (includeMeta?: boolean): Promise<object>
47
+ getInfo (): Promise<{ type: string; version: string }>
48
+ getDispatchFunc (): Promise<Function>
49
+ getDispatchTarget (): Promise<Function | string>
50
+ getOpenapiSchema (): Promise<object>
51
+ getGraphqlSchema (): Promise<string>
52
+ setConnectionStatus (status: string): Promise<void>
53
+ setOpenapiSchema (schema: object): Promise<void>
54
+ setGraphqlSchema (schema: string): Promise<void>
55
+ setCustomHealthCheck (
56
+ healthCheck: () =>
57
+ | boolean
58
+ | Promise<boolean>
59
+ | { status: boolean; statusCode?: number; body?: string }
60
+ | Promise<{ status: boolean; statusCode?: number; body?: string }>
61
+ ): Promise<void>
62
+ setCustomReadinessCheck (
63
+ readinessCheck: () =>
64
+ | boolean
65
+ | Promise<boolean>
66
+ | { status: boolean; statusCode?: number; body?: string }
67
+ | Promise<{ status: boolean; statusCode?: number; body?: string }>
68
+ ): Promise<void>
69
+ collectMetrics (): Promise<any>
70
+ getMetrics ({ format: string }): Promise<string | Array<object>>
71
+ getMeta (): Promise<object>
72
+ inject (injectParams: string | object): Promise<{
73
+ statusCode: number
74
+ statusMessage: string
75
+ headers: object
76
+ body: object
77
+ }>
78
+ log (options: { message: string; level: string }): Promise<void>
79
+ getBootstrapDependencies (): Promise<Dependency[]>
80
+ getWatchConfig (): Promise<{
81
+ enabled: boolean
82
+ path: string
83
+ allow?: string[]
84
+ ignore?: string[]
85
+ }>
86
+
87
+ _initializeLogger (options: object): Promise<void>
88
+ _collectMetrics (): Promise<void>
89
+ }
package/index.js CHANGED
@@ -1,145 +1,8 @@
1
- import { ConfigManager } from '@platformatic/config'
2
- import { detectApplicationType } from '@platformatic/utils'
3
- import jsonPatch from 'fast-json-patch'
4
- import { readFile } from 'node:fs/promises'
5
- import { createRequire } from 'node:module'
6
- import { relative, resolve } from 'node:path'
7
- import { workerData } from 'node:worker_threads'
8
- import pino from 'pino'
9
- import { packageJson, schema } from './lib/schema.js'
10
- import { importFile } from './lib/utils.js'
11
-
12
- const importStackablePackageMarker = '__pltImportStackablePackage.js'
13
-
14
- function isImportFailedError (error, pkg) {
15
- if (error.code !== 'ERR_MODULE_NOT_FOUND' && error.code !== 'MODULE_NOT_FOUND') {
16
- return false
17
- }
18
-
19
- const match = error.message.match(/Cannot find package '(.+)' imported from (.+)/)
20
-
21
- return match?.[1] === pkg || error.requireStack?.[0].endsWith(importStackablePackageMarker)
22
- }
23
-
24
- async function importStackablePackage (directory, pkg) {
25
- try {
26
- try {
27
- // Try regular import
28
- return await import(pkg)
29
- } catch (e) {
30
- if (!isImportFailedError(e, pkg)) {
31
- throw e
32
- }
33
-
34
- // Scope to the service
35
- const require = createRequire(resolve(directory, importStackablePackageMarker))
36
- const imported = require.resolve(pkg)
37
- return await importFile(imported)
38
- }
39
- } catch (e) {
40
- if (!isImportFailedError(e, pkg)) {
41
- throw e
42
- }
43
-
44
- const serviceDirectory = workerData ? relative(workerData.dirname, directory) : directory
45
- throw new Error(
46
- `Unable to import package '${pkg}'. Please add it as a dependency in the package.json file in the folder ${serviceDirectory}.`
47
- )
48
- }
49
- }
50
-
51
- export async function importStackableAndConfig (root, config, context) {
52
- let rootPackageJson
53
- try {
54
- rootPackageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf-8'))
55
- } catch {
56
- rootPackageJson = {}
57
- }
58
-
59
- const hadConfig = !!config
60
-
61
- if (!config) {
62
- config = await ConfigManager.findConfigFile(root, 'application')
63
- }
64
-
65
- const appType = await detectApplicationType(root, rootPackageJson)
66
-
67
- if (!appType) {
68
- throw new Error(`Unable to detect application type in ${root}.`)
69
- }
70
-
71
- const { label, name: moduleName } = appType
72
-
73
- if (context) {
74
- const serviceRoot = relative(process.cwd(), root)
75
-
76
- if (!hadConfig && context.serviceId && !(await ConfigManager.findConfigFile(root)) && context.worker?.index === 0) {
77
- const autodetectDescription =
78
- moduleName === '@platformatic/node' ? 'is a generic Node.js application' : `is using ${label}`
79
-
80
- const logger = pino({ level: context.serverConfig?.logger?.level ?? 'warn', name: context.serviceId })
81
-
82
- logger.warn(`We have auto-detected that service "${context.serviceId}" ${autodetectDescription}.`)
83
- logger.warn(
84
- `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".`
85
- )
86
- logger.warn(`Also don't forget to add "${moduleName}" to the service dependencies.`)
87
- logger.warn('You can also run "wattpm import" to do this automatically.\n')
88
- }
89
- }
90
-
91
- const stackable = await importStackablePackage(root, moduleName)
92
-
93
- return {
94
- stackable,
95
- config,
96
- autodetectDescription:
97
- moduleName === '@platformatic/node' ? 'is a generic Node.js application' : `is using ${label}`,
98
- moduleName
99
- }
100
- }
101
-
102
- async function buildStackable (opts) {
103
- const hadConfig = !!opts.config
104
- const { stackable, config } = await importStackableAndConfig(opts.context.directory, opts.config, opts.context)
105
- opts.config = config
106
-
107
- if (!hadConfig && typeof stackable.createDefaultConfig === 'function') {
108
- opts.config = await stackable.createDefaultConfig?.(opts)
109
- }
110
-
111
- return stackable.buildStackable(opts)
112
- }
113
-
114
- /* c8 ignore next 3 */
115
- export async function transformConfig () {
116
- const patch = workerData?.serviceConfig?.configPatch
117
-
118
- if (Array.isArray(patch)) {
119
- this.current = jsonPatch.applyPatch(this.current, patch).newDocument
120
- }
121
- }
122
-
123
- export const schemaOptions = {
124
- useDefaults: true,
125
- coerceTypes: true,
126
- allErrors: true,
127
- strict: false
128
- }
129
-
130
- export default {
131
- configType: 'nodejs',
132
- configManagerConfig: {
133
- schemaOptions,
134
- transformConfig
135
- },
136
- buildStackable,
137
- schema,
138
- version: packageJson.version
139
- }
140
-
141
1
  export * from './lib/base.js'
2
+ export * from './lib/config.js'
3
+ export * from './lib/creation.js'
142
4
  export * as errors from './lib/errors.js'
5
+ export * from './lib/modules.js'
143
6
  export { schema, schemaComponents } from './lib/schema.js'
144
7
  export * from './lib/utils.js'
145
8
  export * from './lib/worker/child-manager.js'
package/lib/base.js CHANGED
@@ -1,5 +1,5 @@
1
- import { client, collectMetrics } from '@platformatic/metrics'
2
- import { buildPinoOptions, deepmerge, executeWithTimeout, kTimeout } from '@platformatic/utils'
1
+ import { buildPinoOptions, deepmerge, executeWithTimeout, kMetadata, kTimeout } from '@platformatic/foundation'
2
+ import { client, collectMetrics, ensureMetricsGroup } from '@platformatic/metrics'
3
3
  import { parseCommandString } from 'execa'
4
4
  import { spawn } from 'node:child_process'
5
5
  import EventEmitter, { once } from 'node:events'
@@ -11,7 +11,6 @@ import pino from 'pino'
11
11
  import { NonZeroExitCode } from './errors.js'
12
12
  import { cleanBasePath } from './utils.js'
13
13
  import { ChildManager } from './worker/child-manager.js'
14
-
15
14
  const kITC = Symbol.for('plt.runtime.itc')
16
15
 
17
16
  export class BaseStackable extends EventEmitter {
@@ -22,47 +21,38 @@ export class BaseStackable extends EventEmitter {
22
21
  #subprocessStarted
23
22
  #metricsCollected
24
23
 
25
- constructor (type, version, options, root, configManager, standardStreams = {}) {
24
+ constructor (type, version, root, config, context, standardStreams = {}) {
26
25
  super()
27
26
 
28
- options.context.worker ??= { count: 1, index: 0 }
29
-
30
27
  this.type = type
31
28
  this.version = version
32
- this.serviceId = options.context.serviceId
33
- this.workerId = options.context.worker.count > 1 ? options.context.worker.index : undefined
34
- this.telemetryConfig = options.context.telemetryConfig
35
- this.options = options
36
29
  this.root = root
37
- this.configManager = configManager
38
- this.serverConfig = deepmerge(options.context.serverConfig ?? {}, configManager.current.server ?? {})
30
+ this.config = config
31
+ this.context = context ?? {}
32
+ this.context.worker ??= { count: 1, index: 0 }
33
+ this.standardStreams = standardStreams
34
+
35
+ this.serviceId = this.context.serviceId
36
+ this.workerId = this.context.worker.count > 1 ? this.context.worker.index : undefined
37
+ this.telemetryConfig = this.context.telemetryConfig
38
+ this.serverConfig = deepmerge(this.context.serverConfig ?? {}, config.server ?? {})
39
39
  this.openapiSchema = null
40
40
  this.graphqlSchema = null
41
41
  this.connectionString = null
42
42
  this.basePath = null
43
- this.isEntrypoint = options.context.isEntrypoint
44
- this.isProduction = options.context.isProduction
45
- this.metricsRegistry = new client.Registry()
43
+ this.isEntrypoint = this.context.isEntrypoint
44
+ this.isProduction = this.context.isProduction
46
45
  this.#metricsCollected = false
47
46
  this.customHealthCheck = null
48
47
  this.customReadinessCheck = null
49
48
  this.clientWs = null
50
- this.runtimeConfig = deepmerge(options.context?.runtimeConfig ?? {}, workerData?.config ?? {})
49
+ this.runtimeConfig = deepmerge(this.context?.runtimeConfig ?? {}, workerData?.config ?? {})
51
50
  this.stdout = standardStreams?.stdout ?? process.stdout
52
51
  this.stderr = standardStreams?.stderr ?? process.stderr
53
52
  this.subprocessForceClose = false
54
53
  this.subprocessTerminationSignal = 'SIGINT'
55
54
 
56
- const loggerOptions = deepmerge(this.runtimeConfig?.logger ?? {}, this.configManager.current?.logger ?? {})
57
- const pinoOptions = buildPinoOptions(
58
- loggerOptions,
59
- this.serverConfig?.logger,
60
- this.serviceId,
61
- this.workerId,
62
- options,
63
- this.root
64
- )
65
- this.logger = pino(pinoOptions, standardStreams?.stdout)
55
+ this.logger = this._initializeLogger()
66
56
 
67
57
  // Setup globals
68
58
  this.registerGlobals({
@@ -77,28 +67,68 @@ export class BaseStackable extends EventEmitter {
77
67
  setBasePath: this.setBasePath.bind(this),
78
68
  runtimeBasePath: this.runtimeConfig?.basePath ?? null,
79
69
  invalidateHttpCache: this.#invalidateHttpCache.bind(this),
80
- prometheus: { client, registry: this.metricsRegistry },
81
70
  setCustomHealthCheck: this.setCustomHealthCheck.bind(this),
82
71
  setCustomReadinessCheck: this.setCustomReadinessCheck.bind(this),
83
72
  notifyConfig: this.notifyConfig.bind(this),
84
73
  logger: this.logger
85
74
  })
75
+
76
+ if (globalThis.platformatic.prometheus) {
77
+ this.metricsRegistry = globalThis.platformatic.prometheus.registry
78
+ } else {
79
+ this.metricsRegistry = new client.Registry()
80
+ this.registerGlobals({ prometheus: { client, registry: this.metricsRegistry } })
81
+ }
82
+ }
83
+
84
+ init () {
85
+ return this.updateContext()
86
+ }
87
+
88
+ updateContext (_context) {
89
+ // No-op by default
90
+ }
91
+
92
+ start () {
93
+ throw new Error('BaseStackable.start must be overriden by the subclasses')
94
+ }
95
+
96
+ stop () {
97
+ throw new Error('BaseStackable.stop must be overriden by the subclasses')
98
+ }
99
+
100
+ build () {
101
+ // No-op by default
102
+ }
103
+
104
+ // Alias for stop
105
+ close () {
106
+ return this.stop()
107
+ }
108
+
109
+ inject () {
110
+ throw new Error('BaseStackable.inject must be overriden by the subclasses')
86
111
  }
87
112
 
88
113
  getUrl () {
89
114
  return this.url
90
115
  }
91
116
 
92
- async getConfig () {
93
- return this.configManager.current
117
+ async getConfig (includeMeta = false) {
118
+ if (includeMeta) {
119
+ return this.config
120
+ }
121
+
122
+ const { [kMetadata]: _, ...config } = this.config
123
+ return config
94
124
  }
95
125
 
96
126
  async getEnv () {
97
- return this.configManager.env
127
+ return this.config[kMetadata].env
98
128
  }
99
129
 
100
130
  async getWatchConfig () {
101
- const config = this.configManager.current
131
+ const config = this.config
102
132
 
103
133
  const enabled = config.watch?.enabled !== false
104
134
 
@@ -123,7 +153,7 @@ export class BaseStackable extends EventEmitter {
123
153
  }
124
154
 
125
155
  async getDispatchTarget () {
126
- return this.getUrl() ?? this.getDispatchFunc()
156
+ return this.getUrl() ?? (await this.getDispatchFunc())
127
157
  }
128
158
 
129
159
  getMeta () {
@@ -242,7 +272,7 @@ export class BaseStackable extends EventEmitter {
242
272
  }
243
273
 
244
274
  async startWithCommand (command, loader, scripts) {
245
- const config = this.configManager.current
275
+ const config = this.config
246
276
  const basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
247
277
 
248
278
  const context = await this.getChildManagerContext(basePath)
@@ -274,6 +304,13 @@ export class BaseStackable extends EventEmitter {
274
304
  this.basePath = path
275
305
  })
276
306
 
307
+ // This is not really important for the URL but sometimes it also a sign
308
+ // that the process has been replaced and thus we need to update the client WebSocket
309
+ this.childManager.on('url', (url, clientWs) => {
310
+ this.url = url
311
+ this.clientWs = clientWs
312
+ })
313
+
277
314
  try {
278
315
  await this.childManager.inject()
279
316
  this.subprocess = await this.spawn(command)
@@ -342,7 +379,7 @@ export class BaseStackable extends EventEmitter {
342
379
 
343
380
  return {
344
381
  id: this.id,
345
- config: this.configManager.current,
382
+ config: this.config,
346
383
  serviceId: this.serviceId,
347
384
  workerId: this.workerId,
348
385
  // Always use URL to avoid serialization problem in Windows
@@ -388,46 +425,62 @@ export class BaseStackable extends EventEmitter {
388
425
  this.emit('config', config)
389
426
  }
390
427
 
428
+ _initializeLogger () {
429
+ const loggerOptions = deepmerge(this.runtimeConfig?.logger ?? {}, this.config?.logger ?? {})
430
+ const pinoOptions = buildPinoOptions(
431
+ loggerOptions,
432
+ this.serverConfig?.logger,
433
+ this.serviceId,
434
+ this.workerId,
435
+ this.context,
436
+ this.root
437
+ )
438
+
439
+ return pino(pinoOptions, this.standardStreams?.stdout)
440
+ }
441
+
391
442
  async _collectMetrics () {
392
443
  if (this.#metricsCollected) {
393
444
  return
394
445
  }
395
446
 
396
447
  this.#metricsCollected = true
448
+
449
+ if (this.context.metricsConfig === false) {
450
+ return
451
+ }
452
+
397
453
  await this.#collectMetrics()
398
454
  this.#setHttpCacheMetrics()
399
455
  }
400
456
 
401
457
  async #collectMetrics () {
402
- let metricsConfig = this.options.context.metricsConfig
403
- if (metricsConfig !== false) {
404
- metricsConfig = {
405
- defaultMetrics: true,
406
- httpMetrics: true,
407
- ...metricsConfig
408
- }
409
-
410
- if (this.childManager && this.clientWs) {
411
- await this.childManager.send(this.clientWs, 'collectMetrics', {
412
- serviceId: this.serviceId,
413
- workerId: this.workerId,
414
- metricsConfig
415
- })
416
- return
417
- }
458
+ const metricsConfig = {
459
+ defaultMetrics: true,
460
+ httpMetrics: true,
461
+ ...this.context.metricsConfig
462
+ }
418
463
 
419
- await collectMetrics(
420
- this.serviceId,
421
- this.workerId,
422
- metricsConfig,
423
- this.metricsRegistry
424
- )
464
+ if (this.childManager && this.clientWs) {
465
+ await this.childManager.send(this.clientWs, 'collectMetrics', {
466
+ serviceId: this.serviceId,
467
+ workerId: this.workerId,
468
+ metricsConfig
469
+ })
470
+ return
425
471
  }
472
+
473
+ await collectMetrics(this.serviceId, this.workerId, metricsConfig, this.metricsRegistry)
426
474
  }
427
475
 
428
476
  #setHttpCacheMetrics () {
429
477
  const { client, registry } = globalThis.platformatic.prometheus
430
478
 
479
+ // Metrics already registered, no need to register them again
480
+ if (ensureMetricsGroup(registry, 'http.cache')) {
481
+ return
482
+ }
483
+
431
484
  const cacheHitMetric = new client.Counter({
432
485
  name: 'http_cache_hit_count',
433
486
  help: 'Number of http cache hits',
package/lib/config.js ADDED
@@ -0,0 +1,81 @@
1
+ import {
2
+ listRecognizedConfigurationFiles,
3
+ NoConfigFileFoundError,
4
+ findConfigurationFile as utilsFindConfigurationFile
5
+ } from '@platformatic/foundation'
6
+ import jsonPatch from 'fast-json-patch'
7
+ import { stat } from 'node:fs/promises'
8
+ import { dirname, resolve as resolvePath } from 'node:path'
9
+ import { workerData } from 'node:worker_threads'
10
+
11
+ export async function findConfigurationFile (root, suffixes) {
12
+ const file = await utilsFindConfigurationFile(root, suffixes)
13
+
14
+ if (!file) {
15
+ const err = new NoConfigFileFoundError()
16
+ err.message = `No config file found in the directory ${root}. Please create one of the following files: ${listRecognizedConfigurationFiles(suffixes, ['json']).join(', ')}`
17
+
18
+ throw err
19
+ }
20
+
21
+ return resolvePath(root, file)
22
+ }
23
+
24
+ export async function resolve (fileOrDirectory, sourceOrConfig, suffixes) {
25
+ if (sourceOrConfig && typeof sourceOrConfig !== 'string') {
26
+ return {
27
+ root: fileOrDirectory,
28
+ source: sourceOrConfig
29
+ }
30
+ } else if (typeof fileOrDirectory === 'string' && typeof sourceOrConfig === 'string') {
31
+ return {
32
+ root: fileOrDirectory,
33
+ source: resolvePath(fileOrDirectory, sourceOrConfig)
34
+ }
35
+ }
36
+
37
+ try {
38
+ const fileInfo = await stat(fileOrDirectory)
39
+
40
+ if (fileInfo.isFile()) {
41
+ return {
42
+ root: dirname(fileOrDirectory),
43
+ source: fileOrDirectory
44
+ }
45
+ }
46
+ } catch {
47
+ // No-op
48
+ }
49
+
50
+ return {
51
+ root: fileOrDirectory,
52
+ source: await findConfigurationFile(fileOrDirectory, suffixes)
53
+ }
54
+ }
55
+
56
+ export async function transform (config) {
57
+ const patch = workerData?.serviceConfig?.configPatch
58
+
59
+ if (!config) {
60
+ return config
61
+ }
62
+
63
+ if (Array.isArray(patch)) {
64
+ config = jsonPatch.applyPatch(config, patch).newDocument
65
+ }
66
+
67
+ if (config.watch === undefined) {
68
+ config.watch = { enabled: workerData?.config?.watch ?? false }
69
+ } else if (typeof config.watch !== 'object') {
70
+ config.watch = { enabled: config.watch || false }
71
+ }
72
+
73
+ return config
74
+ }
75
+
76
+ export const validationOptions = {
77
+ useDefaults: true,
78
+ coerceTypes: true,
79
+ allErrors: true,
80
+ strict: false
81
+ }
@@ -0,0 +1,9 @@
1
+ import { resolve } from './config.js'
2
+ import { importStackableAndConfig } from './modules.js'
3
+
4
+ export async function create (fileOrDirectory, sourceOrConfig, context) {
5
+ const { root, source } = await resolve(fileOrDirectory, sourceOrConfig)
6
+ const { stackable } = await importStackableAndConfig(root, source, context)
7
+
8
+ return stackable.create(root, source, context)
9
+ }
package/lib/modules.js ADDED
@@ -0,0 +1,101 @@
1
+ import { detectApplicationType, findConfigurationFile } from '@platformatic/foundation'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
4
+ import { relative, resolve } from 'node:path'
5
+ import { workerData } from 'node:worker_threads'
6
+ import pino from 'pino'
7
+ import { packageJson } from './schema.js'
8
+ import { importFile } from './utils.js'
9
+
10
+ const importStackablePackageMarker = '__pltImportStackablePackage.js'
11
+
12
+ export function isImportFailedError (error, pkg) {
13
+ if (error.code !== 'ERR_MODULE_NOT_FOUND' && error.code !== 'MODULE_NOT_FOUND') {
14
+ return false
15
+ }
16
+
17
+ const match = error.message.match(/Cannot find package '(.+)' imported from (.+)/)
18
+
19
+ return match?.[1] === pkg || error.requireStack?.[0].endsWith(importStackablePackageMarker)
20
+ }
21
+
22
+ export async function importStackablePackage (directory, pkg) {
23
+ let imported
24
+ try {
25
+ try {
26
+ // Try regular import
27
+ imported = await import(pkg)
28
+ } catch (e) {
29
+ if (!isImportFailedError(e, pkg)) {
30
+ throw e
31
+ }
32
+
33
+ // Scope to the service
34
+ const require = createRequire(resolve(directory, importStackablePackageMarker))
35
+ const toImport = require.resolve(pkg)
36
+ imported = await importFile(toImport)
37
+ }
38
+ } catch (e) {
39
+ if (!isImportFailedError(e, pkg)) {
40
+ throw e
41
+ }
42
+
43
+ const serviceDirectory = workerData ? relative(workerData.dirname, directory) : directory
44
+ throw new Error(
45
+ `Unable to import package '${pkg}'. Please add it as a dependency in the package.json file in the folder ${serviceDirectory}.`
46
+ )
47
+ }
48
+
49
+ return imported.default ?? imported
50
+ }
51
+
52
+ export async function importStackableAndConfig (root, config, context) {
53
+ let rootPackageJson
54
+ try {
55
+ rootPackageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf-8'))
56
+ } catch {
57
+ rootPackageJson = {}
58
+ }
59
+
60
+ const hadConfig = !!config
61
+
62
+ if (!config) {
63
+ config = await findConfigurationFile(root, 'application')
64
+ }
65
+
66
+ const appType = await detectApplicationType(root, rootPackageJson)
67
+
68
+ if (!appType) {
69
+ throw new Error(`Unable to detect application type in ${root}.`)
70
+ }
71
+
72
+ const { label, name: moduleName } = appType
73
+
74
+ if (context) {
75
+ const serviceRoot = relative(process.cwd(), root)
76
+
77
+ if (!hadConfig && context.serviceId && !(await findConfigurationFile(root)) && context.worker?.index === 0) {
78
+ const autodetectDescription =
79
+ moduleName === '@platformatic/node' ? 'is a generic Node.js application' : `is using ${label}`
80
+
81
+ const logger = pino({ level: context.serverConfig?.logger?.level ?? 'warn', name: context.serviceId })
82
+
83
+ logger.warn(`We have auto-detected that service "${context.serviceId}" ${autodetectDescription}.`)
84
+ logger.warn(
85
+ `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".`
86
+ )
87
+ logger.warn(`Also don't forget to add "${moduleName}" to the service dependencies.`)
88
+ logger.warn('You can also run "wattpm import" to do this automatically.\n')
89
+ }
90
+ }
91
+
92
+ const stackable = await importStackablePackage(root, moduleName)
93
+
94
+ return {
95
+ stackable,
96
+ config,
97
+ autodetectDescription:
98
+ moduleName === '@platformatic/node' ? 'is a generic Node.js application' : `is using ${label}`,
99
+ moduleName
100
+ }
101
+ }
package/lib/schema.js CHANGED
@@ -1,11 +1,27 @@
1
- import { schemaComponents as utilsSchemaComponents } from '@platformatic/utils'
1
+ import { schemaComponents as utilsSchemaComponents } from '@platformatic/foundation'
2
2
  import { readFileSync } from 'node:fs'
3
3
 
4
4
  export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
5
+ export const version = packageJson.version
5
6
 
7
+ // This is used by applications to have common properties.
6
8
  const application = {
9
+ type: 'object',
10
+ properties: {},
11
+ additionalProperties: false,
12
+ required: [],
13
+ default: {}
14
+ }
15
+
16
+ /*
17
+ For legacy purposes, when an application is buildable (like Astro), it should use for the application properties.
18
+ Make sure this always extends the `application` schema above.
19
+ */
20
+
21
+ const buildableApplication = {
7
22
  type: 'object',
8
23
  properties: {
24
+ ...application.properties,
9
25
  basePath: {
10
26
  type: 'string'
11
27
  },
@@ -44,7 +60,10 @@ const application = {
44
60
  }
45
61
  },
46
62
  additionalProperties: false,
47
- default: {}
63
+ required: [...application.required],
64
+ default: {
65
+ ...application.default
66
+ }
48
67
  }
49
68
 
50
69
  const watch = {
@@ -59,12 +78,12 @@ const watch = {
59
78
  ]
60
79
  }
61
80
 
62
- export const schemaComponents = { application, watch }
81
+ export const schemaComponents = { application, buildableApplication, watch }
63
82
 
64
83
  export const schema = {
65
84
  $id: `https://schemas.platformatic.dev/@platformatic/basic/${packageJson.version}.json`,
66
85
  $schema: 'http://json-schema.org/draft-07/schema#',
67
- title: 'Platformatic Stackable',
86
+ title: 'Platformatic Basic Config',
68
87
  type: 'object',
69
88
  properties: {
70
89
  $schema: {
@@ -1,5 +1,5 @@
1
+ import { createDirectory, ensureLoggableError } from '@platformatic/foundation'
1
2
  import { ITC } from '@platformatic/itc'
2
- import { createDirectory, ensureLoggableError } from '@platformatic/utils'
3
3
  import { once } from 'node:events'
4
4
  import { rm, writeFile } from 'node:fs/promises'
5
5
  import { createServer } from 'node:http'
@@ -1,13 +1,12 @@
1
- import { ITC } from '@platformatic/itc'
2
- import { client, collectMetrics } from '@platformatic/metrics'
3
1
  import {
4
2
  buildPinoFormatters,
5
3
  buildPinoTimestamp,
6
4
  disablePinoDirectWrite,
7
- ensureFlushedWorkerStdio,
8
5
  ensureLoggableError,
9
6
  features
10
- } from '@platformatic/utils'
7
+ } from '@platformatic/foundation'
8
+ import { ITC } from '@platformatic/itc'
9
+ import { client, collectMetrics } from '@platformatic/metrics'
11
10
  import diagnosticChannel, { tracingChannel } from 'node:diagnostics_channel'
12
11
  import { EventEmitter, once } from 'node:events'
13
12
  import { readFile } from 'node:fs/promises'
@@ -83,7 +82,7 @@ export class ChildProcess extends ITC {
83
82
  getMetrics: (...args) => {
84
83
  return this.#getMetrics(...args)
85
84
  },
86
- close: (signal) => {
85
+ close: signal => {
87
86
  let handled = false
88
87
 
89
88
  try {
@@ -294,7 +293,6 @@ export class ChildProcess extends ITC {
294
293
 
295
294
  #setupLogger () {
296
295
  disablePinoDirectWrite()
297
- ensureFlushedWorkerStdio()
298
296
 
299
297
  // Since this is executed by user code, make sure we only override this in the main thread
300
298
  // The rest will be intercepted by the BaseStackable.
@@ -1,5 +1,5 @@
1
+ import { ensureLoggableError } from '@platformatic/foundation'
1
2
  import { generateRequest, sanitize } from '@platformatic/itc'
2
- import { ensureLoggableError } from '@platformatic/utils'
3
3
  import { once } from 'node:events'
4
4
  import { platform } from 'node:os'
5
5
  import { workerData } from 'node:worker_threads'
@@ -1,8 +1,8 @@
1
- import { features, withResolvers } from '@platformatic/utils'
1
+ import { features } from '@platformatic/foundation'
2
2
  import { subscribe, tracingChannel, unsubscribe } from 'node:diagnostics_channel'
3
3
 
4
4
  export function createServerListener (overridePort = true, overrideHost) {
5
- const { promise, resolve, reject } = withResolvers()
5
+ const { promise, resolve, reject } = Promise.withResolvers()
6
6
 
7
7
  const subscribers = {
8
8
  asyncStart ({ options }) {
@@ -48,7 +48,7 @@ export function createServerListener (overridePort = true, overrideHost) {
48
48
  }
49
49
 
50
50
  export function createChildProcessListener () {
51
- const { promise, resolve } = withResolvers()
51
+ const { promise, resolve } = Promise.withResolvers()
52
52
 
53
53
  const handler = ({ process: child }) => {
54
54
  unsubscribe('child_process', handler)
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@platformatic/basic",
3
- "version": "2.74.3",
3
+ "version": "3.0.0-alpha.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
6
7
  "type": "module",
7
8
  "repository": {
8
9
  "type": "git",
@@ -18,17 +19,16 @@
18
19
  "@fastify/error": "^4.0.0",
19
20
  "execa": "^9.3.1",
20
21
  "fast-json-patch": "^3.1.1",
21
- "pino": "^9.3.2",
22
+ "pino": "^9.9.0",
22
23
  "pino-abstract-transport": "^2.0.0",
23
24
  "semver": "^7.6.3",
24
25
  "split2": "^4.2.0",
25
26
  "undici": "^7.0.0",
26
27
  "ws": "^8.18.0",
27
- "@platformatic/itc": "2.74.3",
28
- "@platformatic/config": "2.74.3",
29
- "@platformatic/metrics": "2.74.3",
30
- "@platformatic/telemetry": "2.74.3",
31
- "@platformatic/utils": "2.74.3"
28
+ "@platformatic/itc": "3.0.0-alpha.2",
29
+ "@platformatic/metrics": "3.0.0-alpha.2",
30
+ "@platformatic/telemetry": "3.0.0-alpha.2",
31
+ "@platformatic/foundation": "3.0.0-alpha.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "borp": "^0.20.0",
@@ -37,13 +37,15 @@
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.3",
41
40
  "neostandard": "^0.12.0",
42
41
  "typescript": "^5.5.4"
43
42
  },
43
+ "engines": {
44
+ "node": ">=22.18.0"
45
+ },
44
46
  "scripts": {
45
- "test": "npm run lint && borp --concurrency=1 --no-timeout",
46
- "coverage": "npm run lint && borp -C -X test -X test/fixtures --concurrency=1 --no-timeout",
47
+ "test": "npm run lint && borp --concurrency=1 --timeout 1200000",
48
+ "coverage": "npm run lint && borp -C -X test -X test/fixtures --concurrency=1 --timeout 1200000",
47
49
  "gen-schema": "node lib/schema.js > schema.json",
48
50
  "gen-types": "json2ts > config.d.ts < schema.json",
49
51
  "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/basic/2.74.3.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/basic/3.0.0-alpha.2.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
- "title": "Platformatic Stackable",
4
+ "title": "Platformatic Basic Config",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "$schema": {