@platformatic/foundation 3.0.0-alpha.2
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/LICENSE +201 -0
- package/NOTICE +13 -0
- package/eslint.config.js +64 -0
- package/index.d.ts +248 -0
- package/index.js +12 -0
- package/lib/cli.js +231 -0
- package/lib/configuration.js +525 -0
- package/lib/errors.js +58 -0
- package/lib/execution.js +13 -0
- package/lib/file-system.js +188 -0
- package/lib/logger.js +180 -0
- package/lib/module.js +175 -0
- package/lib/node.js +24 -0
- package/lib/object.js +35 -0
- package/lib/schema.js +1189 -0
- package/lib/string.js +95 -0
- package/package.json +53 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import toml from '@iarna/toml'
|
|
2
|
+
import Ajv from 'ajv'
|
|
3
|
+
import { parse as parseEnvFile } from 'dotenv'
|
|
4
|
+
import jsonPatch from 'fast-json-patch'
|
|
5
|
+
import JSON5 from 'json5'
|
|
6
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { dirname, extname, isAbsolute, parse, resolve } from 'node:path'
|
|
9
|
+
import { parse as rawParseYAML, stringify as stringifyYAML } from 'yaml'
|
|
10
|
+
import {
|
|
11
|
+
AddAModulePropertyToTheConfigOrAddAKnownSchemaError,
|
|
12
|
+
CannotParseConfigFileError,
|
|
13
|
+
ConfigurationDoesNotValidateAgainstSchemaError,
|
|
14
|
+
InvalidConfigFileExtensionError,
|
|
15
|
+
RootMissingError,
|
|
16
|
+
SourceMissingError
|
|
17
|
+
} from './errors.js'
|
|
18
|
+
import { isFileAccessible } from './file-system.js'
|
|
19
|
+
import { loadModule, splitModuleFromVersion } from './module.js'
|
|
20
|
+
|
|
21
|
+
const { parse: parseJSON5, stringify: rawStringifyJSON5 } = JSON5
|
|
22
|
+
const { parse: parseTOML, stringify: stringifyTOML } = toml
|
|
23
|
+
|
|
24
|
+
const kReplaceEnvIgnore = Symbol('plt.foundation.replaceEnvIgnore')
|
|
25
|
+
|
|
26
|
+
export const kMetadata = Symbol('plt.foundation.metadata')
|
|
27
|
+
export const envVariablePattern = /(?:\{{1,2})([a-z0-9_]+)(?:\}{1,2})/i
|
|
28
|
+
|
|
29
|
+
export const knownConfigurationFilesExtensions = ['json', 'json5', 'yaml', 'yml', 'toml', 'tml']
|
|
30
|
+
|
|
31
|
+
// Important: do not put $ in any RegExp since we might use the querystring to deliver additional information
|
|
32
|
+
export const knownConfigurationFilesSchemas = [
|
|
33
|
+
/^https:\/\/platformatic.dev\/schemas\/(v?)(?<version>[^/]+)\/(?<module>.*)/,
|
|
34
|
+
/^https:\/\/schemas.platformatic.dev\/@platformatic\/(?<module>.*)\/(v?)(?<version>[^/]+)\.json/,
|
|
35
|
+
/^https:\/\/schemas.platformatic.dev\/(?<module>wattpm)\/(v?)(?<version>[^/]+)\.json/
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
function parseYAML (raw, ...args) {
|
|
39
|
+
const bracesRegexp = /{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}/gi
|
|
40
|
+
const stringRegexp = /(["'])(?:(?=(\\?))\2.)*?\1/gi
|
|
41
|
+
|
|
42
|
+
const stringMatches = [...raw.matchAll(stringRegexp)]
|
|
43
|
+
|
|
44
|
+
raw = raw.replace(bracesRegexp, (match, p1, offset) => {
|
|
45
|
+
for (const stringMatch of stringMatches) {
|
|
46
|
+
const stringStart = stringMatch.index
|
|
47
|
+
const stringEnd = stringMatch.index + stringMatch[0].length
|
|
48
|
+
if (offset >= stringStart && offset <= stringEnd) return match
|
|
49
|
+
}
|
|
50
|
+
return `'${match}'`
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return rawParseYAML(raw, ...args)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function stringifyJSON (data) {
|
|
57
|
+
return JSON.stringify(data, null, 2)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function stringifyJSON5 (data) {
|
|
61
|
+
return rawStringifyJSON5(data, null, 2)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getParser (path) {
|
|
65
|
+
let parser
|
|
66
|
+
|
|
67
|
+
switch (extname(path)) {
|
|
68
|
+
case '.yaml':
|
|
69
|
+
case '.yml':
|
|
70
|
+
parser = parseYAML
|
|
71
|
+
break
|
|
72
|
+
case '.json':
|
|
73
|
+
parser = JSON.parse
|
|
74
|
+
break
|
|
75
|
+
case '.json5':
|
|
76
|
+
parser = parseJSON5
|
|
77
|
+
break
|
|
78
|
+
case '.toml':
|
|
79
|
+
case '.tml':
|
|
80
|
+
parser = parseTOML
|
|
81
|
+
break
|
|
82
|
+
default:
|
|
83
|
+
throw new InvalidConfigFileExtensionError()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return parser
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getStringifier (path) {
|
|
90
|
+
let stringifer
|
|
91
|
+
switch (extname(path)) {
|
|
92
|
+
case '.yaml':
|
|
93
|
+
case '.yml':
|
|
94
|
+
stringifer = stringifyYAML
|
|
95
|
+
break
|
|
96
|
+
case '.json':
|
|
97
|
+
stringifer = stringifyJSON
|
|
98
|
+
break
|
|
99
|
+
case '.json5':
|
|
100
|
+
stringifer = stringifyJSON5
|
|
101
|
+
break
|
|
102
|
+
case '.toml':
|
|
103
|
+
case '.tml':
|
|
104
|
+
stringifer = stringifyTOML
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
throw new InvalidConfigFileExtensionError()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return stringifer
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function printValidationErrors (err) {
|
|
114
|
+
const tabularData = err.validation.map(err => {
|
|
115
|
+
return { path: err.path, message: err.message }
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
console.table(tabularData, ['path', 'message'])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function listRecognizedConfigurationFiles (suffixes, extensions) {
|
|
122
|
+
if (typeof suffixes === 'undefined' || suffixes === null) {
|
|
123
|
+
suffixes = ['runtime', 'service', 'application', 'db', 'composer']
|
|
124
|
+
} else if (suffixes && !Array.isArray(suffixes)) {
|
|
125
|
+
suffixes = [suffixes]
|
|
126
|
+
} else if (!suffixes) {
|
|
127
|
+
suffixes = []
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!extensions) {
|
|
131
|
+
extensions = knownConfigurationFilesExtensions
|
|
132
|
+
} else if (!Array.isArray(extensions)) {
|
|
133
|
+
extensions = [extensions]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const files = []
|
|
137
|
+
|
|
138
|
+
for (const ext of extensions) {
|
|
139
|
+
files.push(`watt.${ext}`)
|
|
140
|
+
files.push(`platformatic.${ext}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const suffix of suffixes) {
|
|
144
|
+
for (const ext of extensions) {
|
|
145
|
+
files.push(`watt.${suffix}.${ext}`)
|
|
146
|
+
files.push(`platformatic.${suffix}.${ext}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return files
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function extractModuleFromSchemaUrl (config, throwOnMissing = false) {
|
|
154
|
+
if (typeof config?.module === 'string') {
|
|
155
|
+
return config
|
|
156
|
+
} else if (typeof config?.$schema !== 'string') {
|
|
157
|
+
if (throwOnMissing) {
|
|
158
|
+
throw new AddAModulePropertyToTheConfigOrAddAKnownSchemaError()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const matching = knownConfigurationFilesSchemas.map(matcher => config.$schema.match(matcher)).find(m => m)
|
|
165
|
+
|
|
166
|
+
if (!matching) {
|
|
167
|
+
if (throwOnMissing) {
|
|
168
|
+
throw new AddAModulePropertyToTheConfigOrAddAKnownSchemaError()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const mod = matching.groups.module
|
|
175
|
+
const version = matching.groups.version
|
|
176
|
+
|
|
177
|
+
return { module: `@platformatic/${mod === 'wattpm' ? 'runtime' : mod}`, version }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function findConfigurationFile (root, suffixes, extensions, candidates) {
|
|
181
|
+
if (!candidates) {
|
|
182
|
+
candidates = listRecognizedConfigurationFiles(suffixes, extensions)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const existingFiles = await Promise.all(
|
|
186
|
+
candidates.map(async fileName => {
|
|
187
|
+
const accessible = await isFileAccessible(fileName, root)
|
|
188
|
+
return accessible ? fileName : null
|
|
189
|
+
})
|
|
190
|
+
)
|
|
191
|
+
return existingFiles.find(v => v !== null) || null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function findConfigurationFileRecursive (root, configurationFile, schemas, suffixes) {
|
|
195
|
+
if (schemas && !Array.isArray(schemas)) {
|
|
196
|
+
schemas = [schemas]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let current = root
|
|
200
|
+
|
|
201
|
+
const candidates = listRecognizedConfigurationFiles(suffixes)
|
|
202
|
+
|
|
203
|
+
while (!configurationFile) {
|
|
204
|
+
// Find a wattpm.json or watt.json file
|
|
205
|
+
configurationFile = await findConfigurationFile(current, null, null, candidates)
|
|
206
|
+
|
|
207
|
+
// If a file is found, verify it actually represents a watt or runtime configuration
|
|
208
|
+
if (configurationFile) {
|
|
209
|
+
const configuration = await loadConfigurationFile(resolve(current, configurationFile))
|
|
210
|
+
|
|
211
|
+
if (schemas) {
|
|
212
|
+
const schemaMatch = extractModuleFromSchemaUrl(configuration)
|
|
213
|
+
/* c8 ignore next - else */
|
|
214
|
+
const moduleMatch = typeof schemaMatch === 'string' ? schemaMatch : schemaMatch?.module
|
|
215
|
+
if (!schemas.includes(moduleMatch)) {
|
|
216
|
+
configurationFile = null
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!configurationFile) {
|
|
222
|
+
const newCurrent = dirname(current)
|
|
223
|
+
|
|
224
|
+
if (newCurrent === current) {
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
current = newCurrent
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (typeof configurationFile !== 'string') {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const resolved = resolve(current, configurationFile)
|
|
237
|
+
return resolved
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function loadConfigurationFile (configurationFile) {
|
|
241
|
+
try {
|
|
242
|
+
const parse = getParser(configurationFile)
|
|
243
|
+
return parse(await readFile(configurationFile, 'utf-8'))
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new CannotParseConfigFileError(configurationFile, error, { cause: error })
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function saveConfigurationFile (configurationFile, config) {
|
|
250
|
+
const stringifer = getStringifier(configurationFile)
|
|
251
|
+
return writeFile(configurationFile, stringifer(config), 'utf-8')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createValidator (schema, validationOptions, context = {}) {
|
|
255
|
+
const ajv = new Ajv(validationOptions)
|
|
256
|
+
|
|
257
|
+
ajv.addKeyword({
|
|
258
|
+
keyword: 'resolvePath',
|
|
259
|
+
type: 'string',
|
|
260
|
+
schemaType: 'boolean',
|
|
261
|
+
// TODO@ShogunPanda: figure out how to implement this via the new `code` option in Ajv
|
|
262
|
+
validate: (schema, path, parentSchema, data) => {
|
|
263
|
+
if (typeof path !== 'string' || path.trim() === '') {
|
|
264
|
+
return !!parentSchema.allowEmptyPaths
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (context.fixPaths !== false) {
|
|
268
|
+
const resolved = resolve(context.root, path)
|
|
269
|
+
data.parentData[data.parentDataProperty] = resolved
|
|
270
|
+
}
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
ajv.addKeyword({ keyword: 'allowEmptyPaths', type: 'string', schemaType: 'boolean' })
|
|
276
|
+
|
|
277
|
+
ajv.addKeyword({
|
|
278
|
+
keyword: 'resolveModule',
|
|
279
|
+
type: 'string',
|
|
280
|
+
schemaType: 'boolean',
|
|
281
|
+
// TODO@ShogunPanda: figure out how to implement this via the new `code` option in Ajv
|
|
282
|
+
validate: (_schema, path, _parentSchema, data) => {
|
|
283
|
+
if (typeof path !== 'string' || path.trim() === '') {
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (context.fixPaths === false) {
|
|
288
|
+
return true
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const require = createRequire(resolve(context.root, 'noop.js'))
|
|
292
|
+
try {
|
|
293
|
+
const resolved = require.resolve(path)
|
|
294
|
+
data.parentData[data.parentDataProperty] = resolved
|
|
295
|
+
return true
|
|
296
|
+
} catch {
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
ajv.addKeyword({
|
|
303
|
+
keyword: 'typeof',
|
|
304
|
+
validate: function validate (schema, value, _, data) {
|
|
305
|
+
// eslint-disable-next-line valid-typeof
|
|
306
|
+
if (typeof value === schema) {
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
validate.errors = [{ message: `"${data.parentDataProperty}" shoud be a ${schema}.`, params: data.parentData }]
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
return ajv.compile(schema)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function loadEnv (root, ignoreProcessEnv = false, additionalEnv = {}) {
|
|
319
|
+
if (!isAbsolute(root)) {
|
|
320
|
+
root = resolve(process.cwd(), root)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let currentPath = root
|
|
324
|
+
const rootPath = parse(root).root
|
|
325
|
+
|
|
326
|
+
// Search for the .env file in the current directory and its parents
|
|
327
|
+
let envFile
|
|
328
|
+
while (currentPath !== rootPath) {
|
|
329
|
+
const candidate = resolve(currentPath, '.env')
|
|
330
|
+
|
|
331
|
+
if (await isFileAccessible(candidate)) {
|
|
332
|
+
envFile = candidate
|
|
333
|
+
break
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
currentPath = dirname(currentPath)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// If not found, check the current working directory
|
|
340
|
+
if (!envFile) {
|
|
341
|
+
const cwdCandidate = resolve(process.cwd(), '.env')
|
|
342
|
+
|
|
343
|
+
if (await isFileAccessible(cwdCandidate)) {
|
|
344
|
+
envFile = cwdCandidate
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const baseEnv = ignoreProcessEnv ? {} : process.env
|
|
349
|
+
const envFromFile = envFile ? parseEnvFile(await readFile(envFile, 'utf-8')) : {}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
...baseEnv,
|
|
353
|
+
...additionalEnv,
|
|
354
|
+
...envFromFile
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function replaceEnv (config, env, onMissingEnv, ignore) {
|
|
359
|
+
// First of all, apply the ignore list
|
|
360
|
+
if (ignore) {
|
|
361
|
+
for (let path of ignore) {
|
|
362
|
+
// Migrate JSON Path to JSON Pointer
|
|
363
|
+
if (path.startsWith('$')) {
|
|
364
|
+
path = '/' + path.slice(2).replaceAll('.', '/')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const value = jsonPatch.getValueByPointer(config, path)
|
|
369
|
+
|
|
370
|
+
if (typeof value !== 'undefined') {
|
|
371
|
+
jsonPatch.applyOperation(config, {
|
|
372
|
+
op: 'add',
|
|
373
|
+
path,
|
|
374
|
+
value: { [kReplaceEnvIgnore]: true, originalValue: value }
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
// No-op, the path does not exist
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (typeof config === 'object' && config !== null) {
|
|
384
|
+
if (config[kReplaceEnvIgnore]) {
|
|
385
|
+
return config.originalValue
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (const key of Object.keys(config)) {
|
|
389
|
+
config[key] = replaceEnv(config[key], env, onMissingEnv)
|
|
390
|
+
}
|
|
391
|
+
} else if (typeof config === 'string') {
|
|
392
|
+
let matches = config.match(envVariablePattern)
|
|
393
|
+
|
|
394
|
+
while (matches) {
|
|
395
|
+
const [template, key] = matches
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const replacement = env[key] ?? onMissingEnv?.(key) ?? ''
|
|
399
|
+
config = config.replace(template, replacement)
|
|
400
|
+
|
|
401
|
+
matches = config.match(envVariablePattern)
|
|
402
|
+
} catch (error) {
|
|
403
|
+
throw new CannotParseConfigFileError(error.message, { cause: error })
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return config
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function loadConfiguration (source, schema, options = {}) {
|
|
412
|
+
const {
|
|
413
|
+
validate,
|
|
414
|
+
validationOptions,
|
|
415
|
+
transform,
|
|
416
|
+
upgrade,
|
|
417
|
+
env: additionalEnv,
|
|
418
|
+
ignoreProcessEnv,
|
|
419
|
+
replaceEnv: shouldReplaceEnv,
|
|
420
|
+
replaceEnvIgnore,
|
|
421
|
+
onMissingEnv,
|
|
422
|
+
fixPaths,
|
|
423
|
+
logger,
|
|
424
|
+
skipMetadata
|
|
425
|
+
} = {
|
|
426
|
+
validate: !!schema,
|
|
427
|
+
validationOptions: {},
|
|
428
|
+
ignoreProcessEnv: false,
|
|
429
|
+
replaceEnv: true,
|
|
430
|
+
replaceEnvIgnore: [],
|
|
431
|
+
fixPaths: true,
|
|
432
|
+
...options
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let root = options.root
|
|
436
|
+
|
|
437
|
+
if (typeof source === 'undefined') {
|
|
438
|
+
throw new SourceMissingError()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let config = source
|
|
442
|
+
if (typeof source === 'string') {
|
|
443
|
+
root = dirname(source)
|
|
444
|
+
config = await loadConfigurationFile(source)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!root) {
|
|
448
|
+
throw new RootMissingError()
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const env = await loadEnv(root, ignoreProcessEnv, additionalEnv)
|
|
452
|
+
env.PLT_ROOT = root
|
|
453
|
+
|
|
454
|
+
if (shouldReplaceEnv) {
|
|
455
|
+
config = replaceEnv(config, env, onMissingEnv, replaceEnvIgnore)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const moduleInfo = extractModuleFromSchemaUrl(config)
|
|
459
|
+
|
|
460
|
+
if (upgrade) {
|
|
461
|
+
let version = moduleInfo?.version
|
|
462
|
+
|
|
463
|
+
if (!version && config.module) {
|
|
464
|
+
version = splitModuleFromVersion(config.module).version
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (version) {
|
|
468
|
+
config = await upgrade(logger, config, version)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (validate) {
|
|
473
|
+
if (typeof schema === 'undefined') {
|
|
474
|
+
throw new SourceMissingError()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const validator = createValidator(schema, validationOptions, { root, fixPaths })
|
|
478
|
+
const valid = validator(config)
|
|
479
|
+
|
|
480
|
+
if (!valid) {
|
|
481
|
+
const validationErrors = []
|
|
482
|
+
let errors = ':'
|
|
483
|
+
|
|
484
|
+
for (const validationError of validator.errors) {
|
|
485
|
+
/* c8 ignore next - else */
|
|
486
|
+
const path = validationError.instancePath === '' ? '/' : validationError.instancePath
|
|
487
|
+
|
|
488
|
+
validationErrors.push({ path, message: validationError.message, params: validationError.params })
|
|
489
|
+
errors += `\n - ${path}: ${validationError.message}`
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const error = new ConfigurationDoesNotValidateAgainstSchemaError()
|
|
493
|
+
error.message += errors + '\n'
|
|
494
|
+
Object.defineProperty(error, 'validationErrors', { value: validationErrors })
|
|
495
|
+
|
|
496
|
+
throw error
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!skipMetadata) {
|
|
501
|
+
config[kMetadata] = {
|
|
502
|
+
root,
|
|
503
|
+
env,
|
|
504
|
+
path: typeof source === 'string' ? source : null,
|
|
505
|
+
module: moduleInfo?.module ?? null
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (typeof transform === 'function') {
|
|
510
|
+
try {
|
|
511
|
+
config = await transform(config, schema, options)
|
|
512
|
+
} catch (error) {
|
|
513
|
+
throw new CannotParseConfigFileError(error.message, { cause: error })
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return config
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function loadConfigurationModule (root, config, pkg) {
|
|
521
|
+
pkg ??= extractModuleFromSchemaUrl(config, true).module
|
|
522
|
+
|
|
523
|
+
const require = createRequire(resolve(root, 'noop.js'))
|
|
524
|
+
return loadModule(require, pkg)
|
|
525
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import createError from '@fastify/error'
|
|
2
|
+
|
|
3
|
+
export const ERROR_PREFIX = 'PLT'
|
|
4
|
+
|
|
5
|
+
export function ensureLoggableError (error) {
|
|
6
|
+
Reflect.defineProperty(error, 'message', { enumerable: true })
|
|
7
|
+
|
|
8
|
+
if ('code' in error) {
|
|
9
|
+
Reflect.defineProperty(error, 'code', { enumerable: true })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if ('stack' in error) {
|
|
13
|
+
Reflect.defineProperty(error, 'stack', { enumerable: true })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return error
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// This is needed if an error is received via ensureLoggableError->postMessage
|
|
20
|
+
export function ensureError (error) {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
return error
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const err = new Error(error.message)
|
|
26
|
+
Object.assign(err, error)
|
|
27
|
+
return err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const PathOptionRequiredError = createError(`${ERROR_PREFIX}_PATH_OPTION_REQUIRED`, 'path option is required')
|
|
31
|
+
export const NoConfigFileFoundError = createError(`${ERROR_PREFIX}_NO_CONFIG_FILE_FOUND`, 'no config file found')
|
|
32
|
+
export const InvalidConfigFileExtensionError = createError(
|
|
33
|
+
`${ERROR_PREFIX}_INVALID_CONFIG_FILE_EXTENSION`,
|
|
34
|
+
'Invalid config file extension. Only yml, yaml, json, json5, toml, tml are supported.'
|
|
35
|
+
)
|
|
36
|
+
export const AddAModulePropertyToTheConfigOrAddAKnownSchemaError = createError(
|
|
37
|
+
`${ERROR_PREFIX}_ADD_A_MODULE_PROPERTY_TO_THE_CONFIG_OR_ADD_A_KNOWN_SCHEMA`,
|
|
38
|
+
'Add a module property to the config or add a known $schema.'
|
|
39
|
+
)
|
|
40
|
+
export const CannotParseConfigFileError = createError(
|
|
41
|
+
`${ERROR_PREFIX}_CANNOT_PARSE_CONFIG_FILE`,
|
|
42
|
+
'Cannot parse config file. %s'
|
|
43
|
+
)
|
|
44
|
+
export const SourceMissingError = createError(`${ERROR_PREFIX}_SOURCE_MISSING`, 'Source missing.')
|
|
45
|
+
export const RootMissingError = createError(
|
|
46
|
+
`${ERROR_PREFIX}_ROOT_MISSING`,
|
|
47
|
+
'Provide the root option to loadConfiguration when using an object as source.'
|
|
48
|
+
)
|
|
49
|
+
export const SchemaMustBeDefinedError = createError(
|
|
50
|
+
`${ERROR_PREFIX}_SCHEMA_MUST_BE_DEFINED`,
|
|
51
|
+
'schema must be defined',
|
|
52
|
+
undefined,
|
|
53
|
+
TypeError
|
|
54
|
+
)
|
|
55
|
+
export const ConfigurationDoesNotValidateAgainstSchemaError = createError(
|
|
56
|
+
`${ERROR_PREFIX}_CONFIGURATION_DOES_NOT_VALIDATE_AGAINST_SCHEMA`,
|
|
57
|
+
'The configuration does not validate against the configuration schema'
|
|
58
|
+
)
|
package/lib/execution.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Unpromise } from '@watchable/unpromise'
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
|
+
|
|
4
|
+
export const kTimeout = Symbol('plt.utils.timeout')
|
|
5
|
+
|
|
6
|
+
export async function executeWithTimeout (promise, timeout, timeoutValue = kTimeout) {
|
|
7
|
+
const ac = new AbortController()
|
|
8
|
+
|
|
9
|
+
return Unpromise.race([promise, sleep(timeout, timeoutValue, { signal: ac.signal, ref: false })]).then(value => {
|
|
10
|
+
ac.abort()
|
|
11
|
+
return value
|
|
12
|
+
})
|
|
13
|
+
}
|