@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/config.d.ts +602 -673
- package/index.d.ts +22 -27
- package/index.js +290 -30
- package/lib/config.js +270 -367
- package/lib/errors.js +27 -13
- package/lib/generator.js +249 -127
- package/lib/logger.js +3 -3
- package/lib/management-api.js +22 -65
- package/lib/management-handlers.js +3 -5
- package/lib/memory.js +29 -0
- package/lib/policies.js +27 -0
- package/lib/predictive-scaling.js +910 -0
- package/lib/predictive-worker-scaler.js +378 -0
- package/lib/runtime.js +1081 -572
- package/lib/scheduler.js +6 -41
- package/lib/schema.js +77 -15
- package/lib/upgrade.js +16 -1
- package/lib/versions/v4.0.0.js +31 -0
- package/lib/worker/controller.js +89 -109
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +248 -66
- package/lib/worker/main.js +50 -98
- package/lib/worker/messaging.js +42 -1
- package/lib/worker/round-robin-map.js +10 -3
- package/lib/worker/symbols.js +6 -2
- package/package.json +33 -24
- package/schema.json +252 -433
- package/.gitignore +0 -3
- package/.npmignore +0 -2
- package/lib/scaling-algorithm.js +0 -256
- package/lib/worker-scaler.js +0 -249
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
|
-
'
|
|
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
|
|
1
|
+
import { createEnvFileTool, defaultPackageManager, generateDashedName, safeRemove } from '@platformatic/foundation'
|
|
2
|
+
import { LegacyConfigurationFileError, serializeConfiguration } from '@platformatic/foundation/loader'
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '@platformatic/
|
|
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: '>=
|
|
21
|
+
node: '>=24.20.0'
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
this
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
this.
|
|
168
|
-
|
|
169
|
-
this.
|
|
170
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
359
|
-
|
|
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
|
|
407
|
-
const applicationPackageJson =
|
|
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.
|
|
538
|
-
contents:
|
|
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
|
-
|
|
629
|
-
|
|
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:
|
|
635
|
-
contents:
|
|
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.
|
|
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.
|
|
200
|
-
'service.version': config.
|
|
199
|
+
'service.name': config.tracing.applicationName,
|
|
200
|
+
'service.version': config.tracing.version
|
|
201
201
|
},
|
|
202
202
|
logRecordProcessorOptions: [
|
|
203
203
|
{
|