@sebastianwessel/isostate-cli 0.1.0
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/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +637 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/diagnostics.d.ts +18 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +635 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +3 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/runtime-digest.d.ts +4 -0
- package/dist/runtime-digest.d.ts.map +1 -0
- package/dist/static-bundle.d.ts +3 -0
- package/dist/static-bundle.d.ts.map +1 -0
- package/package.json +44 -0
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, rm, mkdir, writeFile, rename, stat, copyFile } from 'node:fs/promises';
|
|
3
|
+
import { extname, dirname, basename, join, resolve, isAbsolute, sep } from 'node:path';
|
|
4
|
+
import { fromJson, fromJs, parseScene, validateScene, compileScene, toJs, toJson } from '@sebastianwessel/isostate/dsl';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
function formatValidationError(error) {
|
|
8
|
+
return formatIssue('ERROR', error);
|
|
9
|
+
}
|
|
10
|
+
function formatValidationWarning(warning) {
|
|
11
|
+
return formatIssue('WARN', warning);
|
|
12
|
+
}
|
|
13
|
+
function formatThrownError(error) {
|
|
14
|
+
if (isStructuredError(error) && error.code) {
|
|
15
|
+
return `ERROR ${error.code} ${error.message}`;
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return `ERROR CLI_ERROR ${error.message}`;
|
|
19
|
+
}
|
|
20
|
+
return 'ERROR CLI_ERROR Unknown CLI failure';
|
|
21
|
+
}
|
|
22
|
+
function formatIssue(level, issue) {
|
|
23
|
+
const location = issue.location ? ` ${formatLocation(issue.location)}` : '';
|
|
24
|
+
return `${level} ${issue.code}${location} ${issue.message}`;
|
|
25
|
+
}
|
|
26
|
+
function formatLocation(location) {
|
|
27
|
+
const parts = [
|
|
28
|
+
location.file,
|
|
29
|
+
location.line === undefined ? undefined : String(location.line),
|
|
30
|
+
location.column === undefined ? undefined : String(location.column)
|
|
31
|
+
].filter((part) => part !== undefined && part.length > 0);
|
|
32
|
+
return parts.length === 0 ? '' : `(${parts.join(':')})`;
|
|
33
|
+
}
|
|
34
|
+
function isStructuredError(error) {
|
|
35
|
+
return error instanceof Error && 'code' in error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runtimeDigest(bundle) {
|
|
39
|
+
const { _digest, ...bundleWithoutDigest } = bundle;
|
|
40
|
+
return sha256Hex(canonicalStringify(bundleWithoutDigest));
|
|
41
|
+
}
|
|
42
|
+
function sha256Hex(input) {
|
|
43
|
+
return createHash('sha256').update(input).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
function canonicalStringify(value) {
|
|
46
|
+
return JSON.stringify(normalizeValue(value));
|
|
47
|
+
}
|
|
48
|
+
function normalizeValue(value) {
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
return value.map((item) => item === undefined ? null : normalizeValue(item));
|
|
51
|
+
}
|
|
52
|
+
if (!isPlainObject(value))
|
|
53
|
+
return value;
|
|
54
|
+
const normalized = {};
|
|
55
|
+
for (const key of Object.keys(value).sort()) {
|
|
56
|
+
const child = value[key];
|
|
57
|
+
if (child !== undefined) {
|
|
58
|
+
normalized[key] = normalizeValue(child);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
function isPlainObject(value) {
|
|
64
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function inspectCommand(args, io) {
|
|
68
|
+
const parsed = parseInspectArgs(args);
|
|
69
|
+
if (!parsed.ok) {
|
|
70
|
+
io.stderr.error(parsed.error);
|
|
71
|
+
return { exitCode: 1 };
|
|
72
|
+
}
|
|
73
|
+
const source = await readFile(parsed.input, 'utf8');
|
|
74
|
+
const bundle = parseBundle(parsed.input, source);
|
|
75
|
+
validateDigest(bundle);
|
|
76
|
+
const assetCount = Object.keys(bundle.assets ?? {}).length;
|
|
77
|
+
const floorSize = bundle.floor.size.join('x');
|
|
78
|
+
io.stdout.log(`format: ${bundle._format}`);
|
|
79
|
+
io.stdout.log(`version: ${bundle._version}`);
|
|
80
|
+
io.stdout.log(`scenes: ${bundle.scenes.length}`);
|
|
81
|
+
io.stdout.log(`assets: ${assetCount}`);
|
|
82
|
+
io.stdout.log(`layers: ${bundle.layers.length}`);
|
|
83
|
+
io.stdout.log(`floor: ${floorSize}`);
|
|
84
|
+
io.stdout.log(`digest: ${bundle._digest}`);
|
|
85
|
+
return { exitCode: 0 };
|
|
86
|
+
}
|
|
87
|
+
function parseInspectArgs(args) {
|
|
88
|
+
const positionals = args.filter((arg) => !arg.startsWith('-'));
|
|
89
|
+
const input = positionals.at(0);
|
|
90
|
+
if (!input) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: 'ERROR MISSING_INPUT Expected a runtime bundle file'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (positionals.length > 1) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: 'ERROR EXTRA_INPUT Expected exactly one runtime bundle file'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const unknown = args.find((arg) => arg.startsWith('-'));
|
|
103
|
+
if (unknown) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: `ERROR UNKNOWN_OPTION Unknown option ${unknown}`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, input };
|
|
110
|
+
}
|
|
111
|
+
function parseBundle(path, source) {
|
|
112
|
+
const extension = extname(path).toLowerCase();
|
|
113
|
+
if (extension === '.json')
|
|
114
|
+
return fromJson(source);
|
|
115
|
+
if (extension === '.js')
|
|
116
|
+
return fromJs(source);
|
|
117
|
+
const error = new Error(`Unsupported runtime bundle extension for ${path}`);
|
|
118
|
+
Object.defineProperty(error, 'code', {
|
|
119
|
+
value: 'UNSUPPORTED_BUNDLE_FORMAT',
|
|
120
|
+
enumerable: true
|
|
121
|
+
});
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
function validateDigest(bundle) {
|
|
125
|
+
if (bundle._format !== 'isostate-runtime-bundle' || !bundle._version) {
|
|
126
|
+
const error = new Error('Invalid runtime bundle identity fields');
|
|
127
|
+
Object.defineProperty(error, 'code', {
|
|
128
|
+
value: 'INVALID_RUNTIME_BUNDLE',
|
|
129
|
+
enumerable: true
|
|
130
|
+
});
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
if (runtimeDigest(bundle) !== bundle._digest) {
|
|
134
|
+
const error = new Error('Invalid runtime bundle digest');
|
|
135
|
+
Object.defineProperty(error, 'code', {
|
|
136
|
+
value: 'INVALID_RUNTIME_BUNDLE_DIGEST',
|
|
137
|
+
enumerable: true
|
|
138
|
+
});
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const DEFAULT_PUBLIC_ASSET_BASE = './assets';
|
|
144
|
+
const DEFAULT_SCENE_NAME = 'scene';
|
|
145
|
+
const RUNTIME_FILE = 'isostate.runtime.js';
|
|
146
|
+
const RUNTIME_SOURCE = new URL('../../core/dist/browser/isostate.runtime.js', import.meta.url);
|
|
147
|
+
const PACKAGE_JSON = new URL('../package.json', import.meta.url);
|
|
148
|
+
async function bundleCommand(args, io) {
|
|
149
|
+
const parsed = parseBundleArgs(args);
|
|
150
|
+
if (!parsed.ok) {
|
|
151
|
+
io.stderr.error(parsed.error);
|
|
152
|
+
return { exitCode: 1 };
|
|
153
|
+
}
|
|
154
|
+
const source = await readFile(parsed.input, 'utf8');
|
|
155
|
+
const document = parseScene(source);
|
|
156
|
+
const report = validateScene(document);
|
|
157
|
+
for (const warning of report.warnings) {
|
|
158
|
+
io.stderr.error(formatValidationWarning(warning));
|
|
159
|
+
}
|
|
160
|
+
if (!report.isValid) {
|
|
161
|
+
for (const error of report.errors) {
|
|
162
|
+
io.stderr.error(formatValidationError(error));
|
|
163
|
+
}
|
|
164
|
+
return { exitCode: 1 };
|
|
165
|
+
}
|
|
166
|
+
const bundle = compileScene(document);
|
|
167
|
+
const assetPlan = await planAssets(document, bundle, parsed);
|
|
168
|
+
const rewrittenBundle = rewriteAssetUrls(bundle, assetPlan);
|
|
169
|
+
const manifest = await createManifest(parsed, rewrittenBundle, assetPlan);
|
|
170
|
+
await writeBundleDirectory(parsed, rewrittenBundle, assetPlan, manifest);
|
|
171
|
+
io.stdout.log(`BUNDLED ${parsed.out}`);
|
|
172
|
+
return { exitCode: 0 };
|
|
173
|
+
}
|
|
174
|
+
function parseBundleArgs(args) {
|
|
175
|
+
const inputs = positionalArgs$1(args, new Set([
|
|
176
|
+
'--out',
|
|
177
|
+
'--asset-dir',
|
|
178
|
+
'--public-asset-base',
|
|
179
|
+
'--scene-name',
|
|
180
|
+
'--runtime'
|
|
181
|
+
]));
|
|
182
|
+
const firstInput = inputs.at(0);
|
|
183
|
+
if (!firstInput) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
error: 'ERROR MISSING_INPUT Expected an input .isostate.yaml file'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (inputs.length > 1) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: 'ERROR EXTRA_INPUT Expected exactly one input .isostate.yaml file'
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const parsed = {
|
|
196
|
+
input: firstInput,
|
|
197
|
+
out: '',
|
|
198
|
+
assetDir: dirname(firstInput),
|
|
199
|
+
publicAssetBase: DEFAULT_PUBLIC_ASSET_BASE,
|
|
200
|
+
sceneName: DEFAULT_SCENE_NAME,
|
|
201
|
+
runtime: 'copy'
|
|
202
|
+
};
|
|
203
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
204
|
+
const arg = args[index];
|
|
205
|
+
if (!arg.startsWith('-'))
|
|
206
|
+
continue;
|
|
207
|
+
const value = args[index + 1];
|
|
208
|
+
if (!value || value.startsWith('-')) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: `ERROR MISSING_OPTION ${arg} requires a value`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (!applyBundleOption(parsed, arg, value)) {
|
|
215
|
+
return { ok: false, error: `ERROR UNKNOWN_OPTION Unknown option ${arg}` };
|
|
216
|
+
}
|
|
217
|
+
index += 1;
|
|
218
|
+
}
|
|
219
|
+
if (!parsed.out) {
|
|
220
|
+
return { ok: false, error: 'ERROR MISSING_OPTION --out requires a path' };
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, ...parsed };
|
|
223
|
+
}
|
|
224
|
+
function applyBundleOption(args, option, value) {
|
|
225
|
+
switch (option) {
|
|
226
|
+
case '--out':
|
|
227
|
+
args.out = value;
|
|
228
|
+
return true;
|
|
229
|
+
case '--asset-dir':
|
|
230
|
+
args.assetDir = value;
|
|
231
|
+
return true;
|
|
232
|
+
case '--public-asset-base':
|
|
233
|
+
args.publicAssetBase = value;
|
|
234
|
+
return true;
|
|
235
|
+
case '--scene-name':
|
|
236
|
+
args.sceneName = value;
|
|
237
|
+
return true;
|
|
238
|
+
case '--runtime':
|
|
239
|
+
if (value !== 'copy' && value !== 'external' && value !== 'none') {
|
|
240
|
+
throw codedError('UNSUPPORTED_RUNTIME_MODE', `Unsupported runtime mode "${value}"`);
|
|
241
|
+
}
|
|
242
|
+
args.runtime = value;
|
|
243
|
+
return true;
|
|
244
|
+
default:
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function planAssets(document, bundle, args) {
|
|
249
|
+
const assetIds = Object.keys(bundle.assets ?? {}).sort();
|
|
250
|
+
const usedFiles = new Set();
|
|
251
|
+
const plans = [];
|
|
252
|
+
for (const id of assetIds) {
|
|
253
|
+
const source = resolveAssetSource(document, id, args.assetDir);
|
|
254
|
+
await assertSvgFile(id, source);
|
|
255
|
+
const file = uniqueAssetFile(id, basename(source), usedFiles);
|
|
256
|
+
plans.push({
|
|
257
|
+
id,
|
|
258
|
+
source,
|
|
259
|
+
file,
|
|
260
|
+
url: publicAssetUrl(args.publicAssetBase, file)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return plans;
|
|
264
|
+
}
|
|
265
|
+
function resolveAssetSource(document, id, assetDir) {
|
|
266
|
+
const entry = document.header.assets.find((asset) => asset.id === id);
|
|
267
|
+
if (!entry) {
|
|
268
|
+
throw codedError('ASSET_SOURCE_MISSING', `Asset "${id}" is not declared`);
|
|
269
|
+
}
|
|
270
|
+
const rawPath = entry.path ?? entry.id;
|
|
271
|
+
const withExtension = extname(rawPath) ? rawPath : `${rawPath}.svg`;
|
|
272
|
+
return isAbsolute(withExtension)
|
|
273
|
+
? withExtension
|
|
274
|
+
: resolve(assetDir, withExtension);
|
|
275
|
+
}
|
|
276
|
+
async function assertSvgFile(id, source) {
|
|
277
|
+
if (extname(source).toLowerCase() !== '.svg') {
|
|
278
|
+
throw codedError('ASSET_NOT_SVG', `Asset "${id}" must resolve to an SVG file: ${source}`);
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const stats = await stat(source);
|
|
282
|
+
if (stats.isFile())
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
throw codedError('ASSET_RESOLUTION_FAILED', `Unable to resolve asset "${id}" at ${source}`);
|
|
287
|
+
}
|
|
288
|
+
throw codedError('ASSET_NOT_FILE', `Asset "${id}" is not a file: ${source}`);
|
|
289
|
+
}
|
|
290
|
+
function uniqueAssetFile(id, sourceBasename, usedFiles) {
|
|
291
|
+
if (!usedFiles.has(sourceBasename)) {
|
|
292
|
+
usedFiles.add(sourceBasename);
|
|
293
|
+
return sourceBasename;
|
|
294
|
+
}
|
|
295
|
+
const candidate = `${id}-${sourceBasename}`;
|
|
296
|
+
usedFiles.add(candidate);
|
|
297
|
+
return candidate;
|
|
298
|
+
}
|
|
299
|
+
function rewriteAssetUrls(bundle, assetPlan) {
|
|
300
|
+
if (assetPlan.length === 0)
|
|
301
|
+
return bundle;
|
|
302
|
+
const assets = { ...(bundle.assets ?? {}) };
|
|
303
|
+
for (const asset of assetPlan) {
|
|
304
|
+
assets[asset.id] = { ...assets[asset.id], url: asset.url };
|
|
305
|
+
}
|
|
306
|
+
const rewritten = { ...bundle, assets };
|
|
307
|
+
return { ...rewritten, _digest: runtimeDigest(rewritten) };
|
|
308
|
+
}
|
|
309
|
+
async function createManifest(args, bundle, assetPlan) {
|
|
310
|
+
const assets = [];
|
|
311
|
+
for (const asset of assetPlan) {
|
|
312
|
+
const bytes = await readFile(asset.source);
|
|
313
|
+
assets.push({
|
|
314
|
+
id: asset.id,
|
|
315
|
+
source: slashPath(asset.source),
|
|
316
|
+
file: slashPath(join('assets', asset.file)),
|
|
317
|
+
url: asset.url,
|
|
318
|
+
digest: sha256Hex(bytes)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const runtime = args.runtime === 'copy'
|
|
322
|
+
? { mode: args.runtime, file: RUNTIME_FILE }
|
|
323
|
+
: { mode: args.runtime };
|
|
324
|
+
return {
|
|
325
|
+
format: 'isostate-static-bundle',
|
|
326
|
+
version: await packageVersion(),
|
|
327
|
+
generatedAt: new Date().toISOString(),
|
|
328
|
+
source: { file: slashPath(args.input) },
|
|
329
|
+
runtime,
|
|
330
|
+
scene: {
|
|
331
|
+
file: `${args.sceneName}.isostate.js`,
|
|
332
|
+
format: 'js',
|
|
333
|
+
digest: bundle._digest
|
|
334
|
+
},
|
|
335
|
+
assets
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function writeBundleDirectory(args, bundle, assetPlan, manifest) {
|
|
339
|
+
const out = resolve(args.out);
|
|
340
|
+
const temporary = `${out}.tmp-${process.pid}-${Date.now()}`;
|
|
341
|
+
const backup = `${out}.bak-${process.pid}-${Date.now()}`;
|
|
342
|
+
try {
|
|
343
|
+
await rm(temporary, { recursive: true, force: true });
|
|
344
|
+
await rm(backup, { recursive: true, force: true });
|
|
345
|
+
await mkdir(join(temporary, 'assets'), { recursive: true });
|
|
346
|
+
await writeFile(join(temporary, `${args.sceneName}.isostate.js`), toJs(bundle), 'utf8');
|
|
347
|
+
await writeRuntimeArtifact(args.runtime, temporary);
|
|
348
|
+
await copyAssets(assetPlan, temporary);
|
|
349
|
+
await writeFile(join(temporary, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
350
|
+
let hasExistingOutput = false;
|
|
351
|
+
try {
|
|
352
|
+
await rename(out, backup);
|
|
353
|
+
hasExistingOutput = true;
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (!isMissingPathError(error))
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await rename(temporary, out);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (hasExistingOutput) {
|
|
364
|
+
await rename(backup, out);
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
if (hasExistingOutput) {
|
|
369
|
+
await rm(backup, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
await rm(temporary, { recursive: true, force: true });
|
|
374
|
+
await rm(backup, { recursive: true, force: true });
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function writeRuntimeArtifact(mode, outDir) {
|
|
379
|
+
if (mode !== 'copy')
|
|
380
|
+
return;
|
|
381
|
+
await copyFile(RUNTIME_SOURCE, join(outDir, RUNTIME_FILE));
|
|
382
|
+
}
|
|
383
|
+
async function copyAssets(assetPlan, outDir) {
|
|
384
|
+
for (const asset of assetPlan) {
|
|
385
|
+
await copyFile(asset.source, join(outDir, 'assets', asset.file));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function packageVersion() {
|
|
389
|
+
const json = JSON.parse(await readFile(PACKAGE_JSON, 'utf8'));
|
|
390
|
+
return typeof json.version === 'string' ? json.version : '0.0.0';
|
|
391
|
+
}
|
|
392
|
+
function positionalArgs$1(args, optionsWithValues) {
|
|
393
|
+
const positionals = [];
|
|
394
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
395
|
+
const arg = args[index];
|
|
396
|
+
if (arg === undefined)
|
|
397
|
+
continue;
|
|
398
|
+
if (optionsWithValues.has(arg)) {
|
|
399
|
+
index += 1;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (!arg.startsWith('-')) {
|
|
403
|
+
positionals.push(arg);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return positionals;
|
|
407
|
+
}
|
|
408
|
+
function publicAssetUrl(base, file) {
|
|
409
|
+
const trimmed = base.replace(/\/+$/, '');
|
|
410
|
+
return trimmed ? `${trimmed}/${file}` : file;
|
|
411
|
+
}
|
|
412
|
+
function slashPath(path) {
|
|
413
|
+
return path.split(sep).join('/');
|
|
414
|
+
}
|
|
415
|
+
function codedError(code, message) {
|
|
416
|
+
const error = new Error(message);
|
|
417
|
+
Object.defineProperty(error, 'code', { value: code, enumerable: true });
|
|
418
|
+
return error;
|
|
419
|
+
}
|
|
420
|
+
function isMissingPathError(error) {
|
|
421
|
+
return (error instanceof Error &&
|
|
422
|
+
'code' in error &&
|
|
423
|
+
error.code === 'ENOENT');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const DEFAULT_OUT = 'build/scene.isostate.js';
|
|
427
|
+
async function runCli(args, io = { stdout: console, stderr: console }) {
|
|
428
|
+
const [command, ...rest] = args;
|
|
429
|
+
try {
|
|
430
|
+
switch (command) {
|
|
431
|
+
case 'validate':
|
|
432
|
+
return await validateCommand(rest, io);
|
|
433
|
+
case 'compile':
|
|
434
|
+
return await compileCommand(rest, io);
|
|
435
|
+
case 'bundle':
|
|
436
|
+
return await bundleCommand(rest, io);
|
|
437
|
+
case 'inspect':
|
|
438
|
+
return await inspectCommand(rest, io);
|
|
439
|
+
case undefined:
|
|
440
|
+
io.stderr.error('ERROR MISSING_COMMAND Expected a command');
|
|
441
|
+
return { exitCode: 1 };
|
|
442
|
+
default:
|
|
443
|
+
io.stderr.error(`ERROR UNKNOWN_COMMAND Unknown command "${command}"`);
|
|
444
|
+
return { exitCode: 1 };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
io.stderr.error(formatThrownError(error));
|
|
449
|
+
return { exitCode: 1 };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function validateCommand(args, io) {
|
|
453
|
+
const parsed = parseInputArgs(args);
|
|
454
|
+
if (!parsed.ok) {
|
|
455
|
+
io.stderr.error(parsed.error);
|
|
456
|
+
return { exitCode: 1 };
|
|
457
|
+
}
|
|
458
|
+
const source = await readInput(parsed.input);
|
|
459
|
+
const document = parseScene(source);
|
|
460
|
+
const report = validateScene(document);
|
|
461
|
+
for (const warning of report.warnings) {
|
|
462
|
+
io.stderr.error(formatValidationWarning(warning));
|
|
463
|
+
}
|
|
464
|
+
if (!report.isValid) {
|
|
465
|
+
for (const error of report.errors) {
|
|
466
|
+
io.stderr.error(formatValidationError(error));
|
|
467
|
+
}
|
|
468
|
+
return { exitCode: 1 };
|
|
469
|
+
}
|
|
470
|
+
io.stdout.log(`OK ${parsed.input} (${report.errors.length} errors, ${report.warnings.length} warnings)`);
|
|
471
|
+
return { exitCode: 0 };
|
|
472
|
+
}
|
|
473
|
+
async function compileCommand(args, io) {
|
|
474
|
+
const parsed = parseCompileArgs(args);
|
|
475
|
+
if (!parsed.ok) {
|
|
476
|
+
io.stderr.error(parsed.error);
|
|
477
|
+
return { exitCode: 1 };
|
|
478
|
+
}
|
|
479
|
+
const source = await readInput(parsed.input);
|
|
480
|
+
const document = parseScene(source);
|
|
481
|
+
const report = validateScene(document);
|
|
482
|
+
for (const warning of report.warnings) {
|
|
483
|
+
io.stderr.error(formatValidationWarning(warning));
|
|
484
|
+
}
|
|
485
|
+
if (!report.isValid) {
|
|
486
|
+
for (const error of report.errors) {
|
|
487
|
+
io.stderr.error(formatValidationError(error));
|
|
488
|
+
}
|
|
489
|
+
return { exitCode: 1 };
|
|
490
|
+
}
|
|
491
|
+
const bundle = compileScene(document);
|
|
492
|
+
const output = parsed.format === 'js'
|
|
493
|
+
? toJs(bundle, { minify: !parsed.pretty })
|
|
494
|
+
: toJson(bundle);
|
|
495
|
+
await writeAtomic(parsed.out, output);
|
|
496
|
+
io.stdout.log(`WROTE ${parsed.out}`);
|
|
497
|
+
return { exitCode: 0 };
|
|
498
|
+
}
|
|
499
|
+
function parseInputArgs(args) {
|
|
500
|
+
const unknown = args.find((arg) => arg.startsWith('-'));
|
|
501
|
+
if (unknown) {
|
|
502
|
+
return {
|
|
503
|
+
ok: false,
|
|
504
|
+
error: `ERROR UNKNOWN_OPTION Unknown option ${unknown}`
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const positionals = positionalArgs(args, new Set());
|
|
508
|
+
const input = positionals.at(0);
|
|
509
|
+
if (!input) {
|
|
510
|
+
return {
|
|
511
|
+
ok: false,
|
|
512
|
+
error: 'ERROR MISSING_INPUT Expected an input .isostate.yaml file'
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (positionals.length > 1) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
error: 'ERROR EXTRA_INPUT Expected exactly one input .isostate.yaml file'
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return { ok: true, input };
|
|
522
|
+
}
|
|
523
|
+
function parseCompileArgs(args) {
|
|
524
|
+
const input = positionalArgs(args, new Set(['--out', '--format'])).at(0);
|
|
525
|
+
const positionals = positionalArgs(args, new Set(['--out', '--format']));
|
|
526
|
+
if (!input) {
|
|
527
|
+
return {
|
|
528
|
+
ok: false,
|
|
529
|
+
error: 'ERROR MISSING_INPUT Expected an input .isostate.yaml file'
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (positionals.length > 1) {
|
|
533
|
+
return {
|
|
534
|
+
ok: false,
|
|
535
|
+
error: 'ERROR EXTRA_INPUT Expected exactly one input .isostate.yaml file'
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
let out = DEFAULT_OUT;
|
|
539
|
+
let format;
|
|
540
|
+
let pretty = false;
|
|
541
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
542
|
+
const arg = args[index];
|
|
543
|
+
if (arg === '--pretty') {
|
|
544
|
+
pretty = true;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (arg === '--out') {
|
|
548
|
+
const value = args[index + 1];
|
|
549
|
+
if (!value || value.startsWith('-')) {
|
|
550
|
+
return {
|
|
551
|
+
ok: false,
|
|
552
|
+
error: 'ERROR MISSING_OPTION --out requires a path'
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
out = value;
|
|
556
|
+
index += 1;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (arg === '--format') {
|
|
560
|
+
const value = args[index + 1];
|
|
561
|
+
if (!value || value.startsWith('-')) {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
error: 'ERROR MISSING_OPTION --format requires js or json'
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
format = value;
|
|
568
|
+
index += 1;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (arg.startsWith('-')) {
|
|
572
|
+
return { ok: false, error: `ERROR UNKNOWN_OPTION Unknown option ${arg}` };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const inferredFormat = format ?? inferFormat(out);
|
|
576
|
+
if (inferredFormat !== 'js' && inferredFormat !== 'json') {
|
|
577
|
+
return {
|
|
578
|
+
ok: false,
|
|
579
|
+
error: `ERROR UNSUPPORTED_FORMAT Unsupported format "${inferredFormat}"`
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return { ok: true, input, out, format: inferredFormat, pretty };
|
|
583
|
+
}
|
|
584
|
+
function positionalArgs(args, optionsWithValues) {
|
|
585
|
+
const positionals = [];
|
|
586
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
587
|
+
const arg = args[index];
|
|
588
|
+
if (arg === undefined)
|
|
589
|
+
continue;
|
|
590
|
+
if (optionsWithValues.has(arg)) {
|
|
591
|
+
index += 1;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (!arg.startsWith('-')) {
|
|
595
|
+
positionals.push(arg);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return positionals;
|
|
599
|
+
}
|
|
600
|
+
function inferFormat(out) {
|
|
601
|
+
const extension = extname(out).toLowerCase();
|
|
602
|
+
if (extension === '.json')
|
|
603
|
+
return 'json';
|
|
604
|
+
return 'js';
|
|
605
|
+
}
|
|
606
|
+
async function readInput(path) {
|
|
607
|
+
try {
|
|
608
|
+
return await readFile(path, 'utf8');
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
throw wrapFsError('FILE_READ_FAILED', `Unable to read ${path}`, error);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async function writeAtomic(path, contents) {
|
|
615
|
+
const absolute = resolve(path);
|
|
616
|
+
const directory = dirname(absolute);
|
|
617
|
+
const temporary = `${absolute}.tmp-${process.pid}-${Date.now()}`;
|
|
618
|
+
try {
|
|
619
|
+
await mkdir(directory, { recursive: true });
|
|
620
|
+
await writeFile(temporary, contents, 'utf8');
|
|
621
|
+
await rename(temporary, absolute);
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
await rm(temporary, { force: true });
|
|
625
|
+
throw wrapFsError('FILE_WRITE_FAILED', `Unable to write ${path}`, error);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function wrapFsError(code, message, cause) {
|
|
629
|
+
const error = new Error(message);
|
|
630
|
+
Object.defineProperty(error, 'code', { value: code, enumerable: true });
|
|
631
|
+
Object.defineProperty(error, 'cause', { value: cause });
|
|
632
|
+
return error;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const result = await runCli(process.argv.slice(2));
|
|
636
|
+
process.exitCode = result.exitCode;
|
|
637
|
+
//# sourceMappingURL=bin.js.map
|