@auraindustry/aurajs 0.0.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/benchmarks/perf-thresholds.json +102 -0
- package/package.json +47 -0
- package/src/.gitkeep +0 -0
- package/src/asset-pack.mjs +316 -0
- package/src/build-contract.mjs +71 -0
- package/src/bundler.mjs +698 -0
- package/src/cli.mjs +764 -0
- package/src/config.mjs +460 -0
- package/src/conformance-runner.mjs +322 -0
- package/src/conformance.mjs +5407 -0
- package/src/headless-test.mjs +4314 -0
- package/src/host-binary.mjs +423 -0
- package/src/perf-benchmark-runner.mjs +233 -0
- package/src/perf-benchmark.mjs +549 -0
- package/src/postinstall.mjs +26 -0
- package/src/scaffold.mjs +74 -0
- package/templates/starter/aura.config.json +28 -0
- package/templates/starter/src/main.js +226 -0
package/src/config.mjs
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Error
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export class ConfigError extends Error {
|
|
10
|
+
constructor(message, details = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'ConfigError';
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Legacy compatibility normalization
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize known legacy/spec-shaped config fields into the current schema.
|
|
23
|
+
*
|
|
24
|
+
* Supported legacy forms:
|
|
25
|
+
* - top-level `name` / `version` -> `identity.name` / `identity.version`
|
|
26
|
+
* - `build.icon` -> `identity.icon`
|
|
27
|
+
* - `build.targets` (ignored; build v1 uses current host target)
|
|
28
|
+
* - `modules: string[]` -> `modules: { physics, network, multiplayer, steam }`
|
|
29
|
+
*/
|
|
30
|
+
function normalizeLegacyConfig(raw) {
|
|
31
|
+
if (raw === null || raw === undefined) return {};
|
|
32
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) return raw;
|
|
33
|
+
|
|
34
|
+
const normalized = { ...raw };
|
|
35
|
+
const legacyIdentity = {};
|
|
36
|
+
|
|
37
|
+
// Legacy metadata keys from older scaffold/spec variants.
|
|
38
|
+
if (typeof normalized.name === 'string') {
|
|
39
|
+
legacyIdentity.name = normalized.name;
|
|
40
|
+
}
|
|
41
|
+
if (typeof normalized.version === 'string') {
|
|
42
|
+
legacyIdentity.version = normalized.version;
|
|
43
|
+
}
|
|
44
|
+
delete normalized.name;
|
|
45
|
+
delete normalized.version;
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
normalized.build !== null &&
|
|
49
|
+
normalized.build !== undefined &&
|
|
50
|
+
typeof normalized.build === 'object' &&
|
|
51
|
+
!Array.isArray(normalized.build)
|
|
52
|
+
) {
|
|
53
|
+
const build = { ...normalized.build };
|
|
54
|
+
delete build.targets;
|
|
55
|
+
if (typeof build.icon === 'string') {
|
|
56
|
+
legacyIdentity.icon = build.icon;
|
|
57
|
+
}
|
|
58
|
+
delete build.icon;
|
|
59
|
+
normalized.build = build;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
normalized.identity !== null &&
|
|
64
|
+
normalized.identity !== undefined &&
|
|
65
|
+
typeof normalized.identity === 'object' &&
|
|
66
|
+
!Array.isArray(normalized.identity)
|
|
67
|
+
) {
|
|
68
|
+
normalized.identity = {
|
|
69
|
+
...legacyIdentity,
|
|
70
|
+
...normalized.identity,
|
|
71
|
+
};
|
|
72
|
+
} else if (Object.keys(legacyIdentity).length > 0) {
|
|
73
|
+
normalized.identity = { ...legacyIdentity };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(normalized.modules)) {
|
|
77
|
+
const modules = {
|
|
78
|
+
physics: false,
|
|
79
|
+
network: false,
|
|
80
|
+
multiplayer: false,
|
|
81
|
+
steam: false,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const entry of normalized.modules) {
|
|
85
|
+
if (typeof entry !== 'string') {
|
|
86
|
+
throw new ConfigError(
|
|
87
|
+
`Invalid aura.config:\n - modules array entries must be strings, got: ${JSON.stringify(entry)}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const key = entry.trim().toLowerCase();
|
|
91
|
+
if (key === 'physics') {
|
|
92
|
+
modules.physics = true;
|
|
93
|
+
} else if (key === 'network' || key === 'net') {
|
|
94
|
+
modules.network = true;
|
|
95
|
+
} else if (key === 'multiplayer' || key === 'mp') {
|
|
96
|
+
modules.multiplayer = true;
|
|
97
|
+
} else if (key === 'steam') {
|
|
98
|
+
modules.steam = true;
|
|
99
|
+
} else if (key.length > 0) {
|
|
100
|
+
throw new ConfigError(
|
|
101
|
+
`Invalid aura.config:\n - Unknown module in legacy modules array: "${entry}"`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
normalized.modules = modules;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
normalized.modules !== null &&
|
|
111
|
+
normalized.modules !== undefined &&
|
|
112
|
+
typeof normalized.modules === 'object' &&
|
|
113
|
+
!Array.isArray(normalized.modules)
|
|
114
|
+
) {
|
|
115
|
+
const modules = { ...normalized.modules };
|
|
116
|
+
if (Object.prototype.hasOwnProperty.call(modules, 'net')) {
|
|
117
|
+
if (!Object.prototype.hasOwnProperty.call(modules, 'network')) {
|
|
118
|
+
modules.network = modules.net;
|
|
119
|
+
}
|
|
120
|
+
delete modules.net;
|
|
121
|
+
}
|
|
122
|
+
normalized.modules = modules;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Schema definition
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Config field descriptors.
|
|
134
|
+
*
|
|
135
|
+
* Each leaf field has:
|
|
136
|
+
* type — "string" | "number" | "boolean" | "enum"
|
|
137
|
+
* default — default value (or a function returning it when mode-dependent)
|
|
138
|
+
* values — (enum only) array of valid values
|
|
139
|
+
* validate — optional extra validation function; return string on error
|
|
140
|
+
*/
|
|
141
|
+
const SCHEMA = {
|
|
142
|
+
identity: {
|
|
143
|
+
name: { type: 'string', default: 'AuraJS Game' },
|
|
144
|
+
version: { type: 'string', default: '0.1.0', validate: semverLike },
|
|
145
|
+
executable: { type: 'string', default: 'game', validate: safeExecutableName },
|
|
146
|
+
icon: { type: 'string', default: null, validate: relativeAssetLikePath },
|
|
147
|
+
},
|
|
148
|
+
window: {
|
|
149
|
+
title: { type: 'string', default: 'AuraJS Game' },
|
|
150
|
+
width: { type: 'number', default: 800, validate: positiveInteger },
|
|
151
|
+
height: { type: 'number', default: 600, validate: positiveInteger },
|
|
152
|
+
resizable: { type: 'boolean', default: true },
|
|
153
|
+
fullscreen: { type: 'boolean', default: false },
|
|
154
|
+
vsync: { type: 'boolean', default: true },
|
|
155
|
+
hidpi: { type: 'boolean', default: true },
|
|
156
|
+
},
|
|
157
|
+
build: {
|
|
158
|
+
entry: { type: 'string', default: 'src/main.js' },
|
|
159
|
+
outDir: { type: 'string', default: 'build' },
|
|
160
|
+
assetDir: { type: 'string', default: 'assets' },
|
|
161
|
+
assetMode: { type: 'enum', default: 'embed', values: ['embed', 'sibling'] },
|
|
162
|
+
minify: { type: 'boolean', default: (mode) => mode !== 'dev' },
|
|
163
|
+
sourcemap: { type: 'boolean', default: (mode) => mode === 'dev' },
|
|
164
|
+
},
|
|
165
|
+
modules: {
|
|
166
|
+
physics: { type: 'boolean', default: false },
|
|
167
|
+
network: { type: 'boolean', default: false },
|
|
168
|
+
multiplayer: { type: 'boolean', default: false },
|
|
169
|
+
steam: { type: 'boolean', default: false },
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Validation helpers
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
function positiveInteger(value, path) {
|
|
178
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
179
|
+
return `${path} must be a positive integer, got: ${JSON.stringify(value)}`;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function semverLike(value, path) {
|
|
185
|
+
const normalized = String(value || '').trim();
|
|
186
|
+
const semverPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
187
|
+
if (!semverPattern.test(normalized)) {
|
|
188
|
+
return `${path} must be a semantic version (for example 1.2.3), got: ${JSON.stringify(value)}`;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function safeExecutableName(value, path) {
|
|
194
|
+
const normalized = String(value || '').trim();
|
|
195
|
+
if (normalized.length === 0) {
|
|
196
|
+
return `${path} must be a non-empty string`;
|
|
197
|
+
}
|
|
198
|
+
if (/[/\\]/.test(normalized)) {
|
|
199
|
+
return `${path} must not include path separators, got: ${JSON.stringify(value)}`;
|
|
200
|
+
}
|
|
201
|
+
if (!/[A-Za-z0-9]/.test(normalized)) {
|
|
202
|
+
return `${path} must include at least one alphanumeric character, got: ${JSON.stringify(value)}`;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function relativeAssetLikePath(value, path) {
|
|
208
|
+
const normalized = String(value || '').trim();
|
|
209
|
+
if (normalized.length === 0) {
|
|
210
|
+
return `${path} must be a non-empty string`;
|
|
211
|
+
}
|
|
212
|
+
if (/^(?:[A-Za-z]+:)?[/\\]{1,2}/.test(normalized)) {
|
|
213
|
+
return `${path} must be a project-relative asset path, got: ${JSON.stringify(value)}`;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Core validation + defaults
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate a raw config object against the schema and merge with defaults.
|
|
224
|
+
*
|
|
225
|
+
* @param {object} raw — User-supplied config (may be partial or empty).
|
|
226
|
+
* @param {object} opts — { mode: "dev" | "build" | "run" }
|
|
227
|
+
* @returns {object} Deeply-frozen config object.
|
|
228
|
+
* @throws {ConfigError} On validation failure.
|
|
229
|
+
*/
|
|
230
|
+
export function validateConfig(raw, opts = {}) {
|
|
231
|
+
const mode = opts.mode || 'build';
|
|
232
|
+
const errors = [];
|
|
233
|
+
const result = {};
|
|
234
|
+
|
|
235
|
+
if (raw !== null && raw !== undefined && (typeof raw !== 'object' || Array.isArray(raw))) {
|
|
236
|
+
throw new ConfigError('Config must be an object (or null/undefined for defaults).');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const source = normalizeLegacyConfig(raw || {});
|
|
240
|
+
const windowTitleExplicitlyConfigured = Boolean(
|
|
241
|
+
source?.window
|
|
242
|
+
&& typeof source.window === 'object'
|
|
243
|
+
&& !Array.isArray(source.window)
|
|
244
|
+
&& Object.prototype.hasOwnProperty.call(source.window, 'title'),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Reject unknown top-level keys.
|
|
248
|
+
for (const key of Object.keys(source)) {
|
|
249
|
+
if (!SCHEMA[key]) {
|
|
250
|
+
errors.push(`Unknown top-level config key: "${key}".`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const [section, fields] of Object.entries(SCHEMA)) {
|
|
255
|
+
const userSection = source[section];
|
|
256
|
+
|
|
257
|
+
if (userSection !== undefined && userSection !== null) {
|
|
258
|
+
if (typeof userSection !== 'object' || Array.isArray(userSection)) {
|
|
259
|
+
errors.push(`"${section}" must be an object.`);
|
|
260
|
+
result[section] = buildSectionDefaults(section, fields, mode);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// Reject unknown keys inside section.
|
|
264
|
+
for (const key of Object.keys(userSection)) {
|
|
265
|
+
if (!fields[key]) {
|
|
266
|
+
errors.push(`Unknown config key: "${section}.${key}".`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
result[section] = {};
|
|
272
|
+
|
|
273
|
+
for (const [field, descriptor] of Object.entries(fields)) {
|
|
274
|
+
const path = `${section}.${field}`;
|
|
275
|
+
const userValue = userSection?.[field];
|
|
276
|
+
|
|
277
|
+
if (userValue === undefined || userValue === null) {
|
|
278
|
+
result[section][field] = resolveDefault(descriptor.default, mode);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Type check.
|
|
283
|
+
const typeError = checkType(descriptor, userValue, path);
|
|
284
|
+
if (typeError) {
|
|
285
|
+
errors.push(typeError);
|
|
286
|
+
result[section][field] = resolveDefault(descriptor.default, mode);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Extra validation.
|
|
291
|
+
if (descriptor.validate) {
|
|
292
|
+
const extra = descriptor.validate(userValue, path);
|
|
293
|
+
if (extra) {
|
|
294
|
+
errors.push(extra);
|
|
295
|
+
result[section][field] = resolveDefault(descriptor.default, mode);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
result[section][field] = userValue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (errors.length > 0) {
|
|
305
|
+
throw new ConfigError(
|
|
306
|
+
`Invalid aura.config:\n${errors.map((e) => ` - ${e}`).join('\n')}`,
|
|
307
|
+
{ errors },
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!windowTitleExplicitlyConfigured && typeof result.identity?.name === 'string') {
|
|
312
|
+
result.window.title = result.identity.name;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return deepFreeze(result);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resolveDefault(def, mode) {
|
|
319
|
+
return typeof def === 'function' ? def(mode) : def;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function buildSectionDefaults(section, fields, mode) {
|
|
323
|
+
const out = {};
|
|
324
|
+
for (const [field, descriptor] of Object.entries(fields)) {
|
|
325
|
+
out[field] = resolveDefault(descriptor.default, mode);
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function checkType(descriptor, value, path) {
|
|
331
|
+
switch (descriptor.type) {
|
|
332
|
+
case 'string':
|
|
333
|
+
if (typeof value !== 'string') {
|
|
334
|
+
return `${path} must be a string, got: ${JSON.stringify(value)}`;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
|
|
338
|
+
case 'number':
|
|
339
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
340
|
+
return `${path} must be a finite number, got: ${JSON.stringify(value)}`;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
|
|
344
|
+
case 'boolean':
|
|
345
|
+
if (typeof value !== 'boolean') {
|
|
346
|
+
return `${path} must be a boolean, got: ${JSON.stringify(value)}`;
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
|
|
350
|
+
case 'enum':
|
|
351
|
+
if (!descriptor.values.includes(value)) {
|
|
352
|
+
return `${path} must be one of [${descriptor.values.map((v) => `"${v}"`).join(', ')}], got: ${JSON.stringify(value)}`;
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
|
|
356
|
+
default:
|
|
357
|
+
return `${path}: unknown schema type "${descriptor.type}".`;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Deep freeze
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
function deepFreeze(obj) {
|
|
366
|
+
Object.freeze(obj);
|
|
367
|
+
for (const value of Object.values(obj)) {
|
|
368
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
369
|
+
deepFreeze(value);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return obj;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// File loading
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
const CONFIG_NAMES = ['aura.config.js', 'aura.config.mjs', 'aura.config.json'];
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Load an aura config from the project root.
|
|
383
|
+
*
|
|
384
|
+
* Searches for aura.config.js, aura.config.mjs, aura.config.json in order.
|
|
385
|
+
* Falls back to all defaults when no file is found.
|
|
386
|
+
*
|
|
387
|
+
* @param {object} opts
|
|
388
|
+
* @param {string} opts.projectRoot — Absolute path to project root.
|
|
389
|
+
* @param {string} opts.mode — "dev" | "build" | "run".
|
|
390
|
+
* @returns {Promise<object>} Deeply-frozen config object.
|
|
391
|
+
* @throws {ConfigError} On invalid config.
|
|
392
|
+
*/
|
|
393
|
+
export async function loadConfig(opts = {}) {
|
|
394
|
+
const projectRoot = resolve(opts.projectRoot || process.cwd());
|
|
395
|
+
const mode = opts.mode || 'build';
|
|
396
|
+
|
|
397
|
+
for (const name of CONFIG_NAMES) {
|
|
398
|
+
const filePath = join(projectRoot, name);
|
|
399
|
+
if (!existsSync(filePath)) continue;
|
|
400
|
+
|
|
401
|
+
let raw;
|
|
402
|
+
|
|
403
|
+
if (name.endsWith('.json')) {
|
|
404
|
+
try {
|
|
405
|
+
const text = readFileSync(filePath, 'utf8');
|
|
406
|
+
raw = JSON.parse(text);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
throw new ConfigError(`Failed to parse ${name}: ${err.message}`, { file: filePath });
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
// .js or .mjs — use dynamic import.
|
|
412
|
+
try {
|
|
413
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
414
|
+
const mod = await import(fileUrl);
|
|
415
|
+
raw = mod.default !== undefined ? mod.default : mod;
|
|
416
|
+
} catch (err) {
|
|
417
|
+
throw new ConfigError(`Failed to load ${name}: ${err.message}`, { file: filePath });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return validateConfig(raw, { mode });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// No config file found — return defaults.
|
|
425
|
+
return validateConfig({}, { mode });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Synchronous variant for .json-only loading (or defaults).
|
|
430
|
+
* Falls back to async loadConfig if a .js/.mjs file is found.
|
|
431
|
+
*
|
|
432
|
+
* @param {object} opts
|
|
433
|
+
* @returns {object|null} Frozen config, or null if async loading is required.
|
|
434
|
+
*/
|
|
435
|
+
export function loadConfigSync(opts = {}) {
|
|
436
|
+
const projectRoot = resolve(opts.projectRoot || process.cwd());
|
|
437
|
+
const mode = opts.mode || 'build';
|
|
438
|
+
|
|
439
|
+
for (const name of CONFIG_NAMES) {
|
|
440
|
+
const filePath = join(projectRoot, name);
|
|
441
|
+
if (!existsSync(filePath)) continue;
|
|
442
|
+
|
|
443
|
+
if (!name.endsWith('.json')) {
|
|
444
|
+
// Cannot synchronously load .js/.mjs — caller must use async loadConfig.
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let raw;
|
|
449
|
+
try {
|
|
450
|
+
const text = readFileSync(filePath, 'utf8');
|
|
451
|
+
raw = JSON.parse(text);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
throw new ConfigError(`Failed to parse ${name}: ${err.message}`, { file: filePath });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return validateConfig(raw, { mode });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return validateConfig({}, { mode });
|
|
460
|
+
}
|