@platformatic/runtime 3.72.0 → 4.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/index.d.ts CHANGED
@@ -7,10 +7,15 @@ import { JSONSchemaType } from 'ajv'
7
7
  import * as colorette from 'colorette'
8
8
  import { EventEmitter } from 'node:events'
9
9
  import { Logger } from 'pino'
10
- import { PlatformaticRuntimeConfig } from './config.js'
10
+ import { MetricsOptions, PlatformaticRuntimeConfig } from './config.js'
11
11
 
12
12
  export type RuntimeConfiguration = Configuration<PlatformaticRuntimeConfig>
13
13
 
14
+ export interface MetricsConfigUpdateResult {
15
+ success: boolean
16
+ config: MetricsOptions
17
+ }
18
+
14
19
  export type ApplicationCommandContext = {
15
20
  colorette: typeof colorette
16
21
  parseArgs: typeof parseArgs
@@ -37,7 +42,11 @@ export interface LoopbackMessagingOptions {
37
42
  }
38
43
 
39
44
  export namespace errors {
45
+ export const AddressInUseError: (port: number, firstApplication: string, secondApplication: string) => FastifyError
46
+ export const WorkerAddressInUseError: (port: number, application: string) => FastifyError
40
47
  export const RuntimeExitedError: () => FastifyError
48
+ export const MetricsDisabledError: () => FastifyError
49
+ export const FailedToUpdateMetricsConfigError: (worker: string, reason: string) => FastifyError
41
50
  export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
42
51
  export const ApplicationNotFoundError: (id: string) => FastifyError
43
52
  export const ApplicationNotStartedError: (id: string) => FastifyError
@@ -46,8 +55,6 @@ export namespace errors {
46
55
  export const RuntimeNotStartedError: () => FastifyError
47
56
  export const ConfigPathMustBeStringError: () => FastifyError
48
57
  export const NoConfigFileFoundError: (id: string) => FastifyError
49
- export const InvalidEntrypointError: (entrypoint: string) => FastifyError
50
- export const MissingEntrypointError: () => FastifyError
51
58
  export const MissingDependencyError: (dependency: string) => FastifyError
52
59
  export const InspectAndInspectBrkError: () => FastifyError
53
60
  export const InspectorPortError: () => FastifyError
@@ -86,9 +93,9 @@ export namespace symbols {
86
93
  export const kHealthCheckTimer: unique symbol
87
94
  export const kHealthMetricsTimer: unique symbol
88
95
  export const kLastHealthCheckELU: unique symbol
89
- export const kLastWorkerScalerELU: unique symbol
90
96
  export const kWorkerStatus: unique symbol
91
97
  export const kWorkerHealthSignals: unique symbol
98
+ export const kWorkerUrl: unique symbol
92
99
  export const kStderrMarker: string
93
100
  export const kWorkersBroadcast: unique symbol
94
101
  }
@@ -110,23 +117,29 @@ export interface InjectResponse {
110
117
  rawPayload: ArrayBuffer
111
118
  }
112
119
 
120
+ export type ServingState = 'listening' | 'mesh-only' | 'background' | 'inactive'
121
+
113
122
  export interface ApplicationDetails {
114
123
  id: string
115
124
  type?: string
125
+ /** v3 only: the path to the application's configuration file. v4 reports configPath. */
116
126
  config?: string
127
+ /** v4 only: the configuration file the loader decided on, absent for an inline definition. */
128
+ configPath?: string
117
129
  path?: string
118
130
  status?: string
131
+ /** How the application serves, as opposed to whether it runs. Absent when it is not started. */
132
+ servingState?: ServingState
119
133
  dependencies?: string[]
120
134
  version?: string
121
135
  localUrl?: string
122
- entrypoint?: boolean
123
136
  sourceMaps?: boolean
124
137
  workers?: number
125
138
  url?: string | null
139
+ urls?: string[]
126
140
  }
127
141
 
128
142
  export interface ApplicationsTopology {
129
- entrypoint: string
130
143
  production: boolean
131
144
  applications: ApplicationDetails[]
132
145
  }
@@ -157,14 +170,6 @@ export interface SchedulerJobBase {
157
170
  nextRunAt?: string | null
158
171
  }
159
172
 
160
- export interface ConfiguredSchedulerJob extends SchedulerJobBase {
161
- source: 'config'
162
- callbackUrl: string
163
- method: string
164
- headers?: Record<string, string>
165
- body?: string | Record<string, unknown>
166
- }
167
-
168
173
  export interface ApplicationSchedulerJob extends SchedulerJobBase {
169
174
  source: 'application'
170
175
  applicationId: string
@@ -172,7 +177,7 @@ export interface ApplicationSchedulerJob extends SchedulerJobBase {
172
177
  tasks: string[]
173
178
  }
174
179
 
175
- export type SchedulerJob = ConfiguredSchedulerJob | ApplicationSchedulerJob
180
+ export type SchedulerJob = ApplicationSchedulerJob
176
181
 
177
182
  export interface SchedulerRunResult {
178
183
  name: string
@@ -194,10 +199,14 @@ export interface RuntimeMetadata {
194
199
  execPath: string
195
200
  nodeVersion: string
196
201
  projectDir: string
202
+ /** The configuration file the loader decided on. Null when the runtime was built from an object. */
203
+ configPath: string | null
204
+ /** The autoload declaration, with its path resolved. Null when the project does not autoload. */
205
+ autoload: { path: string, exclude?: string[], mappings?: Record<string, unknown> } | null
197
206
  packageName: string | null
198
207
  packageVersion: string | null
199
- url: string | null
200
208
  platformaticVersion: string
209
+ urls: Record<string, string>
201
210
  }
202
211
 
203
212
  export interface WorkerLifecycleEvent {
@@ -234,9 +243,7 @@ export interface HealthMetricsEvent extends WorkerLifecycleEvent {
234
243
  healthSignals: RuntimeHealthSignal[]
235
244
  }
236
245
 
237
- export declare class ManagementClient {
238
- constructor (allowedOperations?: string[])
239
-
246
+ export interface ManagementClient {
240
247
  getRuntimeStatus (): Promise<string>
241
248
  getRuntimeMetadata (): Promise<RuntimeMetadata>
242
249
  getRuntimeConfig (): Promise<Record<string, unknown>>
@@ -258,7 +265,7 @@ export declare class ManagementClient {
258
265
  startApplication (id: string): Promise<void>
259
266
  stopApplication (id: string): Promise<void>
260
267
  restartApplication (id: string): Promise<void>
261
- restart (applications?: string[]): Promise<string>
268
+ restart (applications?: string[]): Promise<void>
262
269
  addApplications (applications: unknown[], start?: boolean): Promise<ApplicationDetails[]>
263
270
  removeApplications (ids: string[]): Promise<ApplicationDetails[]>
264
271
  inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
@@ -370,12 +377,11 @@ export type RuntimeExtension = (
370
377
 
371
378
  export declare class Runtime extends EventEmitter {
372
379
  init (): Promise<void>
373
- start (silent?: boolean): Promise<string | undefined>
380
+ start (silent?: boolean): Promise<Record<string, string>>
374
381
  stop (silent?: boolean): Promise<void>
375
382
  close (silent?: boolean): Promise<void>
376
- restart (applications?: string[]): Promise<string | undefined>
383
+ restart (applications?: string[]): Promise<void>
377
384
  inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
378
- getUrl (): string | undefined
379
385
  getRuntimeStatus (): string
380
386
  getRuntimeMetadata (): Promise<RuntimeMetadata>
381
387
  getRuntimeEnv (): Record<string, string>
@@ -386,6 +392,7 @@ export declare class Runtime extends EventEmitter {
386
392
  getRuntimeConfig (includeMeta: true): RuntimeConfiguration
387
393
  getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
388
394
  getApplicationsIds (): string[]
395
+ getUrls (applicationId?: string): Record<string, string>
389
396
  /**
390
397
  * Returns topology for every configured application.
391
398
  * When `allowUnloaded` is `true`, applications without running workers are
@@ -419,6 +426,7 @@ export declare class Runtime extends EventEmitter {
419
426
  getApplicationOpenapiSchema (id: string): Promise<unknown>
420
427
  getApplicationGraphqlSchema (id: string): Promise<unknown>
421
428
  getMetrics (format?: string): Promise<{ metrics: unknown }>
429
+ updateMetricsConfig (metricsConfig: Partial<MetricsOptions>): Promise<MetricsConfigUpdateResult>
422
430
  /**
423
431
  * Returns the current shared context. Synchronous on the main thread.
424
432
  * Do not mutate it in place — use `updateSharedContext()` so changes are
@@ -472,11 +480,6 @@ export declare class Runtime extends EventEmitter {
472
480
  runApplicationScheduledTasks (id: string, scheduleId: string, scheduledTime: number): Promise<unknown>
473
481
  }
474
482
 
475
- export function wrapInRuntimeConfig (
476
- config: Configuration<unknown>,
477
- context?: ConfigurationOptions
478
- ): Promise<RuntimeConfiguration>
479
-
480
483
  export declare const version: string
481
484
 
482
485
  export declare function loadConfiguration (
package/index.js CHANGED
@@ -1,20 +1,27 @@
1
- import { resolve, validationOptions } from '@platformatic/basic'
1
+ import { importCapabilityPackage, resolve } from '@platformatic/basic'
2
2
  import {
3
3
  abstractLogger,
4
4
  ensureLoggableError,
5
- extractModuleFromSchemaUrl,
6
5
  findRuntimeConfigurationFile,
7
6
  kMetadata,
8
- loadConfigurationModule,
9
- loadConfiguration as utilsLoadConfiguration
7
+ loadConfigurationModule
10
8
  } from '@platformatic/foundation'
9
+ import {
10
+ findDecidingFile,
11
+ isProductionCommand,
12
+ LegacyConfigurationFileError,
13
+ loadConfiguration as loadFileConfiguration,
14
+ loadObjectConfiguration,
15
+ validateCapabilityConfiguration
16
+ } from '@platformatic/foundation/loader'
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 { dirname, resolve as resolvePath } from 'node:path'
21
+ import { transformConfiguration } from './lib/config.js'
14
22
  import { NodeInspectorFlagsNotSupportedError } from './lib/errors.js'
15
23
  import { Runtime } from './lib/runtime.js'
16
- import { schema } from './lib/schema.js'
17
- import { upgrade } from './lib/upgrade.js'
24
+ import { configurationSchema } from './lib/schema.js'
18
25
 
19
26
  async function restartRuntime (runtime) {
20
27
  runtime.logger.info('Received SIGUSR2, restarting all applications ...')
@@ -59,34 +66,264 @@ function handleSignal (runtime, config) {
59
66
  })
60
67
  }
61
68
 
69
+ /*
70
+ The configuration is code, evaluated by the loader in foundation: it walks to the deciding file,
71
+ resolves both views of the env ladder, evaluates the root in a worker and fans out one worker per
72
+ per-app file. Everything the runtime transform used to discover — the application list, each
73
+ application's capability, its schema and its worker environment — arrives already resolved.
74
+
75
+ Routing by filename keeps the legacy path intact while the in-tree JSON fixtures are converted; the
76
+ legacy half is what a later commit deletes, not something this one has to keep working around.
77
+ */
78
+ /*
79
+ `--config` names a configuration; it does not widen what a configuration may be called. The four
80
+ names are the format, and `resolveNamedConfigurationFile` is what refuses anything else -- it
81
+ separates a legacy name, which gets the migrate hint, from an unrecognised one, which gets told
82
+ what the four names are. Deciding here instead produced one of those answers for both.
83
+
84
+ What `--config` does change is *where*: a named file need not sit where discovery would look.
85
+ */
86
+ function isNamedConfigurationPath (path) {
87
+ return /\.(json|json5|ya?ml|to?ml|js|mjs|ts|mts)$/.test(path)
88
+ }
89
+
90
+ async function findConfigurationForSource (configOrRoot, sourceOrConfig) {
91
+ // A programmatic object source is not this path: the object entry point is
92
+ // loadObjectConfiguration, which skips the root eval worker entirely.
93
+ if (sourceOrConfig && typeof sourceOrConfig !== 'string') {
94
+ return null
95
+ }
96
+
97
+ if (typeof sourceOrConfig === 'string') {
98
+ const named = resolvePath(configOrRoot, sourceOrConfig)
99
+
100
+ // Named outright: the loader decides whether the name is one it accepts, and says which it is.
101
+ return isNamedConfigurationPath(named) ? named : null
102
+ }
103
+
104
+ if (typeof configOrRoot !== 'string') {
105
+ return null
106
+ }
107
+
108
+ if (isNamedConfigurationPath(configOrRoot)) {
109
+ return configOrRoot
110
+ }
111
+
112
+ /*
113
+ Only an actual directory is a place to search from. findDecidingFile walks toward the env root,
114
+ so handing it a path that does not exist would answer with whatever project sits above it: a
115
+ typo'd --config would boot the parent rather than failing. Discovery looks in the named directory
116
+ and nowhere else, and a wrong path has to stay an error.
117
+ */
118
+ const stats = await stat(configOrRoot).catch(() => null)
119
+
120
+ if (!stats?.isDirectory()) {
121
+ return null
122
+ }
123
+
124
+ /*
125
+ A legacy configuration found by the walk is refused, with the error that names migrate. It used to
126
+ be swallowed so the legacy loader below could answer instead; that loader is gone, and the refusal
127
+ is the correct answer again.
128
+ */
129
+ const deciding = await findDecidingFile(configOrRoot, { throwOnMissing: false })
130
+
131
+ return deciding?.path ?? null
132
+ }
133
+
62
134
  export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
135
+ // Checked before the legacy resolver, which throws when it finds no legacy file — a project has
136
+ // none by construction.
137
+ const decidingConfigurationFile = await findConfigurationForSource(configOrRoot, sourceOrConfig)
138
+
139
+ if (decidingConfigurationFile) {
140
+ return loadRuntimeConfiguration(decidingConfigurationFile, context)
141
+ }
142
+
143
+ /*
144
+ Level 0. A directory with no configuration file of any kind is not an error: the loader
145
+ synthesizes one in memory from what the directory contains. Reaching the legacy resolver instead
146
+ made a configless project fail to load, which is what the removed temporary-file fallback --
147
+ detect the type, write a watt.json into the user's tree, then load that -- existed to paper
148
+ over. Nothing is written to disk now.
149
+ */
150
+ if (!sourceOrConfig && typeof configOrRoot === 'string' && !isNamedConfigurationPath(configOrRoot)) {
151
+ const stats = await stat(configOrRoot).catch(() => null)
152
+
153
+ if (stats?.isDirectory()) {
154
+ return loadRuntimeConfiguration({ cwd: configOrRoot }, context)
155
+ }
156
+ }
157
+
63
158
  const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'runtime')
64
159
 
65
- // First of all, load the configuration without any validation
66
- const config = await utilsLoadConfiguration(source, null, {
67
- root,
68
- envFile: context?.envFile
69
- })
160
+ /*
161
+ A configuration handed over as an object rather than named as a file: what an embedder builds in
162
+ memory, and what the ICC generates. There is nothing to evaluate, so it skips the root eval
163
+ worker and joins the pipeline at validation.
164
+ */
165
+ if (source && typeof source !== 'string') {
166
+ return loadRuntimeConfiguration({ root, source }, context)
167
+ }
168
+
169
+ /*
170
+ A path that reaches here named a file the walk did not recognise as configuration, so the walk's
171
+ own refusal is the answer -- it names the file and tells the reader to run migrate, which is more
172
+ use than loading a legacy configuration into a runtime that no longer implements what it says.
173
+ */
174
+ throw new LegacyConfigurationFileError(source)
175
+ }
176
+
177
+ /*
178
+ `target` is either a configuration file to evaluate or `{ root, source }` for a configuration
179
+ handed over as an object -- what an embedder builds in memory and what the ICC generates. The two
180
+ differ only in where the configuration comes from: an object skips the root eval worker, since
181
+ there is nothing to evaluate, and everything after that is the same pipeline.
182
+ */
183
+ async function loadRuntimeConfiguration (target, context) {
184
+ /*
185
+ The command decides `production` when the caller did not, rather than the other way round:
186
+ `build` produces production artifacts, so a build that evaluated as a development boot read the
187
+ development env files and gave every callback `production: false` while writing what `start`
188
+ would later run.
189
+ */
190
+ const diagnostics = context?.logger ?? abstractLogger
191
+ const explicitProduction = context?.isProduction ?? context?.production
192
+ const command = context?.command ?? (explicitProduction ? 'start' : 'dev')
193
+ const production = explicitProduction ?? isProductionCommand(command)
194
+
195
+ /*
196
+ The environment the loader treats as real. An embedder can add to it, and can ask for a
197
+ hermetic runtime that sees none of this process's -- the loader honours both through loadEnv, and
198
+ a runtime that ignored them would quietly hand every worker the parent's environment.
199
+ */
200
+ const realEnv = context?.ignoreProcessEnv ? { ...context?.env } : { ...process.env, ...context?.env }
70
201
 
71
- if (config.envfile) {
72
- // The context is optional, so it might not have been provided at all
73
- context ??= {}
74
- context.envFile = config.envfile
202
+ const objectSource = typeof target !== 'string' && target.source !== undefined
203
+ const shared = {
204
+ realEnv,
205
+ command,
206
+ mode: context?.mode,
207
+ production,
208
+ customEnvFile: context?.envFile,
209
+ /*
210
+ Step 3 of the pipeline validates orchestration in the eval worker, before autoload expansion
211
+ acts on it -- without the schema that step is skipped, and a malformed `autoload` reaches the
212
+ filesystem walk as a raw TypeError instead of a validation error naming the property.
213
+ */
214
+ schema: context?.schema ?? configurationSchema,
215
+ /*
216
+ The evaluation deadline. A configuration that never resolves — an awaited fetch to a dead
217
+ host, a forgotten promise — otherwise hangs the boot rather than failing it, and the default
218
+ is a guess about how long a reasonable one takes. A deployment that knows better says so.
219
+ */
220
+ timeout: context?.configTimeout,
221
+ // The one file --debug-config evaluates in this process when an inspector is attached, so a
222
+ // breakpoint in it is reachable at all: a throwaway worker dies before one can attach.
223
+ inProcessTarget: context?.inProcessTarget,
224
+ /*
225
+ Validation imports each capability's schema, so it needs the capability installed — not
226
+ merely declared, which is all the detector needs. That moves the moment a capability must be
227
+ resolvable earlier than it once had to be: loading used to succeed and fail later, in the worker.
228
+
229
+ A caller that is loading only to discover the topology — a tool about to install those
230
+ dependencies, for instance — can say so rather than being made to install first.
231
+ */
232
+ validateCapabilities: context?.validateCapabilities ?? true,
233
+ // The runtime is where the bundled capability copies live, so it is the fallback scope for
234
+ // both the schema import and the version stamp — the application's own dependencies still
235
+ // come first.
236
+ runtimeScope: import.meta.filename,
237
+ /*
238
+ These are the loader's user-facing diagnostics — the standalone-boot warning, the zero-config
239
+ inference notice, the capability the detector chose — and they were being handed to
240
+ abstractLogger, which is a no-op. Every one of them was discarded, including the warning the
241
+ format relies on to tell someone that nothing the root says is being applied.
242
+
243
+ A caller with a real logger passes it. The default stays silent because an embedder loading a
244
+ configuration has not asked for anything on its stderr.
245
+ */
246
+ onWarning: warning => diagnostics.warn(warning.message),
247
+ onInfo: info => diagnostics.info(info.message)
75
248
  }
76
249
 
77
- const mod = extractModuleFromSchemaUrl(config)
78
- if (mod?.module !== '@platformatic/runtime') {
79
- return wrapInRuntimeConfig(config, context)
250
+ let loaded
251
+ if (objectSource) {
252
+ loaded = await loadObjectConfiguration({ root: target.root, source: target.source, ...shared })
253
+ } else if (typeof target === 'string') {
254
+ loaded = await loadFileConfiguration({ cwd: dirname(target), configPath: target, ...shared })
255
+ } else {
256
+ // No file was named and none was found: the loader searches from this directory and, finding
257
+ // nothing, synthesizes.
258
+ loaded = await loadFileConfiguration({ cwd: target.cwd, ...shared })
80
259
  }
81
260
 
82
- return utilsLoadConfiguration(source, context?.schema ?? schema, {
83
- validationOptions,
84
- transform,
85
- upgrade,
86
- replaceEnv: true,
87
- root,
88
- ...context
261
+ const config = loaded.config
262
+
263
+ /*
264
+ The documented pipeline is validation with useDefaults, then kMetadata, then transform. The eval
265
+ worker's own check is a shape check that injects nothing — deliberately, so the resolve
266
+ projection carries authored values — so this is where the runtime schema's defaults arrive, and
267
+ without it the runtime reaches for settings like gracefulShutdown that nothing supplied.
268
+ */
269
+ validateCapabilityConfiguration(config, context?.schema ?? configurationSchema, {
270
+ id: 'runtime',
271
+ module: '@platformatic/runtime',
272
+ root: loaded.root
89
273
  })
274
+
275
+ // kMetadata is symbol-keyed and cannot cross a worker boundary, so each worker rebuilds it; this
276
+ // is the main process's copy. env is the runtime's own view — the real environment under the root
277
+ // env block and the root's env files — and every application worker gets the per-application
278
+ // environment the loader resolved instead, which this one is not a substitute for.
279
+ config[kMetadata] = {
280
+ root: loaded.root,
281
+ path: loaded.configPath,
282
+ env: loaded.env ?? process.env,
283
+ module: '@platformatic/runtime',
284
+ /*
285
+ The evaluation context, kept beside the envelope rather than inside it: applications added
286
+ after boot are evaluated with the same command, mode and production flags the boot pass used,
287
+ and its presence is also what tells the add path this runtime resolves configuration main-side.
288
+ A legacy runtime has no such context and keeps resolving configuration files worker-side.
289
+ */
290
+ loader: {
291
+ command,
292
+ mode: loaded.mode,
293
+ production: loaded.production,
294
+ /*
295
+ The environment contract of the boot, carried so an application added while the runtime
296
+ runs is enveloped under the same one: the realEnv the boot resolved ladders against
297
+ (hermetic under ignoreProcessEnv, augmented by context.env) and the --env file that
298
+ replaces every application's env-file rung. Without them the add path defaulted to
299
+ process.env and the conventional .env chain -- an environment the boot says cannot exist.
300
+ */
301
+ realEnv,
302
+ customEnvFile: context?.envFile,
303
+ /*
304
+ Everything the evaluation actually read: the configuration files, their import graph minus
305
+ node_modules, the env files consulted, and the directories whose membership decides the
306
+ application list. `wattpm dev` reloads on any of it -- watching only the deciding file meant
307
+ a config split across a helper module reloaded for none of its own edits.
308
+ */
309
+ watchTargets: loaded.watchTargets,
310
+ /*
311
+ Every entry carrying a `url`, recorded on the way past -- before the enabled filter, which
312
+ is the point. `enabled` is the supported way to keep an application out of a boot, and an
313
+ entry it hides from `resolve` is one whose clone never arrives.
314
+ */
315
+ resolveCandidates: loaded.resolveCandidates
316
+ }
317
+ }
318
+
319
+ /*
320
+ A caller-supplied transform replaces the built-in one, exactly as it does on the legacy path where
321
+ it arrives as part of ...context. Ignoring it here meant a test helper or an embedder that
322
+ customized the configuration was silently not consulted.
323
+ */
324
+ const apply = context?.transform ?? transformConfiguration
325
+
326
+ return apply(config, null, context)
90
327
  }
91
328
 
92
329
  export async function loadApplicationsCommands (executableName = '', configurationFile = null) {
@@ -111,7 +348,13 @@ export async function loadApplicationsCommands (executableName = '', configurati
111
348
  throw new Error('No runtime configuration file found.')
112
349
  }
113
350
 
114
- config = await loadConfiguration(file)
351
+ /*
352
+ 'exec' is every non-boot evaluation, and enumerating a capability's commands is one: nothing
353
+ starts. Without it this evaluated as a development boot — development env files, and
354
+ `production: false` handed to every callback — which is a context no invocation of these
355
+ commands actually runs in.
356
+ */
357
+ config = await loadConfiguration(file, null, { command: 'exec' })
115
358
 
116
359
  /* c8 ignore next 3 - Hard to test */
117
360
  if (!config) {
@@ -123,8 +366,12 @@ export async function loadApplicationsCommands (executableName = '', configurati
123
366
 
124
367
  for (const application of config.applications) {
125
368
  try {
126
- const applicationConfig = await utilsLoadConfiguration(application.config)
127
- const pkg = await loadConfigurationModule(application.path, applicationConfig)
369
+ // An application arrives with its configuration already evaluated and its capability
370
+ // already named, so there is nothing to read and nothing to infer.
371
+ const applicationConfig = application.resolvedConfig
372
+ const pkg = application.module
373
+ ? await importCapabilityPackage(application.path, application.module)
374
+ : await loadConfigurationModule(application.path, applicationConfig)
128
375
 
129
376
  if (pkg.createCommands) {
130
377
  const definition = await pkg.createCommands(application.id)
@@ -175,7 +422,20 @@ export async function create (configOrRoot, sourceOrConfig, context) {
175
422
  return runtime
176
423
  }
177
424
 
178
- export { prepareApplication, transform, wrapInRuntimeConfig } from './lib/config.js'
425
+ /*
426
+ The one entry point for an application added while the runtime is running -- what an extension
427
+ calls before `addApplications`. It replaces the exported `prepareApplication`, which is the legacy
428
+ half of the same job: an added application has to be evaluated the way boot evaluates
429
+ one, and an entry that skips that arrives without `resolvedConfig` and fails as an unhelpful
430
+ "unable to initialize the worker".
431
+ */
432
+ export { prepareAddedApplications } from './lib/config.js'
433
+
434
+ // The one transform there is: the legacy one died with its last caller, the wizard's legacy-JSON
435
+ // branch, and the dispatch that told the two apart went with it.
436
+ export async function transform (config, schema, context) {
437
+ return transformConfiguration(config, schema, context)
438
+ }
179
439
  export * as errors from './lib/errors.js'
180
440
  export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
181
441
  export { setupLoopbackMessaging } from './lib/loopback-messaging.js'