@platformatic/foundation 3.68.0 → 4.0.0-new-config.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/cli.js +39 -1
- package/lib/schema.js +10 -23
- package/lib/v4/canonicalize.js +206 -0
- package/lib/v4/capability-resolution.js +149 -0
- package/lib/v4/classify.js +46 -0
- package/lib/v4/context.js +52 -0
- package/lib/v4/detect.js +112 -0
- package/lib/v4/env.js +208 -0
- package/lib/v4/errors.js +131 -0
- package/lib/v4/eval-worker.js +47 -0
- package/lib/v4/evaluate.js +180 -0
- package/lib/v4/filenames.js +116 -0
- package/lib/v4/identifiers.js +81 -0
- package/lib/v4/index.js +21 -0
- package/lib/v4/load.js +889 -0
- package/lib/v4/pipeline.js +233 -0
- package/lib/v4/scope.js +184 -0
- package/lib/v4/serving.js +82 -0
- package/lib/v4/topology.js +137 -0
- package/lib/v4/validate.js +185 -0
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { CapabilitySchemaNotFoundError, InvalidApplicationConfigurationError } from './errors.js'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
The AJV custom keywords are one of the deliberately-kept pieces, carried over as code by explicit
|
|
9
|
+
decision rather than by surviving a refactor. They are re-implemented here rather than imported
|
|
10
|
+
from the v3 configuration module, which leaves foundation with migrate's legacy reader.
|
|
11
|
+
|
|
12
|
+
The root they resolve against is the application's, not the runtime's: a capability's config is
|
|
13
|
+
written where the application lives, so a relative path in it means a path from there.
|
|
14
|
+
*/
|
|
15
|
+
export function createCapabilityValidator (schema, { root, fixPaths = true, useDefaults = true } = {}) {
|
|
16
|
+
// Coercion is disabled in v4. Its only justification was placeholder strings, and on the genuine
|
|
17
|
+
// unions that survive the audit — boolean | number, boolean | object — AJV coercion is a
|
|
18
|
+
// documented hazard in this very codebase.
|
|
19
|
+
const ajv = new Ajv({ useDefaults, coerceTypes: false, allErrors: true, strict: false })
|
|
20
|
+
|
|
21
|
+
ajv.addKeyword({
|
|
22
|
+
keyword: 'resolvePath',
|
|
23
|
+
type: 'string',
|
|
24
|
+
schemaType: 'boolean',
|
|
25
|
+
validate (_schema, path, parentSchema, data) {
|
|
26
|
+
if (typeof path !== 'string' || path.trim() === '') {
|
|
27
|
+
return Boolean(parentSchema.allowEmptyPaths)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (fixPaths) {
|
|
31
|
+
data.parentData[data.parentDataProperty] = resolve(root, path)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
ajv.addKeyword({ keyword: 'allowEmptyPaths', type: 'string', schemaType: 'boolean' })
|
|
39
|
+
|
|
40
|
+
ajv.addKeyword({
|
|
41
|
+
keyword: 'resolveModule',
|
|
42
|
+
type: 'string',
|
|
43
|
+
schemaType: 'boolean',
|
|
44
|
+
validate (_schema, path, _parentSchema, data) {
|
|
45
|
+
if (typeof path !== 'string' || path.trim() === '') {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!fixPaths) {
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
data.parentData[data.parentDataProperty] = createRequire(join(root, 'noop.js')).resolve(path)
|
|
55
|
+
return true
|
|
56
|
+
} catch {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
ajv.addKeyword({
|
|
63
|
+
keyword: 'typeof',
|
|
64
|
+
validate: function validate (schema, value, _parentSchema, data) {
|
|
65
|
+
// eslint-disable-next-line valid-typeof
|
|
66
|
+
if (typeof value === schema) {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
validate.errors = [{ message: `"${data.parentDataProperty}" should be a ${schema}.`, params: data.parentData }]
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return ajv.compile(schema)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
The schema is imported through the capability's light subpath, resolved application-scoped first
|
|
80
|
+
with the runtime-bundled fallback — the canonical capability resolution order, so the schema copy
|
|
81
|
+
that validates is the same copy whose implementation the worker will load.
|
|
82
|
+
|
|
83
|
+
The subpath is part of the v4 capability contract, and it is light only in import cost: it
|
|
84
|
+
executes in the main process with full privileges, like any capability code. Falling back to the
|
|
85
|
+
package's main entry is a transitional step: until every capability ships the subpath, boot would
|
|
86
|
+
otherwise not be able to validate at all, and a validator that skips what it cannot import is not
|
|
87
|
+
a validator. Removing the fallback is part of the capability work.
|
|
88
|
+
*/
|
|
89
|
+
export async function importCapabilitySchema (module, applicationRoot, { runtimeScope } = {}) {
|
|
90
|
+
const scopes = [
|
|
91
|
+
{ scope: 'application', require: createRequire(join(applicationRoot, 'noop.js')) },
|
|
92
|
+
// The bundled fallback resolves from the caller's position, not from this module's: foundation
|
|
93
|
+
// is the lowest package in the graph and depends on no capability, so resolving from here would
|
|
94
|
+
// make "runtime-bundled" name a place no capability has ever been installed.
|
|
95
|
+
{ scope: 'runtime', require: createRequire(runtimeScope ?? import.meta.filename) }
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
for (const { scope, require } of scopes) {
|
|
99
|
+
for (const [via, specifier] of [
|
|
100
|
+
['subpath', `${module}/schema`],
|
|
101
|
+
['entry', module]
|
|
102
|
+
]) {
|
|
103
|
+
let resolved
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
resolved = require.resolve(specifier)
|
|
107
|
+
} catch {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
A package without an exports map resolves <name>/schema through the filesystem, and the
|
|
113
|
+
extension search finds schema.json -- the generated JSON Schema that sits at the root of
|
|
114
|
+
most capabilities -- before anything else. That file is data, not the subpath: importing it
|
|
115
|
+
as a module fails for needing an import attribute, and even with one it carries none of the
|
|
116
|
+
metadata the contract asks the subpath to export. Skipping it lets the entry fallback
|
|
117
|
+
answer, which is where an exports-less capability keeps its schema.
|
|
118
|
+
*/
|
|
119
|
+
if (!/\.(js|mjs|cjs|ts|mts|cts)$/.test(resolved)) {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const loaded = await import(pathToFileURL(resolved).href)
|
|
124
|
+
|
|
125
|
+
if (loaded?.schema) {
|
|
126
|
+
return {
|
|
127
|
+
scope,
|
|
128
|
+
via,
|
|
129
|
+
path: resolved,
|
|
130
|
+
schema: loaded.schema,
|
|
131
|
+
// The package-level metadata main-side preparation needs besides the schema. An absent
|
|
132
|
+
// servesWithoutPort means 'worker', which is what the serving predicate reads.
|
|
133
|
+
metadata: {
|
|
134
|
+
skipTelemetryHooks: loaded.skipTelemetryHooks ?? false,
|
|
135
|
+
modulesToLoad: loaded.modulesToLoad ?? [],
|
|
136
|
+
servesWithoutPort: loaded.servesWithoutPort ?? 'worker'
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new CapabilitySchemaNotFoundError(module, applicationRoot)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
AJV puts the offending property name in params rather than in the message, so the commonest
|
|
148
|
+
mistake of all — a typo in an option name — reads as "must NOT have additional properties" and
|
|
149
|
+
leaves the author to find which one. Naming it is the difference between an error you can act on
|
|
150
|
+
and one you have to bisect.
|
|
151
|
+
*/
|
|
152
|
+
function describeFailure (error) {
|
|
153
|
+
if (error.params?.additionalProperty) {
|
|
154
|
+
return `must NOT have the additional property '${error.params.additionalProperty}'`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (error.params?.allowedValues) {
|
|
158
|
+
return `${error.message} (${error.params.allowedValues.join(', ')})`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return error.message
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function validateCapabilityConfiguration (config, schema, { id, module, root, fixPaths = true } = {}) {
|
|
165
|
+
const validator = createCapabilityValidator(schema, { root, fixPaths })
|
|
166
|
+
|
|
167
|
+
if (validator(config)) {
|
|
168
|
+
return config
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const failures = validator.errors.map(error => ({
|
|
172
|
+
path: error.instancePath === '' ? '/' : error.instancePath,
|
|
173
|
+
message: describeFailure(error),
|
|
174
|
+
params: error.params
|
|
175
|
+
}))
|
|
176
|
+
|
|
177
|
+
const error = new InvalidApplicationConfigurationError(
|
|
178
|
+
id,
|
|
179
|
+
module,
|
|
180
|
+
failures.map(failure => `\n - ${failure.path}: ${failure.message}`).join('')
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
Object.defineProperty(error, 'validationErrors', { value: failures })
|
|
184
|
+
throw error
|
|
185
|
+
}
|