@platformatic/runtime 4.0.0-new-config.5 → 4.0.0-new-config.7
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 +97 -0
- package/lib/errors.js +4 -0
- package/lib/generator.js +42 -11
- package/package.json +16 -16
- package/schema.json +1 -1
package/lib/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, resolve as resolvePath } from 'node:path'
|
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
ApplicationsPortsOverlapError,
|
|
8
9
|
InspectAndInspectBrkError,
|
|
9
10
|
InspectorHostError,
|
|
10
11
|
InspectorPortError,
|
|
@@ -211,6 +212,100 @@ export async function prepareAddedApplications (config, entries, existingIds = [
|
|
|
211
212
|
return prepared
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
const hostnameWildcards = new Set(['0.0.0.0', '::', '[::]'])
|
|
216
|
+
|
|
217
|
+
// The listener an application declares in its own configuration, when it can be determined without
|
|
218
|
+
// starting it. A port coming from an environment variable which was not replaced, an ephemeral port
|
|
219
|
+
// and a missing server block all yield null, in which case only the start time check applies.
|
|
220
|
+
function declaredListener (applicationConfig) {
|
|
221
|
+
const server = applicationConfig?.server
|
|
222
|
+
|
|
223
|
+
if (!server) {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const port = Number(server.port)
|
|
228
|
+
|
|
229
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { port, hostname: server.hostname, perWorker: server.portAssignment === 'perWorkerIncrement' }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Two declared listeners can only be compared when both hostnames are known. When a hostname is
|
|
237
|
+
// omitted the capability picks its own default, so unless the other side is a wildcard - which
|
|
238
|
+
// overlaps with anything - the comparison is left to the start time check.
|
|
239
|
+
function declaredListenersOverlap (first, second) {
|
|
240
|
+
const hostname = first.hostname?.toLowerCase()
|
|
241
|
+
const otherHostname = second.hostname?.toLowerCase()
|
|
242
|
+
|
|
243
|
+
// A wildcard overlaps with any other hostname, known or not
|
|
244
|
+
if (hostnameWildcards.has(hostname) || hostnameWildcards.has(otherHostname)) {
|
|
245
|
+
return true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Both applications rely on the same capability default, so they do collide
|
|
249
|
+
if (typeof hostname === 'undefined' && typeof otherHostname === 'undefined') {
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Only one of the two defaults is unknown, so nothing can be concluded here
|
|
254
|
+
if (typeof hostname === 'undefined' || typeof otherHostname === 'undefined') {
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return hostname === otherHostname
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function describeDeclaredPorts (listener) {
|
|
262
|
+
const { port, last } = listener
|
|
263
|
+
return port === last ? `port ${port}` : `ports ${port}-${last}, one per worker`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Rejects applications whose declared ports overlap before any of them is started, so that the
|
|
267
|
+
// failure does not depend on which worker happens to report its URL first. This is best effort:
|
|
268
|
+
// applications whose port cannot be determined here are checked when their workers start.
|
|
269
|
+
function verifyApplicationsPorts (applications) {
|
|
270
|
+
const listeners = []
|
|
271
|
+
|
|
272
|
+
for (const application of applications) {
|
|
273
|
+
// v4 keeps the validated capability configuration on the application (resolvedConfig), so the
|
|
274
|
+
// declared listener is read from it directly rather than being stashed on a symbol during a
|
|
275
|
+
// separate config-loading pass, as the v3 path did.
|
|
276
|
+
const listener = declaredListener(application.resolvedConfig)
|
|
277
|
+
|
|
278
|
+
// Dynamic scaling changes how many ports the application ends up using, so leave it to the start time check
|
|
279
|
+
if (!listener || application.workers?.dynamic) {
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const workers = listener.perWorker ? (application.workers?.static ?? 1) : 1
|
|
284
|
+
listeners.push({ ...listener, id: application.id, last: listener.port + workers - 1 })
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
288
|
+
for (let j = i + 1; j < listeners.length; j++) {
|
|
289
|
+
const first = listeners[i]
|
|
290
|
+
const second = listeners[j]
|
|
291
|
+
|
|
292
|
+
const port = Math.max(first.port, second.port)
|
|
293
|
+
|
|
294
|
+
if (port > Math.min(first.last, second.last) || !declaredListenersOverlap(first, second)) {
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
throw new ApplicationsPortsOverlapError(
|
|
299
|
+
first.id,
|
|
300
|
+
describeDeclaredPorts(first),
|
|
301
|
+
second.id,
|
|
302
|
+
describeDeclaredPorts(second),
|
|
303
|
+
port
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
214
309
|
// Everything an application needs regardless of how its capability was determined. v3 discovers
|
|
215
310
|
// that by loading the application's config file; v4 has it from the loader's envelope before any
|
|
216
311
|
// worker exists.
|
|
@@ -303,6 +398,8 @@ export async function finalizeConfiguration (config, applications, context, prod
|
|
|
303
398
|
await prepare(config, applications[i], defaultWorkers)
|
|
304
399
|
}
|
|
305
400
|
|
|
401
|
+
verifyApplicationsPorts(applications)
|
|
402
|
+
|
|
306
403
|
if (typeof config.metrics === 'boolean') {
|
|
307
404
|
config.metrics = {
|
|
308
405
|
enabled: config.metrics,
|
package/lib/errors.js
CHANGED
|
@@ -15,6 +15,10 @@ 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
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
|
+
)
|
|
18
22
|
export const WorkerAddressInUseError = createError(
|
|
19
23
|
`${ERROR_PREFIX}_WORKER_EADDR_IN_USE`,
|
|
20
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.'
|
package/lib/generator.js
CHANGED
|
@@ -78,6 +78,7 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
78
78
|
async generatePackageJson () {
|
|
79
79
|
const template = {
|
|
80
80
|
name: `${this.runtimeName}`,
|
|
81
|
+
type: 'module',
|
|
81
82
|
scripts: {
|
|
82
83
|
dev: this.config.devCommand ?? 'wattpm dev',
|
|
83
84
|
build: this.config.buildCommand ?? 'wattpm build',
|
|
@@ -187,6 +188,23 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
187
188
|
|
|
188
189
|
setApplicationsConfigValues () {
|
|
189
190
|
let newApplicationOrdinal = 0
|
|
191
|
+
// A runtime of exactly one application has no mesh of siblings to fall back on: with the v4
|
|
192
|
+
// entrypoint 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
|
|
190
208
|
this.applications.forEach(({ name, application }) => {
|
|
191
209
|
if (!application.config) {
|
|
192
210
|
// set default config
|
|
@@ -195,6 +213,7 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
195
213
|
const existingOrdinal = this.existingApplications.indexOf(name)
|
|
196
214
|
const ordinal = existingOrdinal === -1 ? this.existingApplications.length + newApplicationOrdinal++ : existingOrdinal
|
|
197
215
|
this.setApplicationPort(application, ordinal)
|
|
216
|
+
application.config.entrypoint = totalApplications === 1
|
|
198
217
|
})
|
|
199
218
|
}
|
|
200
219
|
|
|
@@ -205,14 +224,11 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
/*
|
|
208
|
-
The root is the
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
case the `$schema` marker exists for: the wizard writes this file before anything is installed
|
|
214
|
-
and reads it back on a later run, so a root that imported `wattpm` could not be read in the
|
|
215
|
-
state that produced it. A person editing the file afterwards can switch to the imported form.
|
|
227
|
+
The root is spelled the same way an application is: it imports `defineConfig` 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).
|
|
216
232
|
*/
|
|
217
233
|
serializeConfigFile (config) {
|
|
218
234
|
/*
|
|
@@ -225,7 +241,13 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
225
241
|
// thing, and leaving them would write the placeholder's own text where an expression belongs.
|
|
226
242
|
const resolved = this.resolveScaffoldedPlaceholders(rest)
|
|
227
243
|
|
|
228
|
-
|
|
244
|
+
/*
|
|
245
|
+
The root is spelled the same way an application is: a factory call, `defineConfig` 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 defineConfig 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 { defineConfig } from 'wattpm'\n\nexport default defineConfig(${serializeConfiguration(resolved)})\n`
|
|
229
251
|
}
|
|
230
252
|
|
|
231
253
|
async _getConfigFileContents () {
|
|
@@ -648,6 +670,13 @@ export class WrappedGenerator extends BaseGenerator {
|
|
|
648
670
|
async #updateEnvironment () {
|
|
649
671
|
this.addEnvVars(getRuntimeBaseEnvVars(this.config), { overwrite: false, default: true })
|
|
650
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
|
+
|
|
651
680
|
this.addFile({
|
|
652
681
|
path: '',
|
|
653
682
|
file: '.env',
|
|
@@ -713,11 +742,13 @@ export class WrappedGenerator extends BaseGenerator {
|
|
|
713
742
|
wrapped around.
|
|
714
743
|
|
|
715
744
|
Its capability is spelled as a plain object rather than a factory call because this file is
|
|
716
|
-
read before anything is installed, and an import cannot be resolved in that state.
|
|
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.
|
|
717
748
|
*/
|
|
718
749
|
const config = {
|
|
719
750
|
...this.resolveScaffoldedPlaceholders(getRuntimeWrappableProperties()),
|
|
720
|
-
application: {
|
|
751
|
+
application: this.resolveScaffoldedPlaceholders({ module: this.module, server: { port: '{PORT}' } })
|
|
721
752
|
}
|
|
722
753
|
|
|
723
754
|
this.addFile({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "4.0.0-new-config.
|
|
3
|
+
"version": "4.0.0-new-config.7",
|
|
4
4
|
"description": "Platformatic runtime",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"typescript": "~6.0.3",
|
|
40
40
|
"undici-oidc-interceptor": "^0.9.0",
|
|
41
41
|
"why-is-node-running": "^2.2.2",
|
|
42
|
-
"@platformatic/composer": "4.0.0-new-config.
|
|
43
|
-
"@platformatic/db": "4.0.0-new-config.
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/
|
|
46
|
-
"@platformatic/service": "4.0.0-new-config.
|
|
47
|
-
"@platformatic/sql-graphql": "4.0.0-new-config.
|
|
48
|
-
"@platformatic/sql-mapper": "4.0.0-new-config.
|
|
49
|
-
"@platformatic/wattpm-pprof-capture": "4.0.0-new-config.
|
|
42
|
+
"@platformatic/composer": "4.0.0-new-config.7",
|
|
43
|
+
"@platformatic/db": "4.0.0-new-config.7",
|
|
44
|
+
"@platformatic/node": "4.0.0-new-config.7",
|
|
45
|
+
"@platformatic/gateway": "4.0.0-new-config.7",
|
|
46
|
+
"@platformatic/service": "4.0.0-new-config.7",
|
|
47
|
+
"@platformatic/sql-graphql": "4.0.0-new-config.7",
|
|
48
|
+
"@platformatic/sql-mapper": "4.0.0-new-config.7",
|
|
49
|
+
"@platformatic/wattpm-pprof-capture": "4.0.0-new-config.7"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -78,13 +78,13 @@
|
|
|
78
78
|
"undici": "^8.5.0",
|
|
79
79
|
"undici-thread-interceptor": "^2.1.1",
|
|
80
80
|
"ws": "^8.16.0",
|
|
81
|
-
"@platformatic/
|
|
82
|
-
"@platformatic/
|
|
83
|
-
"@platformatic/globals": "4.0.0-new-config.
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/
|
|
86
|
-
"@platformatic/tracing": "4.0.0-new-config.
|
|
87
|
-
"@platformatic/metrics": "4.0.0-new-config.
|
|
81
|
+
"@platformatic/foundation": "4.0.0-new-config.7",
|
|
82
|
+
"@platformatic/generators": "4.0.0-new-config.7",
|
|
83
|
+
"@platformatic/globals": "4.0.0-new-config.7",
|
|
84
|
+
"@platformatic/itc": "4.0.0-new-config.7",
|
|
85
|
+
"@platformatic/basic": "4.0.0-new-config.7",
|
|
86
|
+
"@platformatic/tracing": "4.0.0-new-config.7",
|
|
87
|
+
"@platformatic/metrics": "4.0.0-new-config.7"
|
|
88
88
|
},
|
|
89
89
|
"engines": {
|
|
90
90
|
"node": ">=22.19.0"
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/4.0.0-new-config.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/4.0.0-new-config.7.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|