@odori/cli 0.0.5 → 0.0.7
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/bin/odori.mjs +33 -11
- package/dist/{chunk-RHG23EWW.js → chunk-STVORYOF.js} +87 -15
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{registry-snapshot-JEVXYGS2.js → registry-snapshot-ADKSTGDR.js} +3 -3
- package/package.json +3 -3
- package/src/commands/init.ts +34 -1
- package/src/commands/test.ts +64 -11
- package/src/registry-snapshot.json +3 -3
- package/src/render.ts +18 -1
- package/studio/index.html +26 -0
- package/studio/src/components/CanvasStage.tsx +40 -3
- package/studio/src/components/Inspector.tsx +36 -30
- package/studio/src/components/Navigator.tsx +59 -1
- package/studio/src/components/Transport.tsx +41 -6
- package/studio/src/components/ui.tsx +14 -10
- package/studio/src/studio.css +173 -19
package/bin/odori.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {existsSync} from "node:fs";
|
|
3
|
-
import {createRequire} from "node:module";
|
|
4
3
|
import {dirname, resolve} from "node:path";
|
|
5
4
|
import {fileURLToPath, pathToFileURL} from "node:url";
|
|
6
5
|
|
|
@@ -19,21 +18,44 @@ import {fileURLToPath, pathToFileURL} from "node:url";
|
|
|
19
18
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
19
|
const built = resolve(here, "..", "dist", "cli.js");
|
|
21
20
|
|
|
21
|
+
/*
|
|
22
|
+
* import.meta.resolve, not createRequire().resolve.
|
|
23
|
+
*
|
|
24
|
+
* The published runtime is ESM only: its exports declare `import` and no
|
|
25
|
+
* `require`, so a CommonJS resolver cannot see it at all and throws
|
|
26
|
+
* ERR_PACKAGE_PATH_NOT_EXPORTED. The catch below then answered "yes, source",
|
|
27
|
+
* which meant every installed copy took the transform branch. Not just the
|
|
28
|
+
* pause this was written to avoid, but a real failure: the CLI would run its
|
|
29
|
+
* TypeScript through tsx while Node was already stripping types itself, and
|
|
30
|
+
* loading a project's odori.config.ts landed in a require(esm) cycle that
|
|
31
|
+
* Node refuses outright. odori doctor could not run in a new project.
|
|
32
|
+
*
|
|
33
|
+
* import.meta.resolve honours the same conditions the CLI's own imports use,
|
|
34
|
+
* so it answers the question that was actually being asked.
|
|
35
|
+
*/
|
|
22
36
|
const runtimeIsSource = () => {
|
|
23
37
|
try {
|
|
24
|
-
return /\.tsx?$/.test(
|
|
38
|
+
return /\.tsx?$/.test(import.meta.resolve("odori"));
|
|
25
39
|
} catch {
|
|
26
40
|
// Unresolvable from here: let the transform handle whatever it is.
|
|
27
41
|
return true;
|
|
28
42
|
}
|
|
29
43
|
};
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
/*
|
|
46
|
+
* The transform is registered either way, because it is not there for the
|
|
47
|
+
* CLI. A project's videos are .tsx, and Node's own type stripping erases
|
|
48
|
+
* types without understanding JSX, so `odori list` on a real project fails
|
|
49
|
+
* with "Unknown file extension .tsx" the moment nothing is registered.
|
|
50
|
+
*
|
|
51
|
+
* What the branch decides is only where the CLI's own code comes from: its
|
|
52
|
+
* build when there is one, its source inside this repository, where `odori`
|
|
53
|
+
* resolves to a .tsx the build could not have linked against.
|
|
54
|
+
*/
|
|
55
|
+
const {register} = await import("tsx/esm/api");
|
|
56
|
+
register();
|
|
57
|
+
|
|
58
|
+
const {run} = await import(
|
|
59
|
+
existsSync(built) && !runtimeIsSource() ? pathToFileURL(built).href : "../src/cli.ts"
|
|
60
|
+
);
|
|
61
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
@@ -317,7 +317,7 @@ var toComponent = (item) => ({
|
|
|
317
317
|
});
|
|
318
318
|
var snapshotItems = async () => {
|
|
319
319
|
try {
|
|
320
|
-
const loaded = await import("./registry-snapshot-
|
|
320
|
+
const loaded = await import("./registry-snapshot-ADKSTGDR.js");
|
|
321
321
|
return loaded.default.items;
|
|
322
322
|
} catch {
|
|
323
323
|
throw new Error(
|
|
@@ -1757,9 +1757,10 @@ var renderUrl = (origin, target, frame) => {
|
|
|
1757
1757
|
if (target.prepared !== void 0) params.set("prepared", encodeParam(target.prepared));
|
|
1758
1758
|
return `${origin}/?${params.toString()}`;
|
|
1759
1759
|
};
|
|
1760
|
+
var BROWSER_ARGS = ["--enable-unsafe-swiftshader"];
|
|
1760
1761
|
var openRenderPage = async (origin, target, config) => {
|
|
1761
1762
|
const executablePath = await browserExecutable(config);
|
|
1762
|
-
const browser = await chromium.launch({ executablePath, headless: true });
|
|
1763
|
+
const browser = await chromium.launch({ executablePath, headless: true, args: BROWSER_ARGS });
|
|
1763
1764
|
const page = await browser.newPage({
|
|
1764
1765
|
viewport: { width: target.width, height: target.height },
|
|
1765
1766
|
deviceScaleFactor: 1
|
|
@@ -2990,7 +2991,7 @@ var doctorCommand = async (root = process.cwd()) => {
|
|
|
2990
2991
|
};
|
|
2991
2992
|
|
|
2992
2993
|
// src/commands/init.ts
|
|
2993
|
-
import { mkdir as mkdir15, writeFile as writeFile16 } from "fs/promises";
|
|
2994
|
+
import { mkdir as mkdir15, readFile as readFile15, writeFile as writeFile16 } from "fs/promises";
|
|
2994
2995
|
import { existsSync as existsSync19 } from "fs";
|
|
2995
2996
|
import { relative as relative10, resolve as resolve22 } from "path";
|
|
2996
2997
|
|
|
@@ -3125,8 +3126,28 @@ var initCommand = async (root = process.cwd()) => {
|
|
|
3125
3126
|
await writeFile16(file, contents, "utf8");
|
|
3126
3127
|
log.success(`Created ${relative10(root, file)}`);
|
|
3127
3128
|
}
|
|
3129
|
+
await ensureModuleType(root);
|
|
3128
3130
|
log.detail("Next: odori doctor, then odori add title-reveal end-card, then odori dev.");
|
|
3129
3131
|
};
|
|
3132
|
+
var ensureModuleType = async (root) => {
|
|
3133
|
+
const file = resolve22(root, "package.json");
|
|
3134
|
+
if (!existsSync19(file)) {
|
|
3135
|
+
log.warn('No package.json here. Odori needs an ESM package: run npm init, then add "type": "module".');
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
let manifest;
|
|
3139
|
+
try {
|
|
3140
|
+
manifest = JSON.parse(await readFile15(file, "utf8"));
|
|
3141
|
+
} catch {
|
|
3142
|
+
log.warn('package.json is not readable JSON, so "type": "module" was not set. Odori needs it.');
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
if (manifest.type === "module") return;
|
|
3146
|
+
manifest.type = "module";
|
|
3147
|
+
await writeFile16(file, `${JSON.stringify(manifest, null, 2)}
|
|
3148
|
+
`, "utf8");
|
|
3149
|
+
log.success('Set "type": "module" in package.json');
|
|
3150
|
+
};
|
|
3130
3151
|
|
|
3131
3152
|
// src/commands/inspect.ts
|
|
3132
3153
|
import { isOdoriSchema, resolveEntryLayout as resolveEntryLayout5 } from "odori";
|
|
@@ -3339,7 +3360,7 @@ var checkInstalledContracts = async (config, videos) => {
|
|
|
3339
3360
|
};
|
|
3340
3361
|
|
|
3341
3362
|
// src/determinism.ts
|
|
3342
|
-
import { readdir as readdir8, readFile as
|
|
3363
|
+
import { readdir as readdir8, readFile as readFile16 } from "fs/promises";
|
|
3343
3364
|
import { existsSync as existsSync21 } from "fs";
|
|
3344
3365
|
import { join as join7, relative as relative11, resolve as resolve25 } from "path";
|
|
3345
3366
|
var FORBIDDEN = [
|
|
@@ -3386,7 +3407,7 @@ var checkDeterminism = async (config) => {
|
|
|
3386
3407
|
if (!existsSync21(root)) return [];
|
|
3387
3408
|
const files = await walk2(root);
|
|
3388
3409
|
const findings = await Promise.all(
|
|
3389
|
-
files.map(async (file) => scanSource(await
|
|
3410
|
+
files.map(async (file) => scanSource(await readFile16(file, "utf8"), relative11(config.root, file)))
|
|
3390
3411
|
);
|
|
3391
3412
|
return findings.flat();
|
|
3392
3413
|
};
|
|
@@ -3407,15 +3428,36 @@ var CANVAS_SCRIPT = `(() => {
|
|
|
3407
3428
|
});
|
|
3408
3429
|
|
|
3409
3430
|
return canvases.map(function (canvas, index) {
|
|
3410
|
-
|
|
3411
|
-
if (!context || canvas.width === 0 || canvas.height === 0) {
|
|
3431
|
+
if (canvas.width === 0 || canvas.height === 0) {
|
|
3412
3432
|
return {index: index, hash: "no-context", blank: true};
|
|
3413
3433
|
}
|
|
3414
3434
|
var data;
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3435
|
+
var isGl = false;
|
|
3436
|
+
var context = canvas.getContext("2d");
|
|
3437
|
+
if (context) {
|
|
3438
|
+
try {
|
|
3439
|
+
data = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
return {index: index, hash: "tainted", blank: false};
|
|
3442
|
+
}
|
|
3443
|
+
} else {
|
|
3444
|
+
/*
|
|
3445
|
+
* A canvas that already holds a GL context returns null for "2d", and
|
|
3446
|
+
* reading that as an empty canvas reported every shader video as having
|
|
3447
|
+
* drawn nothing while it was drawing correctly. Pixels come back through
|
|
3448
|
+
* readPixels instead, which is the only way to see a GL surface.
|
|
3449
|
+
*
|
|
3450
|
+
* Worth knowing when this comes back empty: a drawing buffer is cleared
|
|
3451
|
+
* once it has been composited unless the context was asked for with
|
|
3452
|
+
* preserveDrawingBuffer, so an author who has not set that gets a real
|
|
3453
|
+
* frame on screen and nothing here.
|
|
3454
|
+
*/
|
|
3455
|
+
var gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
3456
|
+
if (!gl) return {index: index, hash: "no-context", blank: true};
|
|
3457
|
+
var pixels4 = new Uint8Array(canvas.width * canvas.height * 4);
|
|
3458
|
+
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels4);
|
|
3459
|
+
data = pixels4;
|
|
3460
|
+
isGl = true;
|
|
3419
3461
|
}
|
|
3420
3462
|
|
|
3421
3463
|
// A prime stride over the whole buffer rather than a coarse grid: a grid
|
|
@@ -3431,7 +3473,7 @@ var CANVAS_SCRIPT = `(() => {
|
|
|
3431
3473
|
hash =
|
|
3432
3474
|
((hash << 5) + hash + data[offset] + data[offset + 1] * 3 + data[offset + 2] * 7 + data[offset + 3] * 11) | 0;
|
|
3433
3475
|
}
|
|
3434
|
-
return {index: index, hash: String(hash), blank: opaque === 0};
|
|
3476
|
+
return {index: index, hash: String(hash), blank: opaque === 0, gl: isGl};
|
|
3435
3477
|
});
|
|
3436
3478
|
})()`;
|
|
3437
3479
|
var FRAME_SCRIPT = `(() => {
|
|
@@ -3446,6 +3488,11 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3446
3488
|
|
|
3447
3489
|
for (var index = 0; index < nodes.length; index += 1) {
|
|
3448
3490
|
var node = nodes[index];
|
|
3491
|
+
/* The subtree an effect surface is photographing is deliberately parked
|
|
3492
|
+
outside the frame. It is the input to a canvas, not something a viewer
|
|
3493
|
+
ever sees, so judging its position or its type size is judging the
|
|
3494
|
+
wrong thing. */
|
|
3495
|
+
if (node.closest("[data-odori-capture]")) continue;
|
|
3449
3496
|
var box = node.getBoundingClientRect();
|
|
3450
3497
|
if (box.width === 0 || box.height === 0) continue;
|
|
3451
3498
|
var style = getComputedStyle(node);
|
|
@@ -3462,6 +3509,18 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3462
3509
|
if (!media && text.length === 0) continue;
|
|
3463
3510
|
|
|
3464
3511
|
var label = text.length > 0 ? '"' + text.slice(0, 32) + '"' : "<" + node.tagName.toLowerCase() + ">";
|
|
3512
|
+
|
|
3513
|
+
/*
|
|
3514
|
+
* Chrome is exempt from the readability floor.
|
|
3515
|
+
*
|
|
3516
|
+
* A component that recreates somebody else's app is right to draw that
|
|
3517
|
+
* app's sidebar at the size that app draws it. Slack's rail really is
|
|
3518
|
+
* 16px, and enlarging it until this check is happy produces a Slack that
|
|
3519
|
+
* does not look like Slack. That text is furniture: it says "this is
|
|
3520
|
+
* Slack", and nobody is meant to read it. What has to be legible is the
|
|
3521
|
+
* content the video is actually about, which is what stays checked.
|
|
3522
|
+
*/
|
|
3523
|
+
var chrome = node.closest("[data-odori-chrome]") !== null;
|
|
3465
3524
|
if (
|
|
3466
3525
|
box.right > bounds.right + 1 ||
|
|
3467
3526
|
box.left < bounds.left - 1 ||
|
|
@@ -3474,8 +3533,18 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3474
3533
|
// Normalize against the shorter side, the same reference useDesignScale
|
|
3475
3534
|
// uses, so a vertical cut is not judged as if it were letterboxed.
|
|
3476
3535
|
var reference = Math.min(bounds.width, bounds.height);
|
|
3477
|
-
|
|
3478
|
-
|
|
3536
|
+
/*
|
|
3537
|
+
* What the glyphs actually measure on screen, not what the stylesheet
|
|
3538
|
+
* asked for. A scene that pushes in on a surface makes its text bigger,
|
|
3539
|
+
* and reading font-size alone called that a violation while the viewer
|
|
3540
|
+
* was looking at type half again as large. The ratio of the painted box
|
|
3541
|
+
* to the laid-out box is every ancestor transform multiplied together,
|
|
3542
|
+
* which is exactly the correction wanted, and it cancels the root's own
|
|
3543
|
+
* fit scale because the reference is measured through it too.
|
|
3544
|
+
*/
|
|
3545
|
+
var zoom = node.offsetHeight > 0 ? box.height / node.offsetHeight : 1;
|
|
3546
|
+
var relative = ((parseFloat(style.fontSize) * zoom) / reference) * 1080;
|
|
3547
|
+
if (!chrome && text.length > 0 && relative > 0 && relative < 20) {
|
|
3479
3548
|
var note = label + " at " + Math.round(relative) + "px";
|
|
3480
3549
|
if (small.indexOf(note) < 0) small.push(note);
|
|
3481
3550
|
}
|
|
@@ -3530,7 +3599,10 @@ var testVideo = async (origin, video, config, failures, quiet = false) => {
|
|
|
3530
3599
|
for (const canvas of canvases) {
|
|
3531
3600
|
const second = again.find((item) => item.index === canvas.index);
|
|
3532
3601
|
if (canvas.blank) {
|
|
3533
|
-
failures.push({
|
|
3602
|
+
failures.push({
|
|
3603
|
+
video: id,
|
|
3604
|
+
message: canvas.gl ? `Frame ${frame}: canvas ${canvas.index} read back empty. A WebGL drawing buffer is cleared once it has been composited, so ask for the context with {preserveDrawingBuffer: true}.` : `Frame ${frame}: canvas ${canvas.index} drew nothing.`
|
|
3605
|
+
});
|
|
3534
3606
|
continue;
|
|
3535
3607
|
}
|
|
3536
3608
|
if (canvas.hash === "tainted") {
|
package/dist/cli.js
CHANGED