@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 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?: string;
47
- useHttp?: boolean;
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 declare class ManagementClient {
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<string>
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 | undefined>
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<string | undefined>
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 { transform, wrapInRuntimeConfig } from './lib/config.js'
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
- const applicationConfig = await utilsLoadConfiguration(application.config)
127
- const pkg = await loadConfigurationModule(application.path, applicationConfig)
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, transform, wrapInRuntimeConfig } from './lib/config.js'
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 config = await loadConfiguration(application.config)
251
- pkg = await loadConfigurationModule(application.path, config)
284
+ const applicationConfig = await loadConfiguration(application.config)
285
+ pkg = await loadConfigurationModule(application.path, applicationConfig)
252
286
 
253
- application.type = extractModuleFromSchemaUrl(config, true).module
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
- const application = await prepareApplication(config, applications[i], defaultWorkers)
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
+ }