@platformatic/runtime 4.0.0-new-config.5 → 4.0.0-new-config.6
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 +33 -10
- 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,15 @@ 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
|
+
const totalApplications = this.existingApplications.length + this.applications.length
|
|
190
200
|
this.applications.forEach(({ name, application }) => {
|
|
191
201
|
if (!application.config) {
|
|
192
202
|
// set default config
|
|
@@ -195,6 +205,7 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
195
205
|
const existingOrdinal = this.existingApplications.indexOf(name)
|
|
196
206
|
const ordinal = existingOrdinal === -1 ? this.existingApplications.length + newApplicationOrdinal++ : existingOrdinal
|
|
197
207
|
this.setApplicationPort(application, ordinal)
|
|
208
|
+
application.config.entrypoint = totalApplications === 1
|
|
198
209
|
})
|
|
199
210
|
}
|
|
200
211
|
|
|
@@ -205,14 +216,11 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
205
216
|
}
|
|
206
217
|
|
|
207
218
|
/*
|
|
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.
|
|
219
|
+
The root is spelled the same way an application is: it imports `defineConfig` from wattpm and
|
|
220
|
+
calls it, so the editor types and checks the shape the loader would otherwise only reject at
|
|
221
|
+
boot. The import means the file cannot be evaluated before its dependencies are installed --
|
|
222
|
+
which is why `wattpm install`/`create` install the root's own dependencies before reading it to
|
|
223
|
+
discover the applications (see installDependencies in wattpm-utils).
|
|
216
224
|
*/
|
|
217
225
|
serializeConfigFile (config) {
|
|
218
226
|
/*
|
|
@@ -225,7 +233,13 @@ export class RuntimeGenerator extends BaseGenerator {
|
|
|
225
233
|
// thing, and leaving them would write the placeholder's own text where an expression belongs.
|
|
226
234
|
const resolved = this.resolveScaffoldedPlaceholders(rest)
|
|
227
235
|
|
|
228
|
-
|
|
236
|
+
/*
|
|
237
|
+
The root is spelled the same way an application is: a factory call, `defineConfig` from
|
|
238
|
+
wattpm, whose whole job is to type its argument so the editor completes and checks the shape
|
|
239
|
+
the loader would otherwise only reject at boot. wattpm's defineConfig imports nothing, so the
|
|
240
|
+
root evaluates in the eval worker without paying for the runtime it would otherwise pull in.
|
|
241
|
+
*/
|
|
242
|
+
return `import { defineConfig } from 'wattpm'\n\nexport default defineConfig(${serializeConfiguration(resolved)})\n`
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
async _getConfigFileContents () {
|
|
@@ -648,6 +662,13 @@ export class WrappedGenerator extends BaseGenerator {
|
|
|
648
662
|
async #updateEnvironment () {
|
|
649
663
|
this.addEnvVars(getRuntimeBaseEnvVars(this.config), { overwrite: false, default: true })
|
|
650
664
|
|
|
665
|
+
// A wrapped project is a runtime of one application by construction, so that application is the
|
|
666
|
+
// one that must face the network -- otherwise the wrap would leave the code it was built around
|
|
667
|
+
// reachable from nowhere. The port is registered here so #createConfigFile can read it back as
|
|
668
|
+
// `Number(process.env.PORT || 3042)`; bare PORT rather than a scoped name, because the wrapped
|
|
669
|
+
// application is the root's own singular one and is what a hosting platform's PORT addresses.
|
|
670
|
+
this.addEnvVar('PORT', 3042, { overwrite: false, default: true })
|
|
671
|
+
|
|
651
672
|
this.addFile({
|
|
652
673
|
path: '',
|
|
653
674
|
file: '.env',
|
|
@@ -717,7 +738,9 @@ export class WrappedGenerator extends BaseGenerator {
|
|
|
717
738
|
*/
|
|
718
739
|
const config = {
|
|
719
740
|
...this.resolveScaffoldedPlaceholders(getRuntimeWrappableProperties()),
|
|
720
|
-
application: {
|
|
741
|
+
application: {
|
|
742
|
+
config: this.resolveScaffoldedPlaceholders({ module: this.module, server: { port: '{PORT}' } })
|
|
743
|
+
}
|
|
721
744
|
}
|
|
722
745
|
|
|
723
746
|
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.6",
|
|
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/gateway": "4.0.0-new-config.
|
|
45
|
-
"@platformatic/
|
|
46
|
-
"@platformatic/
|
|
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.6",
|
|
43
|
+
"@platformatic/db": "4.0.0-new-config.6",
|
|
44
|
+
"@platformatic/gateway": "4.0.0-new-config.6",
|
|
45
|
+
"@platformatic/service": "4.0.0-new-config.6",
|
|
46
|
+
"@platformatic/node": "4.0.0-new-config.6",
|
|
47
|
+
"@platformatic/sql-graphql": "4.0.0-new-config.6",
|
|
48
|
+
"@platformatic/sql-mapper": "4.0.0-new-config.6",
|
|
49
|
+
"@platformatic/wattpm-pprof-capture": "4.0.0-new-config.6"
|
|
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/basic": "4.0.0-new-config.
|
|
82
|
-
"@platformatic/foundation": "4.0.0-new-config.
|
|
83
|
-
"@platformatic/
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/
|
|
86
|
-
"@platformatic/
|
|
87
|
-
"@platformatic/metrics": "4.0.0-new-config.
|
|
81
|
+
"@platformatic/basic": "4.0.0-new-config.6",
|
|
82
|
+
"@platformatic/foundation": "4.0.0-new-config.6",
|
|
83
|
+
"@platformatic/generators": "4.0.0-new-config.6",
|
|
84
|
+
"@platformatic/itc": "4.0.0-new-config.6",
|
|
85
|
+
"@platformatic/tracing": "4.0.0-new-config.6",
|
|
86
|
+
"@platformatic/globals": "4.0.0-new-config.6",
|
|
87
|
+
"@platformatic/metrics": "4.0.0-new-config.6"
|
|
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.6.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|