@platformatic/runtime 4.0.0-new-config.2 → 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/lib/config.js CHANGED
@@ -1,28 +1,16 @@
1
- import { importCapabilityAndConfig, validationOptions } from '@platformatic/basic'
2
- import {
3
- extractModuleFromSchemaUrl,
4
- findConfigurationFile,
5
- kMetadata,
6
- loadConfiguration,
7
- loadConfigurationModule,
8
- loadModule,
9
- omitProperties,
10
- runtimeUnwrappablePropertiesList
11
- } from '@platformatic/foundation'
12
- import { readdir, readFile } from 'node:fs/promises'
13
- import { createRequire } from 'node:module'
1
+ import { createDirectory, kMetadata, loadModule } from '@platformatic/foundation'
14
2
  import { loadAdditionalApplications } from '@platformatic/foundation/lib/v4/index.js'
15
- import { isAbsolute, join, resolve as resolvePath } from 'node:path'
3
+ import { createRequire, findPackageJSON } from 'node:module'
4
+ import { dirname, resolve as resolvePath } from 'node:path'
16
5
  import { fileURLToPath } from 'node:url'
17
6
 
18
7
  import {
19
8
  InspectAndInspectBrkError,
20
9
  InspectorHostError,
21
10
  InspectorPortError,
22
- InvalidArgumentError
11
+ InvalidArgumentError,
12
+ MissingDependencyError
23
13
  } from './errors.js'
24
- import { schema } from './schema.js'
25
- import { upgrade } from './upgrade.js'
26
14
 
27
15
  // The runtime package's own entry point, which is where the bundled capability copies live.
28
16
  const runtimeScopePath = fileURLToPath(new URL('../index.js', import.meta.url))
@@ -83,6 +71,10 @@ function parseWorkers (config, prefix, defaultWorkers = { static: 1, dynamic: fa
83
71
  config.workers = {}
84
72
  }
85
73
 
74
+ // What this entry asked for, before the defaults are folded in.
75
+ const declaredMinimum = config.workers.minimum
76
+ const declaredStatic = config.workers.static
77
+
86
78
  // Fill missing values from defaults
87
79
  for (const key of ['minimum', 'maximum', 'static', 'dynamic']) {
88
80
  if (typeof config.workers[key] === 'undefined' && typeof defaultWorkers[key] !== 'undefined') {
@@ -97,6 +89,16 @@ function parseWorkers (config, prefix, defaultWorkers = { static: 1, dynamic: fa
97
89
  config.workers.maximum = t
98
90
  }
99
91
 
92
+ /*
93
+ `static` is how many workers the application starts with, and an entry that declared its own
94
+ `minimum` has already said. The runtime-wide default -- which is 1 unless the project set one --
95
+ must not override it: `{ dynamic: true, minimum: 2 }` otherwise starts a single worker and
96
+ leaves the autoscaler to climb to the floor the entry asked for.
97
+ */
98
+ if (typeof declaredStatic === 'undefined' && typeof declaredMinimum !== 'undefined') {
99
+ config.workers.static = declaredMinimum
100
+ }
101
+
100
102
  if (typeof config.workers.static === 'undefined') {
101
103
  config.workers.static = config.workers.minimum
102
104
  }
@@ -132,49 +134,6 @@ export function autoDetectPprofCapture (config) {
132
134
  return config
133
135
  }
134
136
 
135
- export async function wrapInRuntimeConfig (config, context) {
136
- let applicationId = 'main'
137
- try {
138
- const packageJson = JSON.parse(await readFile(join(config[kMetadata].root, 'package.json'), 'utf-8'))
139
- applicationId = packageJson?.name ?? 'main'
140
-
141
- if (applicationId.startsWith('@')) {
142
- applicationId = applicationId.split('/')[1]
143
- }
144
- } catch (err) {
145
- // on purpose, the package.json might be missing
146
- }
147
-
148
- const production = context?.isProduction ?? context?.production
149
-
150
- const runtimeConfig = config.runtime ?? {}
151
-
152
- // Important: do not change the order of the properties in this object
153
- /* c8 ignore next */
154
- const wrapped = {
155
- $schema: schema.$id,
156
- watch: !production,
157
- ...omitProperties(runtimeConfig, runtimeUnwrappablePropertiesList),
158
- applications: [
159
- {
160
- id: applicationId,
161
- path: config[kMetadata].root,
162
- config: config[kMetadata].path,
163
- ...(runtimeConfig.application ?? {})
164
- }
165
- ]
166
- }
167
-
168
- return loadConfiguration(wrapped, context?.schema ?? schema, {
169
- validationOptions,
170
- transform,
171
- upgrade,
172
- replaceEnv: true,
173
- root: config[kMetadata].root,
174
- ...context
175
- })
176
- }
177
-
178
137
  export function parseInspectorOptions (config, inspect, inspectBreak) {
179
138
  const hasInspect = inspect != null
180
139
  const hasInspectBrk = inspectBreak != null
@@ -229,16 +188,6 @@ export function parseInspectorOptions (config, inspect, inspectBreak) {
229
188
  export async function prepareAddedApplications (config, entries, existingIds = []) {
230
189
  const metadata = config[kMetadata]
231
190
 
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
191
  const { applications } = await loadAdditionalApplications({
243
192
  configPath: metadata.path,
244
193
  entries,
@@ -247,6 +196,8 @@ export async function prepareAddedApplications (config, entries, existingIds = [
247
196
  command: metadata.v4.command,
248
197
  mode: metadata.v4.mode,
249
198
  production: metadata.v4.production,
199
+ realEnv: metadata.v4.realEnv,
200
+ customEnvFile: metadata.v4.customEnvFile,
250
201
  // The runtime is the fallback scope for capability resolution, the same as at boot.
251
202
  runtimeScope: runtimeScopePath
252
203
  })
@@ -260,60 +211,6 @@ export async function prepareAddedApplications (config, entries, existingIds = [
260
211
  return prepared
261
212
  }
262
213
 
263
- export async function prepareApplication (config, application, defaultWorkers) {
264
- // We need to have absolute paths here, ot the `loadConfig` will fail
265
- // Make sure we don't resolve if env var was not replaced
266
- if (application.path && !isAbsolute(application.path) && !application.path.match(/^\{.*\}$/)) {
267
- application.path = resolvePath(config[kMetadata].root, application.path)
268
- }
269
-
270
- if (application.path && application.config) {
271
- application.config = resolvePath(application.path, application.config)
272
- }
273
-
274
- // Skip capability detection for external services (url without path)
275
- // These services will have their path resolved later in runtime.js #setupApplication
276
- // Attempting to detect capability here would cause slow glob operations on the cwd
277
- if (application.url && !application.path) {
278
- application.type = 'unknown'
279
- } else {
280
- try {
281
- let pkg
282
-
283
- if (application.config) {
284
- const applicationConfig = await loadConfiguration(application.config)
285
- pkg = await loadConfigurationModule(application.path, applicationConfig)
286
-
287
- application.type = extractModuleFromSchemaUrl(applicationConfig, true).module
288
- application.skipTelemetryHooks = pkg.skipTelemetryHooks
289
- } else {
290
- const { moduleName, capability } = await importCapabilityAndConfig(application.path)
291
- pkg = capability
292
-
293
- application.type = moduleName
294
- }
295
-
296
- application.skipTelemetryHooks = pkg.skipTelemetryHooks
297
-
298
- // This is needed to work around Rust bug on dylibs:
299
- // https://github.com/rust-lang/rust/issues/91979
300
- // https://github.com/rollup/rollup/issues/5761
301
- const _require = createRequire(application.path)
302
- for (const m of pkg.modulesToLoad ?? []) {
303
- const toLoad = _require.resolve(m)
304
- loadModule(_require, toLoad).catch(() => {})
305
- }
306
- } catch (err) {
307
- // This should not happen, it happens on running some unit tests if we prepare the runtime
308
- // when not all the applications configs are available. Given that we are running this only
309
- // to ddetermine the type of the application, it's safe to ignore this error and default to unknown
310
- application.type = 'unknown'
311
- }
312
- }
313
-
314
- return finalizeApplication(config, application, defaultWorkers)
315
- }
316
-
317
214
  // Everything an application needs regardless of how its capability was determined. v3 discovers
318
215
  // that by loading the application's config file; v4 has it from the loader's envelope before any
319
216
  // worker exists.
@@ -337,7 +234,7 @@ export function finalizeApplication (config, application, defaultWorkers) {
337
234
 
338
235
  /*
339
236
  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
237
+ subpath — which carries skipTracingHooks and modulesToLoad — all main-side, before any worker
341
238
  existed. So there is nothing to discover here, and in particular no application config file to
342
239
  re-read: that is the whole point of evaluating configuration exactly once per load.
343
240
  */
@@ -346,14 +243,37 @@ export async function prepareV4Application (config, application, defaultWorkers)
346
243
  // capability payload as data and never re-reads a file.
347
244
  application.resolvedConfig = application.config ?? {}
348
245
  application.config = undefined
246
+
247
+ /*
248
+ A module application names an npm package as its capability instead of describing one in a file.
249
+ The package is resolved from the runtime root, and its own directory (sourcePath) is kept
250
+ separate from the application's writable root (path) — which the module treats as a working
251
+ directory and which need not exist yet. Keyed on moduleApplication, not module: a regular
252
+ application also carries a module (its detected capability), and must not be resolved this way.
253
+ */
254
+ if (application.moduleApplication) {
255
+ if (!application.path) {
256
+ throw new InvalidArgumentError(`Application "${application.id}" must define path when module is set`)
257
+ }
258
+
259
+ try {
260
+ application.sourcePath = dirname(findPackageJSON(application.module, resolvePath(config[kMetadata].root, 'noop.js')))
261
+ } catch (error) {
262
+ throw new MissingDependencyError(application.module, { cause: error })
263
+ }
264
+
265
+ application.moduleRoot = config[kMetadata].root
266
+ await createDirectory(application.path)
267
+ }
268
+
349
269
  application.type = application.module ?? 'unknown'
350
- application.skipTelemetryHooks = application.capabilityMetadata?.skipTelemetryHooks ?? false
270
+ application.skipTracingHooks = application.capabilityMetadata?.skipTracingHooks ?? false
351
271
 
352
272
  // This is needed to work around a Rust bug on dylibs, as in the v3 path.
353
273
  const modulesToLoad = application.capabilityMetadata?.modulesToLoad ?? []
354
274
 
355
275
  if (modulesToLoad.length > 0 && application.path) {
356
- const _require = createRequire(application.path)
276
+ const _require = createRequire(application.sourcePath ?? application.path)
357
277
 
358
278
  for (const m of modulesToLoad) {
359
279
  try {
@@ -368,130 +288,6 @@ export async function prepareV4Application (config, application, defaultWorkers)
368
288
  return finalizeApplication(config, application, defaultWorkers)
369
289
  }
370
290
 
371
- function isApplicationEnabled (application, environment) {
372
- const { enabled } = application
373
-
374
- if (typeof enabled === 'undefined') {
375
- return true
376
- }
377
-
378
- if (typeof enabled === 'string') {
379
- return enabled !== 'false'
380
- }
381
-
382
- if (typeof enabled === 'object' && enabled !== null) {
383
- return enabled[environment] ?? true
384
- }
385
-
386
- return enabled
387
- }
388
-
389
- export async function transform (config, _, context) {
390
- const production = context?.isProduction ?? context?.production
391
- const environment = production ? 'production' : 'development'
392
- const applications = [...(config.applications ?? []), ...(config.services ?? []), ...(config.web ?? [])]
393
-
394
- const watchType = typeof config.watch
395
- if (watchType === 'string') {
396
- config.watch = config.watch === 'true'
397
- } else if (watchType === 'undefined') {
398
- config.watch = !production
399
- }
400
-
401
- // Migrate the old verticalScaler property, only applied if the new settings are not set, otherwise workers takes precedence
402
- // TODO: Remove in the next major version
403
- if (config.verticalScaler) {
404
- config.workers ??= {}
405
- config.workers.total ??= config.verticalScaler.maxTotalWorkers
406
- config.workers.dynamic ??= config.verticalScaler.enabled
407
- config.workers.minimum ??= config.verticalScaler.minWorkers ?? 1
408
- config.workers.maximum ??= config.verticalScaler.maxWorkers ?? config.verticalScaler.maxTotalWorkers ?? 1
409
- config.workers.maxMemory ??= config.verticalScaler.maxTotalMemory
410
-
411
- if (config.verticalScaler.cooldownSec) {
412
- config.workers.cooldown ??= config.verticalScaler.cooldownSec * 1000
413
- }
414
- if (config.verticalScaler.gracePeriod) {
415
- config.workers.gracePeriod ??= config.verticalScaler.gracePeriod
416
- }
417
- if (config.verticalScaler.scaleUpELU) {
418
- config.workers.scaleUpELU ??= config.verticalScaler.scaleUpELU
419
- }
420
- if (config.verticalScaler.scaleDownELU) {
421
- config.workers.scaleDownELU ??= config.verticalScaler.scaleDownELU
422
- }
423
-
424
- if (config.verticalScaler.applications) {
425
- for (const appId in config.verticalScaler.applications) {
426
- let appConfig = applications.find((app) => app.id === appId)
427
- if (!appConfig) {
428
- appConfig = { id: appId, workers: {} }
429
- applications.push(appConfig)
430
- }
431
-
432
- const scaleConfig = config.verticalScaler.applications[appId]
433
- const workersConfig = appConfig.workers ??= {}
434
-
435
- workersConfig.minimum ??= scaleConfig.minWorkers ?? config.workers.minimum
436
- workersConfig.maximum ??= scaleConfig.maxWorkers ?? config.workers.maximum
437
-
438
- if (scaleConfig.scaleUpELU) {
439
- workersConfig.scaleUpELU ??= scaleConfig.scaleUpELU
440
- }
441
- if (scaleConfig.scaleDownELU) {
442
- workersConfig.scaleDownELU ??= scaleConfig.scaleDownELU
443
- }
444
- }
445
- }
446
-
447
- config.verticalScaler = undefined
448
- }
449
-
450
- if (config.autoload) {
451
- const { exclude = [], mappings = {} } = config.autoload
452
- let { path } = config.autoload
453
-
454
- path = resolvePath(config[kMetadata].root, path)
455
- const entries = await readdir(path, { withFileTypes: true })
456
-
457
- for (let i = 0; i < entries.length; ++i) {
458
- const entry = entries[i]
459
-
460
- if (exclude.includes(entry.name) || !entry.isDirectory()) {
461
- continue
462
- }
463
-
464
- const mapping = mappings[entry.name] ?? {}
465
- const id = mapping.id ?? entry.name
466
- const entryPath = join(path, entry.name)
467
-
468
- let config
469
- const configFilename = mapping.config ?? (await findConfigurationFile(entryPath))
470
-
471
- if (typeof configFilename === 'string') {
472
- config = join(entryPath, configFilename)
473
- }
474
-
475
- const application = { id, config, path: entryPath, ...mapping }
476
- const existingApplicationId = applications.findIndex(application => application.id === id)
477
-
478
- if (existingApplicationId !== -1) {
479
- applications[existingApplicationId] = { ...application, ...applications[existingApplicationId] }
480
- } else {
481
- applications.push(application)
482
- }
483
- }
484
- }
485
-
486
- for (let i = applications.length - 1; i >= 0; --i) {
487
- if (!isApplicationEnabled(applications[i], environment)) {
488
- applications.splice(i, 1)
489
- }
490
- }
491
-
492
- return finalizeConfiguration(config, applications, context, production, prepareApplication)
493
- }
494
-
495
291
  // The half of the transform the loader does not own: inspector options, worker counts, the
496
292
  // per-application preparation, and the runtime-level normalizations. v4 reaches this directly,
497
293
  // having already expanded autoload and resolved enabled in the eval worker.
package/lib/errors.js CHANGED
@@ -103,6 +103,10 @@ export const NoConfigFileFoundError = createError(
103
103
  "No config file found for application '%s'"
104
104
  )
105
105
  export const MissingDependencyError = createError(`${ERROR_PREFIX}_MISSING_DEPENDENCY`, 'Missing dependency: "%s"')
106
+ export const InvalidApplicationModuleError = createError(
107
+ `${ERROR_PREFIX}_INVALID_APPLICATION_MODULE`,
108
+ 'Application module "%s" must export a create function'
109
+ )
106
110
  export const InspectAndInspectBrkError = createError(
107
111
  `${ERROR_PREFIX}_INSPECT_AND_INSPECT_BRK`,
108
112
  '--inspect and --inspect-brk cannot be used together'
@@ -232,3 +236,7 @@ export const WorkerInterceptorJoinTimeoutError = createError(
232
236
  `${ERROR_PREFIX}_WORKER_INTERCEPTOR_JOIN_TIMEOUT`,
233
237
  'The %s failed to join the mesh network in %dms.'
234
238
  )
239
+ export const MixedServingStateError = createError(
240
+ `${ERROR_PREFIX}_MIXED_SERVING_STATE`,
241
+ 'The workers of the application "%s" disagree about how it serves: %s. One application is one thing; workers that disagree about whether it serves HTTP would have the mesh route a share of requests to a worker that destroys them.'
242
+ )