@platformatic/runtime 3.67.0 → 4.0.0-new-config.0
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 +5 -43
- package/index.d.ts +10 -12
- package/index.js +191 -6
- package/lib/config.js +125 -60
- package/lib/errors.js +5 -13
- package/lib/generator.js +19 -92
- package/lib/management-api.js +3 -5
- package/lib/management-handlers.js +3 -5
- package/lib/runtime.js +377 -183
- package/lib/schema.js +1 -1
- package/lib/upgrade.js +16 -1
- package/lib/versions/v4.0.0.js +31 -0
- package/lib/worker/controller.js +51 -55
- package/lib/worker/itc.js +5 -6
- package/lib/worker/main.js +4 -19
- package/lib/worker/symbols.js +2 -0
- package/lib/worker-scaler.js +8 -10
- package/package.json +17 -16
- package/schema.json +33 -147
package/config.d.ts
CHANGED
|
@@ -29,7 +29,6 @@ export type PlatformaticRuntimeConfig = {
|
|
|
29
29
|
build?: boolean;
|
|
30
30
|
}
|
|
31
31
|
)[];
|
|
32
|
-
entrypoint?: string;
|
|
33
32
|
basePath?: string;
|
|
34
33
|
autoload?: {
|
|
35
34
|
path: string;
|
|
@@ -43,8 +42,11 @@ export type PlatformaticRuntimeConfig = {
|
|
|
43
42
|
| {
|
|
44
43
|
[k: string]: boolean;
|
|
45
44
|
};
|
|
46
|
-
config?:
|
|
47
|
-
|
|
45
|
+
config?:
|
|
46
|
+
| string
|
|
47
|
+
| {
|
|
48
|
+
[k: string]: unknown;
|
|
49
|
+
};
|
|
48
50
|
reuseTcpPorts?: boolean;
|
|
49
51
|
workers?:
|
|
50
52
|
| number
|
|
@@ -218,46 +220,6 @@ export type PlatformaticRuntimeConfig = {
|
|
|
218
220
|
};
|
|
219
221
|
[k: string]: unknown;
|
|
220
222
|
};
|
|
221
|
-
server?: {
|
|
222
|
-
hostname?: string;
|
|
223
|
-
port?: number | string;
|
|
224
|
-
/**
|
|
225
|
-
* Configures how entrypoint server worker ports are assigned. When set to shared, all workers listen on the same port. When set to perWorkerIncrement, each worker will use its own port, starting from port (worker 0).
|
|
226
|
-
*/
|
|
227
|
-
portAssignment?: "shared" | "perWorkerIncrement";
|
|
228
|
-
/**
|
|
229
|
-
* The maximum length of the queue of pending connections
|
|
230
|
-
*/
|
|
231
|
-
backlog?: number;
|
|
232
|
-
http2?: boolean;
|
|
233
|
-
https?: {
|
|
234
|
-
allowHTTP1?: boolean;
|
|
235
|
-
key:
|
|
236
|
-
| string
|
|
237
|
-
| {
|
|
238
|
-
path?: string;
|
|
239
|
-
}
|
|
240
|
-
| (
|
|
241
|
-
| string
|
|
242
|
-
| {
|
|
243
|
-
path?: string;
|
|
244
|
-
}
|
|
245
|
-
)[];
|
|
246
|
-
cert:
|
|
247
|
-
| string
|
|
248
|
-
| {
|
|
249
|
-
path?: string;
|
|
250
|
-
}
|
|
251
|
-
| (
|
|
252
|
-
| string
|
|
253
|
-
| {
|
|
254
|
-
path?: string;
|
|
255
|
-
}
|
|
256
|
-
)[];
|
|
257
|
-
requestCert?: boolean;
|
|
258
|
-
rejectUnauthorized?: boolean;
|
|
259
|
-
};
|
|
260
|
-
};
|
|
261
223
|
reuseTcpPorts?: boolean;
|
|
262
224
|
startTimeout?: number;
|
|
263
225
|
restartOnError?: boolean | number;
|
package/index.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface LoopbackMessagingOptions {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export namespace errors {
|
|
40
|
+
export const AddressInUseError: (port: number, firstApplication: string, secondApplication: string) => FastifyError
|
|
41
|
+
export const WorkerAddressInUseError: (port: number, application: string) => FastifyError
|
|
40
42
|
export const RuntimeExitedError: () => FastifyError
|
|
41
43
|
export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
|
|
42
44
|
export const ApplicationNotFoundError: (id: string) => FastifyError
|
|
@@ -46,8 +48,6 @@ export namespace errors {
|
|
|
46
48
|
export const RuntimeNotStartedError: () => FastifyError
|
|
47
49
|
export const ConfigPathMustBeStringError: () => FastifyError
|
|
48
50
|
export const NoConfigFileFoundError: (id: string) => FastifyError
|
|
49
|
-
export const InvalidEntrypointError: (entrypoint: string) => FastifyError
|
|
50
|
-
export const MissingEntrypointError: () => FastifyError
|
|
51
51
|
export const MissingDependencyError: (dependency: string) => FastifyError
|
|
52
52
|
export const InspectAndInspectBrkError: () => FastifyError
|
|
53
53
|
export const InspectorPortError: () => FastifyError
|
|
@@ -89,6 +89,7 @@ export namespace symbols {
|
|
|
89
89
|
export const kLastWorkerScalerELU: unique symbol
|
|
90
90
|
export const kWorkerStatus: unique symbol
|
|
91
91
|
export const kWorkerHealthSignals: unique symbol
|
|
92
|
+
export const kWorkerUrl: unique symbol
|
|
92
93
|
export const kStderrMarker: string
|
|
93
94
|
export const kWorkersBroadcast: unique symbol
|
|
94
95
|
}
|
|
@@ -119,14 +120,13 @@ export interface ApplicationDetails {
|
|
|
119
120
|
dependencies?: string[]
|
|
120
121
|
version?: string
|
|
121
122
|
localUrl?: string
|
|
122
|
-
entrypoint?: boolean
|
|
123
123
|
sourceMaps?: boolean
|
|
124
124
|
workers?: number
|
|
125
125
|
url?: string | null
|
|
126
|
+
urls?: string[]
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
export interface ApplicationsTopology {
|
|
129
|
-
entrypoint: string
|
|
130
130
|
production: boolean
|
|
131
131
|
applications: ApplicationDetails[]
|
|
132
132
|
}
|
|
@@ -196,8 +196,8 @@ export interface RuntimeMetadata {
|
|
|
196
196
|
projectDir: string
|
|
197
197
|
packageName: string | null
|
|
198
198
|
packageVersion: string | null
|
|
199
|
-
url: string | null
|
|
200
199
|
platformaticVersion: string
|
|
200
|
+
urls: Record<string, string>
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
export interface WorkerLifecycleEvent {
|
|
@@ -234,9 +234,7 @@ export interface HealthMetricsEvent extends WorkerLifecycleEvent {
|
|
|
234
234
|
healthSignals: RuntimeHealthSignal[]
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
export
|
|
238
|
-
constructor (allowedOperations?: string[])
|
|
239
|
-
|
|
237
|
+
export interface ManagementClient {
|
|
240
238
|
getRuntimeStatus (): Promise<string>
|
|
241
239
|
getRuntimeMetadata (): Promise<RuntimeMetadata>
|
|
242
240
|
getRuntimeConfig (): Promise<Record<string, unknown>>
|
|
@@ -257,7 +255,7 @@ export declare class ManagementClient {
|
|
|
257
255
|
startApplication (id: string): Promise<void>
|
|
258
256
|
stopApplication (id: string): Promise<void>
|
|
259
257
|
restartApplication (id: string): Promise<void>
|
|
260
|
-
restart (applications?: string[]): Promise<
|
|
258
|
+
restart (applications?: string[]): Promise<void>
|
|
261
259
|
addApplications (applications: unknown[], start?: boolean): Promise<ApplicationDetails[]>
|
|
262
260
|
removeApplications (ids: string[]): Promise<ApplicationDetails[]>
|
|
263
261
|
inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
|
|
@@ -369,12 +367,11 @@ export type RuntimeExtension = (
|
|
|
369
367
|
|
|
370
368
|
export declare class Runtime extends EventEmitter {
|
|
371
369
|
init (): Promise<void>
|
|
372
|
-
start (silent?: boolean): Promise<string
|
|
370
|
+
start (silent?: boolean): Promise<Record<string, string>>
|
|
373
371
|
stop (silent?: boolean): Promise<void>
|
|
374
372
|
close (silent?: boolean): Promise<void>
|
|
375
|
-
restart (applications?: string[]): Promise<
|
|
373
|
+
restart (applications?: string[]): Promise<void>
|
|
376
374
|
inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
|
|
377
|
-
getUrl (): string | undefined
|
|
378
375
|
getRuntimeStatus (): string
|
|
379
376
|
getRuntimeMetadata (): Promise<RuntimeMetadata>
|
|
380
377
|
getRuntimeEnv (): Record<string, string>
|
|
@@ -385,6 +382,7 @@ export declare class Runtime extends EventEmitter {
|
|
|
385
382
|
getRuntimeConfig (includeMeta: true): RuntimeConfiguration
|
|
386
383
|
getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
|
|
387
384
|
getApplicationsIds (): string[]
|
|
385
|
+
getUrls (applicationId?: string): Record<string, string>
|
|
388
386
|
/**
|
|
389
387
|
* Returns topology for every configured application.
|
|
390
388
|
* When `allowUnloaded` is `true`, applications without running workers are
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve, validationOptions } from '@platformatic/basic'
|
|
1
|
+
import { importCapabilityPackage, resolve, validationOptions } from '@platformatic/basic'
|
|
2
2
|
import {
|
|
3
3
|
abstractLogger,
|
|
4
4
|
ensureLoggableError,
|
|
@@ -8,9 +8,17 @@ import {
|
|
|
8
8
|
loadConfigurationModule,
|
|
9
9
|
loadConfiguration as utilsLoadConfiguration
|
|
10
10
|
} from '@platformatic/foundation'
|
|
11
|
+
import {
|
|
12
|
+
isConfigurationFileName,
|
|
13
|
+
findDecidingFile,
|
|
14
|
+
loadConfiguration as loadV4Configuration,
|
|
15
|
+
validateCapabilityConfiguration
|
|
16
|
+
} from '@platformatic/foundation/lib/v4/index.js'
|
|
11
17
|
import closeWithGrace from 'close-with-grace'
|
|
18
|
+
import { stat } from 'node:fs/promises'
|
|
12
19
|
import inspector from 'node:inspector'
|
|
13
|
-
import {
|
|
20
|
+
import { basename, dirname, resolve as resolvePath } from 'node:path'
|
|
21
|
+
import { transform as v3Transform, transformV4, wrapInRuntimeConfig } from './lib/config.js'
|
|
14
22
|
import { NodeInspectorFlagsNotSupportedError } from './lib/errors.js'
|
|
15
23
|
import { Runtime } from './lib/runtime.js'
|
|
16
24
|
import { schema } from './lib/schema.js'
|
|
@@ -59,7 +67,86 @@ function handleSignal (runtime, config) {
|
|
|
59
67
|
})
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
/*
|
|
71
|
+
v4 configuration is code, evaluated by the loader in foundation: it walks to the deciding file,
|
|
72
|
+
resolves both views of the env ladder, evaluates the root in a worker and fans out one worker per
|
|
73
|
+
per-app file. Everything the runtime transform used to discover — the application list, each
|
|
74
|
+
application's capability, its schema and its worker environment — arrives already resolved.
|
|
75
|
+
|
|
76
|
+
Routing by filename keeps the v3 path intact while the in-tree JSON fixtures are converted; the
|
|
77
|
+
v3 half is what a later commit deletes, not something this one has to keep working around.
|
|
78
|
+
*/
|
|
79
|
+
/*
|
|
80
|
+
A configuration named outright does not have to carry one of the four discoverable names -- that
|
|
81
|
+
is what discovery is for, and --config exists precisely to point at something discovery would not
|
|
82
|
+
find. What decides the dialect is the extension: v4 configuration is code, and v3 configuration is
|
|
83
|
+
a document.
|
|
84
|
+
*/
|
|
85
|
+
function isV4ConfigurationPath (path) {
|
|
86
|
+
return isConfigurationFileName(basename(path)) || /\.(js|mjs|ts|mts)$/.test(path)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function findV4ConfigurationFile (configOrRoot, sourceOrConfig) {
|
|
90
|
+
// A programmatic object source is not this path: the v4 object entry point is
|
|
91
|
+
// loadObjectConfiguration, which skips the root eval worker entirely.
|
|
92
|
+
if (sourceOrConfig && typeof sourceOrConfig !== 'string') {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof sourceOrConfig === 'string') {
|
|
97
|
+
const named = resolvePath(configOrRoot, sourceOrConfig)
|
|
98
|
+
|
|
99
|
+
return isV4ConfigurationPath(named) ? named : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof configOrRoot !== 'string') {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isV4ConfigurationPath(configOrRoot)) {
|
|
107
|
+
return configOrRoot
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
Only an actual directory is a place to search from. findDecidingFile walks toward the env root,
|
|
112
|
+
so handing it a path that does not exist would answer with whatever project sits above it: a
|
|
113
|
+
typo'd --config would boot the parent rather than failing. v3 looks in the named directory and
|
|
114
|
+
nowhere else, and a wrong path has to stay an error.
|
|
115
|
+
*/
|
|
116
|
+
const stats = await stat(configOrRoot).catch(() => null)
|
|
117
|
+
|
|
118
|
+
if (!stats?.isDirectory()) {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const deciding = await findDecidingFile(configOrRoot, { throwOnMissing: false })
|
|
124
|
+
|
|
125
|
+
return deciding?.path ?? null
|
|
126
|
+
} catch (error) {
|
|
127
|
+
/*
|
|
128
|
+
The walk refuses a v3 configuration wherever it consults, which is right once v3 is gone and
|
|
129
|
+
wrong while it is still supported: here a v3 file means "this project is not v4", and the v3
|
|
130
|
+
loader below is the one that should answer. Deleting this catch is part of deleting the v3
|
|
131
|
+
path, and at that point the refusal becomes the correct answer again.
|
|
132
|
+
*/
|
|
133
|
+
if (error.code === 'PLT_LEGACY_CONFIGURATION_FILE') {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw error
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
62
141
|
export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
|
|
142
|
+
// Checked before the v3 resolver, which throws when it finds no v3 file — a v4-only project has
|
|
143
|
+
// none by construction.
|
|
144
|
+
const v4ConfigurationFile = await findV4ConfigurationFile(configOrRoot, sourceOrConfig)
|
|
145
|
+
|
|
146
|
+
if (v4ConfigurationFile) {
|
|
147
|
+
return loadV4RuntimeConfiguration(v4ConfigurationFile, context)
|
|
148
|
+
}
|
|
149
|
+
|
|
63
150
|
const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'runtime')
|
|
64
151
|
|
|
65
152
|
// First of all, load the configuration without any validation
|
|
@@ -81,7 +168,7 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
|
|
|
81
168
|
|
|
82
169
|
return utilsLoadConfiguration(source, context?.schema ?? schema, {
|
|
83
170
|
validationOptions,
|
|
84
|
-
transform,
|
|
171
|
+
transform: v3Transform,
|
|
85
172
|
upgrade,
|
|
86
173
|
replaceEnv: true,
|
|
87
174
|
root,
|
|
@@ -89,6 +176,86 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
|
|
|
89
176
|
})
|
|
90
177
|
}
|
|
91
178
|
|
|
179
|
+
async function loadV4RuntimeConfiguration (configurationFile, context) {
|
|
180
|
+
const production = context?.isProduction ?? context?.production ?? false
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
The environment the loader treats as real. An embedder can add to it, and can ask for a
|
|
184
|
+
hermetic runtime that sees none of this process's -- v3 honours both through loadEnv, and a v4
|
|
185
|
+
runtime that ignored them would quietly hand every worker the parent's environment.
|
|
186
|
+
*/
|
|
187
|
+
const realEnv = context?.ignoreProcessEnv ? { ...context?.env } : { ...process.env, ...context?.env }
|
|
188
|
+
|
|
189
|
+
const loaded = await loadV4Configuration({
|
|
190
|
+
cwd: dirname(configurationFile),
|
|
191
|
+
configPath: configurationFile,
|
|
192
|
+
realEnv,
|
|
193
|
+
command: context?.command ?? (production ? 'start' : 'dev'),
|
|
194
|
+
mode: context?.mode,
|
|
195
|
+
production,
|
|
196
|
+
customEnvFile: context?.envFile,
|
|
197
|
+
/*
|
|
198
|
+
Validation imports each capability's schema, so it needs the capability installed — not
|
|
199
|
+
merely declared, which is all the detector needs. That moves the moment a capability must be
|
|
200
|
+
resolvable earlier than v3 ever needed it: v3 loaded fine and failed later, in the worker.
|
|
201
|
+
|
|
202
|
+
A caller that is loading only to discover the topology — a tool about to install those
|
|
203
|
+
dependencies, for instance — can say so rather than being made to install first.
|
|
204
|
+
*/
|
|
205
|
+
validateCapabilities: context?.validateCapabilities ?? true,
|
|
206
|
+
// The runtime is where the bundled capability copies live, so it is the fallback scope for
|
|
207
|
+
// both the schema import and the version stamp — the application's own dependencies still
|
|
208
|
+
// come first.
|
|
209
|
+
runtimeScope: import.meta.filename,
|
|
210
|
+
onWarning: warning => abstractLogger.warn(warning.message),
|
|
211
|
+
onInfo: info => abstractLogger.info(info.message)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const config = loaded.config
|
|
215
|
+
|
|
216
|
+
/*
|
|
217
|
+
The documented pipeline is validation with useDefaults, then kMetadata, then transform. The eval
|
|
218
|
+
worker's own check is a shape check that injects nothing — deliberately, so the resolve
|
|
219
|
+
projection carries authored values — so this is where the runtime schema's defaults arrive, and
|
|
220
|
+
without it the runtime reaches for settings like gracefulShutdown that nothing supplied.
|
|
221
|
+
*/
|
|
222
|
+
validateCapabilityConfiguration(config, context?.schema ?? schema, {
|
|
223
|
+
id: 'runtime',
|
|
224
|
+
module: '@platformatic/runtime',
|
|
225
|
+
root: loaded.root
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// kMetadata is symbol-keyed and cannot cross a worker boundary, so each worker rebuilds it; this
|
|
229
|
+
// is the main process's copy. env is the runtime's own view — every application worker gets the
|
|
230
|
+
// per-application environment the loader resolved, not this one.
|
|
231
|
+
config[kMetadata] = {
|
|
232
|
+
root: loaded.root,
|
|
233
|
+
path: loaded.configPath,
|
|
234
|
+
env: process.env,
|
|
235
|
+
module: '@platformatic/runtime',
|
|
236
|
+
/*
|
|
237
|
+
The evaluation context, kept beside the envelope rather than inside it: applications added
|
|
238
|
+
after boot are evaluated with the same command, mode and production flags the boot pass used,
|
|
239
|
+
and its presence is also what tells the add path this runtime is v4 at all. A v3 runtime has
|
|
240
|
+
no such context and keeps resolving configuration files worker-side.
|
|
241
|
+
*/
|
|
242
|
+
v4: {
|
|
243
|
+
command: context?.command ?? (production ? 'start' : 'dev'),
|
|
244
|
+
mode: loaded.mode,
|
|
245
|
+
production: loaded.production
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/*
|
|
250
|
+
A caller-supplied transform replaces the built-in one, exactly as it does on the v3 path where
|
|
251
|
+
it arrives as part of ...context. Ignoring it here meant a test helper or an embedder that
|
|
252
|
+
customized the configuration was silently not consulted.
|
|
253
|
+
*/
|
|
254
|
+
const apply = context?.transform ?? transformV4
|
|
255
|
+
|
|
256
|
+
return apply(config, null, context)
|
|
257
|
+
}
|
|
258
|
+
|
|
92
259
|
export async function loadApplicationsCommands (executableName = '', configurationFile = null) {
|
|
93
260
|
const applications = {}
|
|
94
261
|
const commands = {}
|
|
@@ -123,8 +290,16 @@ export async function loadApplicationsCommands (executableName = '', configurati
|
|
|
123
290
|
|
|
124
291
|
for (const application of config.applications) {
|
|
125
292
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
293
|
+
/*
|
|
294
|
+
A v4 application arrives with its configuration already evaluated and its capability
|
|
295
|
+
already named, so there is nothing to read and nothing to infer. Reading application.config
|
|
296
|
+
as a path threw into the catch below, which skipped the application silently -- the command
|
|
297
|
+
it contributes then simply did not exist, and the CLI said the command was unknown.
|
|
298
|
+
*/
|
|
299
|
+
const applicationConfig = application.resolvedConfig ?? (await utilsLoadConfiguration(application.config))
|
|
300
|
+
const pkg = application.module
|
|
301
|
+
? await importCapabilityPackage(application.path, application.module)
|
|
302
|
+
: await loadConfigurationModule(application.path, applicationConfig)
|
|
128
303
|
|
|
129
304
|
if (pkg.createCommands) {
|
|
130
305
|
const definition = await pkg.createCommands(application.id)
|
|
@@ -175,7 +350,17 @@ export async function create (configOrRoot, sourceOrConfig, context) {
|
|
|
175
350
|
return runtime
|
|
176
351
|
}
|
|
177
352
|
|
|
178
|
-
export { prepareApplication,
|
|
353
|
+
export { prepareApplication, wrapInRuntimeConfig } from './lib/config.js'
|
|
354
|
+
|
|
355
|
+
/*
|
|
356
|
+
The exported transform dispatches on the dialect. Callers wrap it -- they call it and then adjust
|
|
357
|
+
the result -- and a wrapper that reached the v3 transform with a v4 configuration would run the
|
|
358
|
+
wrong pipeline over it: autoload expansion and enabled filtering have already happened in the
|
|
359
|
+
eval worker. v4 configurations are recognizable by the evaluation context on kMetadata.
|
|
360
|
+
*/
|
|
361
|
+
export async function transform (config, schema, context) {
|
|
362
|
+
return config?.[kMetadata]?.v4 ? transformV4(config, schema, context) : v3Transform(config, schema, context)
|
|
363
|
+
}
|
|
179
364
|
export * as errors from './lib/errors.js'
|
|
180
365
|
export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
|
|
181
366
|
export { setupLoopbackMessaging } from './lib/loopback-messaging.js'
|
package/lib/config.js
CHANGED
|
@@ -11,18 +11,22 @@ import {
|
|
|
11
11
|
} from '@platformatic/foundation'
|
|
12
12
|
import { readdir, readFile } from 'node:fs/promises'
|
|
13
13
|
import { createRequire } from 'node:module'
|
|
14
|
+
import { loadAdditionalApplications } from '@platformatic/foundation/lib/v4/index.js'
|
|
14
15
|
import { isAbsolute, join, resolve as resolvePath } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
15
17
|
|
|
16
18
|
import {
|
|
17
19
|
InspectAndInspectBrkError,
|
|
18
20
|
InspectorHostError,
|
|
19
21
|
InspectorPortError,
|
|
20
|
-
InvalidArgumentError
|
|
21
|
-
InvalidEntrypointError
|
|
22
|
+
InvalidArgumentError
|
|
22
23
|
} from './errors.js'
|
|
23
24
|
import { schema } from './schema.js'
|
|
24
25
|
import { upgrade } from './upgrade.js'
|
|
25
26
|
|
|
27
|
+
// The runtime package's own entry point, which is where the bundled capability copies live.
|
|
28
|
+
const runtimeScopePath = fileURLToPath(new URL('../index.js', import.meta.url))
|
|
29
|
+
|
|
26
30
|
// Validate and coerce workers values early to avoid runtime hangs when invalid
|
|
27
31
|
function coercePositiveInteger (value) {
|
|
28
32
|
if (typeof value === 'number') {
|
|
@@ -141,17 +145,6 @@ export async function wrapInRuntimeConfig (config, context) {
|
|
|
141
145
|
// on purpose, the package.json might be missing
|
|
142
146
|
}
|
|
143
147
|
|
|
144
|
-
// Carry over only the server properties that the user actually set, so
|
|
145
|
-
// we do not end up with a `{ hostname: undefined, ... }` object that then
|
|
146
|
-
// gets populated by schema defaults or trips truthy checks downstream.
|
|
147
|
-
const server = {}
|
|
148
|
-
if (config.server) {
|
|
149
|
-
for (const key of ['hostname', 'port', 'http2', 'https']) {
|
|
150
|
-
if (config.server[key] !== undefined) {
|
|
151
|
-
server[key] = config.server[key]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
148
|
const production = context?.isProduction ?? context?.production
|
|
156
149
|
|
|
157
150
|
const runtimeConfig = config.runtime ?? {}
|
|
@@ -160,10 +153,8 @@ export async function wrapInRuntimeConfig (config, context) {
|
|
|
160
153
|
/* c8 ignore next */
|
|
161
154
|
const wrapped = {
|
|
162
155
|
$schema: schema.$id,
|
|
163
|
-
...(Object.keys(server).length > 0 ? { server } : {}),
|
|
164
156
|
watch: !production,
|
|
165
157
|
...omitProperties(runtimeConfig, runtimeUnwrappablePropertiesList),
|
|
166
|
-
entrypoint: applicationId,
|
|
167
158
|
applications: [
|
|
168
159
|
{
|
|
169
160
|
id: applicationId,
|
|
@@ -226,6 +217,49 @@ export function parseInspectorOptions (config, inspect, inspectBreak) {
|
|
|
226
217
|
config.watch = false
|
|
227
218
|
}
|
|
228
219
|
|
|
220
|
+
/*
|
|
221
|
+
The one entry point for applications added while the runtime is running, shared by the HTTP route
|
|
222
|
+
and the management ITC handler so the two cannot drift.
|
|
223
|
+
|
|
224
|
+
Under v4 an added application is evaluated exactly as boot evaluates one -- its configuration
|
|
225
|
+
file is found in its own directory, its environment is resolved main-side, and it arrives with
|
|
226
|
+
resolvedConfig. Skipping that leaves the worker to look for a v3 file name that v4 never writes,
|
|
227
|
+
which fails as an unhelpful "unable to initialize the worker".
|
|
228
|
+
*/
|
|
229
|
+
export async function prepareAddedApplications (config, entries, existingIds = []) {
|
|
230
|
+
const metadata = config[kMetadata]
|
|
231
|
+
|
|
232
|
+
if (!metadata?.v4) {
|
|
233
|
+
const prepared = []
|
|
234
|
+
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
prepared.push(await prepareApplication(config, entry, config.workers))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return prepared
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { applications } = await loadAdditionalApplications({
|
|
243
|
+
configPath: metadata.path,
|
|
244
|
+
entries,
|
|
245
|
+
existingIds,
|
|
246
|
+
rootEnvBlock: config.env,
|
|
247
|
+
command: metadata.v4.command,
|
|
248
|
+
mode: metadata.v4.mode,
|
|
249
|
+
production: metadata.v4.production,
|
|
250
|
+
// The runtime is the fallback scope for capability resolution, the same as at boot.
|
|
251
|
+
runtimeScope: runtimeScopePath
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const prepared = []
|
|
255
|
+
|
|
256
|
+
for (const application of applications) {
|
|
257
|
+
prepared.push(await prepareV4Application(config, application, config.workers))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return prepared
|
|
261
|
+
}
|
|
262
|
+
|
|
229
263
|
export async function prepareApplication (config, application, defaultWorkers) {
|
|
230
264
|
// We need to have absolute paths here, ot the `loadConfig` will fail
|
|
231
265
|
// Make sure we don't resolve if env var was not replaced
|
|
@@ -247,10 +281,10 @@ export async function prepareApplication (config, application, defaultWorkers) {
|
|
|
247
281
|
let pkg
|
|
248
282
|
|
|
249
283
|
if (application.config) {
|
|
250
|
-
const
|
|
251
|
-
pkg = await loadConfigurationModule(application.path,
|
|
284
|
+
const applicationConfig = await loadConfiguration(application.config)
|
|
285
|
+
pkg = await loadConfigurationModule(application.path, applicationConfig)
|
|
252
286
|
|
|
253
|
-
application.type = extractModuleFromSchemaUrl(
|
|
287
|
+
application.type = extractModuleFromSchemaUrl(applicationConfig, true).module
|
|
254
288
|
application.skipTelemetryHooks = pkg.skipTelemetryHooks
|
|
255
289
|
} else {
|
|
256
290
|
const { moduleName, capability } = await importCapabilityAndConfig(application.path)
|
|
@@ -277,10 +311,16 @@ export async function prepareApplication (config, application, defaultWorkers) {
|
|
|
277
311
|
}
|
|
278
312
|
}
|
|
279
313
|
|
|
314
|
+
return finalizeApplication(config, application, defaultWorkers)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Everything an application needs regardless of how its capability was determined. v3 discovers
|
|
318
|
+
// that by loading the application's config file; v4 has it from the loader's envelope before any
|
|
319
|
+
// worker exists.
|
|
320
|
+
export function finalizeApplication (config, application, defaultWorkers) {
|
|
280
321
|
// Validate and coerce per-service workers
|
|
281
322
|
parseWorkers(application, `Service "${application.id}"`, defaultWorkers)
|
|
282
323
|
|
|
283
|
-
application.entrypoint = application.id === config.entrypoint
|
|
284
324
|
application.dependencies ??= []
|
|
285
325
|
application.localUrl = `http://${application.id}.plt.local`
|
|
286
326
|
|
|
@@ -295,6 +335,39 @@ export async function prepareApplication (config, application, defaultWorkers) {
|
|
|
295
335
|
return application
|
|
296
336
|
}
|
|
297
337
|
|
|
338
|
+
/*
|
|
339
|
+
The v4 preparation. The loader resolved the path, selected the capability and imported its schema
|
|
340
|
+
subpath — which carries skipTelemetryHooks and modulesToLoad — all main-side, before any worker
|
|
341
|
+
existed. So there is nothing to discover here, and in particular no application config file to
|
|
342
|
+
re-read: that is the whole point of evaluating configuration exactly once per load.
|
|
343
|
+
*/
|
|
344
|
+
export async function prepareV4Application (config, application, defaultWorkers) {
|
|
345
|
+
// resolvedConfig replaces v3's config file path in workerData: the worker receives the validated
|
|
346
|
+
// capability payload as data and never re-reads a file.
|
|
347
|
+
application.resolvedConfig = application.config ?? {}
|
|
348
|
+
application.config = undefined
|
|
349
|
+
application.type = application.module ?? 'unknown'
|
|
350
|
+
application.skipTelemetryHooks = application.capabilityMetadata?.skipTelemetryHooks ?? false
|
|
351
|
+
|
|
352
|
+
// This is needed to work around a Rust bug on dylibs, as in the v3 path.
|
|
353
|
+
const modulesToLoad = application.capabilityMetadata?.modulesToLoad ?? []
|
|
354
|
+
|
|
355
|
+
if (modulesToLoad.length > 0 && application.path) {
|
|
356
|
+
const _require = createRequire(application.path)
|
|
357
|
+
|
|
358
|
+
for (const m of modulesToLoad) {
|
|
359
|
+
try {
|
|
360
|
+
loadModule(_require, _require.resolve(m)).catch(() => {})
|
|
361
|
+
} catch {
|
|
362
|
+
// A module the capability names but the application cannot resolve is not fatal here; the
|
|
363
|
+
// worker reports it with its own context when it actually needs it.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return finalizeApplication(config, application, defaultWorkers)
|
|
369
|
+
}
|
|
370
|
+
|
|
298
371
|
function isApplicationEnabled (application, environment) {
|
|
299
372
|
const { enabled } = application
|
|
300
373
|
|
|
@@ -357,7 +430,7 @@ export async function transform (config, _, context) {
|
|
|
357
430
|
}
|
|
358
431
|
|
|
359
432
|
const scaleConfig = config.verticalScaler.applications[appId]
|
|
360
|
-
const workersConfig = appConfig.workers
|
|
433
|
+
const workersConfig = appConfig.workers ??= {}
|
|
361
434
|
|
|
362
435
|
workersConfig.minimum ??= scaleConfig.minWorkers ?? config.workers.minimum
|
|
363
436
|
workersConfig.maximum ??= scaleConfig.maxWorkers ?? config.workers.maximum
|
|
@@ -416,54 +489,22 @@ export async function transform (config, _, context) {
|
|
|
416
489
|
}
|
|
417
490
|
}
|
|
418
491
|
|
|
492
|
+
return finalizeConfiguration(config, applications, context, production, prepareApplication)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// The half of the transform the loader does not own: inspector options, worker counts, the
|
|
496
|
+
// per-application preparation, and the runtime-level normalizations. v4 reaches this directly,
|
|
497
|
+
// having already expanded autoload and resolved enabled in the eval worker.
|
|
498
|
+
export async function finalizeConfiguration (config, applications, context, production, prepare) {
|
|
419
499
|
config.inspectorOptions = undefined
|
|
420
500
|
parseInspectorOptions(config, context?.inspect, context?.inspectBreak)
|
|
421
501
|
|
|
422
|
-
let hasValidEntrypoint = false
|
|
423
|
-
|
|
424
502
|
// Root-level workers
|
|
425
503
|
parseWorkers(config, 'Runtime', { static: 1, dynamic: false })
|
|
426
504
|
const defaultWorkers = config.workers
|
|
427
505
|
|
|
428
506
|
for (let i = 0; i < applications.length; ++i) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (application.entrypoint) {
|
|
432
|
-
hasValidEntrypoint = true
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// If there is no entrypoint, autodetect one
|
|
437
|
-
if (!config.entrypoint) {
|
|
438
|
-
// If there is only one application, it becomes the entrypoint
|
|
439
|
-
if (applications.length === 1) {
|
|
440
|
-
applications[0].entrypoint = true
|
|
441
|
-
config.entrypoint = applications[0].id
|
|
442
|
-
hasValidEntrypoint = true
|
|
443
|
-
} else {
|
|
444
|
-
// Search if exactly application uses @platformatic/gateway
|
|
445
|
-
const gateways = []
|
|
446
|
-
|
|
447
|
-
for (const application of applications) {
|
|
448
|
-
if (!application.config) {
|
|
449
|
-
continue
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (application.type === '@platformatic/gateway') {
|
|
453
|
-
gateways.push(application.id)
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (gateways.length === 1) {
|
|
458
|
-
applications.find(s => s.id === gateways[0]).entrypoint = true
|
|
459
|
-
config.entrypoint = gateways[0]
|
|
460
|
-
hasValidEntrypoint = true
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (!hasValidEntrypoint && config.entrypoint && !context.allowMissingEntrypoint) {
|
|
466
|
-
throw new InvalidEntrypointError(config.entrypoint)
|
|
507
|
+
await prepare(config, applications[i], defaultWorkers)
|
|
467
508
|
}
|
|
468
509
|
|
|
469
510
|
if (typeof config.metrics === 'boolean') {
|
|
@@ -503,3 +544,27 @@ export async function transform (config, _, context) {
|
|
|
503
544
|
|
|
504
545
|
return config
|
|
505
546
|
}
|
|
547
|
+
|
|
548
|
+
/*
|
|
549
|
+
The v4 transform. What is absent is the point:
|
|
550
|
+
|
|
551
|
+
- no alias merging, because `services` and `web` do not exist in v4;
|
|
552
|
+
- no autoload expansion and no `enabled` filtering, because both ran in the root eval worker,
|
|
553
|
+
which is the only place either runs. Leaving them reachable here would let a second expansion
|
|
554
|
+
re-merge entries and read the filesystem after the authoritative snapshot already exists;
|
|
555
|
+
- no verticalScaler migration, which the schema audit removes.
|
|
556
|
+
|
|
557
|
+
The `autoload` declaration survives as data beside the expanded list — `GET /metadata` reports it
|
|
558
|
+
and `applications:add --save` needs it to decide whether a new application belongs in a mapping
|
|
559
|
+
or as an explicit entry. It is carried, never re-executed.
|
|
560
|
+
*/
|
|
561
|
+
export async function transformV4 (config, _, context) {
|
|
562
|
+
const production = context?.isProduction ?? context?.production
|
|
563
|
+
const applications = config.applications ?? []
|
|
564
|
+
|
|
565
|
+
if (typeof config.watch === 'undefined') {
|
|
566
|
+
config.watch = !production
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return finalizeConfiguration(config, applications, context, production, prepareV4Application)
|
|
570
|
+
}
|