@odori/cli 0.0.4 → 0.0.6
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-NYXWEZU2.js → chunk-6CE7U5KB.js} +124 -39
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/dist/{registry-snapshot-MSH2EA36.js → registry-snapshot-ADKSTGDR.js} +15 -14
- package/package.json +3 -3
- package/src/cli.ts +26 -4
- package/src/commands/add.ts +12 -1
- package/src/commands/dev.ts +4 -1
- package/src/commands/exportVideo.ts +6 -0
- package/src/commands/init.ts +34 -1
- package/src/commands/test.ts +24 -2
- package/src/commands/update.ts +58 -7
- package/src/jobs.ts +1 -1
- package/src/registry-snapshot.json +15 -15
- package/src/render.ts +8 -1
- package/studio/index.html +26 -0
- package/studio/src/Studio.tsx +6 -22
- package/studio/src/components/CanvasStage.tsx +40 -3
- package/studio/src/components/ExportPanel.tsx +90 -6
- package/studio/src/components/Inspector.tsx +36 -30
- package/studio/src/components/Navigator.tsx +64 -2
- package/studio/src/components/Settings.tsx +109 -0
- package/studio/src/components/Transport.tsx +136 -58
- package/studio/src/components/ui.tsx +20 -1
- package/studio/src/settings.ts +87 -0
- package/studio/src/studio.css +269 -20
- package/studio/src/views/VideosView.tsx +12 -5
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(
|
|
@@ -465,8 +465,9 @@ var installAsset = async (config, component, options = {}) => {
|
|
|
465
465
|
};
|
|
466
466
|
|
|
467
467
|
// src/commands/update.ts
|
|
468
|
-
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
468
|
+
import { mkdir as mkdir4, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
469
469
|
import { existsSync as existsSync6 } from "fs";
|
|
470
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
470
471
|
import { relative as relative4, resolve as resolve6 } from "path";
|
|
471
472
|
import { hashString } from "odori";
|
|
472
473
|
|
|
@@ -530,16 +531,24 @@ var formatDiff = (lines, context = 2) => {
|
|
|
530
531
|
};
|
|
531
532
|
|
|
532
533
|
// src/commands/update.ts
|
|
533
|
-
var
|
|
534
|
+
var LOCKFILE = "odori.lock.json";
|
|
535
|
+
var provenanceFile = (config) => resolve6(config.root, LOCKFILE);
|
|
536
|
+
var legacyProvenanceFile = (config) => resolve6(config.root, config.outDir, "components.json");
|
|
534
537
|
var readProvenance = async (config) => {
|
|
535
|
-
const file = provenanceFile(config);
|
|
538
|
+
const file = existsSync6(provenanceFile(config)) ? provenanceFile(config) : legacyProvenanceFile(config);
|
|
536
539
|
if (!existsSync6(file)) return {};
|
|
537
|
-
|
|
540
|
+
try {
|
|
541
|
+
return JSON.parse(await readFile5(file, "utf8"));
|
|
542
|
+
} catch {
|
|
543
|
+
log.warn(`${relative4(config.root, file)} is not readable JSON. Ignoring it.`);
|
|
544
|
+
return {};
|
|
545
|
+
}
|
|
538
546
|
};
|
|
539
547
|
var writeProvenance = async (config, provenance) => {
|
|
540
|
-
await mkdir4(
|
|
548
|
+
await mkdir4(config.root, { recursive: true });
|
|
541
549
|
await writeFile5(provenanceFile(config), `${JSON.stringify(provenance, null, 2)}
|
|
542
550
|
`, "utf8");
|
|
551
|
+
await rm2(legacyProvenanceFile(config), { force: true });
|
|
543
552
|
};
|
|
544
553
|
var componentStatus = async (config, only) => {
|
|
545
554
|
const { items: registry } = await resolveRegistry(config);
|
|
@@ -583,11 +592,25 @@ var LABELS = {
|
|
|
583
592
|
diverged: "modified locally and updated upstream",
|
|
584
593
|
missing: "files missing"
|
|
585
594
|
};
|
|
595
|
+
var explainEmpty = async (config, named) => {
|
|
596
|
+
if (named.length > 0) {
|
|
597
|
+
log.detail(`${named.join(", ")} ${named.length === 1 ? "is" : "are"} not recorded in ${LOCKFILE}.`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const components = resolve6(config.root, config.componentsDir);
|
|
601
|
+
const installed = existsSync6(components) ? (await readdir2(components)).filter((e) => !e.startsWith(".")) : [];
|
|
602
|
+
if (installed.length === 0) {
|
|
603
|
+
log.detail("No registry components are installed yet. Run odori add first.");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
log.warn(`${installed.length} components are in ${config.componentsDir} but none are recorded in ${LOCKFILE}.`);
|
|
607
|
+
log.detail("Run odori add <name> to re-record them, or commit the lockfile if a teammate has one.");
|
|
608
|
+
};
|
|
586
609
|
var diffCommand = async (names, options = {}) => {
|
|
587
610
|
const config = await loadConfig(process.cwd());
|
|
588
611
|
const statuses = await componentStatus(config, names.length > 0 ? names : void 0);
|
|
589
612
|
if (statuses.length === 0) {
|
|
590
|
-
|
|
613
|
+
await explainEmpty(config, names);
|
|
591
614
|
return;
|
|
592
615
|
}
|
|
593
616
|
for (const status of statuses) {
|
|
@@ -613,7 +636,7 @@ var updateCommand = async (names, options = {}) => {
|
|
|
613
636
|
const config = await loadConfig(process.cwd());
|
|
614
637
|
const statuses = await componentStatus(config, names.length > 0 ? names : void 0);
|
|
615
638
|
if (statuses.length === 0) {
|
|
616
|
-
|
|
639
|
+
await explainEmpty(config, names);
|
|
617
640
|
return;
|
|
618
641
|
}
|
|
619
642
|
const provenance = await readProvenance(config);
|
|
@@ -662,6 +685,7 @@ var addCommand = async (names, options = {}) => {
|
|
|
662
685
|
else log.warn(`registry: the copy built into this CLI. It may be older than ${registryUrl(config)}.`);
|
|
663
686
|
const queue = [...names.map(normalizeComponentName)];
|
|
664
687
|
const installed = [];
|
|
688
|
+
const kept = [];
|
|
665
689
|
while (queue.length > 0) {
|
|
666
690
|
const name = queue.shift();
|
|
667
691
|
if (installed.includes(name)) continue;
|
|
@@ -717,7 +741,8 @@ var addCommand = async (names, options = {}) => {
|
|
|
717
741
|
const current = hashString2(await readFile6(destination, "utf8"));
|
|
718
742
|
const recorded = provenance[component.name]?.hashes[name2];
|
|
719
743
|
if (current !== recorded) {
|
|
720
|
-
log.warn(`${relative5(config.root, destination)} was modified locally. Keeping your version
|
|
744
|
+
log.warn(`${relative5(config.root, destination)} was modified locally. Keeping your version.`);
|
|
745
|
+
kept.push(relative5(config.root, destination));
|
|
721
746
|
continue;
|
|
722
747
|
}
|
|
723
748
|
}
|
|
@@ -757,6 +782,11 @@ var addCommand = async (names, options = {}) => {
|
|
|
757
782
|
return;
|
|
758
783
|
}
|
|
759
784
|
await writeProvenance(config, provenance);
|
|
785
|
+
if (kept.length > 0) {
|
|
786
|
+
log.warn(`Kept ${kept.length} locally modified ${kept.length === 1 ? "file" : "files"}:`);
|
|
787
|
+
for (const file of kept) log.detail(` ${file}`);
|
|
788
|
+
log.detail(`Run odori diff to see what upstream changed, or odori add <name> --force to replace them.`);
|
|
789
|
+
}
|
|
760
790
|
log.detail("Run odori dev to preview the installed component fixtures.");
|
|
761
791
|
};
|
|
762
792
|
var registryCommand = async () => {
|
|
@@ -785,7 +815,7 @@ import { existsSync as existsSync16 } from "fs";
|
|
|
785
815
|
import { readFile as readFile13 } from "fs/promises";
|
|
786
816
|
|
|
787
817
|
// src/jobs.ts
|
|
788
|
-
import { mkdir as mkdir6, readFile as readFile7, readdir as
|
|
818
|
+
import { mkdir as mkdir6, readFile as readFile7, readdir as readdir3, rename, writeFile as writeFile7 } from "fs/promises";
|
|
789
819
|
import { existsSync as existsSync8 } from "fs";
|
|
790
820
|
import { join as join2, resolve as resolve8 } from "path";
|
|
791
821
|
var buildsDir = (config) => resolve8(config.root, config.outDir, "builds");
|
|
@@ -872,7 +902,7 @@ var reconcileJobs = async (config) => {
|
|
|
872
902
|
var listJobs = async (config, options = {}) => {
|
|
873
903
|
if (!existsSync8(buildsDir(config))) return [];
|
|
874
904
|
if (options.reconcile !== false) await reconcileJobs(config);
|
|
875
|
-
const files = (await
|
|
905
|
+
const files = (await readdir3(buildsDir(config))).filter((file) => file.endsWith(".json"));
|
|
876
906
|
const jobs = [];
|
|
877
907
|
for (const file of files) {
|
|
878
908
|
try {
|
|
@@ -894,13 +924,13 @@ var JobQueue = class {
|
|
|
894
924
|
};
|
|
895
925
|
|
|
896
926
|
// src/discovery.ts
|
|
897
|
-
import { mkdir as mkdir7, readdir as
|
|
927
|
+
import { mkdir as mkdir7, readdir as readdir4, readFile as readFile8, stat, writeFile as writeFile8 } from "fs/promises";
|
|
898
928
|
import { existsSync as existsSync9 } from "fs";
|
|
899
929
|
import { join as join3, relative as relative6, resolve as resolve9, sep as sep2 } from "path";
|
|
900
930
|
import { hashString as hashString3 } from "odori";
|
|
901
931
|
var IGNORED = /* @__PURE__ */ new Set(["node_modules", ".git", ".odori", "out", "dist", ".next"]);
|
|
902
932
|
var walk = async (directory2, files = []) => {
|
|
903
|
-
const entries = await
|
|
933
|
+
const entries = await readdir4(directory2, { withFileTypes: true });
|
|
904
934
|
for (const entry of entries) {
|
|
905
935
|
if (entry.name.startsWith(".") || IGNORED.has(entry.name)) continue;
|
|
906
936
|
const full = join3(directory2, entry.name);
|
|
@@ -1167,7 +1197,7 @@ var createIntegrityResolver = async (config) => {
|
|
|
1167
1197
|
|
|
1168
1198
|
// src/prepare-cache.ts
|
|
1169
1199
|
import { existsSync as existsSync11 } from "fs";
|
|
1170
|
-
import { mkdir as mkdir9, readFile as readFile10, readdir as
|
|
1200
|
+
import { mkdir as mkdir9, readFile as readFile10, readdir as readdir5, rm as rm3, writeFile as writeFile10 } from "fs/promises";
|
|
1171
1201
|
import { join as join4, resolve as resolve11 } from "path";
|
|
1172
1202
|
import { hashValue } from "odori";
|
|
1173
1203
|
|
|
@@ -1199,9 +1229,9 @@ var writePrepareCache = async (config, key, value) => {
|
|
|
1199
1229
|
var clearPrepareCache = async (config, videoId) => {
|
|
1200
1230
|
const target = directory(config);
|
|
1201
1231
|
if (!existsSync11(target)) return 0;
|
|
1202
|
-
const files = await
|
|
1232
|
+
const files = await readdir5(target);
|
|
1203
1233
|
const matches = files.filter((file) => videoId ? file.startsWith(`${fileKey(videoId)}__`) : file.endsWith(".json"));
|
|
1204
|
-
await Promise.all(matches.map((file) =>
|
|
1234
|
+
await Promise.all(matches.map((file) => rm3(join4(target, file), { force: true })));
|
|
1205
1235
|
return matches.length;
|
|
1206
1236
|
};
|
|
1207
1237
|
|
|
@@ -1442,7 +1472,7 @@ var alphaWarning = (format, transparent) => transparent && !format.alpha ? `${fo
|
|
|
1442
1472
|
|
|
1443
1473
|
// src/render.ts
|
|
1444
1474
|
import { spawn as spawn2 } from "child_process";
|
|
1445
|
-
import { copyFile as copyFile2, mkdir as mkdir12, rm as
|
|
1475
|
+
import { copyFile as copyFile2, mkdir as mkdir12, rm as rm4, writeFile as writeFile13 } from "fs/promises";
|
|
1446
1476
|
import { cpus } from "os";
|
|
1447
1477
|
import { dirname as dirname5, join as join6, resolve as resolve16 } from "path";
|
|
1448
1478
|
import { chromium } from "playwright-core";
|
|
@@ -2065,7 +2095,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
2065
2095
|
);
|
|
2066
2096
|
const captureMs = performance.now() - captureStart;
|
|
2067
2097
|
onProgress?.(1, "encoding");
|
|
2068
|
-
const mixInputs = (target.audio ?? []).map((cue) => {
|
|
2098
|
+
const mixInputs = (options.audio === false ? [] : target.audio ?? []).map((cue) => {
|
|
2069
2099
|
const file = resolveCueFile(config, cue.src);
|
|
2070
2100
|
if (!file) log.warn(`Skipping audio cue ${cue.src}: only project-local files can be encoded.`);
|
|
2071
2101
|
return file ? { file, cue } : null;
|
|
@@ -2136,7 +2166,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
2136
2166
|
succeeded = true;
|
|
2137
2167
|
return output;
|
|
2138
2168
|
} finally {
|
|
2139
|
-
if (succeeded && !options.workDir) await
|
|
2169
|
+
if (succeeded && !options.workDir) await rm4(work, { recursive: true, force: true });
|
|
2140
2170
|
else if (!succeeded) log.detail(`Chunks left for inspection in ${work}`);
|
|
2141
2171
|
}
|
|
2142
2172
|
};
|
|
@@ -2534,6 +2564,7 @@ var runJob = async (config, origin, record, video, options = {}) => exportQueue.
|
|
|
2534
2564
|
quality: options.quality ?? record.render?.quality,
|
|
2535
2565
|
scale: options.scale ?? record.render?.scale,
|
|
2536
2566
|
format: options.format ?? (record.render?.format ? resolveFormat(record.render.format, record.output) : void 0),
|
|
2567
|
+
audio: options.audio ?? record.render?.audio,
|
|
2537
2568
|
skipUnchangedFrames: options.skipUnchangedFrames,
|
|
2538
2569
|
signal: controller.signal,
|
|
2539
2570
|
onTimings: (timings) => {
|
|
@@ -2596,6 +2627,7 @@ var exportCommand = async (id, options = {}) => {
|
|
|
2596
2627
|
format: format.name,
|
|
2597
2628
|
quality,
|
|
2598
2629
|
scale,
|
|
2630
|
+
audio: options.audio !== false,
|
|
2599
2631
|
...options.preset ? { preset: options.preset } : {}
|
|
2600
2632
|
});
|
|
2601
2633
|
})();
|
|
@@ -2612,6 +2644,7 @@ var exportCommand = async (id, options = {}) => {
|
|
|
2612
2644
|
quality: options.retry && options.quality === void 0 ? void 0 : quality,
|
|
2613
2645
|
scale: options.retry && options.scale === void 0 ? void 0 : scale,
|
|
2614
2646
|
format: options.retry ? void 0 : format,
|
|
2647
|
+
audio: options.audio,
|
|
2615
2648
|
skipUnchangedFrames: options.skipUnchangedFrames,
|
|
2616
2649
|
onProgress: (next) => {
|
|
2617
2650
|
if (next.status === "rendering" || next.status === "encoding") {
|
|
@@ -2756,7 +2789,8 @@ var devCommand = async (options = {}) => {
|
|
|
2756
2789
|
exportDestination(config),
|
|
2757
2790
|
`${outputName(video.entry.metadata.id)}${format.extension}`
|
|
2758
2791
|
);
|
|
2759
|
-
const
|
|
2792
|
+
const audio = body.audio !== false;
|
|
2793
|
+
const record = await createJob(config, manifest, output, { format: format.name, quality, scale, audio });
|
|
2760
2794
|
json(response, 202, record.job);
|
|
2761
2795
|
void runJob(config, origin, record, video).catch((error) => {
|
|
2762
2796
|
log.error(`Export failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2827,7 +2861,7 @@ var devCommand = async (options = {}) => {
|
|
|
2827
2861
|
|
|
2828
2862
|
// src/commands/doctor.ts
|
|
2829
2863
|
import { constants } from "fs";
|
|
2830
|
-
import { access, mkdir as mkdir13, readFile as readFile14, rm as
|
|
2864
|
+
import { access, mkdir as mkdir13, readFile as readFile14, rm as rm5, writeFile as writeFile14 } from "fs/promises";
|
|
2831
2865
|
import { existsSync as existsSync17 } from "fs";
|
|
2832
2866
|
import { createRequire as createRequire3 } from "module";
|
|
2833
2867
|
import { relative as relative8, resolve as resolve20 } from "path";
|
|
@@ -2901,7 +2935,7 @@ var runChecks = async (root) => {
|
|
|
2901
2935
|
const probe = resolve20(generated, ".doctor");
|
|
2902
2936
|
await writeFile14(probe, "", "utf8");
|
|
2903
2937
|
await access(probe, constants.W_OK);
|
|
2904
|
-
await
|
|
2938
|
+
await rm5(probe, { force: true });
|
|
2905
2939
|
writable = true;
|
|
2906
2940
|
} catch {
|
|
2907
2941
|
writable = false;
|
|
@@ -2909,10 +2943,10 @@ var runChecks = async (root) => {
|
|
|
2909
2943
|
const componentsRoot = resolve20(root, config.componentsDir);
|
|
2910
2944
|
const orphans = [];
|
|
2911
2945
|
if (existsSync17(componentsRoot)) {
|
|
2912
|
-
const { readdir:
|
|
2913
|
-
for (const entry of await
|
|
2946
|
+
const { readdir: readdir9 } = await import("fs/promises");
|
|
2947
|
+
for (const entry of await readdir9(componentsRoot, { withFileTypes: true })) {
|
|
2914
2948
|
if (!entry.isDirectory()) continue;
|
|
2915
|
-
const files = await
|
|
2949
|
+
const files = await readdir9(resolve20(componentsRoot, entry.name));
|
|
2916
2950
|
const source = files.some((file) => /\.tsx$/.test(file) && !file.endsWith(".preview.tsx"));
|
|
2917
2951
|
const fixture = files.some((file) => file.endsWith(".preview.tsx"));
|
|
2918
2952
|
if (source && !fixture) orphans.push(entry.name);
|
|
@@ -2956,12 +2990,12 @@ var doctorCommand = async (root = process.cwd()) => {
|
|
|
2956
2990
|
};
|
|
2957
2991
|
|
|
2958
2992
|
// src/commands/init.ts
|
|
2959
|
-
import { mkdir as mkdir15, writeFile as writeFile16 } from "fs/promises";
|
|
2993
|
+
import { mkdir as mkdir15, readFile as readFile15, writeFile as writeFile16 } from "fs/promises";
|
|
2960
2994
|
import { existsSync as existsSync19 } from "fs";
|
|
2961
2995
|
import { relative as relative10, resolve as resolve22 } from "path";
|
|
2962
2996
|
|
|
2963
2997
|
// src/commands/new.ts
|
|
2964
|
-
import { mkdir as mkdir14, readdir as
|
|
2998
|
+
import { mkdir as mkdir14, readdir as readdir6, writeFile as writeFile15 } from "fs/promises";
|
|
2965
2999
|
import { existsSync as existsSync18 } from "fs";
|
|
2966
3000
|
import { relative as relative9, resolve as resolve21 } from "path";
|
|
2967
3001
|
var titleCase = (value) => value.split(/[-_\s]+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
@@ -3023,7 +3057,7 @@ ${closing}
|
|
|
3023
3057
|
var installedParts = async (config) => {
|
|
3024
3058
|
const componentsDir = resolve21(config.root, config.componentsDir);
|
|
3025
3059
|
if (!existsSync18(componentsDir)) return { title: false, end: false };
|
|
3026
|
-
const entries = (await
|
|
3060
|
+
const entries = (await readdir6(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
3027
3061
|
return { title: entries.includes("title-reveal"), end: entries.includes("end-card") };
|
|
3028
3062
|
};
|
|
3029
3063
|
var newCommand = async (name, options = {}) => {
|
|
@@ -3091,8 +3125,28 @@ var initCommand = async (root = process.cwd()) => {
|
|
|
3091
3125
|
await writeFile16(file, contents, "utf8");
|
|
3092
3126
|
log.success(`Created ${relative10(root, file)}`);
|
|
3093
3127
|
}
|
|
3128
|
+
await ensureModuleType(root);
|
|
3094
3129
|
log.detail("Next: odori doctor, then odori add title-reveal end-card, then odori dev.");
|
|
3095
3130
|
};
|
|
3131
|
+
var ensureModuleType = async (root) => {
|
|
3132
|
+
const file = resolve22(root, "package.json");
|
|
3133
|
+
if (!existsSync19(file)) {
|
|
3134
|
+
log.warn('No package.json here. Odori needs an ESM package: run npm init, then add "type": "module".');
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
let manifest;
|
|
3138
|
+
try {
|
|
3139
|
+
manifest = JSON.parse(await readFile15(file, "utf8"));
|
|
3140
|
+
} catch {
|
|
3141
|
+
log.warn('package.json is not readable JSON, so "type": "module" was not set. Odori needs it.');
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
if (manifest.type === "module") return;
|
|
3145
|
+
manifest.type = "module";
|
|
3146
|
+
await writeFile16(file, `${JSON.stringify(manifest, null, 2)}
|
|
3147
|
+
`, "utf8");
|
|
3148
|
+
log.success('Set "type": "module" in package.json');
|
|
3149
|
+
};
|
|
3096
3150
|
|
|
3097
3151
|
// src/commands/inspect.ts
|
|
3098
3152
|
import { isOdoriSchema, resolveEntryLayout as resolveEntryLayout5 } from "odori";
|
|
@@ -3215,7 +3269,7 @@ import { isOdoriSchema as isOdoriSchema2, resolveEntryLayout as resolveEntryLayo
|
|
|
3215
3269
|
|
|
3216
3270
|
// src/contracts.ts
|
|
3217
3271
|
import { existsSync as existsSync20 } from "fs";
|
|
3218
|
-
import { readdir as
|
|
3272
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
3219
3273
|
import { resolve as resolve24 } from "path";
|
|
3220
3274
|
import { cueUrl as cueUrl2, isCueDefinition as isCueDefinition2, resolveEntryLayout as resolveEntryLayout8 } from "odori";
|
|
3221
3275
|
var primaryFamily = (stack) => (stack.split(",")[0] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
@@ -3284,7 +3338,7 @@ var checkAudioWindows = (cues, brand, videoId) => {
|
|
|
3284
3338
|
};
|
|
3285
3339
|
var checkInstalledContracts = async (config, videos) => {
|
|
3286
3340
|
const componentsDir = resolve24(config.root, config.componentsDir);
|
|
3287
|
-
const onDisk = existsSync20(componentsDir) ? (await
|
|
3341
|
+
const onDisk = existsSync20(componentsDir) ? (await readdir7(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
|
|
3288
3342
|
const names = /* @__PURE__ */ new Set([...Object.keys(await readProvenance(config)), ...onDisk]);
|
|
3289
3343
|
if (names.size === 0) return [];
|
|
3290
3344
|
const { items } = await resolveRegistry(config, { allowNetwork: false });
|
|
@@ -3305,7 +3359,7 @@ var checkInstalledContracts = async (config, videos) => {
|
|
|
3305
3359
|
};
|
|
3306
3360
|
|
|
3307
3361
|
// src/determinism.ts
|
|
3308
|
-
import { readdir as
|
|
3362
|
+
import { readdir as readdir8, readFile as readFile16 } from "fs/promises";
|
|
3309
3363
|
import { existsSync as existsSync21 } from "fs";
|
|
3310
3364
|
import { join as join7, relative as relative11, resolve as resolve25 } from "path";
|
|
3311
3365
|
var FORBIDDEN = [
|
|
@@ -3339,7 +3393,7 @@ var scanSource = (source, file) => {
|
|
|
3339
3393
|
return findings;
|
|
3340
3394
|
};
|
|
3341
3395
|
var walk2 = async (directory2, files = []) => {
|
|
3342
|
-
for (const entry of await
|
|
3396
|
+
for (const entry of await readdir8(directory2, { withFileTypes: true })) {
|
|
3343
3397
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3344
3398
|
const full = join7(directory2, entry.name);
|
|
3345
3399
|
if (entry.isDirectory()) await walk2(full, files);
|
|
@@ -3352,7 +3406,7 @@ var checkDeterminism = async (config) => {
|
|
|
3352
3406
|
if (!existsSync21(root)) return [];
|
|
3353
3407
|
const files = await walk2(root);
|
|
3354
3408
|
const findings = await Promise.all(
|
|
3355
|
-
files.map(async (file) => scanSource(await
|
|
3409
|
+
files.map(async (file) => scanSource(await readFile16(file, "utf8"), relative11(config.root, file)))
|
|
3356
3410
|
);
|
|
3357
3411
|
return findings.flat();
|
|
3358
3412
|
};
|
|
@@ -3428,6 +3482,18 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3428
3482
|
if (!media && text.length === 0) continue;
|
|
3429
3483
|
|
|
3430
3484
|
var label = text.length > 0 ? '"' + text.slice(0, 32) + '"' : "<" + node.tagName.toLowerCase() + ">";
|
|
3485
|
+
|
|
3486
|
+
/*
|
|
3487
|
+
* Chrome is exempt from the readability floor.
|
|
3488
|
+
*
|
|
3489
|
+
* A component that recreates somebody else's app is right to draw that
|
|
3490
|
+
* app's sidebar at the size that app draws it. Slack's rail really is
|
|
3491
|
+
* 16px, and enlarging it until this check is happy produces a Slack that
|
|
3492
|
+
* does not look like Slack. That text is furniture: it says "this is
|
|
3493
|
+
* Slack", and nobody is meant to read it. What has to be legible is the
|
|
3494
|
+
* content the video is actually about, which is what stays checked.
|
|
3495
|
+
*/
|
|
3496
|
+
var chrome = node.closest("[data-odori-chrome]") !== null;
|
|
3431
3497
|
if (
|
|
3432
3498
|
box.right > bounds.right + 1 ||
|
|
3433
3499
|
box.left < bounds.left - 1 ||
|
|
@@ -3440,8 +3506,18 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3440
3506
|
// Normalize against the shorter side, the same reference useDesignScale
|
|
3441
3507
|
// uses, so a vertical cut is not judged as if it were letterboxed.
|
|
3442
3508
|
var reference = Math.min(bounds.width, bounds.height);
|
|
3443
|
-
|
|
3444
|
-
|
|
3509
|
+
/*
|
|
3510
|
+
* What the glyphs actually measure on screen, not what the stylesheet
|
|
3511
|
+
* asked for. A scene that pushes in on a surface makes its text bigger,
|
|
3512
|
+
* and reading font-size alone called that a violation while the viewer
|
|
3513
|
+
* was looking at type half again as large. The ratio of the painted box
|
|
3514
|
+
* to the laid-out box is every ancestor transform multiplied together,
|
|
3515
|
+
* which is exactly the correction wanted, and it cancels the root's own
|
|
3516
|
+
* fit scale because the reference is measured through it too.
|
|
3517
|
+
*/
|
|
3518
|
+
var zoom = node.offsetHeight > 0 ? box.height / node.offsetHeight : 1;
|
|
3519
|
+
var relative = ((parseFloat(style.fontSize) * zoom) / reference) * 1080;
|
|
3520
|
+
if (!chrome && text.length > 0 && relative > 0 && relative < 20) {
|
|
3445
3521
|
var note = label + " at " + Math.round(relative) + "px";
|
|
3446
3522
|
if (small.indexOf(note) < 0) small.push(note);
|
|
3447
3523
|
}
|
|
@@ -3560,6 +3636,8 @@ var testCommand = async (id, options = {}) => {
|
|
|
3560
3636
|
};
|
|
3561
3637
|
|
|
3562
3638
|
// src/cli.ts
|
|
3639
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["force", "dry-run", "json", "no-audio", "no-frame-skip", "no-open", "open", "help", "version"]);
|
|
3640
|
+
var RENAMED = { still: "frame" };
|
|
3563
3641
|
var parseArgs = (argv) => {
|
|
3564
3642
|
const [command2 = "help", ...rest] = argv;
|
|
3565
3643
|
const positionals = [];
|
|
@@ -3574,7 +3652,7 @@ var parseArgs = (argv) => {
|
|
|
3574
3652
|
}
|
|
3575
3653
|
const name = token.slice(2);
|
|
3576
3654
|
const next = rest[index + 1];
|
|
3577
|
-
if (next === void 0 || next.startsWith("--")) flags[name] = true;
|
|
3655
|
+
if (BOOLEAN_FLAGS.has(name) || next === void 0 || next.startsWith("--")) flags[name] = true;
|
|
3578
3656
|
else {
|
|
3579
3657
|
flags[name] = next;
|
|
3580
3658
|
index += 1;
|
|
@@ -3618,7 +3696,7 @@ var COMMAND_FLAGS = {
|
|
|
3618
3696
|
inspect: ["json", "input"],
|
|
3619
3697
|
frame: ["at", "output", "input"],
|
|
3620
3698
|
test: ["json"],
|
|
3621
|
-
export: ["output", "input", "concurrency", "preset", "format", "quality", "scale", "no-frame-skip", "retry"],
|
|
3699
|
+
export: ["output", "input", "concurrency", "preset", "format", "quality", "scale", "no-audio", "no-frame-skip", "retry"],
|
|
3622
3700
|
jobs: [],
|
|
3623
3701
|
help: []
|
|
3624
3702
|
};
|
|
@@ -3687,11 +3765,12 @@ var USAGE = {
|
|
|
3687
3765
|
check, for CI.`,
|
|
3688
3766
|
export: `odori export <id> [--output <path>] [--input <json>] [--concurrency <n>]
|
|
3689
3767
|
[--preset <name>] [--format <name>] [--quality <tier>] [--scale <n>]
|
|
3690
|
-
[--no-frame-skip] [--retry <job>]
|
|
3768
|
+
[--no-audio] [--no-frame-skip] [--retry <job>]
|
|
3691
3769
|
Render and encode a distributable file. --format is mp4, webm, prores, gif,
|
|
3692
3770
|
or png; without it the output's extension decides, and mp4 is the default.
|
|
3693
3771
|
--quality is studio, social, or web. --scale multiplies the output size,
|
|
3694
|
-
0.25 to 2.
|
|
3772
|
+
0.25 to 2. --no-audio writes the picture with no sound. A retry keeps the
|
|
3773
|
+
settings its job was created with.`,
|
|
3695
3774
|
jobs: `odori jobs
|
|
3696
3775
|
List export jobs and their status.`
|
|
3697
3776
|
};
|
|
@@ -3814,6 +3893,7 @@ var run2 = async (argv) => {
|
|
|
3814
3893
|
quality: typeof flags.quality === "string" ? flags.quality : void 0,
|
|
3815
3894
|
scale: numberFlag(flags, "scale"),
|
|
3816
3895
|
format: typeof flags.format === "string" ? flags.format : void 0,
|
|
3896
|
+
audio: flags["no-audio"] === true ? false : void 0,
|
|
3817
3897
|
skipUnchangedFrames: flags["no-frame-skip"] === true ? false : void 0,
|
|
3818
3898
|
retry: typeof flags.retry === "string" ? flags.retry : void 0
|
|
3819
3899
|
});
|
|
@@ -3827,6 +3907,11 @@ var run2 = async (argv) => {
|
|
|
3827
3907
|
log.info(HELP);
|
|
3828
3908
|
return 0;
|
|
3829
3909
|
default: {
|
|
3910
|
+
const renamed = RENAMED[command2];
|
|
3911
|
+
if (renamed) {
|
|
3912
|
+
log.error(`"odori ${command2}" is now "odori ${renamed}".`);
|
|
3913
|
+
return 1;
|
|
3914
|
+
}
|
|
3830
3915
|
const commands = Object.keys(COMMAND_FLAGS).filter((name) => name !== "help");
|
|
3831
3916
|
const suggestion = nearest(command2, commands);
|
|
3832
3917
|
log.error(`Unknown command "${command2}".${suggestion ? ` Did you mean "odori ${suggestion}"?` : ""}`);
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -290,6 +290,13 @@ type RenderOptions = {
|
|
|
290
290
|
/** Container and codec. Defaults to H.264 in MP4. */
|
|
291
291
|
format?: VideoFormat;
|
|
292
292
|
skipUnchangedFrames?: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Mix the composition's cues into the file. Defaults to true, because the
|
|
295
|
+
* score is part of the video. Set false for a silent cut: a loop for a
|
|
296
|
+
* landing page, a clip going into an editor that has its own audio, or a
|
|
297
|
+
* reviewer who just wants the picture.
|
|
298
|
+
*/
|
|
299
|
+
audio?: boolean;
|
|
293
300
|
/** Reuse encoded chunks whose frames still look identical. */
|
|
294
301
|
cache?: boolean;
|
|
295
302
|
signal?: AbortSignal;
|
|
@@ -337,6 +344,7 @@ type JobRender = {
|
|
|
337
344
|
quality?: string;
|
|
338
345
|
scale?: number;
|
|
339
346
|
preset?: string;
|
|
347
|
+
audio?: boolean;
|
|
340
348
|
};
|
|
341
349
|
type JobRecord = {
|
|
342
350
|
job: ExportJob;
|
|
@@ -538,6 +546,7 @@ declare const runJob: (config: ResolvedConfig, origin: string, record: JobRecord
|
|
|
538
546
|
quality?: Quality;
|
|
539
547
|
scale?: number;
|
|
540
548
|
format?: VideoFormat;
|
|
549
|
+
audio?: boolean;
|
|
541
550
|
skipUnchangedFrames?: boolean;
|
|
542
551
|
signal?: AbortSignal;
|
|
543
552
|
onProgress?: (job: ExportJob) => void;
|
|
@@ -550,6 +559,8 @@ declare const exportCommand: (id: string, options?: {
|
|
|
550
559
|
quality?: string;
|
|
551
560
|
scale?: number;
|
|
552
561
|
format?: string;
|
|
562
|
+
/** False writes the picture with no audio track. */
|
|
563
|
+
audio?: boolean;
|
|
553
564
|
skipUnchangedFrames?: boolean;
|
|
554
565
|
retry?: string;
|
|
555
566
|
}) => Promise<ExportJob>;
|