route-graphics 1.44.0 → 1.44.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/README.md +1 -3
- package/bin/route-graphics-render.js +5 -615
- package/dist/RouteGraphics.js +178 -669
- package/dist/cli/routeGraphicsCli.js +64 -10
- package/package.json +17 -10
- package/src/cli/browserLaunch.js +0 -5
- package/src/cli/inspectTimeline.js +0 -72
- package/src/cli/inspectTimeline.test.js +0 -135
- package/src/cli/renderConfig.js +0 -519
- package/src/cli/renderVideo.js +0 -627
- package/src/cli/routeGraphicsCli.js +0 -967
- package/src/cli/stateSelection.js +0 -61
package/README.md
CHANGED
|
@@ -24,7 +24,6 @@ import createRouteGraphics, {
|
|
|
24
24
|
rectPlugin,
|
|
25
25
|
spritePlugin,
|
|
26
26
|
containerPlugin,
|
|
27
|
-
tweenPlugin,
|
|
28
27
|
soundPlugin,
|
|
29
28
|
} from "route-graphics";
|
|
30
29
|
|
|
@@ -45,7 +44,6 @@ await app.init({
|
|
|
45
44
|
rendererFallback: true,
|
|
46
45
|
plugins: {
|
|
47
46
|
elements: [textPlugin, rectPlugin, spritePlugin, containerPlugin],
|
|
48
|
-
animations: [tweenPlugin],
|
|
49
47
|
audio: [soundPlugin],
|
|
50
48
|
},
|
|
51
49
|
eventHandler: (eventName, payload) => {
|
|
@@ -138,7 +136,7 @@ For complete usage details, go to:
|
|
|
138
136
|
- [Shaders](http://route-graphics.routevn.com/docs/guides/shaders/)
|
|
139
137
|
- [Custom Plugins](http://route-graphics.routevn.com/docs/guides/custom-plugins/)
|
|
140
138
|
|
|
141
|
-
Design notes:
|
|
139
|
+
Design notes (see the [documentation index](./docs/README.md) for current contracts):
|
|
142
140
|
|
|
143
141
|
- [Audio Effects](./docs/audio-effects.md)
|
|
144
142
|
- [Command-Controlled Sound Playback](./docs/audio-playback-commands.md)
|
|
@@ -1,618 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import http from "node:http";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { performance } from "node:perf_hooks";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
// Compatibility entry point for the original PNG-only command.
|
|
4
|
+
import { runRouteGraphicsCli } from "../dist/cli/routeGraphicsCli.js";
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
collectAssetDefinitions,
|
|
14
|
-
loadRenderDefinition,
|
|
15
|
-
parseBackgroundColor,
|
|
16
|
-
} from "../src/cli/renderConfig.js";
|
|
17
|
-
|
|
18
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const projectRoot = path.resolve(__dirname, "..");
|
|
20
|
-
const bundlePath = path.join(projectRoot, "dist", "RouteGraphics.js");
|
|
21
|
-
|
|
22
|
-
const usage = `Usage:
|
|
23
|
-
node ./bin/route-graphics-render.js <input.yaml> -o <output.png> [options]
|
|
24
|
-
|
|
25
|
-
Options:
|
|
26
|
-
-o, --output <path> Output PNG path
|
|
27
|
-
--width <pixels> Override render width
|
|
28
|
-
--height <pixels> Override render height
|
|
29
|
-
--state <index> State index when YAML contains multiple states
|
|
30
|
-
--time <ms> Sample animations at a manual time
|
|
31
|
-
--layout-report <path> Write a JSON layout snapshot beside the PNG
|
|
32
|
-
--background-color <value> 0xRRGGBB, #RRGGBB, or decimal
|
|
33
|
-
--browser-executable <path> Use a system Chrome/Chromium executable
|
|
34
|
-
--wait-for-render-complete Wait for renderComplete before capture
|
|
35
|
-
--timeout <ms> Browser-side render timeout (default: 15000)
|
|
36
|
-
-h, --help Show this help
|
|
37
|
-
`;
|
|
38
|
-
|
|
39
|
-
const exitWithError = (message, { showUsage = false } = {}) => {
|
|
40
|
-
console.error(message);
|
|
41
|
-
if (showUsage) {
|
|
42
|
-
console.error("");
|
|
43
|
-
console.error(usage);
|
|
44
|
-
}
|
|
45
|
-
process.exit(1);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const formatDuration = (durationMS) => {
|
|
49
|
-
if (!Number.isFinite(durationMS)) {
|
|
50
|
-
return "unknown";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (durationMS < 1000) {
|
|
54
|
-
return `${Math.round(durationMS)}ms`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return `${(durationMS / 1000).toFixed(2)}s`;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const parseIntegerOption = (label, value) => {
|
|
61
|
-
const parsed = Number.parseInt(String(value), 10);
|
|
62
|
-
if (!Number.isFinite(parsed)) {
|
|
63
|
-
throw new Error(`${label} must be an integer.`);
|
|
64
|
-
}
|
|
65
|
-
return parsed;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const parseCliArgs = (argv) => {
|
|
69
|
-
const options = {
|
|
70
|
-
waitForRenderComplete: false,
|
|
71
|
-
timeoutMS: 15000,
|
|
72
|
-
stateIndex: 0,
|
|
73
|
-
};
|
|
74
|
-
const positionals = [];
|
|
75
|
-
|
|
76
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
77
|
-
const token = argv[index];
|
|
78
|
-
|
|
79
|
-
switch (token) {
|
|
80
|
-
case "-h":
|
|
81
|
-
case "--help":
|
|
82
|
-
options.help = true;
|
|
83
|
-
break;
|
|
84
|
-
case "-o":
|
|
85
|
-
case "--output":
|
|
86
|
-
index += 1;
|
|
87
|
-
options.outputPath = argv[index];
|
|
88
|
-
break;
|
|
89
|
-
case "--width":
|
|
90
|
-
index += 1;
|
|
91
|
-
options.width = parseIntegerOption("Width", argv[index]);
|
|
92
|
-
break;
|
|
93
|
-
case "--height":
|
|
94
|
-
index += 1;
|
|
95
|
-
options.height = parseIntegerOption("Height", argv[index]);
|
|
96
|
-
break;
|
|
97
|
-
case "--state":
|
|
98
|
-
index += 1;
|
|
99
|
-
options.stateIndex = parseIntegerOption("State index", argv[index]);
|
|
100
|
-
break;
|
|
101
|
-
case "--time":
|
|
102
|
-
index += 1;
|
|
103
|
-
options.timeMS = parseIntegerOption("Animation time", argv[index]);
|
|
104
|
-
break;
|
|
105
|
-
case "--layout-report":
|
|
106
|
-
index += 1;
|
|
107
|
-
if (!argv[index] || argv[index].startsWith("-")) {
|
|
108
|
-
throw new Error("Layout report requires an output path.");
|
|
109
|
-
}
|
|
110
|
-
options.layoutReportPath = argv[index];
|
|
111
|
-
break;
|
|
112
|
-
case "--background-color":
|
|
113
|
-
index += 1;
|
|
114
|
-
options.backgroundColor = argv[index];
|
|
115
|
-
break;
|
|
116
|
-
case "--browser-executable":
|
|
117
|
-
index += 1;
|
|
118
|
-
options.browserExecutablePath = argv[index];
|
|
119
|
-
break;
|
|
120
|
-
case "--timeout":
|
|
121
|
-
index += 1;
|
|
122
|
-
options.timeoutMS = parseIntegerOption("Timeout", argv[index]);
|
|
123
|
-
break;
|
|
124
|
-
case "--wait-for-render-complete":
|
|
125
|
-
options.waitForRenderComplete = true;
|
|
126
|
-
break;
|
|
127
|
-
default:
|
|
128
|
-
if (token.startsWith("-")) {
|
|
129
|
-
throw new Error(`Unknown option: ${token}`);
|
|
130
|
-
}
|
|
131
|
-
positionals.push(token);
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (positionals.length > 1) {
|
|
137
|
-
throw new Error("Only one input YAML file can be provided.");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
...options,
|
|
142
|
-
inputPath: positionals[0],
|
|
143
|
-
};
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const toBrowserPath = (assetId) => {
|
|
147
|
-
return `/__asset/${encodeURIComponent(assetId)}`;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const getServedAssetPath = (assetId, assetPath) => {
|
|
151
|
-
const extension = path.extname(assetPath) || "";
|
|
152
|
-
|
|
153
|
-
return `${toBrowserPath(assetId)}${extension}`;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const createRequestHandler = ({ assetRoutes }) => {
|
|
157
|
-
return async (request, response) => {
|
|
158
|
-
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
159
|
-
|
|
160
|
-
if (url.pathname === "/") {
|
|
161
|
-
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
162
|
-
response.end("<!doctype html><html><body></body></html>");
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (url.pathname === "/dist/RouteGraphics.js") {
|
|
167
|
-
response.writeHead(200, {
|
|
168
|
-
"content-type": "text/javascript; charset=utf-8",
|
|
169
|
-
});
|
|
170
|
-
fs.createReadStream(bundlePath).pipe(response);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!url.pathname.startsWith("/__asset/")) {
|
|
175
|
-
response.writeHead(404);
|
|
176
|
-
response.end("Not Found");
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const assetRecord = assetRoutes.get(url.pathname);
|
|
181
|
-
|
|
182
|
-
if (!assetRecord) {
|
|
183
|
-
response.writeHead(404);
|
|
184
|
-
response.end("Unknown asset");
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const stat = await fsPromises.stat(assetRecord.path);
|
|
189
|
-
const rangeHeader = request.headers.range;
|
|
190
|
-
const headers = {
|
|
191
|
-
"accept-ranges": "bytes",
|
|
192
|
-
"content-type": assetRecord.type,
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
if (!rangeHeader) {
|
|
196
|
-
headers["content-length"] = stat.size;
|
|
197
|
-
response.writeHead(200, headers);
|
|
198
|
-
if (request.method === "HEAD") {
|
|
199
|
-
response.end();
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
fs.createReadStream(assetRecord.path).pipe(response);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader);
|
|
207
|
-
if (!match) {
|
|
208
|
-
response.writeHead(416);
|
|
209
|
-
response.end();
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const start = match[1] ? Number.parseInt(match[1], 10) : 0;
|
|
214
|
-
const end = match[2] ? Number.parseInt(match[2], 10) : stat.size - 1;
|
|
215
|
-
|
|
216
|
-
headers["content-length"] = end - start + 1;
|
|
217
|
-
headers["content-range"] = `bytes ${start}-${end}/${stat.size}`;
|
|
218
|
-
response.writeHead(206, headers);
|
|
219
|
-
|
|
220
|
-
if (request.method === "HEAD") {
|
|
221
|
-
response.end();
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
fs.createReadStream(assetRecord.path, { start, end }).pipe(response);
|
|
226
|
-
};
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const startAssetServer = async ({ assetRoutes }) => {
|
|
230
|
-
const server = http.createServer((request, response) => {
|
|
231
|
-
createRequestHandler({ assetRoutes })(request, response).catch((error) => {
|
|
232
|
-
response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
233
|
-
response.end(error.stack ?? error.message);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
await new Promise((resolve, reject) => {
|
|
238
|
-
server.once("error", reject);
|
|
239
|
-
server.listen(0, "127.0.0.1", resolve);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
const address = server.address();
|
|
243
|
-
if (!address || typeof address === "string") {
|
|
244
|
-
throw new Error("Could not determine local render server address.");
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
origin: `http://127.0.0.1:${address.port}`,
|
|
249
|
-
close: () =>
|
|
250
|
-
new Promise((resolve, reject) => {
|
|
251
|
-
server.close((error) => {
|
|
252
|
-
if (error) {
|
|
253
|
-
reject(error);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
resolve();
|
|
257
|
-
});
|
|
258
|
-
}),
|
|
259
|
-
};
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const normalizeBrowserAssets = async ({ assetDefinitions }) => {
|
|
263
|
-
const browserAssets = {};
|
|
264
|
-
const assetRoutes = new Map();
|
|
265
|
-
|
|
266
|
-
for (const [key, definition] of Object.entries(assetDefinitions)) {
|
|
267
|
-
if (definition.kind === "remote") {
|
|
268
|
-
browserAssets[key] = {
|
|
269
|
-
type: definition.type,
|
|
270
|
-
url: definition.url,
|
|
271
|
-
};
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
await fsPromises.access(definition.path, fs.constants.R_OK);
|
|
276
|
-
const servedPath = getServedAssetPath(key, definition.path);
|
|
277
|
-
|
|
278
|
-
assetRoutes.set(servedPath, {
|
|
279
|
-
path: definition.path,
|
|
280
|
-
type: definition.type,
|
|
281
|
-
});
|
|
282
|
-
browserAssets[key] = {
|
|
283
|
-
type: definition.type,
|
|
284
|
-
url: servedPath,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
assetRoutes,
|
|
290
|
-
browserAssets,
|
|
291
|
-
};
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const buildRenderPayload = async ({ cliOptions, definition, inputPath }) => {
|
|
295
|
-
const yamlDir = path.dirname(inputPath);
|
|
296
|
-
const states = definition.states;
|
|
297
|
-
const selectedState = states[cliOptions.stateIndex];
|
|
298
|
-
|
|
299
|
-
if (!selectedState) {
|
|
300
|
-
throw new Error(
|
|
301
|
-
`State index ${cliOptions.stateIndex} is out of range for ${states.length} state(s).`,
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
width: cliOptions.width ?? definition.width ?? 1280,
|
|
307
|
-
height: cliOptions.height ?? definition.height ?? 720,
|
|
308
|
-
backgroundColor: parseBackgroundColor(
|
|
309
|
-
cliOptions.backgroundColor ?? definition.backgroundColor,
|
|
310
|
-
),
|
|
311
|
-
state: selectedState,
|
|
312
|
-
assetDefinitions: collectAssetDefinitions({
|
|
313
|
-
assets: definition.assets,
|
|
314
|
-
states: [selectedState],
|
|
315
|
-
baseDir: yamlDir,
|
|
316
|
-
}),
|
|
317
|
-
};
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const capturePng = async ({
|
|
321
|
-
origin,
|
|
322
|
-
width,
|
|
323
|
-
height,
|
|
324
|
-
backgroundColor,
|
|
325
|
-
state,
|
|
326
|
-
browserAssets,
|
|
327
|
-
timeMS,
|
|
328
|
-
waitForRenderComplete,
|
|
329
|
-
timeoutMS,
|
|
330
|
-
browserExecutablePath,
|
|
331
|
-
includeLayoutReport,
|
|
332
|
-
}) => {
|
|
333
|
-
const browser = await chromium.launch({
|
|
334
|
-
headless: true,
|
|
335
|
-
executablePath: browserExecutablePath,
|
|
336
|
-
args: ["--autoplay-policy=no-user-gesture-required"],
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
try {
|
|
340
|
-
const page = await browser.newPage({
|
|
341
|
-
viewport: {
|
|
342
|
-
width,
|
|
343
|
-
height,
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
const pageErrors = [];
|
|
347
|
-
|
|
348
|
-
page.on("pageerror", (error) => {
|
|
349
|
-
pageErrors.push(error.stack ?? error.message);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
await page.goto(origin, {
|
|
353
|
-
waitUntil: "domcontentloaded",
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
const capture = await page.evaluate(
|
|
357
|
-
async ({ moduleUrl, renderPayload }) => {
|
|
358
|
-
const nextFrame = async (count = 2) => {
|
|
359
|
-
await new Promise((resolve) => {
|
|
360
|
-
let remaining = count;
|
|
361
|
-
const tick = () => {
|
|
362
|
-
if (remaining <= 0) {
|
|
363
|
-
resolve();
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
remaining -= 1;
|
|
367
|
-
requestAnimationFrame(tick);
|
|
368
|
-
};
|
|
369
|
-
requestAnimationFrame(tick);
|
|
370
|
-
});
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
const routeGraphicsModule = await import(moduleUrl);
|
|
374
|
-
const {
|
|
375
|
-
default: createRouteGraphics,
|
|
376
|
-
animatedSpritePlugin,
|
|
377
|
-
containerPlugin,
|
|
378
|
-
createAssetBufferManager,
|
|
379
|
-
inputPlugin,
|
|
380
|
-
particlesPlugin,
|
|
381
|
-
rectPlugin,
|
|
382
|
-
sliderPlugin,
|
|
383
|
-
soundPlugin,
|
|
384
|
-
spritePlugin,
|
|
385
|
-
textPlugin,
|
|
386
|
-
textRevealingPlugin,
|
|
387
|
-
tweenPlugin,
|
|
388
|
-
videoPlugin,
|
|
389
|
-
} = routeGraphicsModule;
|
|
390
|
-
|
|
391
|
-
const app = createRouteGraphics();
|
|
392
|
-
const assetBufferManager = createAssetBufferManager();
|
|
393
|
-
let renderCompleteResolve = () => {};
|
|
394
|
-
let renderTimeoutId = null;
|
|
395
|
-
let renderCompletePromise = Promise.resolve(null);
|
|
396
|
-
|
|
397
|
-
if (renderPayload.waitForRenderComplete) {
|
|
398
|
-
renderCompletePromise = new Promise((resolve, reject) => {
|
|
399
|
-
renderCompleteResolve = resolve;
|
|
400
|
-
renderTimeoutId = window.setTimeout(() => {
|
|
401
|
-
reject(new Error("Timed out waiting for renderComplete."));
|
|
402
|
-
}, renderPayload.timeoutMS);
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
try {
|
|
407
|
-
await app.init({
|
|
408
|
-
width: renderPayload.width,
|
|
409
|
-
height: renderPayload.height,
|
|
410
|
-
backgroundColor: renderPayload.backgroundColor,
|
|
411
|
-
animationPlaybackMode:
|
|
412
|
-
renderPayload.timeMS === null ? "auto" : "manual",
|
|
413
|
-
plugins: {
|
|
414
|
-
elements: [
|
|
415
|
-
textPlugin,
|
|
416
|
-
rectPlugin,
|
|
417
|
-
spritePlugin,
|
|
418
|
-
videoPlugin,
|
|
419
|
-
sliderPlugin,
|
|
420
|
-
inputPlugin,
|
|
421
|
-
containerPlugin,
|
|
422
|
-
textRevealingPlugin,
|
|
423
|
-
animatedSpritePlugin,
|
|
424
|
-
particlesPlugin,
|
|
425
|
-
].filter(Boolean),
|
|
426
|
-
animations: [tweenPlugin].filter(Boolean),
|
|
427
|
-
audio: [soundPlugin].filter(Boolean),
|
|
428
|
-
},
|
|
429
|
-
eventHandler: (eventName, payload) => {
|
|
430
|
-
if (eventName === "renderComplete" && payload?.aborted !== true) {
|
|
431
|
-
renderCompleteResolve(payload);
|
|
432
|
-
}
|
|
433
|
-
},
|
|
434
|
-
debug: false,
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
if (Object.keys(renderPayload.assets).length > 0) {
|
|
438
|
-
await assetBufferManager.load(renderPayload.assets);
|
|
439
|
-
await app.loadAssets(assetBufferManager.getBufferMap());
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
document.body.replaceChildren(app.canvas);
|
|
443
|
-
app.render(renderPayload.state);
|
|
444
|
-
app.render(renderPayload.state);
|
|
445
|
-
|
|
446
|
-
if (renderPayload.timeMS !== null) {
|
|
447
|
-
app.setAnimationTime(renderPayload.timeMS);
|
|
448
|
-
} else if (renderPayload.waitForRenderComplete) {
|
|
449
|
-
await renderCompletePromise;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
await nextFrame(2);
|
|
453
|
-
|
|
454
|
-
try {
|
|
455
|
-
await app.extractBase64();
|
|
456
|
-
} catch {}
|
|
457
|
-
|
|
458
|
-
await nextFrame(2);
|
|
459
|
-
|
|
460
|
-
// Pixi copies the framebuffer synchronously, then encodes it
|
|
461
|
-
// asynchronously. Snapshot geometry before yielding to another frame.
|
|
462
|
-
const imagePromise = app.extractBase64();
|
|
463
|
-
const layoutReport = renderPayload.includeLayoutReport
|
|
464
|
-
? app.getLayoutReport()
|
|
465
|
-
: null;
|
|
466
|
-
return {
|
|
467
|
-
base64: await imagePromise,
|
|
468
|
-
layoutReport,
|
|
469
|
-
};
|
|
470
|
-
} finally {
|
|
471
|
-
if (renderTimeoutId !== null) {
|
|
472
|
-
window.clearTimeout(renderTimeoutId);
|
|
473
|
-
}
|
|
474
|
-
app.destroy();
|
|
475
|
-
}
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
moduleUrl: `${origin}/dist/RouteGraphics.js`,
|
|
479
|
-
renderPayload: {
|
|
480
|
-
width,
|
|
481
|
-
height,
|
|
482
|
-
backgroundColor,
|
|
483
|
-
state,
|
|
484
|
-
assets: browserAssets,
|
|
485
|
-
timeMS: timeMS ?? null,
|
|
486
|
-
waitForRenderComplete,
|
|
487
|
-
timeoutMS,
|
|
488
|
-
includeLayoutReport,
|
|
489
|
-
},
|
|
490
|
-
},
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
if (pageErrors.length > 0) {
|
|
494
|
-
throw new Error(pageErrors.join("\n"));
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return capture;
|
|
498
|
-
} finally {
|
|
499
|
-
await browser.close();
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const writePngOutput = async (outputPath, base64Png) => {
|
|
504
|
-
const commaIndex = base64Png.indexOf(",");
|
|
505
|
-
const payload = commaIndex >= 0 ? base64Png.slice(commaIndex + 1) : base64Png;
|
|
506
|
-
const buffer = Buffer.from(payload, "base64");
|
|
507
|
-
|
|
508
|
-
await fsPromises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
509
|
-
await fsPromises.writeFile(outputPath, buffer);
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const ensureBundleExists = async () => {
|
|
513
|
-
try {
|
|
514
|
-
await fsPromises.access(bundlePath, fs.constants.R_OK);
|
|
515
|
-
} catch {
|
|
516
|
-
throw new Error(
|
|
517
|
-
"dist/RouteGraphics.js is missing. Run `bun run build` before using the renderer CLI.",
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
const main = async () => {
|
|
523
|
-
const startedAt = performance.now();
|
|
524
|
-
let cliOptions;
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
cliOptions = parseCliArgs(process.argv.slice(2));
|
|
528
|
-
} catch (error) {
|
|
529
|
-
exitWithError(error.message, { showUsage: true });
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (cliOptions.help) {
|
|
533
|
-
console.log(usage);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (!cliOptions.inputPath) {
|
|
538
|
-
exitWithError("An input YAML file is required.", { showUsage: true });
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (!cliOptions.outputPath) {
|
|
542
|
-
exitWithError("An output PNG path is required.", { showUsage: true });
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const inputPath = path.resolve(process.cwd(), cliOptions.inputPath);
|
|
546
|
-
const outputPath = path.resolve(process.cwd(), cliOptions.outputPath);
|
|
547
|
-
const layoutReportPath = cliOptions.layoutReportPath
|
|
548
|
-
? path.resolve(process.cwd(), cliOptions.layoutReportPath)
|
|
549
|
-
: null;
|
|
550
|
-
if (layoutReportPath === outputPath || layoutReportPath === inputPath) {
|
|
551
|
-
exitWithError("Layout report must not overwrite the input or PNG output.");
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
await ensureBundleExists();
|
|
555
|
-
|
|
556
|
-
const yamlSource = await fsPromises.readFile(inputPath, "utf8");
|
|
557
|
-
const definition = loadRenderDefinition(yamlSource);
|
|
558
|
-
const renderPayload = await buildRenderPayload({
|
|
559
|
-
cliOptions,
|
|
560
|
-
definition,
|
|
561
|
-
inputPath,
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
try {
|
|
565
|
-
const { assetRoutes, browserAssets } = await normalizeBrowserAssets({
|
|
566
|
-
assetDefinitions: renderPayload.assetDefinitions,
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
const assetServer = await startAssetServer({ assetRoutes });
|
|
570
|
-
|
|
571
|
-
try {
|
|
572
|
-
const renderStartedAt = performance.now();
|
|
573
|
-
const { base64, layoutReport } = await capturePng({
|
|
574
|
-
origin: assetServer.origin,
|
|
575
|
-
width: renderPayload.width,
|
|
576
|
-
height: renderPayload.height,
|
|
577
|
-
backgroundColor: renderPayload.backgroundColor,
|
|
578
|
-
state: renderPayload.state,
|
|
579
|
-
browserAssets,
|
|
580
|
-
timeMS: cliOptions.timeMS,
|
|
581
|
-
waitForRenderComplete: cliOptions.waitForRenderComplete,
|
|
582
|
-
timeoutMS: cliOptions.timeoutMS,
|
|
583
|
-
browserExecutablePath: cliOptions.browserExecutablePath,
|
|
584
|
-
includeLayoutReport: layoutReportPath !== null,
|
|
585
|
-
});
|
|
586
|
-
const renderDurationMS = performance.now() - renderStartedAt;
|
|
587
|
-
|
|
588
|
-
const writeStartedAt = performance.now();
|
|
589
|
-
await writePngOutput(outputPath, base64);
|
|
590
|
-
if (layoutReportPath !== null) {
|
|
591
|
-
await fsPromises.mkdir(path.dirname(layoutReportPath), { recursive: true });
|
|
592
|
-
await fsPromises.writeFile(
|
|
593
|
-
layoutReportPath,
|
|
594
|
-
`${JSON.stringify(layoutReport, null, 2)}\n`,
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
const writeDurationMS = performance.now() - writeStartedAt;
|
|
598
|
-
const totalDurationMS = performance.now() - startedAt;
|
|
599
|
-
|
|
600
|
-
console.log(`Wrote ${outputPath}`);
|
|
601
|
-
console.log(
|
|
602
|
-
`Timing: render=${formatDuration(renderDurationMS)}, write=${formatDuration(writeDurationMS)}, total=${formatDuration(totalDurationMS)}`,
|
|
603
|
-
);
|
|
604
|
-
} finally {
|
|
605
|
-
await assetServer.close();
|
|
606
|
-
}
|
|
607
|
-
} catch (error) {
|
|
608
|
-
if (/Executable doesn't exist|browserType\.launch/i.test(error.message)) {
|
|
609
|
-
exitWithError(
|
|
610
|
-
`${error.message}\nInstall Chromium with \`npx playwright install chromium\` or pass --browser-executable.`,
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
exitWithError(error.stack ?? error.message);
|
|
615
|
-
}
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
void main();
|
|
6
|
+
process.exitCode = await runRouteGraphicsCli({
|
|
7
|
+
argv: ["render", ...process.argv.slice(2)],
|
|
8
|
+
});
|