@platformatic/runtime 3.72.0 → 4.0.0-alpha.1

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/errors.js CHANGED
@@ -13,7 +13,15 @@ export const ERROR_PREFIX = 'PLT_RUNTIME'
13
13
 
14
14
  export const AddressInUseError = createError(
15
15
  `${ERROR_PREFIX}_EADDR_IN_USE`,
16
- 'The current port is in use by another application'
16
+ 'Port %d is already in use by applications "%s" and "%s"'
17
+ )
18
+ export const ApplicationsPortsOverlapError = createError(
19
+ `${ERROR_PREFIX}_APPLICATIONS_PORTS_OVERLAP`,
20
+ 'The applications "%s" (%s) and "%s" (%s) are both configured to listen on port %d. Applications cannot share a port: change one of the ports or make them listen on different hostnames.'
21
+ )
22
+ export const WorkerAddressInUseError = createError(
23
+ `${ERROR_PREFIX}_WORKER_EADDR_IN_USE`,
24
+ 'Port %d is already in use by another worker of the application "%s". Multiple workers can share a port only when the reusePort feature is available in your OS, otherwise set "server.portAssignment" to "perWorkerIncrement" in the application configuration or use a single worker.'
17
25
  )
18
26
  export const RuntimeExitedError = createError(
19
27
  `${ERROR_PREFIX}_RUNTIME_EXIT`,
@@ -90,6 +98,12 @@ export const ApplicationAlreadyStartedError = createError(
90
98
  'Application is already started'
91
99
  )
92
100
  export const RuntimeNotStartedError = createError(`${ERROR_PREFIX}_NOT_STARTED`, 'Application has not been started')
101
+ export class ApplicationShutdownError extends AggregateError {
102
+ constructor (errors) {
103
+ super(errors, 'Application shutdown failed')
104
+ this.code = `${ERROR_PREFIX}_APPLICATION_SHUTDOWN`
105
+ }
106
+ }
93
107
  export const ConfigPathMustBeStringError = createError(
94
108
  `${ERROR_PREFIX}_CONFIG_PATH_MUST_BE_STRING`,
95
109
  'Config path must be a string'
@@ -98,19 +112,15 @@ export const NoConfigFileFoundError = createError(
98
112
  `${ERROR_PREFIX}_NO_CONFIG_FILE_FOUND`,
99
113
  "No config file found for application '%s'"
100
114
  )
101
- export const InvalidEntrypointError = createError(
102
- `${ERROR_PREFIX}_INVALID_ENTRYPOINT`,
103
- "Invalid entrypoint: '%s' does not exist"
104
- )
105
115
  export const ApplicationIdCollisionError = createError(
106
116
  `${ERROR_PREFIX}_APPLICATION_ID_COLLISION`,
107
117
  'The application id "%s" is used by the autoloaded directory "%s" and by a different application defined in the configuration file via %s. Application ids must be unique.'
108
118
  )
109
- export const MissingEntrypointError = createError(
110
- `${ERROR_PREFIX}_MISSING_ENTRYPOINT`,
111
- 'Missing application entrypoint.'
112
- )
113
119
  export const MissingDependencyError = createError(`${ERROR_PREFIX}_MISSING_DEPENDENCY`, 'Missing dependency: "%s"')
120
+ export const InvalidApplicationModuleError = createError(
121
+ `${ERROR_PREFIX}_INVALID_APPLICATION_MODULE`,
122
+ 'Application module "%s" must export a create function'
123
+ )
114
124
  export const InspectAndInspectBrkError = createError(
115
125
  `${ERROR_PREFIX}_INSPECT_AND_INSPECT_BRK`,
116
126
  '--inspect and --inspect-brk cannot be used together'
@@ -154,6 +164,10 @@ export const DuplicateSchedulerJobError = createError(
154
164
  `${ERROR_PREFIX}_DUPLICATE_SCHEDULER_JOB`,
155
165
  'Scheduler "%s" is already registered'
156
166
  )
167
+ export const InvalidSchedulerCronError = createError(
168
+ `${ERROR_PREFIX}_INVALID_SCHEDULER_CRON`,
169
+ 'Invalid cron expression "%s" for scheduler "%s"'
170
+ )
157
171
 
158
172
  export const MissingPprofCapture = createError(
159
173
  `${ERROR_PREFIX}_MISSING_PPROF_CAPTURE`,
@@ -232,10 +246,6 @@ export const HealthSignalTypeMustBeStringError = createError(
232
246
  `${ERROR_PREFIX}_HEALTH_SIGNAL_TYPE_MUST_BE_STRING`,
233
247
  'Health signal type must be a string, received "%s"'
234
248
  )
235
- export const CannotRemoveEntrypointError = createError(
236
- `${ERROR_PREFIX}_CANNOT_REMOVE_ENTRYPOINT`,
237
- 'Cannot remove the entrypoint application.'
238
- )
239
249
  export const WorkerInterceptorNotReadyError = createError(
240
250
  `${ERROR_PREFIX}_WORKER_INTERCEPTOR_NOT_READY`,
241
251
  'The "%s" application worker interceptor is not ready'
@@ -244,3 +254,7 @@ export const WorkerInterceptorJoinTimeoutError = createError(
244
254
  `${ERROR_PREFIX}_WORKER_INTERCEPTOR_JOIN_TIMEOUT`,
245
255
  'The %s failed to join the mesh network in %dms.'
246
256
  )
257
+ export const MixedServingStateError = createError(
258
+ `${ERROR_PREFIX}_MIXED_SERVING_STATE`,
259
+ '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.'
260
+ )
package/lib/generator.js CHANGED
@@ -1,68 +1,47 @@
1
- import createError from '@fastify/error'
1
+ import { createEnvFileTool, defaultPackageManager, generateDashedName, safeRemove } from '@platformatic/foundation'
2
+ import { LegacyConfigurationFileError, serializeConfiguration } from '@platformatic/foundation/loader'
2
3
  import {
3
- createEnvFileTool,
4
- defaultPackageManager,
5
- findConfigurationFile,
6
- generateDashedName,
7
- kMetadata,
8
- loadConfiguration,
9
- loadConfigurationFile,
10
- safeRemove
11
- } from '@platformatic/foundation'
12
- import { BaseGenerator, envObjectToString, getApplicationTemplateFromSchemaUrl } from '@platformatic/generators'
4
+ BaseGenerator,
5
+ appendApplications,
6
+ envObjectToString,
7
+ findAnyConfigurationFile,
8
+ listedApplications,
9
+ rawSource,
10
+ readEnvFile,
11
+ getApplicationTemplateFromSchemaUrl
12
+ } from '@platformatic/generators'
13
13
  import { existsSync } from 'node:fs'
14
14
  import { readFile, readdir, stat } from 'node:fs/promises'
15
15
  import { createRequire } from 'node:module'
16
- import { basename, join } from 'node:path'
16
+ import { basename, join, resolve, sep } from 'node:path'
17
17
  import { pathToFileURL } from 'node:url'
18
- import { transform } from './config.js'
19
- import { schema } from './schema.js'
20
18
  import { getArrayDifference } from './utils.js'
21
19
 
22
20
  const engines = {
23
- node: '>=22.19.0'
21
+ node: '>=24.20.0'
24
22
  }
25
23
 
26
- export const ERROR_PREFIX = 'PLT_RUNTIME_GEN'
27
-
28
- const NoApplicationNamedError = createError(
29
- `${ERROR_PREFIX}_NO_APPLICATION_FOUND`,
30
- "No application named '%s' has been added to this runtime."
31
- )
32
- const NoEntryPointError = createError(`${ERROR_PREFIX}_NO_ENTRYPOINT`, 'No entrypoint had been defined.')
33
-
34
- function getRuntimeWrappableProperties (skipServer) {
35
- const wrappableProperties = {
24
+ function getRuntimeWrappableProperties () {
25
+ return {
36
26
  logger: {
37
27
  level: '{PLT_SERVER_LOGGER_LEVEL}'
38
28
  },
39
29
  managementApi: '{PLT_MANAGEMENT_API}'
40
30
  }
41
-
42
- if (!skipServer) {
43
- wrappableProperties.server = {
44
- hostname: '{PLT_SERVER_HOSTNAME}',
45
- port: '{PORT}'
46
- }
47
- }
48
-
49
- return wrappableProperties
50
31
  }
51
32
 
52
33
  function getRuntimeBaseEnvVars (config) {
53
- const env = {}
54
-
55
- if (!config.skipServer) {
56
- env.PLT_SERVER_HOSTNAME = '127.0.0.1'
57
- env.PORT = config.port || 3042
34
+ return {
35
+ PLT_SERVER_LOGGER_LEVEL: config.logLevel || 'info',
36
+ PLT_MANAGEMENT_API: true
58
37
  }
59
-
60
- env.PLT_SERVER_LOGGER_LEVEL = config.logLevel || 'info'
61
- env.PLT_MANAGEMENT_API = true
62
-
63
- return env
64
38
  }
65
39
 
40
+ /*
41
+ The one thing an import changes about a root: which applications it lists. Applied to the module
42
+ the user has rather than to a re-rendering of it, so their formatting, their comments and the
43
+ expressions their values are written as all survive the edit.
44
+ */
66
45
  export class RuntimeGenerator extends BaseGenerator {
67
46
  constructor (opts) {
68
47
  super({
@@ -73,7 +52,6 @@ export class RuntimeGenerator extends BaseGenerator {
73
52
  this.applicationsFolder = opts.applicationsFolder ?? 'applications'
74
53
  this.applications = []
75
54
  this.existingApplications = []
76
- this.entryPoint = null
77
55
  this.packageManager = opts.packageManager ?? defaultPackageManager
78
56
  }
79
57
 
@@ -97,18 +75,10 @@ export class RuntimeGenerator extends BaseGenerator {
97
75
  application.setRuntime(this)
98
76
  }
99
77
 
100
- setEntryPoint (entryPoint) {
101
- const application =
102
- this.existingApplications.includes(entryPoint) || this.applications.find(svc => svc.name === entryPoint)
103
- if (!application) {
104
- throw new NoApplicationNamedError(entryPoint)
105
- }
106
- this.entryPoint = application
107
- }
108
-
109
78
  async generatePackageJson () {
110
79
  const template = {
111
80
  name: `${this.runtimeName}`,
81
+ type: 'module',
112
82
  scripts: {
113
83
  dev: this.config.devCommand ?? 'wattpm dev',
114
84
  build: this.config.buildCommand ?? 'wattpm build',
@@ -155,19 +125,46 @@ export class RuntimeGenerator extends BaseGenerator {
155
125
  return
156
126
  }
157
127
  this._hasCheckedForExistingConfig = true
158
- const existingConfigFile = this.runtimeConfig ?? (await findConfigurationFile(this.targetDirectory, 'runtime'))
159
- if (existingConfigFile && existsSync(join(this.targetDirectory, existingConfigFile))) {
160
- this.existingConfigRaw = await loadConfigurationFile(join(this.targetDirectory, existingConfigFile))
161
- this.existingConfig = await loadConfiguration(join(this.targetDirectory, existingConfigFile), schema, {
162
- transform,
163
- ignoreProcessEnv: true
164
- })
128
+ /*
129
+ Whichever dialect the project is in: the configuration is a module and is invisible to the
130
+ legacy lookup, so without this the wizard treats a project it scaffolded itself as a new one.
131
+ */
132
+ const existingConfigFile = await findAnyConfigurationFile(this.targetDirectory)
165
133
 
166
- const { PLT_ROOT, ...existingEnvironment } = this.existingConfig[kMetadata].env
167
- this.config.env = existingEnvironment
168
- this.config.port = this.config.env.PORT
169
- this.entryPoint = this.existingConfig.applications.find(svc => svc.entrypoint)
170
- this.existingApplications = this.existingConfig.applications.map(s => s.id)
134
+ if (existingConfigFile && existsSync(join(this.targetDirectory, existingConfigFile))) {
135
+ const existingConfigPath = join(this.targetDirectory, existingConfigFile)
136
+
137
+ this.existingConfigRaw = await this.readConfigurationFile(existingConfigPath, 'root', this.targetDirectory)
138
+ /*
139
+ The file as written, kept so that editing it can be an edit. Re-emitting from the evaluated
140
+ configuration would bake this machine's environment into the user's root -- a reference to
141
+ PLT_SERVER_LOGGER_LEVEL becomes whatever it happens to resolve to here -- and take every
142
+ comment with it.
143
+ */
144
+ this.existingConfigFile = existingConfigFile
145
+ this.existingConfigSource = await readFile(existingConfigPath, 'utf-8')
146
+
147
+ /*
148
+ A legacy root is refused with the hint every other entry point gives, rather than loaded
149
+ and rewritten: continuing wrote the module form over the .json file, and migrate is the
150
+ tool whose whole job is that conversion -- with the refusals and divergence reports the
151
+ wizard has no way to make.
152
+ */
153
+ if (existingConfigFile.endsWith('.json')) {
154
+ throw new LegacyConfigurationFileError(existingConfigPath)
155
+ } else {
156
+ /*
157
+ Derived from the root rather than loaded through it. A full load evaluates every
158
+ application's configuration, and those import their capabilities -- so the wizard would
159
+ need the project's dependencies installed before it could read the project, which is
160
+ exactly the state it is called in to fix.
161
+ */
162
+ const { PLT_ROOT, ...existingEnvironment } = await readEnvFile(this.targetDirectory)
163
+
164
+ this.config.env = existingEnvironment
165
+ this.existingConfig = this.existingConfigRaw
166
+ this.existingApplications = await this.#listExistingApplications(this.existingConfigRaw)
167
+ }
171
168
 
172
169
  this.updateRuntimeConfig(this.existingConfigRaw)
173
170
  this.updateRuntimeEnv(await readFile(join(this.targetDirectory, '.env'), 'utf-8'))
@@ -190,37 +187,87 @@ export class RuntimeGenerator extends BaseGenerator {
190
187
  }
191
188
 
192
189
  setApplicationsConfigValues () {
193
- this.applications.forEach(({ application }) => {
190
+ let newApplicationOrdinal = 0
191
+ // A runtime of exactly one application has no mesh of siblings to fall back on: with the
192
+ // entrypoint field gone, a sole application that declares no port would bind nothing and be reachable
193
+ // from nowhere. So the sole application is marked as the entrypoint here -- the one place the
194
+ // whole application set is known -- and a capability that otherwise scaffolds no port reads the
195
+ // flag and exposes itself on PORT/3042 (@platformatic/node does; the framework capabilities,
196
+ // which also scaffold no port, can adopt the same flag). The count is the gate: the moment a
197
+ // second application exists, nothing is forced and a portless application stays mesh-only, which
198
+ // is what keeps "expose the one" from becoming "expose them all".
199
+ //
200
+ // Counted as distinct names rather than as two array lengths added: the create wizard seeds its
201
+ // uniqueness set from `existingApplications` and pushes each new name back into it, so a freshly
202
+ // scaffolded application is present in both `existingApplications` and `applications`. A plain sum
203
+ // would double-count it and never see a single-application project as single.
204
+ const totalApplications = new Set([
205
+ ...this.existingApplications,
206
+ ...this.applications.map(({ name }) => name)
207
+ ]).size
208
+ this.applications.forEach(({ name, application }) => {
194
209
  if (!application.config) {
195
210
  // set default config
196
211
  application.setConfig()
197
212
  }
213
+ const existingOrdinal = this.existingApplications.indexOf(name)
214
+ const ordinal = existingOrdinal === -1 ? this.existingApplications.length + newApplicationOrdinal++ : existingOrdinal
215
+ this.setApplicationPort(application, ordinal)
216
+ application.config.entrypoint = totalApplications === 1
198
217
  })
199
218
  }
200
219
 
220
+ setApplicationPort (application, ordinal) {
221
+ if (application.config.port === 3042) {
222
+ application.setConfig({ port: 3042 + ordinal })
223
+ }
224
+ }
225
+
226
+ /*
227
+ The root is spelled the same way an application is: it imports `createWattConfig` from wattpm and
228
+ calls it, so the editor types and checks the shape the loader would otherwise only reject at
229
+ boot. The import means the file cannot be evaluated before its dependencies are installed --
230
+ which is why `wattpm install`/`create` install the root's own dependencies before reading it to
231
+ discover the applications (see installDependencies in wattpm-utils).
232
+ */
233
+ serializeConfigFile (config) {
234
+ /*
235
+ Unstamped while this line is still 3.x. The marker is a version declaration, and the loader
236
+ refuses a legacy one outright -- correctly, since that is how it catches a configuration nobody
237
+ migrated. It becomes writable, and worth writing, at 4.0.0.
238
+ */
239
+ const { $schema, module: _module, ...rest } = config
240
+ // Resolved like every other scaffolded value: the root's placeholders are the same kind of
241
+ // thing, and leaving them would write the placeholder's own text where an expression belongs.
242
+ const resolved = this.resolveScaffoldedPlaceholders(rest)
243
+
244
+ /*
245
+ The root is spelled the same way an application is: a factory call, `createWattConfig` from
246
+ wattpm, whose whole job is to type its argument so the editor completes and checks the shape
247
+ the loader would otherwise only reject at boot. wattpm's createWattConfig imports nothing, so the
248
+ root evaluates in the eval worker without paying for the runtime it would otherwise pull in.
249
+ */
250
+ return `import { createWattConfig } from 'wattpm'\n\nexport default createWattConfig(${serializeConfiguration(resolved)})\n`
251
+ }
252
+
201
253
  async _getConfigFileContents () {
202
254
  const config = {
203
255
  $schema: `https://schemas.platformatic.dev/wattpm/${this.platformaticVersion}.json`,
204
- entrypoint: this.entryPoint.name,
205
256
  watch: true,
206
257
  autoload: {
207
258
  path: this.config.autoload || this.applicationsFolder,
208
259
  exclude: ['docs']
209
260
  },
210
- ...getRuntimeWrappableProperties(this.config.skipServer)
261
+ ...getRuntimeWrappableProperties()
211
262
  }
212
263
 
213
264
  return config
214
265
  }
215
266
 
216
267
  async _afterPrepare () {
217
- if (!this.entryPoint) {
218
- throw new NoEntryPointError()
219
- }
220
268
  const applicationsEnv = await this.prepareApplicationFiles()
221
269
  this.addEnvVars({
222
270
  ...this.config.env,
223
- ...this.getRuntimeEnv(),
224
271
  ...applicationsEnv
225
272
  })
226
273
 
@@ -258,21 +305,6 @@ export class RuntimeGenerator extends BaseGenerator {
258
305
 
259
306
  async prepareQuestions () {
260
307
  await this.populateFromExistingConfig()
261
-
262
- if (this.existingConfig) {
263
- return
264
- }
265
-
266
- if (!this.config.skipServer) {
267
- // port
268
- this.questions.push({
269
- type: 'input',
270
- name: 'port',
271
- default: '3042',
272
- message: 'What port do you want to use?',
273
- filter: Number
274
- })
275
- }
276
308
  }
277
309
 
278
310
  setApplicationsDirectory () {
@@ -283,7 +315,11 @@ export class RuntimeGenerator extends BaseGenerator {
283
315
  }
284
316
  let basePath
285
317
  if (this.existingConfig) {
286
- basePath = this.existingConfig.autoload.path
318
+ /*
319
+ Resolved against the project, because the configuration says it relative to itself. The legacy
320
+ loader handed back an absolute path; reading the file directly hands back what it says.
321
+ */
322
+ basePath = resolve(this.targetDirectory, this.existingConfig.autoload.path)
287
323
  } else {
288
324
  basePath = join(this.targetDirectory, this.config.autoload || this.applicationsFolder)
289
325
  }
@@ -322,12 +358,6 @@ export class RuntimeGenerator extends BaseGenerator {
322
358
  // do nothing, makes no sense
323
359
  }
324
360
 
325
- getRuntimeEnv () {
326
- return {
327
- PORT: this.config.port
328
- }
329
- }
330
-
331
361
  async postInstallActions () {
332
362
  for (const { application } of this.applications) {
333
363
  await application.postInstallActions()
@@ -340,11 +370,38 @@ export class RuntimeGenerator extends BaseGenerator {
340
370
  return (await import(pathToFileURL(fileToImport))).Generator
341
371
  }
342
372
 
373
+ /*
374
+ Which applications a root already describes: the entries it lists, and the directories its
375
+ autoload would discover. Neither needs an application's own configuration, which is the point.
376
+ */
377
+ async #listExistingApplications (config) {
378
+ const ids = (config.applications ?? config.services ?? config.web ?? []).map(entry => entry.id).filter(Boolean)
379
+
380
+ if (!config.autoload?.path) {
381
+ return ids
382
+ }
383
+
384
+ const exclude = config.autoload.exclude ?? []
385
+ const directory = join(this.targetDirectory, config.autoload.path)
386
+
387
+ for (const entry of await readdir(directory, { withFileTypes: true }).catch(() => [])) {
388
+ if (entry.isDirectory() && !exclude.includes(entry.name) && !ids.includes(entry.name)) {
389
+ ids.push(entry.name)
390
+ }
391
+ }
392
+
393
+ return ids
394
+ }
395
+
343
396
  async loadFromDir () {
344
397
  const output = {
345
398
  applications: []
346
399
  }
347
- const runtimePkgConfigFileData = JSON.parse(await readFile(join(this.targetDirectory, this.runtimeConfig), 'utf-8'))
400
+ const runtimePkgConfigFileData = await this.readConfigurationFile(
401
+ join(this.targetDirectory, await findAnyConfigurationFile(this.targetDirectory)),
402
+ 'root',
403
+ this.targetDirectory
404
+ )
348
405
  const applicationsPath = join(this.targetDirectory, runtimePkgConfigFileData.autoload.path)
349
406
 
350
407
  // load all applications
@@ -355,8 +412,14 @@ export class RuntimeGenerator extends BaseGenerator {
355
412
  const dirStat = await stat(currentApplicationPath)
356
413
  if (dirStat.isDirectory()) {
357
414
  // load the application config
358
- const configFile = await findConfigurationFile(currentApplicationPath)
359
- const applicationPltJson = JSON.parse(await readFile(join(currentApplicationPath, configFile), 'utf-8'))
415
+ const configFile = await findAnyConfigurationFile(currentApplicationPath)
416
+ // The environment is the runtime's: an application's expressions read the project's .env,
417
+ // which lives at the root beside the configuration that autoloads it.
418
+ const applicationPltJson = await this.readConfigurationFile(
419
+ join(currentApplicationPath, configFile),
420
+ 'application',
421
+ this.targetDirectory
422
+ )
360
423
  // get module to load
361
424
  const template = applicationPltJson.module || getApplicationTemplateFromSchemaUrl(applicationPltJson.$schema)
362
425
  const Generator = await this._getGeneratorForTemplate(currentApplicationPath, template)
@@ -403,8 +466,12 @@ export class RuntimeGenerator extends BaseGenerator {
403
466
 
404
467
  // delete dependencies
405
468
  const applicationPath = join(this.targetDirectory, this.applicationsFolder, s.name)
406
- const configFile = await findConfigurationFile(applicationPath)
407
- const applicationPackageJson = JSON.parse(await readFile(join(applicationPath, configFile), 'utf-8'))
469
+ const configFile = await findAnyConfigurationFile(applicationPath)
470
+ const applicationPackageJson = await this.readConfigurationFile(
471
+ join(applicationPath, configFile),
472
+ 'application',
473
+ this.targetDirectory
474
+ )
408
475
  if (applicationPackageJson.plugins && applicationPackageJson.plugins.packages) {
409
476
  applicationPackageJson.plugins.packages.forEach(p => {
410
477
  delete currrentPackageJson.dependencies[p.name]
@@ -451,6 +518,7 @@ export class RuntimeGenerator extends BaseGenerator {
451
518
  })
452
519
  }
453
520
  applicationInstance.setConfig(baseConfig)
521
+ this.setApplicationPort(applicationInstance, allNewApplicationsNames.indexOf(newApplication.name))
454
522
  applicationInstance.setConfigFields(newApplication.fields)
455
523
 
456
524
  const applicationEnvPrefix = `PLT_${applicationInstance.config.envPrefix}`
@@ -494,18 +562,6 @@ export class RuntimeGenerator extends BaseGenerator {
494
562
  contents: JSON.stringify(currrentPackageJson, null, 2)
495
563
  })
496
564
 
497
- // set new entrypoint if specified
498
- const newEntrypoint = newConfig.entrypoint
499
- if (newEntrypoint) {
500
- // load platformatic.json runtime config
501
- const runtimePkgConfigFileData = JSON.parse(
502
- await readFile(join(this.targetDirectory, this.runtimeConfig), 'utf-8')
503
- )
504
-
505
- this.setEntryPoint(newEntrypoint)
506
- runtimePkgConfigFileData.entrypoint = newEntrypoint
507
- this.updateRuntimeConfig(runtimePkgConfigFileData)
508
- }
509
565
  await this.writeFiles()
510
566
  // save new env
511
567
  await envTool.save()
@@ -532,14 +588,67 @@ export class RuntimeGenerator extends BaseGenerator {
532
588
  }
533
589
 
534
590
  updateRuntimeConfig (config) {
591
+ this.generatedConfig = config
592
+
593
+ // Editing what is there, rather than replacing it with a rendering of what it evaluated to.
594
+ if (this.existingConfigSource && !this.existingConfigFile.endsWith('.json')) {
595
+ this.addFile({
596
+ path: '',
597
+ file: this.existingConfigFile,
598
+ contents: this.#editExistingRoot(config),
599
+ tags: ['runtime-config']
600
+ })
601
+
602
+ return
603
+ }
604
+
535
605
  this.addFile({
536
606
  path: '',
537
- file: this.runtimeConfig,
538
- contents: JSON.stringify(config, null, 2),
607
+ file: this.existingConfigFile ?? this.configurationFileName(),
608
+ contents: this.serializeConfigFile(config),
539
609
  tags: ['runtime-config']
540
610
  })
541
611
  }
542
612
 
613
+ /*
614
+ An existing root is edited rather than rewritten, so that everything it says survives -- see
615
+ `appendApplications`. A shape it cannot edit is reported here rather than papered over by
616
+ writing the file from the loaded configuration, which would be a silent rewrite of every
617
+ reference the root contains.
618
+ */
619
+ #editExistingRoot (config) {
620
+ /*
621
+ The evaluated configuration arrives with autoload already expanded, so its application list
622
+ holds every discovered directory as an explicit entry with an absolute machine path.
623
+ Appending those would bake this machine's layout into the user's root -- and duplicate what
624
+ autoload will discover again on the next boot. Only entries autoload does not cover belong in
625
+ the file: the ones this run scaffolds outside the autoload directory, or a root with no
626
+ autoload at all.
627
+ */
628
+ const autoloadPath = config.autoload?.path
629
+ ? resolve(this.targetDirectory, config.autoload.path) + sep
630
+ : null
631
+ const additions = listedApplications(config).filter(entry => {
632
+ if (!autoloadPath || typeof entry.path !== 'string') {
633
+ return true
634
+ }
635
+
636
+ return !resolve(this.targetDirectory, entry.path).startsWith(autoloadPath)
637
+ })
638
+
639
+ const edited = appendApplications(this.existingConfigSource, additions, entry =>
640
+ this.resolveScaffoldedPlaceholders(entry, rawSource)
641
+ )
642
+
643
+ if (edited === null) {
644
+ throw new Error(
645
+ `Cannot add applications to ${this.existingConfigFile}: its default export is not a configuration object this tool can edit. Add them by hand.`
646
+ )
647
+ }
648
+
649
+ return edited
650
+ }
651
+
543
652
  updateRuntimeEnv (contents) {
544
653
  this.addFile({
545
654
  path: '',
@@ -548,15 +657,6 @@ export class RuntimeGenerator extends BaseGenerator {
548
657
  tags: ['runtime-env']
549
658
  })
550
659
  }
551
-
552
- updateConfigEntryPoint (entrypoint) {
553
- // This can return null if the generator was not supposed to modify the config
554
- const configObject = this.getRuntimeConfigFileObject()
555
- const config = JSON.parse(configObject.contents)
556
- config.entrypoint = entrypoint
557
-
558
- this.updateRuntimeConfig(config)
559
- }
560
660
  }
561
661
 
562
662
  export class WrappedGenerator extends BaseGenerator {
@@ -570,6 +670,13 @@ export class WrappedGenerator extends BaseGenerator {
570
670
  async #updateEnvironment () {
571
671
  this.addEnvVars(getRuntimeBaseEnvVars(this.config), { overwrite: false, default: true })
572
672
 
673
+ // A wrapped project is a runtime of one application by construction, so that application is the
674
+ // one that must face the network -- otherwise the wrap would leave the code it was built around
675
+ // reachable from nowhere. The port is registered here so #createConfigFile can read it back as
676
+ // `Number(process.env.PORT || 3042)`; bare PORT rather than a scoped name, because the wrapped
677
+ // application is the root's own singular one and is what a hosting platform's PORT addresses.
678
+ this.addEnvVar('PORT', 3042, { overwrite: false, default: true })
679
+
573
680
  this.addFile({
574
681
  path: '',
575
682
  file: '.env',
@@ -624,15 +731,30 @@ export class WrappedGenerator extends BaseGenerator {
624
731
  }
625
732
 
626
733
  async #createConfigFile () {
734
+ /*
735
+ The wrapped single-app root. The runtime settings are no longer spelled under a `runtime` key
736
+ inside the application's own configuration; there is no such block, so they are the root's own
737
+ and the application is the singular shorthand.
738
+ */
739
+ /*
740
+ The application is the root's own, named by the singular shorthand. Without it the root
741
+ describes a runtime with nothing in it -- it would load, and start none of the code it was
742
+ wrapped around.
743
+
744
+ Its capability is spelled as a plain object rather than a factory call because this file is
745
+ read before anything is installed, and an import cannot be resolved in that state. The bare
746
+ definition sits directly under `application` -- no `config` wrapper -- because the loader wraps
747
+ a definition (a module marker, no entry keys) into the entry's config slot on its own.
748
+ */
627
749
  const config = {
628
- $schema: `https://schemas.platformatic.dev/${this.module}/${this.platformaticVersion}.json`,
629
- runtime: getRuntimeWrappableProperties(this.config.skipServer)
750
+ ...this.resolveScaffoldedPlaceholders(getRuntimeWrappableProperties()),
751
+ application: this.resolveScaffoldedPlaceholders({ module: this.module, server: { port: '{PORT}' } })
630
752
  }
631
753
 
632
754
  this.addFile({
633
755
  path: '',
634
- file: 'watt.json',
635
- contents: JSON.stringify(config, null, 2)
756
+ file: this.configurationFileName(),
757
+ contents: `export default ${serializeConfiguration(config)}\n`
636
758
  })
637
759
  }
638
760
 
package/lib/logger.js CHANGED
@@ -191,13 +191,13 @@ export async function createLogger (config) {
191
191
 
192
192
  const multiStream = pino.multistream([{ stream: cliStream, level: loggerConfig.level }])
193
193
 
194
- if (config.telemetry && config.logger.openTelemetryExporter) {
194
+ if (config.tracing && config.logger.openTelemetryExporter) {
195
195
  const openTelemetryTransport = pino.transport({
196
196
  target: 'pino-opentelemetry-transport',
197
197
  options: {
198
198
  resourceAttributes: {
199
- 'service.name': config.telemetry.applicationName,
200
- 'service.version': config.telemetry.version
199
+ 'service.name': config.tracing.applicationName,
200
+ 'service.version': config.tracing.version
201
201
  },
202
202
  logRecordProcessorOptions: [
203
203
  {