@platformatic/runtime 4.0.0-new-config.1 → 4.0.0-new-config.5

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
@@ -111,12 +111,19 @@ export interface InjectResponse {
111
111
  rawPayload: ArrayBuffer
112
112
  }
113
113
 
114
+ export type ServingState = 'listening' | 'mesh-only' | 'background' | 'inactive'
115
+
114
116
  export interface ApplicationDetails {
115
117
  id: string
116
118
  type?: string
119
+ /** v3 only: the path to the application's configuration file. v4 reports configPath. */
117
120
  config?: string
121
+ /** v4 only: the configuration file the loader decided on, absent for an inline definition. */
122
+ configPath?: string
118
123
  path?: string
119
124
  status?: string
125
+ /** How the application serves, as opposed to whether it runs. Absent when it is not started. */
126
+ servingState?: ServingState
120
127
  dependencies?: string[]
121
128
  version?: string
122
129
  localUrl?: string
@@ -194,6 +201,10 @@ export interface RuntimeMetadata {
194
201
  execPath: string
195
202
  nodeVersion: string
196
203
  projectDir: string
204
+ /** The configuration file the loader decided on. Null when the runtime was built from an object. */
205
+ configPath: string | null
206
+ /** The autoload declaration, with its path resolved. Null when the project does not autoload. */
207
+ autoload: { path: string, exclude?: string[], mappings?: Record<string, unknown> } | null
197
208
  packageName: string | null
198
209
  packageVersion: string | null
199
210
  platformaticVersion: string
@@ -468,11 +479,6 @@ export declare class Runtime extends EventEmitter {
468
479
  runApplicationScheduledTasks (id: string, scheduleId: string, scheduledTime: number): Promise<unknown>
469
480
  }
470
481
 
471
- export function wrapInRuntimeConfig (
472
- config: Configuration<unknown>,
473
- context?: ConfigurationOptions
474
- ): Promise<RuntimeConfiguration>
475
-
476
482
  export declare const version: string
477
483
 
478
484
  export declare function loadConfiguration (
package/index.js CHANGED
@@ -1,28 +1,27 @@
1
- import { importCapabilityPackage, 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'
11
9
  import {
12
- isConfigurationFileName,
13
10
  findDecidingFile,
11
+ isProductionCommand,
12
+ LegacyConfigurationFileError,
14
13
  loadConfiguration as loadV4Configuration,
14
+ loadObjectConfiguration as loadV4ObjectConfiguration,
15
15
  validateCapabilityConfiguration
16
16
  } from '@platformatic/foundation/lib/v4/index.js'
17
17
  import closeWithGrace from 'close-with-grace'
18
18
  import { stat } from 'node:fs/promises'
19
19
  import inspector from 'node:inspector'
20
- import { basename, dirname, resolve as resolvePath } from 'node:path'
21
- import { transform as v3Transform, transformV4, wrapInRuntimeConfig } from './lib/config.js'
20
+ import { dirname, resolve as resolvePath } from 'node:path'
21
+ import { transformV4 } from './lib/config.js'
22
22
  import { NodeInspectorFlagsNotSupportedError } from './lib/errors.js'
23
23
  import { Runtime } from './lib/runtime.js'
24
- import { schema } from './lib/schema.js'
25
- import { upgrade } from './lib/upgrade.js'
24
+ import { v4Schema } from './lib/schema.js'
26
25
 
27
26
  async function restartRuntime (runtime) {
28
27
  runtime.logger.info('Received SIGUSR2, restarting all applications ...')
@@ -77,13 +76,15 @@ function handleSignal (runtime, config) {
77
76
  v3 half is what a later commit deletes, not something this one has to keep working around.
78
77
  */
79
78
  /*
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.
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.
84
85
  */
85
- function isV4ConfigurationPath (path) {
86
- return isConfigurationFileName(basename(path)) || /\.(js|mjs|ts|mts)$/.test(path)
86
+ function isNamedConfigurationPath (path) {
87
+ return /\.(json|json5|ya?ml|to?ml|js|mjs|ts|mts)$/.test(path)
87
88
  }
88
89
 
89
90
  async function findV4ConfigurationFile (configOrRoot, sourceOrConfig) {
@@ -96,14 +97,15 @@ async function findV4ConfigurationFile (configOrRoot, sourceOrConfig) {
96
97
  if (typeof sourceOrConfig === 'string') {
97
98
  const named = resolvePath(configOrRoot, sourceOrConfig)
98
99
 
99
- return isV4ConfigurationPath(named) ? named : null
100
+ // Named outright: the loader decides whether the name is one it accepts, and says which it is.
101
+ return isNamedConfigurationPath(named) ? named : null
100
102
  }
101
103
 
102
104
  if (typeof configOrRoot !== 'string') {
103
105
  return null
104
106
  }
105
107
 
106
- if (isV4ConfigurationPath(configOrRoot)) {
108
+ if (isNamedConfigurationPath(configOrRoot)) {
107
109
  return configOrRoot
108
110
  }
109
111
 
@@ -119,23 +121,14 @@ async function findV4ConfigurationFile (configOrRoot, sourceOrConfig) {
119
121
  return null
120
122
  }
121
123
 
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
- }
124
+ /*
125
+ A v3 configuration found by the walk is refused, with the error that names migrate. It used to
126
+ be swallowed so the v3 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 })
136
130
 
137
- throw error
138
- }
131
+ return deciding?.path ?? null
139
132
  }
140
133
 
141
134
  export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
@@ -147,37 +140,58 @@ export async function loadConfiguration (configOrRoot, sourceOrConfig, context)
147
140
  return loadV4RuntimeConfiguration(v4ConfigurationFile, context)
148
141
  }
149
142
 
150
- const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'runtime')
151
-
152
- // First of all, load the configuration without any validation
153
- const config = await utilsLoadConfiguration(source, null, {
154
- root,
155
- envFile: context?.envFile
156
- })
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 v3 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)
157
152
 
158
- if (config.envfile) {
159
- // The context is optional, so it might not have been provided at all
160
- context ??= {}
161
- context.envFile = config.envfile
153
+ if (stats?.isDirectory()) {
154
+ return loadV4RuntimeConfiguration({ cwd: configOrRoot }, context)
155
+ }
162
156
  }
163
157
 
164
- const mod = extractModuleFromSchemaUrl(config)
165
- if (mod?.module !== '@platformatic/runtime') {
166
- return wrapInRuntimeConfig(config, context)
158
+ const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'runtime')
159
+
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 v4 pipeline at validation.
164
+ */
165
+ if (source && typeof source !== 'string') {
166
+ return loadV4RuntimeConfiguration({ root, source }, context)
167
167
  }
168
168
 
169
- return utilsLoadConfiguration(source, context?.schema ?? schema, {
170
- validationOptions,
171
- transform: v3Transform,
172
- upgrade,
173
- replaceEnv: true,
174
- root,
175
- ...context
176
- })
169
+ /*
170
+ Everything above this point is v4. A path that reaches here named a file the v4 walk did not
171
+ recognise as configuration, so the walk's own refusal is the answer -- it names the file and
172
+ tells the reader to run migrate, which is more use than loading a v3 configuration into a
173
+ runtime that no longer implements what it says.
174
+ */
175
+ throw new LegacyConfigurationFileError(source)
177
176
  }
178
177
 
179
- async function loadV4RuntimeConfiguration (configurationFile, context) {
180
- const production = context?.isProduction ?? context?.production ?? false
178
+ /*
179
+ `target` is either a configuration file to evaluate or `{ root, source }` for a configuration
180
+ handed over as an object -- what an embedder builds in memory and what the ICC generates. The two
181
+ differ only in where the configuration comes from: an object skips the root eval worker, since
182
+ there is nothing to evaluate, and everything after that is the same pipeline.
183
+ */
184
+ async function loadV4RuntimeConfiguration (target, context) {
185
+ /*
186
+ The command decides `production` when the caller did not, rather than the other way round:
187
+ `build` produces production artifacts, so a build that evaluated as a development boot read the
188
+ development env files and gave every callback `production: false` while writing what `start`
189
+ would later run.
190
+ */
191
+ const diagnostics = context?.logger ?? abstractLogger
192
+ const explicitProduction = context?.isProduction ?? context?.production
193
+ const command = context?.command ?? (explicitProduction ? 'start' : 'dev')
194
+ const production = explicitProduction ?? isProductionCommand(command)
181
195
 
182
196
  /*
183
197
  The environment the loader treats as real. An embedder can add to it, and can ask for a
@@ -186,14 +200,28 @@ async function loadV4RuntimeConfiguration (configurationFile, context) {
186
200
  */
187
201
  const realEnv = context?.ignoreProcessEnv ? { ...context?.env } : { ...process.env, ...context?.env }
188
202
 
189
- const loaded = await loadV4Configuration({
190
- cwd: dirname(configurationFile),
191
- configPath: configurationFile,
203
+ const objectSource = typeof target !== 'string' && target.source !== undefined
204
+ const shared = {
192
205
  realEnv,
193
- command: context?.command ?? (production ? 'start' : 'dev'),
206
+ command,
194
207
  mode: context?.mode,
195
208
  production,
196
209
  customEnvFile: context?.envFile,
210
+ /*
211
+ Step 3 of the pipeline validates orchestration in the eval worker, before autoload expansion
212
+ acts on it -- without the schema that step is skipped, and a malformed `autoload` reaches the
213
+ filesystem walk as a raw TypeError instead of a validation error naming the property.
214
+ */
215
+ schema: context?.schema ?? v4Schema,
216
+ /*
217
+ The evaluation deadline. A configuration that never resolves — an awaited fetch to a dead
218
+ host, a forgotten promise — otherwise hangs the boot rather than failing it, and the default
219
+ is a guess about how long a reasonable one takes. A deployment that knows better says so.
220
+ */
221
+ timeout: context?.configTimeout,
222
+ // The one file --debug-config evaluates in this process when an inspector is attached, so a
223
+ // breakpoint in it is reachable at all: a throwaway worker dies before one can attach.
224
+ inProcessTarget: context?.inProcessTarget,
197
225
  /*
198
226
  Validation imports each capability's schema, so it needs the capability installed — not
199
227
  merely declared, which is all the detector needs. That moves the moment a capability must be
@@ -207,9 +235,29 @@ async function loadV4RuntimeConfiguration (configurationFile, context) {
207
235
  // both the schema import and the version stamp — the application's own dependencies still
208
236
  // come first.
209
237
  runtimeScope: import.meta.filename,
210
- onWarning: warning => abstractLogger.warn(warning.message),
211
- onInfo: info => abstractLogger.info(info.message)
212
- })
238
+ /*
239
+ These are the loader's user-facing diagnostics — the standalone-boot warning, the zero-config
240
+ inference notice, the capability the detector chose — and they were being handed to
241
+ abstractLogger, which is a no-op. Every one of them was discarded, including the warning the
242
+ format relies on to tell someone that nothing the root says is being applied.
243
+
244
+ A caller with a real logger passes it. The default stays silent because an embedder loading a
245
+ configuration has not asked for anything on its stderr.
246
+ */
247
+ onWarning: warning => diagnostics.warn(warning.message),
248
+ onInfo: info => diagnostics.info(info.message)
249
+ }
250
+
251
+ let loaded
252
+ if (objectSource) {
253
+ loaded = await loadV4ObjectConfiguration({ root: target.root, source: target.source, ...shared })
254
+ } else if (typeof target === 'string') {
255
+ loaded = await loadV4Configuration({ cwd: dirname(target), configPath: target, ...shared })
256
+ } else {
257
+ // No file was named and none was found: the loader searches from this directory and, finding
258
+ // nothing, synthesizes.
259
+ loaded = await loadV4Configuration({ cwd: target.cwd, ...shared })
260
+ }
213
261
 
214
262
  const config = loaded.config
215
263
 
@@ -219,19 +267,20 @@ async function loadV4RuntimeConfiguration (configurationFile, context) {
219
267
  projection carries authored values — so this is where the runtime schema's defaults arrive, and
220
268
  without it the runtime reaches for settings like gracefulShutdown that nothing supplied.
221
269
  */
222
- validateCapabilityConfiguration(config, context?.schema ?? schema, {
270
+ validateCapabilityConfiguration(config, context?.schema ?? v4Schema, {
223
271
  id: 'runtime',
224
272
  module: '@platformatic/runtime',
225
273
  root: loaded.root
226
274
  })
227
275
 
228
276
  // 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.
277
+ // is the main process's copy. env is the runtime's own view — the real environment under the root
278
+ // env block and the root's env files — and every application worker gets the per-application
279
+ // environment the loader resolved instead, which this one is not a substitute for.
231
280
  config[kMetadata] = {
232
281
  root: loaded.root,
233
282
  path: loaded.configPath,
234
- env: process.env,
283
+ env: loaded.env ?? process.env,
235
284
  module: '@platformatic/runtime',
236
285
  /*
237
286
  The evaluation context, kept beside the envelope rather than inside it: applications added
@@ -240,9 +289,31 @@ async function loadV4RuntimeConfiguration (configurationFile, context) {
240
289
  no such context and keeps resolving configuration files worker-side.
241
290
  */
242
291
  v4: {
243
- command: context?.command ?? (production ? 'start' : 'dev'),
292
+ command,
244
293
  mode: loaded.mode,
245
- production: loaded.production
294
+ production: loaded.production,
295
+ /*
296
+ The environment contract of the boot, carried so an application added while the runtime
297
+ runs is enveloped under the same one: the realEnv the boot resolved ladders against
298
+ (hermetic under ignoreProcessEnv, augmented by context.env) and the --env file that
299
+ replaces every application's env-file rung. Without them the add path defaulted to
300
+ process.env and the conventional .env chain -- an environment the boot says cannot exist.
301
+ */
302
+ realEnv,
303
+ customEnvFile: context?.envFile,
304
+ /*
305
+ Everything the evaluation actually read: the configuration files, their import graph minus
306
+ node_modules, the env files consulted, and the directories whose membership decides the
307
+ application list. `wattpm dev` reloads on any of it -- watching only the deciding file meant
308
+ a config split across a helper module reloaded for none of its own edits.
309
+ */
310
+ watchTargets: loaded.watchTargets,
311
+ /*
312
+ Every entry carrying a `url`, recorded on the way past -- before the enabled filter, which
313
+ is the point. `enabled` is the supported way to keep an application out of a boot, and an
314
+ entry it hides from `resolve` is one whose clone never arrives.
315
+ */
316
+ resolveCandidates: loaded.resolveCandidates
246
317
  }
247
318
  }
248
319
 
@@ -278,7 +349,13 @@ export async function loadApplicationsCommands (executableName = '', configurati
278
349
  throw new Error('No runtime configuration file found.')
279
350
  }
280
351
 
281
- config = await loadConfiguration(file)
352
+ /*
353
+ 'exec' is every non-boot evaluation, and enumerating a capability's commands is one: nothing
354
+ starts. Without it this evaluated as a development boot — development env files, and
355
+ `production: false` handed to every callback — which is a context no invocation of these
356
+ commands actually runs in.
357
+ */
358
+ config = await loadConfiguration(file, null, { command: 'exec' })
282
359
 
283
360
  /* c8 ignore next 3 - Hard to test */
284
361
  if (!config) {
@@ -290,13 +367,9 @@ export async function loadApplicationsCommands (executableName = '', configurati
290
367
 
291
368
  for (const application of config.applications) {
292
369
  try {
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))
370
+ // A v4 application arrives with its configuration already evaluated and its capability
371
+ // already named, so there is nothing to read and nothing to infer.
372
+ const applicationConfig = application.resolvedConfig
300
373
  const pkg = application.module
301
374
  ? await importCapabilityPackage(application.path, application.module)
302
375
  : await loadConfigurationModule(application.path, applicationConfig)
@@ -350,16 +423,19 @@ export async function create (configOrRoot, sourceOrConfig, context) {
350
423
  return runtime
351
424
  }
352
425
 
353
- export { prepareApplication, wrapInRuntimeConfig } from './lib/config.js'
354
-
355
426
  /*
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.
427
+ The one entry point for an application added while the runtime is running -- what an extension
428
+ calls before `addApplications`. It replaces the exported `prepareApplication`, which is the v3
429
+ half of the same job: under v4 an added application has to be evaluated the way boot evaluates
430
+ one, and an entry that skips that arrives without `resolvedConfig` and fails as an unhelpful
431
+ "unable to initialize the worker".
360
432
  */
433
+ export { prepareAddedApplications } from './lib/config.js'
434
+
435
+ // The one transform there is: the v3 one died with its last caller, the wizard's legacy-JSON
436
+ // branch, and the dispatch that told the two apart went with it.
361
437
  export async function transform (config, schema, context) {
362
- return config?.[kMetadata]?.v4 ? transformV4(config, schema, context) : v3Transform(config, schema, context)
438
+ return transformV4(config, schema, context)
363
439
  }
364
440
  export * as errors from './lib/errors.js'
365
441
  export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'