@platformatic/basic 2.72.0 → 3.0.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/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
1
  import { client, collectMetrics } from '@platformatic/metrics'
2
- import { buildPinoOptions, deepmerge, executeWithTimeout, kTimeout } from '@platformatic/utils'
2
+ import { buildPinoOptions, deepmerge, executeWithTimeout, kMetadata, kTimeout } from '@platformatic/utils'
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,39 @@ 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
43
+ this.isEntrypoint = this.context.isEntrypoint
44
+ this.isProduction = this.context.isProduction
45
45
  this.metricsRegistry = new client.Registry()
46
46
  this.#metricsCollected = false
47
47
  this.customHealthCheck = null
48
48
  this.customReadinessCheck = null
49
49
  this.clientWs = null
50
- this.runtimeConfig = deepmerge(options.context?.runtimeConfig ?? {}, workerData?.config ?? {})
50
+ this.runtimeConfig = deepmerge(this.context?.runtimeConfig ?? {}, workerData?.config ?? {})
51
51
  this.stdout = standardStreams?.stdout ?? process.stdout
52
52
  this.stderr = standardStreams?.stderr ?? process.stderr
53
53
  this.subprocessForceClose = false
54
54
  this.subprocessTerminationSignal = 'SIGINT'
55
55
 
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)
56
+ this.logger = this._initializeLogger()
66
57
 
67
58
  // Setup globals
68
59
  this.registerGlobals({
@@ -85,20 +76,50 @@ export class BaseStackable extends EventEmitter {
85
76
  })
86
77
  }
87
78
 
79
+ init () {
80
+ return this.updateContext()
81
+ }
82
+
83
+ updateContext (context) {
84
+ // No-op
85
+ }
86
+
87
+ start () {
88
+ throw new Error('BaseStackable.start must be overriden by the subclasses')
89
+ }
90
+
91
+ stop () {
92
+ throw new Error('BaseStackable.stop must be overriden by the subclasses')
93
+ }
94
+
95
+ // Alias for stop
96
+ close () {
97
+ return this.stop()
98
+ }
99
+
100
+ inject () {
101
+ throw new Error('BaseStackable.inject must be overriden by the subclasses')
102
+ }
103
+
88
104
  getUrl () {
89
105
  return this.url
90
106
  }
91
107
 
92
- async getConfig () {
93
- return this.configManager.current
108
+ async getConfig (includeMeta = false) {
109
+ if (includeMeta) {
110
+ return this.config
111
+ }
112
+
113
+ const { [kMetadata]: _, ...config } = this.config
114
+ return config
94
115
  }
95
116
 
96
117
  async getEnv () {
97
- return this.configManager.env
118
+ return this.config[kMetadata].env
98
119
  }
99
120
 
100
121
  async getWatchConfig () {
101
- const config = this.configManager.current
122
+ const config = this.config
102
123
 
103
124
  const enabled = config.watch?.enabled !== false
104
125
 
@@ -123,7 +144,7 @@ export class BaseStackable extends EventEmitter {
123
144
  }
124
145
 
125
146
  async getDispatchTarget () {
126
- return this.getUrl() ?? this.getDispatchFunc()
147
+ return this.getUrl() ?? (await this.getDispatchFunc())
127
148
  }
128
149
 
129
150
  getMeta () {
@@ -242,7 +263,7 @@ export class BaseStackable extends EventEmitter {
242
263
  }
243
264
 
244
265
  async startWithCommand (command, loader, scripts) {
245
- const config = this.configManager.current
266
+ const config = this.config
246
267
  const basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
247
268
 
248
269
  const context = await this.getChildManagerContext(basePath)
@@ -342,7 +363,7 @@ export class BaseStackable extends EventEmitter {
342
363
 
343
364
  return {
344
365
  id: this.id,
345
- config: this.configManager.current,
366
+ config: this.config,
346
367
  serviceId: this.serviceId,
347
368
  workerId: this.workerId,
348
369
  // Always use URL to avoid serialization problem in Windows
@@ -388,46 +409,62 @@ export class BaseStackable extends EventEmitter {
388
409
  this.emit('config', config)
389
410
  }
390
411
 
412
+ _initializeLogger () {
413
+ const loggerOptions = deepmerge(this.runtimeConfig?.logger ?? {}, this.config?.logger ?? {})
414
+ const pinoOptions = buildPinoOptions(
415
+ loggerOptions,
416
+ this.serverConfig?.logger,
417
+ this.serviceId,
418
+ this.workerId,
419
+ this.context,
420
+ this.root
421
+ )
422
+
423
+ return pino(pinoOptions, this.standardStreams?.stdout)
424
+ }
425
+
391
426
  async _collectMetrics () {
392
427
  if (this.#metricsCollected) {
393
428
  return
394
429
  }
395
430
 
396
431
  this.#metricsCollected = true
432
+
433
+ if (this.context.metricsConfig === false) {
434
+ return
435
+ }
436
+
397
437
  await this.#collectMetrics()
398
438
  this.#setHttpCacheMetrics()
399
439
  }
400
440
 
401
441
  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
- }
442
+ const metricsConfig = {
443
+ defaultMetrics: true,
444
+ httpMetrics: true,
445
+ ...this.context.metricsConfig
446
+ }
418
447
 
419
- await collectMetrics(
420
- this.serviceId,
421
- this.workerId,
422
- metricsConfig,
423
- this.metricsRegistry
424
- )
448
+ if (this.childManager && this.clientWs) {
449
+ await this.childManager.send(this.clientWs, 'collectMetrics', {
450
+ serviceId: this.serviceId,
451
+ workerId: this.workerId,
452
+ metricsConfig
453
+ })
454
+ return
425
455
  }
456
+
457
+ await collectMetrics(this.serviceId, this.workerId, metricsConfig, this.metricsRegistry)
426
458
  }
427
459
 
428
460
  #setHttpCacheMetrics () {
429
461
  const { client, registry } = globalThis.platformatic.prometheus
430
462
 
463
+ // Metrics already registered, no need to register them again
464
+ if (registry.getSingleMetric('http_cache_hit_count') || registry.getSingleMetric('http_cache_miss_count')) {
465
+ return
466
+ }
467
+
431
468
  const cacheHitMetric = new client.Counter({
432
469
  name: 'http_cache_hit_count',
433
470
  help: 'Number of http cache hits',
@@ -446,66 +483,6 @@ export class BaseStackable extends EventEmitter {
446
483
  globalThis.platformatic.onHttpCacheMiss = () => {
447
484
  cacheMissMetric.inc()
448
485
  }
449
-
450
- const httpStatsFreeMetric = new client.Gauge({
451
- name: 'http_client_stats_free',
452
- help: 'Number of free (idle) http clients (sockets)',
453
- labelNames: ['dispatcher_stats_url'],
454
- registers: [registry]
455
- })
456
- globalThis.platformatic.onHttpStatsFree = (url, val) => {
457
- httpStatsFreeMetric.set({ dispatcher_stats_url: url }, val)
458
- }
459
-
460
- const httpStatsConnectedMetric = new client.Gauge({
461
- name: 'http_client_stats_connected',
462
- help: 'Number of open socket connections',
463
- labelNames: ['dispatcher_stats_url'],
464
- registers: [registry]
465
- })
466
- globalThis.platformatic.onHttpStatsConnected = (url, val) => {
467
- httpStatsConnectedMetric.set({ dispatcher_stats_url: url }, val)
468
- }
469
-
470
- const httpStatsPendingMetric = new client.Gauge({
471
- name: 'http_client_stats_pending',
472
- help: 'Number of pending requests across all clients',
473
- labelNames: ['dispatcher_stats_url'],
474
- registers: [registry]
475
- })
476
- globalThis.platformatic.onHttpStatsPending = (url, val) => {
477
- httpStatsPendingMetric.set({ dispatcher_stats_url: url }, val)
478
- }
479
-
480
- const httpStatsQueuedMetric = new client.Gauge({
481
- name: 'http_client_stats_queued',
482
- help: 'Number of queued requests across all clients',
483
- labelNames: ['dispatcher_stats_url'],
484
- registers: [registry]
485
- })
486
- globalThis.platformatic.onHttpStatsQueued = (url, val) => {
487
- httpStatsQueuedMetric.set({ dispatcher_stats_url: url }, val)
488
- }
489
-
490
- const httpStatsRunningMetric = new client.Gauge({
491
- name: 'http_client_stats_running',
492
- help: 'Number of currently active requests across all clients',
493
- labelNames: ['dispatcher_stats_url'],
494
- registers: [registry]
495
- })
496
- globalThis.platformatic.onHttpStatsRunning = (url, val) => {
497
- httpStatsRunningMetric.set({ dispatcher_stats_url: url }, val)
498
- }
499
-
500
- const httpStatsSizeMetric = new client.Gauge({
501
- name: 'http_client_stats_size',
502
- help: 'Number of active, pending, or queued requests across all clients',
503
- labelNames: ['dispatcher_stats_url'],
504
- registers: [registry]
505
- })
506
- globalThis.platformatic.onHttpStatsSize = (url, val) => {
507
- httpStatsSizeMetric.set({ dispatcher_stats_url: url }, val)
508
- }
509
486
  }
510
487
 
511
488
  async #invalidateHttpCache (opts = {}) {
package/lib/config.js ADDED
@@ -0,0 +1,81 @@
1
+ import {
2
+ listRecognizedConfigurationFiles,
3
+ NoConfigFileFoundError,
4
+ findConfigurationFile as utilsFindConfigurationFile
5
+ } from '@platformatic/utils'
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/utils'
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
@@ -2,6 +2,7 @@ import { schemaComponents as utilsSchemaComponents } from '@platformatic/utils'
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
 
6
7
  const application = {
7
8
  type: 'object',
@@ -64,7 +65,7 @@ export const schemaComponents = { application, watch }
64
65
  export const schema = {
65
66
  $id: `https://schemas.platformatic.dev/@platformatic/basic/${packageJson.version}.json`,
66
67
  $schema: 'http://json-schema.org/draft-07/schema#',
67
- title: 'Platformatic Stackable',
68
+ title: 'Platformatic Basic Config',
68
69
  type: 'object',
69
70
  properties: {
70
71
  $schema: {
@@ -162,7 +162,7 @@ export class ChildManager extends ITC {
162
162
  const childProcessInclude = `--import="${new URL('./child-process.js', import.meta.url)}"`
163
163
 
164
164
  let telemetryInclude = ''
165
- if (this.#context.telemetryConfig && this.#context.telemetryConfig.enabled !== false) {
165
+ if (this.#context.telemetryConfig) {
166
166
  const require = createRequire(import.meta.url)
167
167
  const telemetryPath = require.resolve('@platformatic/telemetry')
168
168
  const openTelemetrySetupPath = join(telemetryPath, '..', 'lib', 'node-telemetry.js')
@@ -4,7 +4,6 @@ import {
4
4
  buildPinoFormatters,
5
5
  buildPinoTimestamp,
6
6
  disablePinoDirectWrite,
7
- ensureFlushedWorkerStdio,
8
7
  ensureLoggableError,
9
8
  features
10
9
  } from '@platformatic/utils'
@@ -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 {
@@ -223,66 +222,6 @@ export class ChildProcess extends ITC {
223
222
  globalThis.platformatic.onHttpCacheMiss = () => {
224
223
  cacheMissMetric.inc()
225
224
  }
226
-
227
- const httpStatsFreeMetric = new client.Gauge({
228
- name: 'http_client_stats_free',
229
- help: 'Number of free (idle) http clients (sockets)',
230
- labelNames: ['dispatcher_stats_url'],
231
- registers: [registry]
232
- })
233
- globalThis.platformatic.onHttpStatsFree = (url, val) => {
234
- httpStatsFreeMetric.set({ dispatcher_stats_url: url }, val)
235
- }
236
-
237
- const httpStatsConnectedMetric = new client.Gauge({
238
- name: 'http_client_stats_connected',
239
- help: 'Number of open socket connections',
240
- labelNames: ['dispatcher_stats_url'],
241
- registers: [registry]
242
- })
243
- globalThis.platformatic.onHttpStatsConnected = (url, val) => {
244
- httpStatsConnectedMetric.set({ dispatcher_stats_url: url }, val)
245
- }
246
-
247
- const httpStatsPendingMetric = new client.Gauge({
248
- name: 'http_client_stats_pending',
249
- help: 'Number of pending requests across all clients',
250
- labelNames: ['dispatcher_stats_url'],
251
- registers: [registry]
252
- })
253
- globalThis.platformatic.onHttpStatsPending = (url, val) => {
254
- httpStatsPendingMetric.set({ dispatcher_stats_url: url }, val)
255
- }
256
-
257
- const httpStatsQueuedMetric = new client.Gauge({
258
- name: 'http_client_stats_queued',
259
- help: 'Number of queued requests across all clients',
260
- labelNames: ['dispatcher_stats_url'],
261
- registers: [registry]
262
- })
263
- globalThis.platformatic.onHttpStatsQueued = (url, val) => {
264
- httpStatsQueuedMetric.set({ dispatcher_stats_url: url }, val)
265
- }
266
-
267
- const httpStatsRunningMetric = new client.Gauge({
268
- name: 'http_client_stats_running',
269
- help: 'Number of currently active requests across all clients',
270
- labelNames: ['dispatcher_stats_url'],
271
- registers: [registry]
272
- })
273
- globalThis.platformatic.onHttpStatsRunning = (url, val) => {
274
- httpStatsRunningMetric.set({ dispatcher_stats_url: url }, val)
275
- }
276
-
277
- const httpStatsSizeMetric = new client.Gauge({
278
- name: 'http_client_stats_size',
279
- help: 'Number of active, pending, or queued requests across all clients',
280
- labelNames: ['dispatcher_stats_url'],
281
- registers: [registry]
282
- })
283
- globalThis.platformatic.onHttpStatsSize = (url, val) => {
284
- httpStatsSizeMetric.set({ dispatcher_stats_url: url }, val)
285
- }
286
225
  }
287
226
 
288
227
  async #getMetrics ({ format } = {}) {
@@ -294,7 +233,6 @@ export class ChildProcess extends ITC {
294
233
 
295
234
  #setupLogger () {
296
235
  disablePinoDirectWrite()
297
- ensureFlushedWorkerStdio()
298
236
 
299
237
  // Since this is executed by user code, make sure we only override this in the main thread
300
238
  // The rest will be intercepted by the BaseStackable.
@@ -1,8 +1,8 @@
1
- import { features, withResolvers } from '@platformatic/utils'
1
+ import { features } from '@platformatic/utils'
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.72.0",
3
+ "version": "3.0.0-alpha.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
6
7
  "type": "module",
7
8
  "repository": {
8
9
  "type": "git",
@@ -24,11 +25,10 @@
24
25
  "split2": "^4.2.0",
25
26
  "undici": "^7.0.0",
26
27
  "ws": "^8.18.0",
27
- "@platformatic/itc": "2.72.0",
28
- "@platformatic/telemetry": "2.72.0",
29
- "@platformatic/config": "2.72.0",
30
- "@platformatic/utils": "2.72.0",
31
- "@platformatic/metrics": "2.72.0"
28
+ "@platformatic/itc": "3.0.0-alpha.1",
29
+ "@platformatic/metrics": "3.0.0-alpha.1",
30
+ "@platformatic/telemetry": "3.0.0-alpha.1",
31
+ "@platformatic/utils": "3.0.0-alpha.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "borp": "^0.20.0",
@@ -37,13 +37,12 @@
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
  },
44
43
  "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",
44
+ "test": "npm run lint && borp --concurrency=1 --timeout 1200000",
45
+ "coverage": "npm run lint && borp -C -X test -X test/fixtures --concurrency=1 --timeout 1200000",
47
46
  "gen-schema": "node lib/schema.js > schema.json",
48
47
  "gen-types": "json2ts > config.d.ts < schema.json",
49
48
  "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.72.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/basic/3.0.0-alpha.1.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": {