@odori/cli 0.0.3 → 0.0.4
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/{chunk-RXLB2CXH.js → chunk-NYXWEZU2.js} +399 -226
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +34 -5
- package/dist/index.js +3 -3
- package/dist/registry-snapshot-MSH2EA36.js +4867 -0
- package/package.json +3 -3
- package/src/assets.ts +90 -0
- package/src/brand-file.ts +16 -4
- package/src/cli.ts +12 -9
- package/src/commands/add.ts +25 -0
- package/src/commands/dev.ts +28 -1
- package/src/commands/doctor.ts +47 -2
- package/src/commands/{still.ts → frame.ts} +23 -9
- package/src/discovery.ts +63 -2
- package/src/index.ts +1 -1
- package/src/registry-snapshot.json +1529 -327
- package/src/registry-source.ts +37 -2
- package/src/server.ts +7 -1
- package/studio/src/components/Inspector.tsx +101 -1
- package/studio/src/components/Navigator.tsx +145 -0
- package/studio/src/lib/highlight.ts +85 -0
- package/studio/src/studio.css +254 -7
- package/studio/src/views/BrandsView.tsx +18 -1
- package/studio/src/views/ComponentsView.tsx +191 -26
- package/studio/src/views/HomeView.tsx +7 -4
- package/studio/src/views/VideosView.tsx +21 -1
- package/studio/src/virtual.d.ts +4 -1
- package/dist/registry-snapshot-BDP6PVYB.js +0 -3559
|
@@ -20,9 +20,9 @@ var log = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
// src/commands/add.ts
|
|
23
|
-
import { mkdir as
|
|
24
|
-
import { existsSync as
|
|
25
|
-
import { relative as
|
|
23
|
+
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
24
|
+
import { existsSync as existsSync7 } from "fs";
|
|
25
|
+
import { relative as relative5, resolve as resolve7 } from "path";
|
|
26
26
|
import { hashString as hashString2 } from "odori";
|
|
27
27
|
|
|
28
28
|
// src/config.ts
|
|
@@ -86,6 +86,7 @@ var brandFiles = async (config) => {
|
|
|
86
86
|
return found.sort((left, right) => left.depth - right.depth).map((entry) => entry.file);
|
|
87
87
|
};
|
|
88
88
|
var registerCueInBrand = async (config, cue, componentName) => {
|
|
89
|
+
const url = "url" in cue ? cue.url : null;
|
|
89
90
|
for (const file of await brandFiles(config)) {
|
|
90
91
|
const source = await readFile(file, "utf8");
|
|
91
92
|
if (source.includes(`"${cue.name}"`) || source.includes(`'${cue.name}'`)) {
|
|
@@ -93,19 +94,24 @@ var registerCueInBrand = async (config, cue, componentName) => {
|
|
|
93
94
|
}
|
|
94
95
|
const block = source.match(/(audio:\s*\{[\s\S]*?cues:\s*\{)([\s\S]*?)(\n(\s*)\},)/);
|
|
95
96
|
const inline = source.match(/(audio:\s*\{[^\n}]*cues:\s*\{)([^\n{}]*)(\})/);
|
|
97
|
+
const value = url ? JSON.stringify(url) : `${cue.export}()`;
|
|
96
98
|
let withCue;
|
|
97
99
|
if (block) {
|
|
98
100
|
const indent = `${block[4]} `;
|
|
99
101
|
const entry = `
|
|
100
|
-
${indent}"${cue.name}": ${
|
|
102
|
+
${indent}"${cue.name}": ${value},`;
|
|
101
103
|
withCue = source.replace(block[0], `${block[1]}${block[2]}${entry}${block[3]}`);
|
|
102
104
|
} else if (inline) {
|
|
103
105
|
const existing = inline[2].trim();
|
|
104
|
-
const entry = `"${cue.name}": ${
|
|
106
|
+
const entry = `"${cue.name}": ${value}`;
|
|
105
107
|
withCue = source.replace(inline[0], `${inline[1]}${existing ? `${existing.replace(/,$/, "")}, ` : ""}${entry}${inline[3]}`);
|
|
106
108
|
} else {
|
|
107
109
|
continue;
|
|
108
110
|
}
|
|
111
|
+
if (url) {
|
|
112
|
+
await writeFile(file, withCue, "utf8");
|
|
113
|
+
return { file: relative(config.root, file), already: false };
|
|
114
|
+
}
|
|
109
115
|
const from = resolve2(config.root, config.componentsDir, componentName, componentName);
|
|
110
116
|
const specifier = relative(resolve2(file, ".."), from).split("\\").join("/");
|
|
111
117
|
const importLine = `import {${cue.export}} from "${specifier.startsWith(".") ? specifier : `./${specifier}`}";`;
|
|
@@ -288,12 +294,21 @@ var resolveWithinRoot = (root, target) => {
|
|
|
288
294
|
};
|
|
289
295
|
var DEFAULT_URL = "https://odori.dev/r/v1";
|
|
290
296
|
var registryUrl = (config) => (config.registryUrl ?? process.env.ODORI_REGISTRY ?? DEFAULT_URL).replace(/\/$/, "");
|
|
297
|
+
var registryOrigin = (config) => {
|
|
298
|
+
const url = registryUrl(config);
|
|
299
|
+
try {
|
|
300
|
+
return new URL(url).origin;
|
|
301
|
+
} catch {
|
|
302
|
+
return url.replace(/\/r(\/v\d+)?$/, "");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
291
305
|
var cacheDir = (url) => resolve4(cacheRoot(), "registry", createHash("sha256").update(url).digest("hex").slice(0, 16));
|
|
292
306
|
var toComponent = (item) => ({
|
|
293
307
|
name: item.name,
|
|
294
308
|
namespaced: item.meta?.namespaced ?? `@odori/${item.name}`,
|
|
295
309
|
kind: item.meta?.kind ?? "component",
|
|
296
310
|
...item.meta?.cue ? { cue: item.meta.cue } : {},
|
|
311
|
+
...item.meta?.asset ? { asset: item.meta.asset } : {},
|
|
297
312
|
family: item.meta?.family ?? "Uncategorized",
|
|
298
313
|
description: item.description ?? "",
|
|
299
314
|
files: item.files.map((file) => file.path.split("/").pop() ?? file.path),
|
|
@@ -302,7 +317,7 @@ var toComponent = (item) => ({
|
|
|
302
317
|
});
|
|
303
318
|
var snapshotItems = async () => {
|
|
304
319
|
try {
|
|
305
|
-
const loaded = await import("./registry-snapshot-
|
|
320
|
+
const loaded = await import("./registry-snapshot-MSH2EA36.js");
|
|
306
321
|
return loaded.default.items;
|
|
307
322
|
} catch {
|
|
308
323
|
throw new Error(
|
|
@@ -393,10 +408,66 @@ Nothing was written. This is a truncated download, a stale proxy, or a tampered
|
|
|
393
408
|
);
|
|
394
409
|
};
|
|
395
410
|
|
|
396
|
-
// src/
|
|
397
|
-
import {
|
|
411
|
+
// src/assets.ts
|
|
412
|
+
import { createHash as createHash2 } from "crypto";
|
|
398
413
|
import { existsSync as existsSync5 } from "fs";
|
|
399
|
-
import {
|
|
414
|
+
import { mkdir as mkdir3, readFile as readFile4, rm, writeFile as writeFile4 } from "fs/promises";
|
|
415
|
+
import { dirname as dirname3, relative as relative3, resolve as resolve5 } from "path";
|
|
416
|
+
var assetCache = (integrity) => resolve5(cacheRoot(), "assets", createHash2("sha256").update(integrity).digest("hex").slice(0, 16));
|
|
417
|
+
var verify = (bytes, expected, source) => {
|
|
418
|
+
const actual = `sha256-${createHash2("sha256").update(bytes).digest("base64")}`;
|
|
419
|
+
if (actual === expected) return;
|
|
420
|
+
throw new Error(
|
|
421
|
+
`The bytes at ${source} do not match the hash the registry published.
|
|
422
|
+
expected ${expected}
|
|
423
|
+
received ${actual}
|
|
424
|
+
Nothing was written. This is a truncated download, a stale proxy, or a tampered file.`
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
var installAsset = async (config, component, options = {}) => {
|
|
428
|
+
const asset = component.asset;
|
|
429
|
+
if (!asset) return false;
|
|
430
|
+
const destination = resolveWithinRoot(config.root, asset.target);
|
|
431
|
+
const exists = existsSync5(destination);
|
|
432
|
+
log.detail(` ${exists ? "replace" : "create "} ${relative3(config.root, destination)}`);
|
|
433
|
+
if (options.dryRun) return false;
|
|
434
|
+
if (exists && !options.force) {
|
|
435
|
+
log.warn(`${relative3(config.root, destination)} already exists. Keeping it. Use --force to replace it.`);
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
const cached = assetCache(asset.integrity);
|
|
439
|
+
let bytes = null;
|
|
440
|
+
if (existsSync5(cached)) {
|
|
441
|
+
const stored = await readFile4(cached);
|
|
442
|
+
try {
|
|
443
|
+
verify(stored, asset.integrity, cached);
|
|
444
|
+
bytes = stored;
|
|
445
|
+
} catch {
|
|
446
|
+
log.detail(" the cached copy did not verify; downloading it again");
|
|
447
|
+
await rm(cached, { force: true });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (!bytes) {
|
|
451
|
+
const url = new URL(asset.url, `${registryOrigin(config)}/`).toString();
|
|
452
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
453
|
+
if (!response.ok) throw new Error(`Could not download ${url}: ${response.status} ${response.statusText}`);
|
|
454
|
+
const downloaded = new Uint8Array(await response.arrayBuffer());
|
|
455
|
+
verify(downloaded, asset.integrity, url);
|
|
456
|
+
await mkdir3(dirname3(cached), { recursive: true });
|
|
457
|
+
await writeFile4(cached, downloaded);
|
|
458
|
+
bytes = downloaded;
|
|
459
|
+
}
|
|
460
|
+
await mkdir3(dirname3(destination), { recursive: true });
|
|
461
|
+
await writeFile4(destination, bytes);
|
|
462
|
+
log.success(`${component.namespaced} to ${relative3(config.root, destination)}`);
|
|
463
|
+
log.detail(` ${component.family} \xB7 ${Math.round(asset.bytes / 1024)} KB \xB7 answers to "${asset.cue}"`);
|
|
464
|
+
return true;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/commands/update.ts
|
|
468
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
469
|
+
import { existsSync as existsSync6 } from "fs";
|
|
470
|
+
import { relative as relative4, resolve as resolve6 } from "path";
|
|
400
471
|
import { hashString } from "odori";
|
|
401
472
|
|
|
402
473
|
// src/diff.ts
|
|
@@ -459,15 +530,15 @@ var formatDiff = (lines, context = 2) => {
|
|
|
459
530
|
};
|
|
460
531
|
|
|
461
532
|
// src/commands/update.ts
|
|
462
|
-
var provenanceFile = (config) =>
|
|
533
|
+
var provenanceFile = (config) => resolve6(config.root, config.outDir, "components.json");
|
|
463
534
|
var readProvenance = async (config) => {
|
|
464
535
|
const file = provenanceFile(config);
|
|
465
|
-
if (!
|
|
466
|
-
return JSON.parse(await
|
|
536
|
+
if (!existsSync6(file)) return {};
|
|
537
|
+
return JSON.parse(await readFile5(file, "utf8"));
|
|
467
538
|
};
|
|
468
539
|
var writeProvenance = async (config, provenance) => {
|
|
469
|
-
await
|
|
470
|
-
await
|
|
540
|
+
await mkdir4(resolve6(config.root, config.outDir), { recursive: true });
|
|
541
|
+
await writeFile5(provenanceFile(config), `${JSON.stringify(provenance, null, 2)}
|
|
471
542
|
`, "utf8");
|
|
472
543
|
};
|
|
473
544
|
var componentStatus = async (config, only) => {
|
|
@@ -483,10 +554,10 @@ var componentStatus = async (config, only) => {
|
|
|
483
554
|
const upstreamFiles = new Map(item.files.map((file) => [file.path.split("/").pop() ?? file.path, file.content]));
|
|
484
555
|
const files = await Promise.all(
|
|
485
556
|
component.files.map(async (file) => {
|
|
486
|
-
const localPath =
|
|
557
|
+
const localPath = resolve6(config.root, config.componentsDir, name, file);
|
|
487
558
|
const content = upstreamFiles.get(file);
|
|
488
559
|
if (content === void 0) throw new Error(`The registry document for "${name}" has no file named ${file}.`);
|
|
489
|
-
const local =
|
|
560
|
+
const local = existsSync6(localPath) ? hashString(await readFile5(localPath, "utf8")) : null;
|
|
490
561
|
return {
|
|
491
562
|
file,
|
|
492
563
|
localPath,
|
|
@@ -523,14 +594,14 @@ var diffCommand = async (names, options = {}) => {
|
|
|
523
594
|
log.title(`@odori/${status.name} ${LABELS[status.state]}`);
|
|
524
595
|
for (const file of status.files) {
|
|
525
596
|
if (file.local === null) {
|
|
526
|
-
log.error(` ${file.file} is missing from ${
|
|
597
|
+
log.error(` ${file.file} is missing from ${relative4(config.root, resolve6(file.localPath, ".."))}`);
|
|
527
598
|
continue;
|
|
528
599
|
}
|
|
529
600
|
if (file.local === file.upstream) {
|
|
530
601
|
log.detail(` ${file.file} identical to upstream`);
|
|
531
602
|
continue;
|
|
532
603
|
}
|
|
533
|
-
const lines = diffLines(await
|
|
604
|
+
const lines = diffLines(await readFile5(file.localPath, "utf8"), file.content);
|
|
534
605
|
const { added, removed } = countChanges(lines);
|
|
535
606
|
log.info(` ${file.file} +${added} -${removed} against upstream`);
|
|
536
607
|
if (options.full) for (const line of formatDiff(lines)) log.detail(` ${line}`);
|
|
@@ -563,8 +634,8 @@ var updateCommand = async (names, options = {}) => {
|
|
|
563
634
|
continue;
|
|
564
635
|
}
|
|
565
636
|
for (const file of status.files) {
|
|
566
|
-
await
|
|
567
|
-
await
|
|
637
|
+
await mkdir4(resolve6(file.localPath, ".."), { recursive: true });
|
|
638
|
+
await writeFile5(file.localPath, file.content, "utf8");
|
|
568
639
|
}
|
|
569
640
|
provenance[status.name] = {
|
|
570
641
|
source: `@odori/${status.name}`,
|
|
@@ -604,34 +675,54 @@ var addCommand = async (names, options = {}) => {
|
|
|
604
675
|
if (provider) queue.push(provider.name);
|
|
605
676
|
else log.warn(`${component.namespaced} needs a "${cue}" cue and no registry entry provides one.`);
|
|
606
677
|
}
|
|
678
|
+
if (component.kind === "asset" && component.asset) {
|
|
679
|
+
const written = await installAsset(config, component, { dryRun: options.dryRun, force: options.force });
|
|
680
|
+
if (!options.dryRun && written) {
|
|
681
|
+
installed.push(component.name);
|
|
682
|
+
const registered = await registerCueInBrand(
|
|
683
|
+
config,
|
|
684
|
+
{ name: component.asset.cue, url: component.asset.url },
|
|
685
|
+
component.name
|
|
686
|
+
);
|
|
687
|
+
if (registered?.already) {
|
|
688
|
+
log.detail(` "${component.asset.cue}" is already registered in ${registered.file}`);
|
|
689
|
+
} else if (registered) {
|
|
690
|
+
log.detail(` registered "${component.asset.cue}" in ${registered.file}`);
|
|
691
|
+
} else {
|
|
692
|
+
log.warn(` No brand with an audio.cues block found. Add it yourself:`);
|
|
693
|
+
log.detail(` audio: {cues: {"${component.asset.cue}": "${component.asset.url}"}}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
607
698
|
const { item, origin } = await resolveItem(config, component.name);
|
|
608
699
|
verifyIntegrity(item, origin);
|
|
609
|
-
const target =
|
|
700
|
+
const target = resolve7(config.root, config.componentsDir, assertSafeName(component.name));
|
|
610
701
|
const hashes = {};
|
|
611
702
|
for (const file of item.files) {
|
|
612
703
|
const destination = resolveWithinRoot(config.root, file.target);
|
|
613
|
-
const exists =
|
|
614
|
-
log.detail(` ${exists ? "replace" : "create "} ${
|
|
704
|
+
const exists = existsSync7(destination);
|
|
705
|
+
log.detail(` ${exists ? "replace" : "create "} ${relative5(config.root, destination)}`);
|
|
615
706
|
}
|
|
616
707
|
if (options.dryRun) {
|
|
617
708
|
installed.push(component.name);
|
|
618
709
|
continue;
|
|
619
710
|
}
|
|
620
|
-
await
|
|
711
|
+
await mkdir5(target, { recursive: true });
|
|
621
712
|
for (const file of item.files) {
|
|
622
713
|
const name2 = file.path.split("/").pop() ?? file.path;
|
|
623
714
|
const destination = resolveWithinRoot(config.root, file.target);
|
|
624
715
|
hashes[name2] = hashString2(file.content);
|
|
625
|
-
if (
|
|
626
|
-
const current = hashString2(await
|
|
716
|
+
if (existsSync7(destination) && !options.force) {
|
|
717
|
+
const current = hashString2(await readFile6(destination, "utf8"));
|
|
627
718
|
const recorded = provenance[component.name]?.hashes[name2];
|
|
628
719
|
if (current !== recorded) {
|
|
629
|
-
log.warn(`${
|
|
720
|
+
log.warn(`${relative5(config.root, destination)} was modified locally. Keeping your version. Use --force to replace it.`);
|
|
630
721
|
continue;
|
|
631
722
|
}
|
|
632
723
|
}
|
|
633
|
-
await
|
|
634
|
-
await
|
|
724
|
+
await mkdir5(resolve7(destination, ".."), { recursive: true });
|
|
725
|
+
await writeFile6(destination, file.content, "utf8");
|
|
635
726
|
}
|
|
636
727
|
provenance[component.name] = {
|
|
637
728
|
source: component.namespaced,
|
|
@@ -640,7 +731,7 @@ var addCommand = async (names, options = {}) => {
|
|
|
640
731
|
hashes
|
|
641
732
|
};
|
|
642
733
|
installed.push(component.name);
|
|
643
|
-
log.success(`${component.namespaced} to ${
|
|
734
|
+
log.success(`${component.namespaced} to ${relative5(config.root, target)}/`);
|
|
644
735
|
if (component.kind === "cue" && component.cue) {
|
|
645
736
|
log.detail(
|
|
646
737
|
` ${component.family} \xB7 ${component.contract.recommendedDurationInFrames} frames \xB7 registers "${component.cue.name}"`
|
|
@@ -688,19 +779,19 @@ var registryCommand = async () => {
|
|
|
688
779
|
};
|
|
689
780
|
|
|
690
781
|
// src/commands/dev.ts
|
|
691
|
-
import { resolve as
|
|
782
|
+
import { relative as relative7, resolve as resolve19 } from "path";
|
|
692
783
|
import { homedir as homedir2 } from "os";
|
|
693
|
-
import { existsSync as
|
|
694
|
-
import { readFile as
|
|
784
|
+
import { existsSync as existsSync16 } from "fs";
|
|
785
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
695
786
|
|
|
696
787
|
// src/jobs.ts
|
|
697
|
-
import { mkdir as
|
|
698
|
-
import { existsSync as
|
|
699
|
-
import { join as join2, resolve as
|
|
700
|
-
var buildsDir = (config) =>
|
|
788
|
+
import { mkdir as mkdir6, readFile as readFile7, readdir as readdir2, rename, writeFile as writeFile7 } from "fs/promises";
|
|
789
|
+
import { existsSync as existsSync8 } from "fs";
|
|
790
|
+
import { join as join2, resolve as resolve8 } from "path";
|
|
791
|
+
var buildsDir = (config) => resolve8(config.root, config.outDir, "builds");
|
|
701
792
|
var jobFile = (config, id) => join2(buildsDir(config), `${id}.json`);
|
|
702
793
|
var createJob = async (config, manifest, output, render) => {
|
|
703
|
-
await
|
|
794
|
+
await mkdir6(buildsDir(config), { recursive: true });
|
|
704
795
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
705
796
|
const job = {
|
|
706
797
|
id: `job-${manifest.manifestHash.slice(0, 10)}-${Date.now().toString(36)}`,
|
|
@@ -714,14 +805,14 @@ var createJob = async (config, manifest, output, render) => {
|
|
|
714
805
|
updatedAt: now
|
|
715
806
|
};
|
|
716
807
|
const record = { job, manifest, output, ...render ? { render } : {} };
|
|
717
|
-
await
|
|
808
|
+
await writeFile7(jobFile(config, job.id), `${JSON.stringify(record, null, 2)}
|
|
718
809
|
`, "utf8");
|
|
719
810
|
return record;
|
|
720
811
|
};
|
|
721
812
|
var readJob = async (config, id) => {
|
|
722
813
|
const file = jobFile(config, id);
|
|
723
|
-
if (!
|
|
724
|
-
return JSON.parse(await
|
|
814
|
+
if (!existsSync8(file)) throw new Error(`Unknown job "${id}". Run odori jobs to list them.`);
|
|
815
|
+
return JSON.parse(await readFile7(file, "utf8"));
|
|
725
816
|
};
|
|
726
817
|
var writeLocks = /* @__PURE__ */ new Map();
|
|
727
818
|
var withJobLock = (id, task) => {
|
|
@@ -736,7 +827,7 @@ var withJobLock = (id, task) => {
|
|
|
736
827
|
var writeRecord = async (config, record) => {
|
|
737
828
|
const file = jobFile(config, record.job.id);
|
|
738
829
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
739
|
-
await
|
|
830
|
+
await writeFile7(temporary, `${JSON.stringify(record, null, 2)}
|
|
740
831
|
`, "utf8");
|
|
741
832
|
await rename(temporary, file);
|
|
742
833
|
};
|
|
@@ -779,13 +870,13 @@ var reconcileJobs = async (config) => {
|
|
|
779
870
|
return stale.length;
|
|
780
871
|
};
|
|
781
872
|
var listJobs = async (config, options = {}) => {
|
|
782
|
-
if (!
|
|
873
|
+
if (!existsSync8(buildsDir(config))) return [];
|
|
783
874
|
if (options.reconcile !== false) await reconcileJobs(config);
|
|
784
875
|
const files = (await readdir2(buildsDir(config))).filter((file) => file.endsWith(".json"));
|
|
785
876
|
const jobs = [];
|
|
786
877
|
for (const file of files) {
|
|
787
878
|
try {
|
|
788
|
-
const raw = await
|
|
879
|
+
const raw = await readFile7(join2(buildsDir(config), file), "utf8");
|
|
789
880
|
jobs.push(JSON.parse(raw).job);
|
|
790
881
|
} catch {
|
|
791
882
|
continue;
|
|
@@ -803,9 +894,9 @@ var JobQueue = class {
|
|
|
803
894
|
};
|
|
804
895
|
|
|
805
896
|
// src/discovery.ts
|
|
806
|
-
import { mkdir as
|
|
807
|
-
import { existsSync as
|
|
808
|
-
import { join as join3, relative as
|
|
897
|
+
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile8, stat, writeFile as writeFile8 } from "fs/promises";
|
|
898
|
+
import { existsSync as existsSync9 } from "fs";
|
|
899
|
+
import { join as join3, relative as relative6, resolve as resolve9, sep as sep2 } from "path";
|
|
809
900
|
import { hashString as hashString3 } from "odori";
|
|
810
901
|
var IGNORED = /* @__PURE__ */ new Set(["node_modules", ".git", ".odori", "out", "dist", ".next"]);
|
|
811
902
|
var walk = async (directory2, files = []) => {
|
|
@@ -827,22 +918,22 @@ var toIdentifier = (value, prefix) => {
|
|
|
827
918
|
};
|
|
828
919
|
var AUDIO_EXTENSIONS = /\.(m4a|mp3|wav|aac|ogg|opus|flac)$/i;
|
|
829
920
|
var discoverAudio = async (config) => {
|
|
830
|
-
const root =
|
|
831
|
-
if (!
|
|
832
|
-
const publicRoot =
|
|
921
|
+
const root = resolve9(config.root, config.audioDir);
|
|
922
|
+
if (!existsSync9(root)) return [];
|
|
923
|
+
const publicRoot = resolve9(config.root, "public");
|
|
833
924
|
const files = (await walk(root)).filter((file) => AUDIO_EXTENSIONS.test(file)).sort();
|
|
834
925
|
return Promise.all(
|
|
835
926
|
files.map(async (file) => ({
|
|
836
|
-
name:
|
|
837
|
-
url: file.startsWith(`${publicRoot}${sep2}`) ? `/${
|
|
838
|
-
relativeFile:
|
|
927
|
+
name: relative6(root, file).replace(AUDIO_EXTENSIONS, "").split(sep2).join("/"),
|
|
928
|
+
url: file.startsWith(`${publicRoot}${sep2}`) ? `/${relative6(publicRoot, file).split(sep2).join("/")}` : `/${relative6(config.root, file).split(sep2).join("/")}`,
|
|
929
|
+
relativeFile: relative6(config.root, file),
|
|
839
930
|
bytes: (await stat(file)).size
|
|
840
931
|
}))
|
|
841
932
|
);
|
|
842
933
|
};
|
|
843
934
|
var discoverProject = async (config) => {
|
|
844
|
-
const videosRoot =
|
|
845
|
-
if (!
|
|
935
|
+
const videosRoot = resolve9(config.root, config.videosDir);
|
|
936
|
+
if (!existsSync9(videosRoot)) {
|
|
846
937
|
throw new Error(`No ${config.videosDir}/ directory found in ${config.root}. Run "odori init" first.`);
|
|
847
938
|
}
|
|
848
939
|
const files = (await walk(videosRoot)).sort();
|
|
@@ -850,18 +941,42 @@ var discoverProject = async (config) => {
|
|
|
850
941
|
const videos = [];
|
|
851
942
|
const previews = [];
|
|
852
943
|
const brands = [];
|
|
944
|
+
const categories = [];
|
|
853
945
|
const hashParts = [];
|
|
854
|
-
const
|
|
946
|
+
const importedBy = {};
|
|
947
|
+
const componentsRoot = resolve9(config.root, config.componentsDir);
|
|
855
948
|
for (const file of files) {
|
|
856
|
-
const relativeFile =
|
|
949
|
+
const relativeFile = relative6(config.root, file);
|
|
857
950
|
let contents = "";
|
|
858
951
|
if (/\.(tsx|ts|css|json)$/.test(file)) {
|
|
859
|
-
contents = await
|
|
952
|
+
contents = await readFile8(file, "utf8");
|
|
860
953
|
hashParts.push(`${relativeFile}:${hashString3(contents)}`);
|
|
861
954
|
}
|
|
862
955
|
const base = file.split(sep2).pop() ?? "";
|
|
956
|
+
if (base === "category.json") {
|
|
957
|
+
const path = relative6(componentsRoot, resolve9(file, "..")).split(sep2).join("/");
|
|
958
|
+
if (!path.startsWith("..")) {
|
|
959
|
+
try {
|
|
960
|
+
const declared = JSON.parse(contents);
|
|
961
|
+
categories.push({
|
|
962
|
+
path,
|
|
963
|
+
...typeof declared.name === "string" ? { name: declared.name } : {},
|
|
964
|
+
...typeof declared.order === "number" ? { order: declared.order } : {}
|
|
965
|
+
});
|
|
966
|
+
} catch (error) {
|
|
967
|
+
log.warn(
|
|
968
|
+
`${relativeFile} is not valid JSON, so that directory names itself: ${error instanceof Error ? error.message : String(error)}`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
863
973
|
if (base === "video.tsx") {
|
|
864
|
-
const
|
|
974
|
+
for (const match of contents.matchAll(/from\s+["'][^"']*\/components\/([^/"']+)\//g)) {
|
|
975
|
+
(importedBy[match[1]] ??= /* @__PURE__ */ new Set()).add(relative6(videosRoot, file).replace(/\/?video\.tsx$/, "") || "video");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (base === "video.tsx") {
|
|
979
|
+
const slug = relative6(videosRoot, file).replace(/\/?video\.tsx$/, "").split(sep2).join("/") || "video";
|
|
865
980
|
videos.push({
|
|
866
981
|
slug,
|
|
867
982
|
file,
|
|
@@ -887,7 +1002,7 @@ var discoverProject = async (config) => {
|
|
|
887
1002
|
// From the whole relative path, like previews: basenames repeat
|
|
888
1003
|
// (`layout.tsx` beside `brands/layout.ts`), identifiers cannot.
|
|
889
1004
|
identifier: toIdentifier(
|
|
890
|
-
`${
|
|
1005
|
+
`${relative6(videosRoot, file).replace(/\.tsx?$/, "").split(sep2).join("-")}-module`,
|
|
891
1006
|
"brands"
|
|
892
1007
|
)
|
|
893
1008
|
});
|
|
@@ -898,15 +1013,19 @@ var discoverProject = async (config) => {
|
|
|
898
1013
|
file,
|
|
899
1014
|
relativeFile,
|
|
900
1015
|
importPath: file,
|
|
901
|
-
identifier: toIdentifier(`${
|
|
1016
|
+
identifier: toIdentifier(`${relative6(videosRoot, file).split(sep2).join("-")}`, "preview")
|
|
902
1017
|
});
|
|
903
1018
|
}
|
|
904
1019
|
}
|
|
905
|
-
|
|
1020
|
+
for (const preview of previews) {
|
|
1021
|
+
const users = importedBy[preview.name];
|
|
1022
|
+
if (users) preview.usedBy = [...users].sort();
|
|
1023
|
+
}
|
|
1024
|
+
return { videos, previews, brands, audio, categories, sourceHash: hashString3(hashParts.join("|")) };
|
|
906
1025
|
};
|
|
907
1026
|
var generateImports = (graph, outDir) => {
|
|
908
1027
|
const importPath = (file) => {
|
|
909
|
-
const relativePath =
|
|
1028
|
+
const relativePath = relative6(outDir, file).split(sep2).join("/");
|
|
910
1029
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
911
1030
|
};
|
|
912
1031
|
const lines = [
|
|
@@ -939,17 +1058,21 @@ var generateImports = (graph, outDir) => {
|
|
|
939
1058
|
return lines.join("\n");
|
|
940
1059
|
};
|
|
941
1060
|
var writeGenerated = async (config, graph) => {
|
|
942
|
-
const outDir =
|
|
943
|
-
await
|
|
1061
|
+
const outDir = resolve9(config.root, config.outDir);
|
|
1062
|
+
await mkdir7(outDir, { recursive: true });
|
|
944
1063
|
const target = join3(outDir, "imports.generated.ts");
|
|
945
|
-
await
|
|
946
|
-
await
|
|
1064
|
+
await writeFile8(target, generateImports(graph, outDir), "utf8");
|
|
1065
|
+
await writeFile8(
|
|
947
1066
|
join3(outDir, "catalog.json"),
|
|
948
1067
|
`${JSON.stringify(
|
|
949
1068
|
{
|
|
950
1069
|
sourceHash: graph.sourceHash,
|
|
951
1070
|
videos: graph.videos.map((video) => ({ slug: video.slug, file: video.relativeFile })),
|
|
952
|
-
previews: graph.previews.map((preview) => ({
|
|
1071
|
+
previews: graph.previews.map((preview) => ({
|
|
1072
|
+
name: preview.name,
|
|
1073
|
+
file: preview.relativeFile,
|
|
1074
|
+
usedBy: preview.usedBy ?? []
|
|
1075
|
+
})),
|
|
953
1076
|
brands: graph.brands.map((brand) => ({ name: brand.name, file: brand.relativeFile })),
|
|
954
1077
|
audio: graph.audio.map((entry) => ({ name: entry.name, url: entry.url, file: entry.relativeFile }))
|
|
955
1078
|
},
|
|
@@ -963,7 +1086,7 @@ var writeGenerated = async (config, graph) => {
|
|
|
963
1086
|
};
|
|
964
1087
|
|
|
965
1088
|
// src/project.ts
|
|
966
|
-
import { resolve as
|
|
1089
|
+
import { resolve as resolve12 } from "path";
|
|
967
1090
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
968
1091
|
import {
|
|
969
1092
|
createRenderManifest,
|
|
@@ -973,45 +1096,45 @@ import {
|
|
|
973
1096
|
} from "odori";
|
|
974
1097
|
|
|
975
1098
|
// src/integrity.ts
|
|
976
|
-
import { createHash as
|
|
977
|
-
import { existsSync as
|
|
978
|
-
import { mkdir as
|
|
979
|
-
import { dirname as
|
|
980
|
-
var cacheFile = (config) =>
|
|
1099
|
+
import { createHash as createHash3 } from "crypto";
|
|
1100
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1101
|
+
import { mkdir as mkdir8, readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
|
|
1102
|
+
import { dirname as dirname4, resolve as resolve10 } from "path";
|
|
1103
|
+
var cacheFile = (config) => resolve10(config.root, config.outDir, "cache", "integrity.json");
|
|
981
1104
|
var readCache = async (config) => {
|
|
982
1105
|
const file = cacheFile(config);
|
|
983
|
-
if (!
|
|
1106
|
+
if (!existsSync10(file)) return {};
|
|
984
1107
|
try {
|
|
985
|
-
return JSON.parse(await
|
|
1108
|
+
return JSON.parse(await readFile9(file, "utf8"));
|
|
986
1109
|
} catch {
|
|
987
1110
|
return {};
|
|
988
1111
|
}
|
|
989
1112
|
};
|
|
990
1113
|
var writeCache = async (config, cache) => {
|
|
991
1114
|
const file = cacheFile(config);
|
|
992
|
-
await
|
|
993
|
-
await
|
|
1115
|
+
await mkdir8(dirname4(file), { recursive: true });
|
|
1116
|
+
await writeFile9(file, `${JSON.stringify(cache, null, 2)}
|
|
994
1117
|
`, "utf8");
|
|
995
1118
|
};
|
|
996
|
-
var sha256 = (bytes) => `sha256-${
|
|
1119
|
+
var sha256 = (bytes) => `sha256-${createHash3("sha256").update(bytes).digest("base64")}`;
|
|
997
1120
|
var localCandidates = (config, url) => [
|
|
998
|
-
|
|
999
|
-
|
|
1121
|
+
resolve10(config.root, "public", url.replace(/^\//, "")),
|
|
1122
|
+
resolve10(config.root, url.replace(/^\//, ""))
|
|
1000
1123
|
];
|
|
1001
|
-
var isServed = (config, file) => file.startsWith(
|
|
1124
|
+
var isServed = (config, file) => file.startsWith(resolve10(config.root, "public") + "/");
|
|
1002
1125
|
var createIntegrityResolver = async (config) => {
|
|
1003
1126
|
const cache = await readCache(config);
|
|
1004
1127
|
const warned = /* @__PURE__ */ new Set();
|
|
1005
1128
|
let dirty = false;
|
|
1006
1129
|
const resolveIntegrity = async (url) => {
|
|
1007
1130
|
if (url.startsWith("/__odori/cue/")) return `cue-${url.slice(url.lastIndexOf("-") + 1).replace(/\.wav$/, "")}`;
|
|
1008
|
-
const local = localCandidates(config, url).find((candidate) =>
|
|
1131
|
+
const local = localCandidates(config, url).find((candidate) => existsSync10(candidate));
|
|
1009
1132
|
if (local) {
|
|
1010
1133
|
if (!isServed(config, local) && !warned.has(url)) {
|
|
1011
1134
|
warned.add(url);
|
|
1012
1135
|
log.warn(`${url} resolves to ${local}, which is outside public/ and will not be served. Move it into public/.`);
|
|
1013
1136
|
}
|
|
1014
|
-
const bytes = await
|
|
1137
|
+
const bytes = await readFile9(local);
|
|
1015
1138
|
const { mtimeMs } = await import("fs/promises").then((fs) => fs.stat(local));
|
|
1016
1139
|
const hit = cache[url];
|
|
1017
1140
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === bytes.byteLength) return hit.integrity;
|
|
@@ -1043,9 +1166,9 @@ var createIntegrityResolver = async (config) => {
|
|
|
1043
1166
|
};
|
|
1044
1167
|
|
|
1045
1168
|
// src/prepare-cache.ts
|
|
1046
|
-
import { existsSync as
|
|
1047
|
-
import { mkdir as
|
|
1048
|
-
import { join as join4, resolve as
|
|
1169
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1170
|
+
import { mkdir as mkdir9, readFile as readFile10, readdir as readdir4, rm as rm2, writeFile as writeFile10 } from "fs/promises";
|
|
1171
|
+
import { join as join4, resolve as resolve11 } from "path";
|
|
1049
1172
|
import { hashValue } from "odori";
|
|
1050
1173
|
|
|
1051
1174
|
// src/paths.ts
|
|
@@ -1053,13 +1176,13 @@ var outputName = (id) => id.split("/").join("-");
|
|
|
1053
1176
|
var fileKey = (id) => id.split("/").join("+");
|
|
1054
1177
|
|
|
1055
1178
|
// src/prepare-cache.ts
|
|
1056
|
-
var directory = (config) =>
|
|
1179
|
+
var directory = (config) => resolve11(config.root, config.outDir, "cache", "prepare");
|
|
1057
1180
|
var prepareCacheKey = (key) => `${fileKey(key.videoId)}__${hashValue(key)}`;
|
|
1058
1181
|
var readPrepareCache = async (config, key) => {
|
|
1059
1182
|
const file = join4(directory(config), `${prepareCacheKey(key)}.json`);
|
|
1060
|
-
if (!
|
|
1183
|
+
if (!existsSync11(file)) return { hit: false, value: void 0 };
|
|
1061
1184
|
try {
|
|
1062
|
-
const entry = JSON.parse(await
|
|
1185
|
+
const entry = JSON.parse(await readFile10(file, "utf8"));
|
|
1063
1186
|
return { hit: true, value: entry.value };
|
|
1064
1187
|
} catch {
|
|
1065
1188
|
return { hit: false, value: void 0 };
|
|
@@ -1068,17 +1191,17 @@ var readPrepareCache = async (config, key) => {
|
|
|
1068
1191
|
var writePrepareCache = async (config, key, value) => {
|
|
1069
1192
|
if (value === void 0) return;
|
|
1070
1193
|
const target = directory(config);
|
|
1071
|
-
await
|
|
1194
|
+
await mkdir9(target, { recursive: true });
|
|
1072
1195
|
const entry = { key, value, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1073
|
-
await
|
|
1196
|
+
await writeFile10(join4(target, `${prepareCacheKey(key)}.json`), `${JSON.stringify(entry, null, 2)}
|
|
1074
1197
|
`, "utf8");
|
|
1075
1198
|
};
|
|
1076
1199
|
var clearPrepareCache = async (config, videoId) => {
|
|
1077
1200
|
const target = directory(config);
|
|
1078
|
-
if (!
|
|
1201
|
+
if (!existsSync11(target)) return 0;
|
|
1079
1202
|
const files = await readdir4(target);
|
|
1080
1203
|
const matches = files.filter((file) => videoId ? file.startsWith(`${fileKey(videoId)}__`) : file.endsWith(".json"));
|
|
1081
|
-
await Promise.all(matches.map((file) =>
|
|
1204
|
+
await Promise.all(matches.map((file) => rm2(join4(target, file), { force: true })));
|
|
1082
1205
|
return matches.length;
|
|
1083
1206
|
};
|
|
1084
1207
|
|
|
@@ -1122,7 +1245,7 @@ var findVideo = (videos, id) => {
|
|
|
1122
1245
|
return found;
|
|
1123
1246
|
};
|
|
1124
1247
|
var runPrepare = async (video, config, graph, input, options = {}) => {
|
|
1125
|
-
const prepareFile =
|
|
1248
|
+
const prepareFile = resolve12(video.file, "..", "prepare.ts");
|
|
1126
1249
|
let prepare;
|
|
1127
1250
|
try {
|
|
1128
1251
|
const module = await import(pathToFileURL2(prepareFile).href);
|
|
@@ -1319,20 +1442,20 @@ var alphaWarning = (format, transparent) => transparent && !format.alpha ? `${fo
|
|
|
1319
1442
|
|
|
1320
1443
|
// src/render.ts
|
|
1321
1444
|
import { spawn as spawn2 } from "child_process";
|
|
1322
|
-
import { copyFile as copyFile2, mkdir as
|
|
1445
|
+
import { copyFile as copyFile2, mkdir as mkdir12, rm as rm3, writeFile as writeFile13 } from "fs/promises";
|
|
1323
1446
|
import { cpus } from "os";
|
|
1324
|
-
import { dirname as
|
|
1447
|
+
import { dirname as dirname5, join as join6, resolve as resolve16 } from "path";
|
|
1325
1448
|
import { chromium } from "playwright-core";
|
|
1326
1449
|
|
|
1327
1450
|
// src/audio-mix.ts
|
|
1328
|
-
import { existsSync as
|
|
1329
|
-
import { resolve as
|
|
1451
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1452
|
+
import { resolve as resolve14 } from "path";
|
|
1330
1453
|
import { duckEnvelope, envelopeAtFrame } from "odori";
|
|
1331
1454
|
|
|
1332
1455
|
// src/cues.ts
|
|
1333
|
-
import { existsSync as
|
|
1334
|
-
import { mkdir as
|
|
1335
|
-
import { basename, resolve as
|
|
1456
|
+
import { existsSync as existsSync12, statSync } from "fs";
|
|
1457
|
+
import { mkdir as mkdir10, writeFile as writeFile11 } from "fs/promises";
|
|
1458
|
+
import { basename, resolve as resolve13 } from "path";
|
|
1336
1459
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
1337
1460
|
import {
|
|
1338
1461
|
SAMPLE_RATE,
|
|
@@ -1343,8 +1466,8 @@ import {
|
|
|
1343
1466
|
isCueDefinition,
|
|
1344
1467
|
resolveEntryLayout as resolveEntryLayout2
|
|
1345
1468
|
} from "odori";
|
|
1346
|
-
var cueCacheDir = (config) =>
|
|
1347
|
-
var cueFile = (config, url) =>
|
|
1469
|
+
var cueCacheDir = (config) => resolve13(config.root, config.outDir, "cues");
|
|
1470
|
+
var cueFile = (config, url) => resolve13(cueCacheDir(config), basename(url));
|
|
1348
1471
|
var materializeCues = async (config, brands, fps) => {
|
|
1349
1472
|
const seen = /* @__PURE__ */ new Map();
|
|
1350
1473
|
for (const brand of brands) {
|
|
@@ -1353,17 +1476,17 @@ var materializeCues = async (config, brands, fps) => {
|
|
|
1353
1476
|
}
|
|
1354
1477
|
}
|
|
1355
1478
|
if (seen.size === 0) return [];
|
|
1356
|
-
await
|
|
1479
|
+
await mkdir10(cueCacheDir(config), { recursive: true });
|
|
1357
1480
|
const written = [];
|
|
1358
1481
|
for (const [url, cue] of seen) {
|
|
1359
1482
|
const file = cueFile(config, url);
|
|
1360
|
-
if (
|
|
1483
|
+
if (existsSync12(file)) {
|
|
1361
1484
|
written.push({ cue, file, rendered: false });
|
|
1362
1485
|
continue;
|
|
1363
1486
|
}
|
|
1364
1487
|
const samples = cueSamples(cue, fps);
|
|
1365
1488
|
const signal = cue.render({ samples, sampleRate: SAMPLE_RATE });
|
|
1366
|
-
await
|
|
1489
|
+
await writeFile11(file, encodeWav(signal));
|
|
1367
1490
|
written.push({ cue, file, rendered: true });
|
|
1368
1491
|
}
|
|
1369
1492
|
return written;
|
|
@@ -1421,13 +1544,13 @@ var resolveCueFile = (config, src) => {
|
|
|
1421
1544
|
if (/^https?:\/\//.test(src)) return null;
|
|
1422
1545
|
if (src.startsWith("/__odori/cue/")) {
|
|
1423
1546
|
const generated = cueFile(config, src);
|
|
1424
|
-
return
|
|
1547
|
+
return existsSync13(generated) ? generated : null;
|
|
1425
1548
|
}
|
|
1426
1549
|
const candidates = [
|
|
1427
|
-
|
|
1428
|
-
|
|
1550
|
+
resolve14(config.root, "public", src.replace(/^\//, "")),
|
|
1551
|
+
resolve14(config.root, src.replace(/^\//, ""))
|
|
1429
1552
|
];
|
|
1430
|
-
return candidates.find((candidate) =>
|
|
1553
|
+
return candidates.find((candidate) => existsSync13(candidate)) ?? null;
|
|
1431
1554
|
};
|
|
1432
1555
|
var volumeFilter = (cue, cues, fps) => {
|
|
1433
1556
|
const authored = cue.gainPoints ?? [];
|
|
@@ -1547,11 +1670,11 @@ var planChunks = ({
|
|
|
1547
1670
|
var chunkFrames = (chunk) => Array.from({ length: length(chunk) }, (_, offset) => chunk.start + offset);
|
|
1548
1671
|
|
|
1549
1672
|
// src/chunk-cache.ts
|
|
1550
|
-
import { existsSync as
|
|
1551
|
-
import { copyFile, mkdir as
|
|
1552
|
-
import { join as join5, resolve as
|
|
1673
|
+
import { existsSync as existsSync14 } from "fs";
|
|
1674
|
+
import { copyFile, mkdir as mkdir11, readFile as readFile11, writeFile as writeFile12 } from "fs/promises";
|
|
1675
|
+
import { join as join5, resolve as resolve15 } from "path";
|
|
1553
1676
|
import { hashValue as hashValue2 } from "odori";
|
|
1554
|
-
var cacheDir2 = (config) =>
|
|
1677
|
+
var cacheDir2 = (config) => resolve15(config.root, config.outDir, "cache", "chunks");
|
|
1555
1678
|
var chunkKey = (identity) => hashValue2({
|
|
1556
1679
|
videoId: identity.videoId,
|
|
1557
1680
|
// The browser that drew the frames is part of what the frames are. Without
|
|
@@ -1576,9 +1699,9 @@ var chunkKey = (identity) => hashValue2({
|
|
|
1576
1699
|
var readChunkRecord = async (config, key) => {
|
|
1577
1700
|
const meta = join5(cacheDir2(config), `${key}.json`);
|
|
1578
1701
|
const media = join5(cacheDir2(config), `${key}.mp4`);
|
|
1579
|
-
if (!
|
|
1702
|
+
if (!existsSync14(meta) || !existsSync14(media)) return null;
|
|
1580
1703
|
try {
|
|
1581
|
-
return JSON.parse(await
|
|
1704
|
+
return JSON.parse(await readFile11(meta, "utf8"));
|
|
1582
1705
|
} catch {
|
|
1583
1706
|
return null;
|
|
1584
1707
|
}
|
|
@@ -1588,10 +1711,10 @@ var useChunkRecord = async (config, key, destination) => {
|
|
|
1588
1711
|
};
|
|
1589
1712
|
var writeChunkRecord = async (config, key, signatures, file) => {
|
|
1590
1713
|
const directory2 = cacheDir2(config);
|
|
1591
|
-
await
|
|
1714
|
+
await mkdir11(directory2, { recursive: true });
|
|
1592
1715
|
await copyFile(file, join5(directory2, `${key}.mp4`));
|
|
1593
1716
|
const record = { key, signatures, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1594
|
-
await
|
|
1717
|
+
await writeFile12(join5(directory2, `${key}.json`), `${JSON.stringify(record)}
|
|
1595
1718
|
`, "utf8");
|
|
1596
1719
|
};
|
|
1597
1720
|
var signaturesMatch = (recorded, observed) => recorded.length === observed.length && recorded.every((signature, index) => signature === observed[index]);
|
|
@@ -1739,7 +1862,7 @@ var ensureFfmpeg = async (config) => {
|
|
|
1739
1862
|
var renderStill = async (origin, target, frame, output, config) => {
|
|
1740
1863
|
const { browser, page, errors } = await openRenderPage(origin, target, config);
|
|
1741
1864
|
try {
|
|
1742
|
-
await
|
|
1865
|
+
await mkdir12(dirname5(output), { recursive: true });
|
|
1743
1866
|
await seekTo(page, frame);
|
|
1744
1867
|
await page.screenshot({ path: output });
|
|
1745
1868
|
if (errors.length > 0) log.warn(`The page reported an error while rendering: ${errors[0]}`);
|
|
@@ -1895,8 +2018,8 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
1895
2018
|
const chunkFormat = chunkable ? format : LOSSLESS;
|
|
1896
2019
|
const skipUnchanged = options.skipUnchangedFrames ?? config.skipUnchangedFrames ?? true;
|
|
1897
2020
|
const cache = options.cache ?? config.cacheChunks ?? true;
|
|
1898
|
-
const work = options.workDir ??
|
|
1899
|
-
await
|
|
2021
|
+
const work = options.workDir ?? resolve16(config.root, config.outDir, "frames", `${target.videoId.split("/").join("-")}-${Date.now().toString(36)}`);
|
|
2022
|
+
await mkdir12(work, { recursive: true });
|
|
1900
2023
|
const concurrency = chunkable ? requested : 1;
|
|
1901
2024
|
const { chunks, lanes } = planChunks({
|
|
1902
2025
|
durationInFrames: target.durationInFrames,
|
|
@@ -1907,7 +2030,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
1907
2030
|
const stats = { captured: 0, reused: 0, cachedChunks: 0 };
|
|
1908
2031
|
let succeeded = false;
|
|
1909
2032
|
try {
|
|
1910
|
-
await
|
|
2033
|
+
await mkdir12(dirname5(output), { recursive: true });
|
|
1911
2034
|
const captureStart = performance.now();
|
|
1912
2035
|
await Promise.all(
|
|
1913
2036
|
lanes.map(
|
|
@@ -1954,12 +2077,12 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
1954
2077
|
await copyFile2(ordered[0], silent);
|
|
1955
2078
|
} else {
|
|
1956
2079
|
const list = join6(work, "chunks.txt");
|
|
1957
|
-
await
|
|
2080
|
+
await writeFile13(list, ordered.map((file) => `file '${file.split("'").join("'\\''")}'`).join("\n"), "utf8");
|
|
1958
2081
|
await run(ffmpeg, ["-y", "-f", "concat", "-safe", "0", "-i", list, "-c", "copy", silent], options.signal);
|
|
1959
2082
|
}
|
|
1960
2083
|
if (!chunkable) {
|
|
1961
2084
|
const destination = format.name === "png" && !output.includes("%") ? sequencePattern(output) : output;
|
|
1962
|
-
if (destination !== output) await
|
|
2085
|
+
if (destination !== output) await mkdir12(dirname5(destination), { recursive: true });
|
|
1963
2086
|
await run(ffmpeg, ["-y", "-i", silent, ...format.args(encode), destination], options.signal);
|
|
1964
2087
|
if (mixInputs.length > 0) {
|
|
1965
2088
|
log.detail(`${format.name} carries no audio track; ${mixInputs.length} cue(s) were not mixed in.`);
|
|
@@ -2013,7 +2136,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
|
|
|
2013
2136
|
succeeded = true;
|
|
2014
2137
|
return output;
|
|
2015
2138
|
} finally {
|
|
2016
|
-
if (succeeded && !options.workDir) await
|
|
2139
|
+
if (succeeded && !options.workDir) await rm3(work, { recursive: true, force: true });
|
|
2017
2140
|
else if (!succeeded) log.detail(`Chunks left for inspection in ${work}`);
|
|
2018
2141
|
}
|
|
2019
2142
|
};
|
|
@@ -2045,25 +2168,25 @@ var openInBrowser = (url) => {
|
|
|
2045
2168
|
};
|
|
2046
2169
|
|
|
2047
2170
|
// src/server.ts
|
|
2048
|
-
import { existsSync as
|
|
2171
|
+
import { existsSync as existsSync15 } from "fs";
|
|
2049
2172
|
import { createRequire as createRequire2 } from "module";
|
|
2050
2173
|
import { fileURLToPath } from "url";
|
|
2051
2174
|
import { createServer } from "vite";
|
|
2052
2175
|
import react from "@vitejs/plugin-react";
|
|
2053
|
-
import { readFile as
|
|
2054
|
-
import { dirname as
|
|
2055
|
-
var cliRoot =
|
|
2056
|
-
var studioRoot =
|
|
2057
|
-
var studioEntry =
|
|
2058
|
-
var installRoot =
|
|
2176
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
2177
|
+
import { dirname as dirname6, resolve as resolve17, sep as sep3 } from "path";
|
|
2178
|
+
var cliRoot = resolve17(dirname6(fileURLToPath(import.meta.url)), "..");
|
|
2179
|
+
var studioRoot = resolve17(cliRoot, "studio");
|
|
2180
|
+
var studioEntry = resolve17(studioRoot, "index.html");
|
|
2181
|
+
var installRoot = resolve17(cliRoot, "..", "..");
|
|
2059
2182
|
var VIRTUAL_ID = "virtual:odori-project";
|
|
2060
2183
|
var RESOLVED_ID = `\0${VIRTUAL_ID}`;
|
|
2061
2184
|
var runtimeSource = (root) => {
|
|
2062
|
-
for (const from of [
|
|
2185
|
+
for (const from of [resolve17(root, "package.json"), import.meta.url]) {
|
|
2063
2186
|
try {
|
|
2064
2187
|
const manifest = createRequire2(from).resolve("odori/package.json");
|
|
2065
|
-
const src =
|
|
2066
|
-
if (
|
|
2188
|
+
const src = resolve17(manifest, "..", "src");
|
|
2189
|
+
if (existsSync15(resolve17(src, "index.tsx"))) return src;
|
|
2067
2190
|
} catch {
|
|
2068
2191
|
}
|
|
2069
2192
|
}
|
|
@@ -2102,6 +2225,8 @@ var odoriProjectPlugin = (config, getGraph) => ({
|
|
|
2102
2225
|
`export const project = ${JSON.stringify({
|
|
2103
2226
|
root: config.root,
|
|
2104
2227
|
videosDir: config.videosDir,
|
|
2228
|
+
componentsDir: config.componentsDir,
|
|
2229
|
+
categories: graph.categories,
|
|
2105
2230
|
exportDir: config.exportDir,
|
|
2106
2231
|
audioDir: config.audioDir,
|
|
2107
2232
|
docsUrl: config.docsUrl,
|
|
@@ -2110,7 +2235,11 @@ var odoriProjectPlugin = (config, getGraph) => ({
|
|
|
2110
2235
|
assets: config.assets ?? [],
|
|
2111
2236
|
files: {
|
|
2112
2237
|
videos: graph.videos.map((video) => ({ id: video.slug, file: video.relativeFile })),
|
|
2113
|
-
previews: graph.previews.map((preview) => ({
|
|
2238
|
+
previews: graph.previews.map((preview) => ({
|
|
2239
|
+
id: preview.name,
|
|
2240
|
+
file: preview.relativeFile,
|
|
2241
|
+
usedBy: preview.usedBy ?? []
|
|
2242
|
+
})),
|
|
2114
2243
|
brands: graph.brands.map((brand) => ({ id: brand.name, file: brand.relativeFile }))
|
|
2115
2244
|
}
|
|
2116
2245
|
})};`
|
|
@@ -2180,15 +2309,15 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2180
2309
|
],
|
|
2181
2310
|
// The project's public/ directory is served at the root, so brand fonts,
|
|
2182
2311
|
// logos, and footage resolve identically in preview and render.
|
|
2183
|
-
publicDir:
|
|
2312
|
+
publicDir: existsSync15(resolve17(config.root, "public")) ? resolve17(config.root, "public") : false,
|
|
2184
2313
|
resolve: {
|
|
2185
2314
|
dedupe: ["react", "react-dom", "odori"],
|
|
2186
2315
|
// Only when the runtime is present as source. A consumer resolves the
|
|
2187
2316
|
// published package through its exports map instead.
|
|
2188
2317
|
alias: odoriSrc ? [
|
|
2189
|
-
{ find: /^odori\/preview$/, replacement:
|
|
2190
|
-
{ find: /^odori\/manifest$/, replacement:
|
|
2191
|
-
{ find: /^odori$/, replacement:
|
|
2318
|
+
{ find: /^odori\/preview$/, replacement: resolve17(odoriSrc, "preview.ts") },
|
|
2319
|
+
{ find: /^odori\/manifest$/, replacement: resolve17(odoriSrc, "manifest.ts") },
|
|
2320
|
+
{ find: /^odori$/, replacement: resolve17(odoriSrc, "index.tsx") }
|
|
2192
2321
|
] : []
|
|
2193
2322
|
},
|
|
2194
2323
|
server: {
|
|
@@ -2218,7 +2347,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2218
2347
|
}, 150);
|
|
2219
2348
|
};
|
|
2220
2349
|
const rediscover = async (file) => {
|
|
2221
|
-
if (!file.startsWith(
|
|
2350
|
+
if (!file.startsWith(resolve17(config.root, config.videosDir))) return;
|
|
2222
2351
|
const isEntry = file.endsWith("video.tsx") || file.endsWith(".preview.tsx") || file.includes(`${sep3}brands${sep3}`);
|
|
2223
2352
|
if (!isEntry) return;
|
|
2224
2353
|
try {
|
|
@@ -2235,7 +2364,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2235
2364
|
};
|
|
2236
2365
|
vite.watcher.on("add", (file) => void rediscover(file));
|
|
2237
2366
|
vite.watcher.on("unlink", (file) => void rediscover(file));
|
|
2238
|
-
vite.watcher.add(
|
|
2367
|
+
vite.watcher.add(resolve17(config.root, config.videosDir));
|
|
2239
2368
|
const reloadConfig = async (file) => {
|
|
2240
2369
|
if (!/odori\.config\.(?:ts|mjs|js)$/.test(file)) return;
|
|
2241
2370
|
try {
|
|
@@ -2252,14 +2381,14 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2252
2381
|
vite.ws.send({ type: "full-reload" });
|
|
2253
2382
|
};
|
|
2254
2383
|
const refreshCues = (file) => {
|
|
2255
|
-
if (!file.startsWith(
|
|
2384
|
+
if (!file.startsWith(resolve17(config.root, config.videosDir) + sep3)) return;
|
|
2256
2385
|
if (!/\.tsx?$/.test(file)) return;
|
|
2257
2386
|
scheduleCueRefresh();
|
|
2258
2387
|
};
|
|
2259
2388
|
vite.watcher.on("change", (file) => refreshCues(file));
|
|
2260
2389
|
vite.watcher.on("change", (file) => void reloadConfig(file));
|
|
2261
2390
|
for (const name of ["odori.config.ts", "odori.config.mjs", "odori.config.js"]) {
|
|
2262
|
-
vite.watcher.add(
|
|
2391
|
+
vite.watcher.add(resolve17(config.root, name));
|
|
2263
2392
|
}
|
|
2264
2393
|
vite.middlewares.use(async (request, response, next) => {
|
|
2265
2394
|
const url = (request.url ?? "/").split("?")[0];
|
|
@@ -2269,7 +2398,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2269
2398
|
return;
|
|
2270
2399
|
}
|
|
2271
2400
|
try {
|
|
2272
|
-
const html = await
|
|
2401
|
+
const html = await readFile12(studioEntry, "utf8");
|
|
2273
2402
|
response.statusCode = 200;
|
|
2274
2403
|
response.setHeader("content-type", "text/html");
|
|
2275
2404
|
response.end(await vite.transformIndexHtml(url, html));
|
|
@@ -2290,7 +2419,7 @@ var startStudioServer = async (initialConfig, options = {}) => {
|
|
|
2290
2419
|
};
|
|
2291
2420
|
|
|
2292
2421
|
// src/commands/exportVideo.ts
|
|
2293
|
-
import { resolve as
|
|
2422
|
+
import { resolve as resolve18 } from "path";
|
|
2294
2423
|
import { resolveEntryLayout as resolveEntryLayout4 } from "odori";
|
|
2295
2424
|
|
|
2296
2425
|
// src/commands/shared.ts
|
|
@@ -2459,7 +2588,7 @@ var exportCommand = async (id, options = {}) => {
|
|
|
2459
2588
|
options.input ?? {},
|
|
2460
2589
|
{ scenes: compiled.scenes, audio: compiled.audio }
|
|
2461
2590
|
);
|
|
2462
|
-
const output =
|
|
2591
|
+
const output = resolve18(
|
|
2463
2592
|
config.root,
|
|
2464
2593
|
options.output ?? `${config.exportDir}/${outputName(id)}${format.extension}`
|
|
2465
2594
|
);
|
|
@@ -2525,8 +2654,8 @@ var json = (response, status, payload) => {
|
|
|
2525
2654
|
response.end(JSON.stringify(payload));
|
|
2526
2655
|
};
|
|
2527
2656
|
var exportDestination = (config) => {
|
|
2528
|
-
const downloads =
|
|
2529
|
-
return
|
|
2657
|
+
const downloads = resolve19(homedir2(), "Downloads");
|
|
2658
|
+
return existsSync16(downloads) ? downloads : resolve19(config.root, config.exportDir);
|
|
2530
2659
|
};
|
|
2531
2660
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
2532
2661
|
var hostOf = (value) => {
|
|
@@ -2585,7 +2714,7 @@ var devCommand = async (options = {}) => {
|
|
|
2585
2714
|
);
|
|
2586
2715
|
const frame = Number(body.frame ?? 0);
|
|
2587
2716
|
const inline = body.inline === true;
|
|
2588
|
-
const file = inline ?
|
|
2717
|
+
const file = inline ? resolve19(config.root, config.outDir, `${outputName(video.entry.metadata.id)}-${frame}.png`) : resolve19(exportDestination(config), `${outputName(video.entry.metadata.id)}-${frame}.png`);
|
|
2589
2718
|
await renderStill(
|
|
2590
2719
|
origin,
|
|
2591
2720
|
targetFor(
|
|
@@ -2601,7 +2730,7 @@ var devCommand = async (options = {}) => {
|
|
|
2601
2730
|
if (inline) {
|
|
2602
2731
|
response.statusCode = 200;
|
|
2603
2732
|
response.setHeader("content-type", "image/png");
|
|
2604
|
-
response.end(await
|
|
2733
|
+
response.end(await readFile13(file));
|
|
2605
2734
|
return;
|
|
2606
2735
|
}
|
|
2607
2736
|
json(response, 200, { id: "still", status: "ready", progress: 1, output: file });
|
|
@@ -2623,7 +2752,7 @@ var devCommand = async (options = {}) => {
|
|
|
2623
2752
|
input,
|
|
2624
2753
|
{ scenes: compiled.scenes, audio: compiled.audio }
|
|
2625
2754
|
);
|
|
2626
|
-
const output =
|
|
2755
|
+
const output = resolve19(
|
|
2627
2756
|
exportDestination(config),
|
|
2628
2757
|
`${outputName(video.entry.metadata.id)}${format.extension}`
|
|
2629
2758
|
);
|
|
@@ -2656,6 +2785,24 @@ var devCommand = async (options = {}) => {
|
|
|
2656
2785
|
json(response, 200, (await readJob(config, id)).job);
|
|
2657
2786
|
return;
|
|
2658
2787
|
}
|
|
2788
|
+
if (request.method === "GET" && url.startsWith("/source")) {
|
|
2789
|
+
const asked = new URL(url, "http://localhost").searchParams.get("file") ?? "";
|
|
2790
|
+
const file = resolve19(config.root, asked);
|
|
2791
|
+
const inside = relative7(config.root, file);
|
|
2792
|
+
const readable = /\.(tsx?|jsx?|css|json|md)$/.test(file);
|
|
2793
|
+
if (!inside || inside.startsWith("..") || !readable) {
|
|
2794
|
+
json(response, 400, { error: `Refusing to read ${asked}.` });
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
try {
|
|
2798
|
+
response.statusCode = 200;
|
|
2799
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2800
|
+
response.end(await readFile13(file, "utf8"));
|
|
2801
|
+
} catch {
|
|
2802
|
+
json(response, 404, { error: `${asked} is not there.` });
|
|
2803
|
+
}
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2659
2806
|
if (request.method === "GET" && url.startsWith("/jobs")) {
|
|
2660
2807
|
json(response, 200, await listJobs(config));
|
|
2661
2808
|
return;
|
|
@@ -2680,16 +2827,16 @@ var devCommand = async (options = {}) => {
|
|
|
2680
2827
|
|
|
2681
2828
|
// src/commands/doctor.ts
|
|
2682
2829
|
import { constants } from "fs";
|
|
2683
|
-
import { access, mkdir as
|
|
2684
|
-
import { existsSync as
|
|
2830
|
+
import { access, mkdir as mkdir13, readFile as readFile14, rm as rm4, writeFile as writeFile14 } from "fs/promises";
|
|
2831
|
+
import { existsSync as existsSync17 } from "fs";
|
|
2685
2832
|
import { createRequire as createRequire3 } from "module";
|
|
2686
|
-
import { relative as
|
|
2833
|
+
import { relative as relative8, resolve as resolve20 } from "path";
|
|
2687
2834
|
var MINIMUM_NODE = 20;
|
|
2688
2835
|
var version = (value) => value.replace(/^v/, "").split(".").map(Number);
|
|
2689
2836
|
var runChecks = async (root) => {
|
|
2690
2837
|
const checks = [];
|
|
2691
2838
|
const config = await loadConfig(root);
|
|
2692
|
-
const require2 = createRequire3(
|
|
2839
|
+
const require2 = createRequire3(resolve20(root, "package.json"));
|
|
2693
2840
|
const [major] = version(process.version);
|
|
2694
2841
|
checks.push({
|
|
2695
2842
|
name: "Node",
|
|
@@ -2700,7 +2847,7 @@ var runChecks = async (root) => {
|
|
|
2700
2847
|
let react2 = "not found";
|
|
2701
2848
|
let reactOk = false;
|
|
2702
2849
|
try {
|
|
2703
|
-
const manifest = JSON.parse(await
|
|
2850
|
+
const manifest = JSON.parse(await readFile14(require2.resolve("react/package.json"), "utf8"));
|
|
2704
2851
|
react2 = manifest.version;
|
|
2705
2852
|
reactOk = version(react2)[0] >= 19;
|
|
2706
2853
|
} catch {
|
|
@@ -2712,16 +2859,16 @@ var runChecks = async (root) => {
|
|
|
2712
2859
|
ok: reactOk,
|
|
2713
2860
|
fix: "The runtime needs React 19. Install it: npm install react@19 react-dom@19"
|
|
2714
2861
|
});
|
|
2715
|
-
const videosDir =
|
|
2862
|
+
const videosDir = resolve20(config.root, config.videosDir);
|
|
2716
2863
|
checks.push({
|
|
2717
2864
|
name: "Source root",
|
|
2718
|
-
detail:
|
|
2719
|
-
ok:
|
|
2865
|
+
detail: existsSync17(videosDir) ? relative8(config.root, videosDir) + "/" : `no ${config.videosDir}/`,
|
|
2866
|
+
ok: existsSync17(videosDir),
|
|
2720
2867
|
fix: 'Run "odori init" to add the videos source root.'
|
|
2721
2868
|
});
|
|
2722
2869
|
checks.push({
|
|
2723
2870
|
name: "Config",
|
|
2724
|
-
detail: config.configPath ?
|
|
2871
|
+
detail: config.configPath ? relative8(config.root, config.configPath) : "defaults (no odori.config.ts)",
|
|
2725
2872
|
// Loading got this far, so a config that exists also parsed.
|
|
2726
2873
|
ok: true
|
|
2727
2874
|
});
|
|
@@ -2747,23 +2894,42 @@ var runChecks = async (root) => {
|
|
|
2747
2894
|
detail: unpinned.length === 0 ? `pinned binaries from ${cacheRoot()}` : `${unpinned.length} of 2 from the host; frames may differ from another machine`,
|
|
2748
2895
|
ok: true
|
|
2749
2896
|
});
|
|
2750
|
-
const generated =
|
|
2897
|
+
const generated = resolve20(config.root, ".odori");
|
|
2751
2898
|
let writable = false;
|
|
2752
2899
|
try {
|
|
2753
|
-
await
|
|
2754
|
-
const probe =
|
|
2755
|
-
await
|
|
2900
|
+
await mkdir13(generated, { recursive: true });
|
|
2901
|
+
const probe = resolve20(generated, ".doctor");
|
|
2902
|
+
await writeFile14(probe, "", "utf8");
|
|
2756
2903
|
await access(probe, constants.W_OK);
|
|
2757
|
-
await
|
|
2904
|
+
await rm4(probe, { force: true });
|
|
2758
2905
|
writable = true;
|
|
2759
2906
|
} catch {
|
|
2760
2907
|
writable = false;
|
|
2761
2908
|
}
|
|
2909
|
+
const componentsRoot = resolve20(root, config.componentsDir);
|
|
2910
|
+
const orphans = [];
|
|
2911
|
+
if (existsSync17(componentsRoot)) {
|
|
2912
|
+
const { readdir: readdir8 } = await import("fs/promises");
|
|
2913
|
+
for (const entry of await readdir8(componentsRoot, { withFileTypes: true })) {
|
|
2914
|
+
if (!entry.isDirectory()) continue;
|
|
2915
|
+
const files = await readdir8(resolve20(componentsRoot, entry.name));
|
|
2916
|
+
const source = files.some((file) => /\.tsx$/.test(file) && !file.endsWith(".preview.tsx"));
|
|
2917
|
+
const fixture = files.some((file) => file.endsWith(".preview.tsx"));
|
|
2918
|
+
if (source && !fixture) orphans.push(entry.name);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
checks.push({
|
|
2922
|
+
name: "Component previews",
|
|
2923
|
+
detail: orphans.length === 0 ? "every component has a fixture" : `${orphans.length} without a fixture: ${orphans.slice(0, 4).join(", ")}${orphans.length > 4 ? ", \u2026" : ""}`,
|
|
2924
|
+
ok: true,
|
|
2925
|
+
warn: orphans.length > 0,
|
|
2926
|
+
fix: `Add a sibling <name>.preview.tsx with defineComponentPreview so Studio can play it on its own. A component with no fixture only ever renders inside a video.`
|
|
2927
|
+
});
|
|
2762
2928
|
checks.push({
|
|
2763
2929
|
name: "Generated cache",
|
|
2764
2930
|
detail: writable ? ".odori/ is writable" : ".odori/ cannot be written",
|
|
2765
2931
|
ok: writable,
|
|
2766
|
-
fix: `Odori writes its import graph and render cache to ${
|
|
2932
|
+
fix: `Odori writes its import graph and render cache to ${relative8(process.cwd(), generated) || ".odori"}. Check the directory's permissions.`
|
|
2767
2933
|
});
|
|
2768
2934
|
return checks;
|
|
2769
2935
|
};
|
|
@@ -2773,12 +2939,15 @@ var doctorCommand = async (root = process.cwd()) => {
|
|
|
2773
2939
|
log.title("odori doctor");
|
|
2774
2940
|
for (const check of checks) {
|
|
2775
2941
|
const label = check.name.padEnd(width);
|
|
2776
|
-
if (check.ok) log.
|
|
2942
|
+
if (check.ok && check.warn) log.warn(`${label} ${check.detail}`);
|
|
2943
|
+
else if (check.ok) log.success(`${label} ${check.detail}`);
|
|
2777
2944
|
else log.error(`${label} ${check.detail}`);
|
|
2778
2945
|
}
|
|
2946
|
+
const warned = checks.filter((check) => check.ok && check.warn);
|
|
2779
2947
|
const failed = checks.filter((check) => !check.ok);
|
|
2780
2948
|
if (failed.length === 0) {
|
|
2781
2949
|
log.detail("Everything a render needs is present.");
|
|
2950
|
+
for (const check of warned) log.detail(` ${check.fix}`);
|
|
2782
2951
|
return 0;
|
|
2783
2952
|
}
|
|
2784
2953
|
log.info("");
|
|
@@ -2787,14 +2956,14 @@ var doctorCommand = async (root = process.cwd()) => {
|
|
|
2787
2956
|
};
|
|
2788
2957
|
|
|
2789
2958
|
// src/commands/init.ts
|
|
2790
|
-
import { mkdir as
|
|
2791
|
-
import { existsSync as
|
|
2792
|
-
import { relative as
|
|
2959
|
+
import { mkdir as mkdir15, writeFile as writeFile16 } from "fs/promises";
|
|
2960
|
+
import { existsSync as existsSync19 } from "fs";
|
|
2961
|
+
import { relative as relative10, resolve as resolve22 } from "path";
|
|
2793
2962
|
|
|
2794
2963
|
// src/commands/new.ts
|
|
2795
|
-
import { mkdir as
|
|
2796
|
-
import { existsSync as
|
|
2797
|
-
import { relative as
|
|
2964
|
+
import { mkdir as mkdir14, readdir as readdir5, writeFile as writeFile15 } from "fs/promises";
|
|
2965
|
+
import { existsSync as existsSync18 } from "fs";
|
|
2966
|
+
import { relative as relative9, resolve as resolve21 } from "path";
|
|
2798
2967
|
var titleCase = (value) => value.split(/[-_\s]+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2799
2968
|
var pascalCase = (value) => titleCase(value).replace(/\s+/g, "");
|
|
2800
2969
|
var videoTemplate = (name, hasLayout) => `import {Scene, Video, defineVideoMetadata} from "odori";
|
|
@@ -2852,27 +3021,27 @@ ${closing}
|
|
|
2852
3021
|
`;
|
|
2853
3022
|
};
|
|
2854
3023
|
var installedParts = async (config) => {
|
|
2855
|
-
const componentsDir =
|
|
2856
|
-
if (!
|
|
3024
|
+
const componentsDir = resolve21(config.root, config.componentsDir);
|
|
3025
|
+
if (!existsSync18(componentsDir)) return { title: false, end: false };
|
|
2857
3026
|
const entries = (await readdir5(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2858
3027
|
return { title: entries.includes("title-reveal"), end: entries.includes("end-card") };
|
|
2859
3028
|
};
|
|
2860
3029
|
var newCommand = async (name, options = {}) => {
|
|
2861
3030
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) throw new Error("Use a lowercase, dash separated name.");
|
|
2862
3031
|
const config = await loadConfig(process.cwd());
|
|
2863
|
-
const directory2 =
|
|
2864
|
-
const file =
|
|
2865
|
-
if (
|
|
2866
|
-
const hasLayout =
|
|
3032
|
+
const directory2 = resolve21(config.root, config.videosDir, name);
|
|
3033
|
+
const file = resolve21(directory2, "video.tsx");
|
|
3034
|
+
if (existsSync18(file)) throw new Error(`${relative9(config.root, file)} already exists.`);
|
|
3035
|
+
const hasLayout = existsSync18(resolve21(config.root, config.videosDir, "layout.tsx"));
|
|
2867
3036
|
const parts = options.blank === true ? { title: false, end: false } : await installedParts(config);
|
|
2868
3037
|
const composed = parts.title || parts.end;
|
|
2869
|
-
await
|
|
2870
|
-
await
|
|
3038
|
+
await mkdir14(directory2, { recursive: true });
|
|
3039
|
+
await writeFile15(
|
|
2871
3040
|
file,
|
|
2872
3041
|
composed ? composedTemplate(name, hasLayout, parts) : videoTemplate(name, hasLayout),
|
|
2873
3042
|
"utf8"
|
|
2874
3043
|
);
|
|
2875
|
-
log.success(`Created ${
|
|
3044
|
+
log.success(`Created ${relative9(config.root, file)}`);
|
|
2876
3045
|
if (composed) log.detail("Composed from the components this project has installed.");
|
|
2877
3046
|
else if (options.blank !== true) {
|
|
2878
3047
|
log.detail("No registry components installed yet: odori add title-reveal end-card");
|
|
@@ -2906,21 +3075,21 @@ export const productLayout = defineVideoLayout({
|
|
|
2906
3075
|
});
|
|
2907
3076
|
`;
|
|
2908
3077
|
var initCommand = async (root = process.cwd()) => {
|
|
2909
|
-
const videosDir =
|
|
2910
|
-
await
|
|
3078
|
+
const videosDir = resolve22(root, defaultConfig.videosDir);
|
|
3079
|
+
await mkdir15(resolve22(videosDir, "components"), { recursive: true });
|
|
2911
3080
|
const files = [
|
|
2912
|
-
[
|
|
2913
|
-
[
|
|
2914
|
-
[
|
|
3081
|
+
[resolve22(root, "odori.config.ts"), CONFIG_TEMPLATE],
|
|
3082
|
+
[resolve22(videosDir, "layout.tsx"), LAYOUT_TEMPLATE],
|
|
3083
|
+
[resolve22(videosDir, "launch", "video.tsx"), videoTemplate("launch", true)]
|
|
2915
3084
|
];
|
|
2916
3085
|
for (const [file, contents] of files) {
|
|
2917
|
-
if (
|
|
2918
|
-
log.detail(`Kept existing ${
|
|
3086
|
+
if (existsSync19(file)) {
|
|
3087
|
+
log.detail(`Kept existing ${relative10(root, file)}`);
|
|
2919
3088
|
continue;
|
|
2920
3089
|
}
|
|
2921
|
-
await
|
|
2922
|
-
await
|
|
2923
|
-
log.success(`Created ${
|
|
3090
|
+
await mkdir15(resolve22(file, ".."), { recursive: true });
|
|
3091
|
+
await writeFile16(file, contents, "utf8");
|
|
3092
|
+
log.success(`Created ${relative10(root, file)}`);
|
|
2924
3093
|
}
|
|
2925
3094
|
log.detail("Next: odori doctor, then odori add title-reveal end-card, then odori dev.");
|
|
2926
3095
|
};
|
|
@@ -3011,15 +3180,16 @@ var listCommand = async () => {
|
|
|
3011
3180
|
}
|
|
3012
3181
|
};
|
|
3013
3182
|
|
|
3014
|
-
// src/commands/
|
|
3015
|
-
import { resolve as
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
}
|
|
3183
|
+
// src/commands/frame.ts
|
|
3184
|
+
import { resolve as resolve23 } from "path";
|
|
3185
|
+
import { framesFromOffset, resolveEntryLayout as resolveEntryLayout7 } from "odori";
|
|
3186
|
+
var frameCommand = async (id, options = {}) => {
|
|
3187
|
+
const at = options.at ?? 0;
|
|
3188
|
+
framesFromOffset(at, 30);
|
|
3021
3189
|
const { config, graph, videos } = await createContext();
|
|
3022
3190
|
const video = findVideo(videos, id);
|
|
3191
|
+
const fps = resolveEntryLayout7(video.entry).format.fps;
|
|
3192
|
+
const frame = framesFromOffset(at, fps);
|
|
3023
3193
|
const output = await withServer(config, async (server) => {
|
|
3024
3194
|
const { durationInFrames, scenes, audio } = await compileInBrowser(server.url, targetFor(video, options.input), config);
|
|
3025
3195
|
const { manifest, input, prepared } = await freezeManifest(
|
|
@@ -3033,21 +3203,21 @@ var stillCommand = async (id, options = {}) => {
|
|
|
3033
3203
|
throw new Error(`Frame ${frame} is past the last frame (${manifest.format.durationInFrames - 1}).`);
|
|
3034
3204
|
}
|
|
3035
3205
|
const target = targetFor({ ...video, durationInFrames: manifest.format.durationInFrames }, input, prepared);
|
|
3036
|
-
const file =
|
|
3206
|
+
const file = resolve23(config.root, options.output ?? `${config.exportDir}/${outputName(id)}-${frame}.png`);
|
|
3037
3207
|
return renderStill(server.url, target, frame, file, config);
|
|
3038
3208
|
});
|
|
3039
|
-
log.success(`
|
|
3209
|
+
log.success(`Frame ${frame} written to ${output}`);
|
|
3040
3210
|
return output;
|
|
3041
3211
|
};
|
|
3042
3212
|
|
|
3043
3213
|
// src/commands/test.ts
|
|
3044
|
-
import { isOdoriSchema as isOdoriSchema2, resolveEntryLayout as
|
|
3214
|
+
import { isOdoriSchema as isOdoriSchema2, resolveEntryLayout as resolveEntryLayout9 } from "odori";
|
|
3045
3215
|
|
|
3046
3216
|
// src/contracts.ts
|
|
3047
|
-
import { existsSync as
|
|
3217
|
+
import { existsSync as existsSync20 } from "fs";
|
|
3048
3218
|
import { readdir as readdir6 } from "fs/promises";
|
|
3049
|
-
import { resolve as
|
|
3050
|
-
import { cueUrl as cueUrl2, isCueDefinition as isCueDefinition2, resolveEntryLayout as
|
|
3219
|
+
import { resolve as resolve24 } from "path";
|
|
3220
|
+
import { cueUrl as cueUrl2, isCueDefinition as isCueDefinition2, resolveEntryLayout as resolveEntryLayout8 } from "odori";
|
|
3051
3221
|
var primaryFamily = (stack) => (stack.split(",")[0] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
3052
3222
|
var SYSTEM_FAMILIES = /* @__PURE__ */ new Set([
|
|
3053
3223
|
"system-ui",
|
|
@@ -3113,8 +3283,8 @@ var checkAudioWindows = (cues, brand, videoId) => {
|
|
|
3113
3283
|
return failures;
|
|
3114
3284
|
};
|
|
3115
3285
|
var checkInstalledContracts = async (config, videos) => {
|
|
3116
|
-
const componentsDir =
|
|
3117
|
-
const onDisk =
|
|
3286
|
+
const componentsDir = resolve24(config.root, config.componentsDir);
|
|
3287
|
+
const onDisk = existsSync20(componentsDir) ? (await readdir6(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
|
|
3118
3288
|
const names = /* @__PURE__ */ new Set([...Object.keys(await readProvenance(config)), ...onDisk]);
|
|
3119
3289
|
if (names.size === 0) return [];
|
|
3120
3290
|
const { items } = await resolveRegistry(config, { allowNetwork: false });
|
|
@@ -3123,7 +3293,7 @@ var checkInstalledContracts = async (config, videos) => {
|
|
|
3123
3293
|
const seen = /* @__PURE__ */ new Set();
|
|
3124
3294
|
const failures = [];
|
|
3125
3295
|
for (const video of videos) {
|
|
3126
|
-
const { brand } =
|
|
3296
|
+
const { brand } = resolveEntryLayout8(video.entry);
|
|
3127
3297
|
for (const failure of checkComponentRequirements(installed, brand, video.entry.metadata.id)) {
|
|
3128
3298
|
const key = `${brand.name}:${failure.message}`;
|
|
3129
3299
|
if (seen.has(key)) continue;
|
|
@@ -3135,9 +3305,9 @@ var checkInstalledContracts = async (config, videos) => {
|
|
|
3135
3305
|
};
|
|
3136
3306
|
|
|
3137
3307
|
// src/determinism.ts
|
|
3138
|
-
import { readdir as readdir7, readFile as
|
|
3139
|
-
import { existsSync as
|
|
3140
|
-
import { join as join7, relative as
|
|
3308
|
+
import { readdir as readdir7, readFile as readFile15 } from "fs/promises";
|
|
3309
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3310
|
+
import { join as join7, relative as relative11, resolve as resolve25 } from "path";
|
|
3141
3311
|
var FORBIDDEN = [
|
|
3142
3312
|
{
|
|
3143
3313
|
pattern: /\bMath\.random\s*\(/,
|
|
@@ -3178,11 +3348,11 @@ var walk2 = async (directory2, files = []) => {
|
|
|
3178
3348
|
return files;
|
|
3179
3349
|
};
|
|
3180
3350
|
var checkDeterminism = async (config) => {
|
|
3181
|
-
const root =
|
|
3182
|
-
if (!
|
|
3351
|
+
const root = resolve25(config.root, config.videosDir);
|
|
3352
|
+
if (!existsSync21(root)) return [];
|
|
3183
3353
|
const files = await walk2(root);
|
|
3184
3354
|
const findings = await Promise.all(
|
|
3185
|
-
files.map(async (file) => scanSource(await
|
|
3355
|
+
files.map(async (file) => scanSource(await readFile15(file, "utf8"), relative11(config.root, file)))
|
|
3186
3356
|
);
|
|
3187
3357
|
return findings.flat();
|
|
3188
3358
|
};
|
|
@@ -3281,7 +3451,7 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3281
3451
|
})()`;
|
|
3282
3452
|
var testVideo = async (origin, video, config, failures, quiet = false) => {
|
|
3283
3453
|
const id = video.entry.metadata.id;
|
|
3284
|
-
const layout =
|
|
3454
|
+
const layout = resolveEntryLayout9(video.entry);
|
|
3285
3455
|
if (isOdoriSchema2(video.entry.metadata.schema)) {
|
|
3286
3456
|
const result = video.entry.metadata.schema.safeParse(video.entry.metadata.defaultProps ?? {});
|
|
3287
3457
|
if (!result.success) failures.push({ video: id, message: `defaultProps fail the schema: ${result.issues.join("; ")}` });
|
|
@@ -3446,7 +3616,7 @@ var COMMAND_FLAGS = {
|
|
|
3446
3616
|
update: ["force"],
|
|
3447
3617
|
list: [],
|
|
3448
3618
|
inspect: ["json", "input"],
|
|
3449
|
-
|
|
3619
|
+
frame: ["at", "output", "input"],
|
|
3450
3620
|
test: ["json"],
|
|
3451
3621
|
export: ["output", "input", "concurrency", "preset", "format", "quality", "scale", "no-frame-skip", "retry"],
|
|
3452
3622
|
jobs: [],
|
|
@@ -3509,8 +3679,9 @@ var USAGE = {
|
|
|
3509
3679
|
Print discovered video ids and formats.`,
|
|
3510
3680
|
inspect: `odori inspect <id> [--json] [--input <json>]
|
|
3511
3681
|
Show resolved layout, inputs, scenes, and assets.`,
|
|
3512
|
-
|
|
3513
|
-
Render one deterministic frame
|
|
3682
|
+
frame: `odori frame <id> --at <time> [--output <path>] [--input <json>]
|
|
3683
|
+
Render one deterministic frame to a PNG. --at is a duration: 4s, 500ms, or
|
|
3684
|
+
120f for frame 120. A bare number is seconds.`,
|
|
3514
3685
|
test: `odori test [id] [--json]
|
|
3515
3686
|
Validate contracts and representative frames. --json emits one object per
|
|
3516
3687
|
check, for CI.`,
|
|
@@ -3538,14 +3709,14 @@ Usage
|
|
|
3538
3709
|
odori update [components] Apply upstream component changes
|
|
3539
3710
|
odori list Print discovered video ids and formats
|
|
3540
3711
|
odori inspect <id> [--json] Show resolved layout, inputs, scenes, assets
|
|
3541
|
-
odori
|
|
3712
|
+
odori frame <id> --at 4s Render one deterministic frame to a PNG
|
|
3542
3713
|
odori test [id] [--json] Validate contracts and representative frames
|
|
3543
3714
|
odori export <id> [--output f] Render and encode a distributable file
|
|
3544
3715
|
odori jobs List export jobs and their status
|
|
3545
3716
|
|
|
3546
3717
|
Options
|
|
3547
3718
|
--input '{"headline":"..."}' Serializable input for the video schema
|
|
3548
|
-
--output <path> Output path for
|
|
3719
|
+
--output <path> Output path for frame and export
|
|
3549
3720
|
--force Replace locally modified component source
|
|
3550
3721
|
--concurrency <n> Parallel render workers for export
|
|
3551
3722
|
--preset <name> x264 preset for export, default medium
|
|
@@ -3622,9 +3793,11 @@ var run2 = async (argv) => {
|
|
|
3622
3793
|
case "inspect":
|
|
3623
3794
|
await inspectCommand(positionals[0] ?? "", { json: flags.json === true, input: parseInput(flags) });
|
|
3624
3795
|
return 0;
|
|
3625
|
-
case "
|
|
3626
|
-
await
|
|
3627
|
-
|
|
3796
|
+
case "frame":
|
|
3797
|
+
await frameCommand(positionals[0] ?? "", {
|
|
3798
|
+
// A duration, so "4s" and "120f" both work; a bare number is
|
|
3799
|
+
// seconds, the way every other time value in Odori reads.
|
|
3800
|
+
at: typeof flags.at === "string" ? flags.at : numberFlag(flags, "at") ?? 0,
|
|
3628
3801
|
output: typeof flags.output === "string" ? flags.output : void 0,
|
|
3629
3802
|
input: parseInput(flags)
|
|
3630
3803
|
});
|
|
@@ -3729,7 +3902,7 @@ export {
|
|
|
3729
3902
|
exportCommand,
|
|
3730
3903
|
jobsCommand,
|
|
3731
3904
|
devCommand,
|
|
3732
|
-
|
|
3905
|
+
frameCommand,
|
|
3733
3906
|
diffLines,
|
|
3734
3907
|
countChanges,
|
|
3735
3908
|
formatDiff,
|