@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 +1 -1
- package/index.d.ts +89 -0
- package/index.js +3 -140
- package/lib/base.js +109 -56
- package/lib/config.js +81 -0
- package/lib/creation.js +9 -0
- package/lib/modules.js +101 -0
- package/lib/schema.js +23 -4
- package/lib/worker/child-manager.js +1 -1
- package/lib/worker/child-process.js +4 -6
- package/lib/worker/child-transport.js +1 -1
- package/lib/worker/listeners.js +3 -3
- package/package.json +12 -10
- package/schema.json +2 -2
package/config.d.ts
CHANGED
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 {
|
|
2
|
-
import {
|
|
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,
|
|
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.
|
|
38
|
-
this.
|
|
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 =
|
|
44
|
-
this.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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
127
|
+
return this.config[kMetadata].env
|
|
98
128
|
}
|
|
99
129
|
|
|
100
130
|
async getWatchConfig () {
|
|
101
|
-
const config = this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
this.
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|
package/lib/creation.js
ADDED
|
@@ -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/
|
|
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
|
-
|
|
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
|
|
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/
|
|
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:
|
|
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'
|
package/lib/worker/listeners.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { features
|
|
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": "
|
|
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.
|
|
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": "
|
|
28
|
-
"@platformatic/
|
|
29
|
-
"@platformatic/
|
|
30
|
-
"@platformatic/
|
|
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 --
|
|
46
|
-
"coverage": "npm run lint && borp -C -X test -X test/fixtures --concurrency=1 --
|
|
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
|
+
"$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
|
|
4
|
+
"title": "Platformatic Basic Config",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
7
7
|
"$schema": {
|