@osmo.inc/cli-linux-arm64 0.1.21 → 0.1.22

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/studio.js CHANGED
@@ -895,6 +895,94 @@ var init_comments = __esm({
895
895
  }
896
896
  });
897
897
 
898
+ // src/project-scenes.ts
899
+ import fs2 from "node:fs/promises";
900
+ import path14 from "node:path";
901
+ import ts4 from "typescript";
902
+ async function listProjectScenes(root) {
903
+ const scenes = [];
904
+ const errors = [];
905
+ const ignored = /* @__PURE__ */ new Set([
906
+ "node_modules",
907
+ "dist",
908
+ "build",
909
+ "target",
910
+ "coverage",
911
+ "references",
912
+ "video-references"
913
+ ]);
914
+ const relative = (file) => path14.relative(root, file).split(path14.sep).join("/");
915
+ async function scan(folder, depth) {
916
+ if (depth > 16) {
917
+ errors.push(`${relative(folder)}: folder nesting exceeds the scan limit`);
918
+ return;
919
+ }
920
+ let entries;
921
+ try {
922
+ entries = await fs2.readdir(folder, { withFileTypes: true });
923
+ } catch (error) {
924
+ errors.push(`${relative(folder) || "."}: ${error.message}`);
925
+ return;
926
+ }
927
+ for (const item of entries) {
928
+ if (item.name.startsWith(".") || ignored.has(item.name) || item.isSymbolicLink())
929
+ continue;
930
+ const file = path14.join(folder, item.name);
931
+ if (item.isDirectory()) {
932
+ await scan(file, depth + 1);
933
+ continue;
934
+ }
935
+ if (!item.isFile() || !item.name.endsWith(".tsx")) continue;
936
+ const entry = relative(file);
937
+ try {
938
+ if ((await fs2.stat(file)).size > 1024 * 1024) {
939
+ errors.push(`${entry}: file exceeds the 1 MB scan limit`);
940
+ continue;
941
+ }
942
+ const text = await fs2.readFile(file, "utf8");
943
+ const ast = ts4.createSourceFile(
944
+ entry,
945
+ text,
946
+ ts4.ScriptTarget.Latest,
947
+ true,
948
+ ts4.ScriptKind.TSX
949
+ );
950
+ let node = ast.statements.find(
951
+ (statement) => ts4.isExportAssignment(statement) && !statement.isExportEquals
952
+ )?.expression;
953
+ while (node && ts4.isParenthesizedExpression(node))
954
+ node = node.expression;
955
+ const head = node && (ts4.isJsxElement(node) ? node.openingElement : ts4.isJsxSelfClosingElement(node) ? node : null);
956
+ if (head?.tagName.getText(ast) !== "Scene") continue;
957
+ const scene = { path: entry };
958
+ for (const attribute of head.attributes.properties) {
959
+ if (!ts4.isJsxAttribute(attribute) || !ts4.isIdentifier(attribute.name))
960
+ continue;
961
+ const name = attribute.name.text;
962
+ if (!["width", "height", "duration", "fps"].includes(name)) continue;
963
+ const value = attribute.initializer && ts4.isJsxExpression(attribute.initializer) ? attribute.initializer.expression : attribute.initializer;
964
+ if (value && ts4.isNumericLiteral(value) && Number.isFinite(Number(value.text)) && Number(value.text) > 0)
965
+ scene[name] = Number(
966
+ value.text
967
+ );
968
+ }
969
+ scenes.push(scene);
970
+ } catch (error) {
971
+ errors.push(`${entry}: ${error.message}`);
972
+ }
973
+ }
974
+ }
975
+ await scan(root, 0);
976
+ scenes.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
977
+ errors.sort();
978
+ return { scenes, errors };
979
+ }
980
+ var init_project_scenes = __esm({
981
+ "src/project-scenes.ts"() {
982
+ "use strict";
983
+ }
984
+ });
985
+
898
986
  // src/asset-response.ts
899
987
  import { pipeline } from "node:stream/promises";
900
988
  async function streamAsset(req, res, asset, contentType) {
@@ -1005,29 +1093,29 @@ var init_assets = __esm({
1005
1093
  });
1006
1094
 
1007
1095
  // src/asset-import.ts
1008
- import fs2 from "node:fs/promises";
1009
- import path14 from "node:path";
1096
+ import fs3 from "node:fs/promises";
1097
+ import path15 from "node:path";
1010
1098
  import { randomUUID as randomUUID5 } from "node:crypto";
1011
1099
  async function importProjectAsset(root, stream, name, kind, limit = Infinity) {
1012
1100
  if (!Object.hasOwn(assetExtensions, kind))
1013
1101
  throw new Error("Unknown asset kind");
1014
1102
  if (!name || name.length > 255 || /[/\\\x00-\x1f]/.test(name))
1015
1103
  throw new Error("Invalid asset filename");
1016
- const extension = path14.extname(name).slice(1).toLowerCase();
1104
+ const extension = path15.extname(name).slice(1).toLowerCase();
1017
1105
  if (!assetExtensions[kind].includes(extension))
1018
1106
  throw new Error(
1019
1107
  `Expected a ${kind} file (${assetExtensions[kind].join(", ")})`
1020
1108
  );
1021
- const folder = path14.join(root, "assets");
1022
- await fs2.mkdir(folder).catch((error) => {
1109
+ const folder = path15.join(root, "assets");
1110
+ await fs3.mkdir(folder).catch((error) => {
1023
1111
  if (error.code !== "EEXIST") throw error;
1024
1112
  });
1025
1113
  const directory = sceneInProject(root, "assets").source;
1026
- const stem = path14.basename(name, path14.extname(name)).normalize("NFKC").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "asset";
1114
+ const stem = path15.basename(name, path15.extname(name)).normalize("NFKC").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "asset";
1027
1115
  const filename2 = `${stem}-${randomUUID5()}.${extension}`;
1028
- const target = path14.join(directory, filename2);
1029
- const temp = path14.join(directory, `.${filename2}.upload`);
1030
- const file = await fs2.open(temp, "wx", 384);
1116
+ const target = path15.join(directory, filename2);
1117
+ const temp = path15.join(directory, `.${filename2}.upload`);
1118
+ const file = await fs3.open(temp, "wx", 384);
1031
1119
  let size = 0;
1032
1120
  try {
1033
1121
  for await (const chunk of stream) {
@@ -1039,11 +1127,11 @@ async function importProjectAsset(root, stream, name, kind, limit = Infinity) {
1039
1127
  }
1040
1128
  if (!size) throw new Error("Asset file is empty");
1041
1129
  await file.close();
1042
- await fs2.link(temp, target);
1043
- return path14.relative(await fs2.realpath(root), target).split(path14.sep).join("/");
1130
+ await fs3.link(temp, target);
1131
+ return path15.relative(await fs3.realpath(root), target).split(path15.sep).join("/");
1044
1132
  } finally {
1045
1133
  await file.close();
1046
- await fs2.rm(temp, { force: true });
1134
+ await fs3.rm(temp, { force: true });
1047
1135
  }
1048
1136
  }
1049
1137
  var init_asset_import = __esm({
@@ -1271,49 +1359,49 @@ var init_storyboard = __esm({
1271
1359
  });
1272
1360
 
1273
1361
  // src/storyboard.ts
1274
- import fs3 from "node:fs/promises";
1275
- import path15 from "node:path";
1362
+ import fs4 from "node:fs/promises";
1363
+ import path16 from "node:path";
1276
1364
  import { createHash as createHash4, randomUUID as randomUUID6 } from "node:crypto";
1277
- import ts4 from "typescript";
1365
+ import ts5 from "typescript";
1278
1366
  function literal(n) {
1279
1367
  if (!n) return void 0;
1280
- if (ts4.isStringLiteral(n) || ts4.isNumericLiteral(n))
1281
- return ts4.isNumericLiteral(n) ? Number(n.text) : n.text;
1282
- if (n.kind === ts4.SyntaxKind.TrueKeyword) return true;
1283
- if (n.kind === ts4.SyntaxKind.FalseKeyword) return false;
1284
- if (ts4.isPrefixUnaryExpression(n) && n.operator === ts4.SyntaxKind.MinusToken)
1368
+ if (ts5.isStringLiteral(n) || ts5.isNumericLiteral(n))
1369
+ return ts5.isNumericLiteral(n) ? Number(n.text) : n.text;
1370
+ if (n.kind === ts5.SyntaxKind.TrueKeyword) return true;
1371
+ if (n.kind === ts5.SyntaxKind.FalseKeyword) return false;
1372
+ if (ts5.isPrefixUnaryExpression(n) && n.operator === ts5.SyntaxKind.MinusToken)
1285
1373
  return -literal(n.operand);
1286
- if (ts4.isArrayLiteralExpression(n)) return n.elements.map(literal);
1287
- if (ts4.isObjectLiteralExpression(n))
1374
+ if (ts5.isArrayLiteralExpression(n)) return n.elements.map(literal);
1375
+ if (ts5.isObjectLiteralExpression(n))
1288
1376
  return Object.fromEntries(
1289
- n.properties.filter(ts4.isPropertyAssignment).map((p) => [p.name.text, literal(p.initializer)])
1377
+ n.properties.filter(ts5.isPropertyAssignment).map((p) => [p.name.text, literal(p.initializer)])
1290
1378
  );
1291
1379
  }
1292
1380
  async function groupInfo(source, relative) {
1293
- const text = await fs3.readFile(source, "utf8"), ast = ts4.createSourceFile(
1381
+ const text = await fs4.readFile(source, "utf8"), ast = ts5.createSourceFile(
1294
1382
  source,
1295
1383
  text,
1296
- ts4.ScriptTarget.Latest,
1384
+ ts5.ScriptTarget.Latest,
1297
1385
  true,
1298
- ts4.ScriptKind.TSX
1386
+ ts5.ScriptKind.TSX
1299
1387
  );
1300
- let root = ast.statements.find(ts4.isExportAssignment)?.expression;
1301
- while (root && ts4.isParenthesizedExpression(root)) root = root.expression;
1302
- const opening3 = root && (ts4.isJsxElement(root) ? root.openingElement : ts4.isJsxSelfClosingElement(root) ? root : null);
1388
+ let root = ast.statements.find(ts5.isExportAssignment)?.expression;
1389
+ while (root && ts5.isParenthesizedExpression(root)) root = root.expression;
1390
+ const opening3 = root && (ts5.isJsxElement(root) ? root.openingElement : ts5.isJsxSelfClosingElement(root) ? root : null);
1303
1391
  if (opening3?.tagName.getText(ast) !== "Group") return null;
1304
1392
  const attrs = new Map(
1305
- opening3.attributes.properties.filter(ts4.isJsxAttribute).map((p) => [
1393
+ opening3.attributes.properties.filter(ts5.isJsxAttribute).map((p) => [
1306
1394
  // A namespaced name (a:b) reads as undefined.
1307
1395
  p.name.text,
1308
- p.initializer && (ts4.isJsxExpression(p.initializer) ? p.initializer.expression : p.initializer)
1396
+ p.initializer && (ts5.isJsxExpression(p.initializer) ? p.initializer.expression : p.initializer)
1309
1397
  ])
1310
1398
  );
1311
1399
  const knobName = attrs.get("knobs")?.getText(ast);
1312
1400
  let knobs = {};
1313
1401
  for (const statement of ast.statements)
1314
- if (ts4.isVariableStatement(statement))
1402
+ if (ts5.isVariableStatement(statement))
1315
1403
  for (const d of statement.declarationList.declarations) {
1316
- if (d.name.getText(ast) === knobName && d.initializer && ts4.isCallExpression(d.initializer) && d.initializer.expression.getText(ast) === "defineKnobs")
1404
+ if (d.name.getText(ast) === knobName && d.initializer && ts5.isCallExpression(d.initializer) && d.initializer.expression.getText(ast) === "defineKnobs")
1317
1405
  knobs = literal(d.initializer.arguments[0]) ?? {};
1318
1406
  }
1319
1407
  const defaults = literal(attrs.get("values")) ?? {};
@@ -1325,7 +1413,7 @@ async function groupInfo(source, relative) {
1325
1413
  };
1326
1414
  }
1327
1415
  function createStoryboard(root) {
1328
- const filename2 = path15.join(root, "storyboard.canvas.json"), cache2 = path15.join(root, ".osmo/cache/storyboard");
1416
+ const filename2 = path16.join(root, "storyboard.canvas.json"), cache2 = path16.join(root, ".osmo/cache/storyboard");
1329
1417
  let queue = Promise.resolve(), undo = [], redo = [], knownRevision, activeRenderer, closed = false;
1330
1418
  const serial = (fn) => {
1331
1419
  const result = queue.then(fn);
@@ -1336,7 +1424,7 @@ function createStoryboard(root) {
1336
1424
  async function read() {
1337
1425
  let document;
1338
1426
  try {
1339
- document = validateBoard(JSON.parse(await fs3.readFile(filename2, "utf8")));
1427
+ document = validateBoard(JSON.parse(await fs4.readFile(filename2, "utf8")));
1340
1428
  } catch (error) {
1341
1429
  if (error.code !== "ENOENT") throw error;
1342
1430
  document = structuredClone(empty);
@@ -1353,17 +1441,17 @@ function createStoryboard(root) {
1353
1441
  const result = [];
1354
1442
  async function scan(dir, depth = 0) {
1355
1443
  if (depth > 8 || result.length >= 100) return;
1356
- for (const e of await fs3.readdir(dir, { withFileTypes: true })) {
1444
+ for (const e of await fs4.readdir(dir, { withFileTypes: true })) {
1357
1445
  if (e.name.startsWith(".") || ["node_modules", "exports", "dist", "target", "assets"].includes(
1358
1446
  e.name
1359
1447
  ))
1360
1448
  continue;
1361
- const absolute = path15.join(dir, e.name);
1449
+ const absolute = path16.join(dir, e.name);
1362
1450
  if (e.isDirectory()) await scan(absolute, depth + 1);
1363
1451
  else if (e.isFile() && e.name.endsWith(".tsx")) {
1364
1452
  const info2 = await groupInfo(
1365
1453
  absolute,
1366
- path15.relative(root, absolute).split(path15.sep).join("/")
1454
+ path16.relative(root, absolute).split(path16.sep).join("/")
1367
1455
  );
1368
1456
  if (info2) result.push(info2);
1369
1457
  }
@@ -1397,10 +1485,10 @@ function createStoryboard(root) {
1397
1485
  async function write(document) {
1398
1486
  const temp = `${filename2}.${randomUUID6()}.tmp`;
1399
1487
  try {
1400
- await fs3.writeFile(temp, JSON.stringify(document, null, 2) + "\n");
1401
- await fs3.rename(temp, filename2);
1488
+ await fs4.writeFile(temp, JSON.stringify(document, null, 2) + "\n");
1489
+ await fs4.rename(temp, filename2);
1402
1490
  } finally {
1403
- await fs3.rm(temp, { force: true });
1491
+ await fs4.rm(temp, { force: true });
1404
1492
  }
1405
1493
  knownRevision = hash(JSON.stringify(document));
1406
1494
  }
@@ -1452,8 +1540,8 @@ function createStoryboard(root) {
1452
1540
  await info(card);
1453
1541
  const { source } = sceneInProject(root, card.source), validation = await check(source);
1454
1542
  if (!validation.ok) throw new Error(validation.errors.join("\n"));
1455
- await fs3.mkdir(cache2, { recursive: true });
1456
- const name = `${randomUUID6()}.mp4`, destination = path15.join(cache2, name);
1543
+ await fs4.mkdir(cache2, { recursive: true });
1544
+ const name = `${randomUUID6()}.mp4`, destination = path16.join(cache2, name);
1457
1545
  const renderer = new Renderer({
1458
1546
  preview: {
1459
1547
  width: card.width,
@@ -1469,7 +1557,7 @@ function createStoryboard(root) {
1469
1557
  await renderer.request("export", { destination, supersample: 1 });
1470
1558
  return { name, revision: current.revision };
1471
1559
  } catch (error) {
1472
- await fs3.rm(destination, { force: true });
1560
+ await fs4.rm(destination, { force: true });
1473
1561
  throw error;
1474
1562
  } finally {
1475
1563
  renderer.close();
@@ -1479,7 +1567,7 @@ function createStoryboard(root) {
1479
1567
  video: async (name) => {
1480
1568
  if (!/^[0-9a-f-]{36}\.mp4$/.test(name))
1481
1569
  throw new Error("Invalid preview");
1482
- return fs3.readFile(path15.join(cache2, name));
1570
+ return fs4.readFile(path16.join(cache2, name));
1483
1571
  },
1484
1572
  close() {
1485
1573
  closed = true;
@@ -1503,10 +1591,10 @@ var init_storyboard2 = __esm({
1503
1591
  // src/asset-metadata.ts
1504
1592
  import { execFile as execFile4 } from "node:child_process";
1505
1593
  import { promisify as promisify4 } from "node:util";
1506
- import path16 from "node:path";
1594
+ import path17 from "node:path";
1507
1595
  async function inspectAsset(file, size) {
1508
1596
  const metadata = { size };
1509
- const extension = path16.extname(file).slice(1).toLowerCase();
1597
+ const extension = path17.extname(file).slice(1).toLowerCase();
1510
1598
  const image = ["png", "jpg", "jpeg", "webp", "gif"].includes(extension);
1511
1599
  const video = ["mp4", "webm", "mov", "m4v"].includes(extension);
1512
1600
  const audio = ["wav", "mp3", "m4a", "aac", "ogg", "flac"].includes(extension);
@@ -1566,9 +1654,9 @@ var init_asset_metadata = __esm({
1566
1654
 
1567
1655
  // src/video-normalization.ts
1568
1656
  import { execFile as execFile5 } from "node:child_process";
1569
- import fs4 from "node:fs/promises";
1657
+ import fs5 from "node:fs/promises";
1570
1658
  import os3 from "node:os";
1571
- import path17 from "node:path";
1659
+ import path18 from "node:path";
1572
1660
  import { promisify as promisify5 } from "node:util";
1573
1661
  function createVideoNormalizer() {
1574
1662
  let directory;
@@ -1590,8 +1678,8 @@ function createVideoNormalizer() {
1590
1678
  const task = remuxQueue.then(async () => {
1591
1679
  if (closed) throw new Error("Video storage is closed");
1592
1680
  if (cache2.has(identity2)) return;
1593
- directory ??= await fs4.mkdtemp(path17.join(os3.tmpdir(), "osmo-video-"));
1594
- const file = path17.join(directory, `${sequence++}.mp4`);
1681
+ directory ??= await fs5.mkdtemp(path18.join(os3.tmpdir(), "osmo-video-"));
1682
+ const file = path18.join(directory, `${sequence++}.mp4`);
1595
1683
  try {
1596
1684
  await exec5(
1597
1685
  process.env.OSMO_FFMPEG ?? "ffmpeg",
@@ -1617,7 +1705,7 @@ function createVideoNormalizer() {
1617
1705
  ],
1618
1706
  { timeout: 6e5, maxBuffer: 1024 * 1024 }
1619
1707
  );
1620
- const size = (await fs4.stat(file)).size;
1708
+ const size = (await fs5.stat(file)).size;
1621
1709
  if (!size) throw new Error("FFmpeg produced an empty playback copy");
1622
1710
  await files(async () => {
1623
1711
  cache2.set(identity2, { file, size });
@@ -1626,12 +1714,12 @@ function createVideoNormalizer() {
1626
1714
  const [key, copy] = cache2.entries().next().value;
1627
1715
  cache2.delete(key);
1628
1716
  bytes -= copy.size;
1629
- await fs4.rm(copy.file, { force: true });
1717
+ await fs5.rm(copy.file, { force: true });
1630
1718
  }
1631
1719
  });
1632
1720
  } catch (cause) {
1633
1721
  cache2.delete(identity2);
1634
- await fs4.rm(file, { force: true });
1722
+ await fs5.rm(file, { force: true });
1635
1723
  const error = cause;
1636
1724
  const detail = error.code === "ENOENT" ? "FFmpeg is not installed or cannot be found" : error.killed ? "FFmpeg exceeded the 10-minute preparation timeout" : error.stderr?.trim() || error.message || "Unknown FFmpeg error";
1637
1725
  throw new Error(
@@ -1655,7 +1743,7 @@ function createVideoNormalizer() {
1655
1743
  if (!cached) return null;
1656
1744
  cache2.delete(identity2);
1657
1745
  cache2.set(identity2, cached);
1658
- return { handle: await fs4.open(cached.file, "r"), size: cached.size };
1746
+ return { handle: await fs5.open(cached.file, "r"), size: cached.size };
1659
1747
  });
1660
1748
  if (opened) return opened;
1661
1749
  await prepare(source, identity2);
@@ -1673,7 +1761,7 @@ function createVideoNormalizer() {
1673
1761
  closed = true;
1674
1762
  await remuxQueue;
1675
1763
  await fileQueue;
1676
- if (directory) await fs4.rm(directory, { recursive: true, force: true });
1764
+ if (directory) await fs5.rm(directory, { recursive: true, force: true });
1677
1765
  cache2.clear();
1678
1766
  }
1679
1767
  return { open, read, close };
@@ -1819,17 +1907,17 @@ var init_document_assets = __esm({
1819
1907
 
1820
1908
  // ../editor/src/runtime-paths.ts
1821
1909
  function withRuntimePaths(root) {
1822
- function visit(node, path25) {
1910
+ function visit(node, path26) {
1823
1911
  if (!isSceneNode(node)) return node;
1824
- if (node.props.hidden === true) path25 = null;
1912
+ if (node.props.hidden === true) path26 = null;
1825
1913
  let index = 0;
1826
1914
  return {
1827
1915
  ...node,
1828
- runtimePath: path25,
1916
+ runtimePath: path26,
1829
1917
  children: node.children.map((child) => {
1830
1918
  const hidden = isSceneNode(child) && (child.props.hidden === true || node.type === "Scene" && node.props.tracks?.[child.props.track]?.hidden === true);
1831
- const omitted = !isSceneNode(child) || hidden || path25 === null || node.type === "Model" || child.type === "Camera";
1832
- return visit(child, omitted ? null : [path25, index++].filter((p) => p !== "").join("."));
1919
+ const omitted = !isSceneNode(child) || hidden || path26 === null || node.type === "Model" || child.type === "Camera";
1920
+ return visit(child, omitted ? null : [path26, index++].filter((p) => p !== "").join("."));
1833
1921
  })
1834
1922
  };
1835
1923
  }
@@ -1842,6 +1930,43 @@ var init_runtime_paths = __esm({
1842
1930
  }
1843
1931
  });
1844
1932
 
1933
+ // ../editor/src/module-parse.ts
1934
+ import ts6 from "typescript";
1935
+ function parseModule(file, text) {
1936
+ const last = holders ? held.get(file) : void 0;
1937
+ if (last) {
1938
+ held.delete(file);
1939
+ heldText -= last.text.length;
1940
+ }
1941
+ const tree = last?.text === text ? last.tree : ts6.createSourceFile(
1942
+ file,
1943
+ text,
1944
+ ts6.ScriptTarget.Latest,
1945
+ true,
1946
+ ts6.ScriptKind.TSX
1947
+ );
1948
+ if (holders) {
1949
+ held.set(file, { text, tree });
1950
+ heldText += text.length;
1951
+ for (const [path26, entry] of held) {
1952
+ if (heldText <= HELD_TEXT) break;
1953
+ held.delete(path26);
1954
+ heldText -= entry.text.length;
1955
+ }
1956
+ }
1957
+ return tree;
1958
+ }
1959
+ var held, holders, heldText, HELD_TEXT;
1960
+ var init_module_parse = __esm({
1961
+ "../editor/src/module-parse.ts"() {
1962
+ "use strict";
1963
+ held = /* @__PURE__ */ new Map();
1964
+ holders = 0;
1965
+ heldText = 0;
1966
+ HELD_TEXT = 16 * 1024 * 1024;
1967
+ }
1968
+ });
1969
+
1845
1970
  // ../editor/src/source-path.ts
1846
1971
  function normalize(value) {
1847
1972
  const parts = [];
@@ -1862,24 +1987,24 @@ var init_source_path = __esm({
1862
1987
  });
1863
1988
 
1864
1989
  // ../editor/src/scene-document.ts
1865
- import ts5 from "typescript";
1990
+ import ts7 from "typescript";
1866
1991
  function decodeJsxLiteral(raw, attribute = false) {
1867
- const code = ts5.transpileModule(
1992
+ const code = ts7.transpileModule(
1868
1993
  attribute ? `const x=<Text value=${raw}/>;` : `const x=<Text>${raw}</Text>;`,
1869
- { compilerOptions: { jsx: ts5.JsxEmit.ReactJSX } }
1994
+ { compilerOptions: { jsx: ts7.JsxEmit.ReactJSX } }
1870
1995
  ).outputText;
1871
- const ast = ts5.createSourceFile(
1996
+ const ast = ts7.createSourceFile(
1872
1997
  "literal.js",
1873
1998
  code,
1874
- ts5.ScriptTarget.Latest,
1999
+ ts7.ScriptTarget.Latest,
1875
2000
  true,
1876
- ts5.ScriptKind.JS
2001
+ ts7.ScriptKind.JS
1877
2002
  );
1878
2003
  let result = "";
1879
2004
  function visit(n) {
1880
- if (ts5.isPropertyAssignment(n) && n.name.text === (attribute ? "value" : "children") && ts5.isStringLiteral(n.initializer))
2005
+ if (ts7.isPropertyAssignment(n) && n.name.text === (attribute ? "value" : "children") && ts7.isStringLiteral(n.initializer))
1881
2006
  result = n.initializer.text;
1882
- ts5.forEachChild(n, visit);
2007
+ ts7.forEachChild(n, visit);
1883
2008
  }
1884
2009
  visit(ast);
1885
2010
  return result;
@@ -1893,13 +2018,7 @@ function createSceneDocument(entry, files) {
1893
2018
  const text = files[file];
1894
2019
  if (typeof text !== "string")
1895
2020
  throw new Error(`Missing animation source: ${file}`);
1896
- const ast = ts5.createSourceFile(
1897
- file,
1898
- text,
1899
- ts5.ScriptTarget.Latest,
1900
- true,
1901
- ts5.ScriptKind.TSX
1902
- );
2021
+ const ast = parseModule(file, text);
1903
2022
  if (ast.parseDiagnostics.length) throw new Error(`Cannot parse ${file}`);
1904
2023
  const imports = {}, locals = /* @__PURE__ */ new Set();
1905
2024
  const location = (n) => ({
@@ -1914,32 +2033,32 @@ function createSceneDocument(entry, files) {
1914
2033
  };
1915
2034
  function read(n) {
1916
2035
  if (!n) return true;
1917
- if (ts5.isParenthesizedExpression(n) || ts5.isJsxExpression(n))
2036
+ if (ts7.isParenthesizedExpression(n) || ts7.isJsxExpression(n))
1918
2037
  return n.expression ? read(n.expression) : void 0;
1919
- if (ts5.isStringLiteral(n) || ts5.isNumericLiteral(n))
1920
- return ts5.isNumericLiteral(n) ? Number(n.text) : ts5.isJsxAttribute(n.parent) && n.text.includes("&") ? decodeJsxLiteral(n.getText(ast), true) : n.text;
1921
- if (n.kind === ts5.SyntaxKind.TrueKeyword) return true;
1922
- if (n.kind === ts5.SyntaxKind.FalseKeyword) return false;
1923
- if (n.kind === ts5.SyntaxKind.NullKeyword) return null;
1924
- if (ts5.isPrefixUnaryExpression(n)) {
2038
+ if (ts7.isStringLiteral(n) || ts7.isNumericLiteral(n))
2039
+ return ts7.isNumericLiteral(n) ? Number(n.text) : ts7.isJsxAttribute(n.parent) && n.text.includes("&") ? decodeJsxLiteral(n.getText(ast), true) : n.text;
2040
+ if (n.kind === ts7.SyntaxKind.TrueKeyword) return true;
2041
+ if (n.kind === ts7.SyntaxKind.FalseKeyword) return false;
2042
+ if (n.kind === ts7.SyntaxKind.NullKeyword) return null;
2043
+ if (ts7.isPrefixUnaryExpression(n)) {
1925
2044
  const v = read(n.operand);
1926
2045
  if (typeof v !== "number") return unsupported(n);
1927
- return n.operator === ts5.SyntaxKind.MinusToken ? -v : v;
2046
+ return n.operator === ts7.SyntaxKind.MinusToken ? -v : v;
1928
2047
  }
1929
- if (ts5.isArrayLiteralExpression(n)) return n.elements.map(read);
1930
- if (ts5.isObjectLiteralExpression(n))
2048
+ if (ts7.isArrayLiteralExpression(n)) return n.elements.map(read);
2049
+ if (ts7.isObjectLiteralExpression(n))
1931
2050
  return Object.fromEntries(
1932
2051
  n.properties.map((p) => {
1933
- if (!ts5.isPropertyAssignment(p) || ts5.isComputedPropertyName(p.name))
2052
+ if (!ts7.isPropertyAssignment(p) || ts7.isComputedPropertyName(p.name))
1934
2053
  return unsupported(p);
1935
2054
  return [p.name.text, read(p.initializer)];
1936
2055
  })
1937
2056
  );
1938
- if (ts5.isIdentifier(n) && locals.has(n.text))
2057
+ if (ts7.isIdentifier(n) && locals.has(n.text))
1939
2058
  return { $ref: `${file}#${n.text}` };
1940
- if (ts5.isPropertyAccessExpression(n) && ts5.isIdentifier(n.expression) && locals.has(n.expression.text))
2059
+ if (ts7.isPropertyAccessExpression(n) && ts7.isIdentifier(n.expression) && locals.has(n.expression.text))
1941
2060
  return { $ref: `${file}#${n.expression.text}`, name: n.name.text };
1942
- if (ts5.isCallExpression(n) && (["blur", "grain"].includes(n.expression.getText(ast)) || bindings[`${file}#${n.expression.getText(ast)}`]?.kind === "effect"))
2061
+ if (ts7.isCallExpression(n) && (["blur", "grain"].includes(n.expression.getText(ast)) || bindings[`${file}#${n.expression.getText(ast)}`]?.kind === "effect"))
1943
2062
  return {
1944
2063
  $effect: bindings[`${file}#${n.expression.getText(ast)}`]?.kind === "effect" ? `${file}#${n.expression.getText(ast)}` : n.expression.getText(ast),
1945
2064
  values: n.arguments[0] ? read(n.arguments[0]) : {}
@@ -1948,7 +2067,7 @@ function createSceneDocument(entry, files) {
1948
2067
  }
1949
2068
  let exported;
1950
2069
  for (const st of ast.statements) {
1951
- if (ts5.isImportDeclaration(st) && st.importClause?.name)
2070
+ if (ts7.isImportDeclaration(st) && st.importClause?.name)
1952
2071
  imports[st.importClause.name.text] = module(
1953
2072
  resolveSceneImport(
1954
2073
  file,
@@ -1956,10 +2075,10 @@ function createSceneDocument(entry, files) {
1956
2075
  ),
1957
2076
  [...stack, file]
1958
2077
  );
1959
- if (ts5.isVariableStatement(st))
2078
+ if (ts7.isVariableStatement(st))
1960
2079
  for (const d of st.declarationList.declarations) {
1961
2080
  const call2 = d.initializer;
1962
- if (!call2 || !ts5.isCallExpression(call2) || !["defineMotion", "defineKnobs", "defineEffect"].includes(
2081
+ if (!call2 || !ts7.isCallExpression(call2) || !["defineMotion", "defineKnobs", "defineEffect"].includes(
1963
2082
  call2.expression.getText(ast)
1964
2083
  ))
1965
2084
  return unsupported(d);
@@ -1971,12 +2090,12 @@ function createSceneDocument(entry, files) {
1971
2090
  source: location(d)
1972
2091
  };
1973
2092
  }
1974
- if (ts5.isExportAssignment(st)) exported = st.expression;
2093
+ if (ts7.isExportAssignment(st)) exported = st.expression;
1975
2094
  }
1976
2095
  function jsx2(n) {
1977
- if (ts5.isParenthesizedExpression(n) || ts5.isJsxExpression(n))
2096
+ if (ts7.isParenthesizedExpression(n) || ts7.isJsxExpression(n))
1978
2097
  return n.expression ? jsx2(n.expression) : [];
1979
- if (ts5.isJsxText(n)) {
2098
+ if (ts7.isJsxText(n)) {
1980
2099
  if (n.text.includes("&")) return [decodeJsxLiteral(n.getText(ast))];
1981
2100
  const lines = n.text.replace(/\r/g, "").split("\n"), parts = [];
1982
2101
  lines.forEach((line2, i) => {
@@ -1987,17 +2106,17 @@ function createSceneDocument(entry, files) {
1987
2106
  });
1988
2107
  return parts.length ? [parts.join(" ")] : [];
1989
2108
  }
1990
- if (ts5.isJsxFragment(n)) return n.children.flatMap(jsx2);
1991
- if (!ts5.isJsxElement(n) && !ts5.isJsxSelfClosingElement(n))
2109
+ if (ts7.isJsxFragment(n)) return n.children.flatMap(jsx2);
2110
+ if (!ts7.isJsxElement(n) && !ts7.isJsxSelfClosingElement(n))
1992
2111
  return [read(n)];
1993
- const opening3 = ts5.isJsxElement(n) ? n.openingElement : n, tag = opening3.tagName.getText(ast);
2112
+ const opening3 = ts7.isJsxElement(n) ? n.openingElement : n, tag = opening3.tagName.getText(ast);
1994
2113
  const props = Object.fromEntries(
1995
2114
  opening3.attributes.properties.map((a) => {
1996
- if (!ts5.isJsxAttribute(a)) return unsupported(a);
2115
+ if (!ts7.isJsxAttribute(a)) return unsupported(a);
1997
2116
  return [a.name.text, read(a.initializer)];
1998
2117
  })
1999
2118
  );
2000
- const children = ts5.isJsxElement(n) ? n.children.flatMap(jsx2) : [];
2119
+ const children = ts7.isJsxElement(n) ? n.children.flatMap(jsx2) : [];
2001
2120
  if ("label" in props) {
2002
2121
  if (tag !== "Group" && !imports[tag])
2003
2122
  throw new Error("label belongs to Groups only");
@@ -2138,6 +2257,7 @@ var init_scene_document = __esm({
2138
2257
  "../editor/src/scene-document.ts"() {
2139
2258
  "use strict";
2140
2259
  init_runtime_paths();
2260
+ init_module_parse();
2141
2261
  init_source_path();
2142
2262
  init_metadata();
2143
2263
  init_types();
@@ -2374,10 +2494,10 @@ var init_document_outline = __esm({
2374
2494
  });
2375
2495
 
2376
2496
  // src/scene-document.ts
2377
- import fs5 from "node:fs/promises";
2378
- import path18 from "node:path";
2497
+ import fs6 from "node:fs/promises";
2498
+ import path19 from "node:path";
2379
2499
  import { createHash as createHash5 } from "node:crypto";
2380
- import ts6 from "typescript";
2500
+ import ts8 from "typescript";
2381
2501
  function createDocumentLoader(root) {
2382
2502
  const videos = createVideoNormalizer();
2383
2503
  const assets = /* @__PURE__ */ new Map();
@@ -2389,16 +2509,16 @@ function createDocumentLoader(root) {
2389
2509
  throw new Error("Cyclic animation imports");
2390
2510
  if (Object.hasOwn(files, file)) return;
2391
2511
  const located = sceneInProject(root, file);
2392
- files[file] = body3 ?? inputs.get(located.source) ?? await fs5.readFile(located.source, "utf8");
2393
- const ast = ts6.createSourceFile(
2512
+ files[file] = body3 ?? inputs.get(located.source) ?? await fs6.readFile(located.source, "utf8");
2513
+ const ast = ts8.createSourceFile(
2394
2514
  file,
2395
2515
  files[file],
2396
- ts6.ScriptTarget.Latest,
2516
+ ts8.ScriptTarget.Latest,
2397
2517
  true,
2398
- ts6.ScriptKind.TSX
2518
+ ts8.ScriptKind.TSX
2399
2519
  );
2400
2520
  for (const st of ast.statements)
2401
- if (ts6.isImportDeclaration(st) && st.importClause?.name)
2521
+ if (ts8.isImportDeclaration(st) && st.importClause?.name)
2402
2522
  await read(
2403
2523
  // A module specifier is a string literal.
2404
2524
  resolveSceneImport(
@@ -2415,7 +2535,7 @@ function createDocumentLoader(root) {
2415
2535
  document.assets = {};
2416
2536
  try {
2417
2537
  for (const ref of documentAssetReferences(document)) {
2418
- const relative = path18.posix.normalize(path18.posix.join(path18.posix.dirname(ref.file), ref.value));
2538
+ const relative = path19.posix.normalize(path19.posix.join(path19.posix.dirname(ref.file), ref.value));
2419
2539
  if (document.assets[relative]) continue;
2420
2540
  try {
2421
2541
  document.assets[relative] = await registerAsset(relative, ref.kind);
@@ -2430,7 +2550,7 @@ function createDocumentLoader(root) {
2430
2550
  return { ...document, ...includeSources ? { sourceFiles: files } : {} };
2431
2551
  }
2432
2552
  async function registerAsset(relative, kind) {
2433
- const located = sceneInProject(root, relative), info = await fs5.stat(located.source);
2553
+ const located = sceneInProject(root, relative), info = await fs6.stat(located.source);
2434
2554
  if (!info.isFile()) throw new Error(`Asset is not a file: ${relative}`);
2435
2555
  const id = createHash5("sha256").update(located.source + "\0" + info.mtimeMs + "\0" + info.size).digest("hex");
2436
2556
  assets.set(id, {
@@ -2441,7 +2561,7 @@ function createDocumentLoader(root) {
2441
2561
  const asset = { id, kind };
2442
2562
  if (kind === "shader") {
2443
2563
  if (info.size > 1024 * 1024) throw new Error("Shader exceeds 1 MB");
2444
- asset.code = await fs5.readFile(located.source, "utf8");
2564
+ asset.code = await fs6.readFile(located.source, "utf8");
2445
2565
  }
2446
2566
  return asset;
2447
2567
  }
@@ -2490,7 +2610,7 @@ function createDocumentLoader(root) {
2490
2610
  const asset = assets.get(id);
2491
2611
  if (!asset)
2492
2612
  throw Object.assign(new Error("Unknown scene asset"), { status: 404 });
2493
- const current = sceneInProject(root, asset.file), info = await fs5.stat(current.source);
2613
+ const current = sceneInProject(root, asset.file), info = await fs6.stat(current.source);
2494
2614
  if (current.source !== asset.file || info.size !== asset.size || info.mtimeMs !== asset.mtime)
2495
2615
  throw Object.assign(
2496
2616
  new Error("Scene asset changed. Refresh the scene."),
@@ -2500,9 +2620,9 @@ function createDocumentLoader(root) {
2500
2620
  }
2501
2621
  async function openAsset(id) {
2502
2622
  const current = await locateAsset(id);
2503
- const extension = path18.extname(current.file).toLowerCase();
2623
+ const extension = path19.extname(current.file).toLowerCase();
2504
2624
  const normalized = [".mp4", ".mov", ".m4v"].includes(extension);
2505
- const opened = normalized ? await videos.open(current.file, id) : { handle: await fs5.open(current.file, "r"), size: current.size };
2625
+ const opened = normalized ? await videos.open(current.file, id) : { handle: await fs6.open(current.file, "r"), size: current.size };
2506
2626
  try {
2507
2627
  await locateAsset(id);
2508
2628
  return { ...opened, extension: normalized ? ".mp4" : extension };
@@ -2542,30 +2662,29 @@ var init_scene_document2 = __esm({
2542
2662
  });
2543
2663
 
2544
2664
  // src/project-resources.ts
2545
- import fs6 from "node:fs/promises";
2546
- import path19 from "node:path";
2665
+ import fs7 from "node:fs/promises";
2666
+ import path20 from "node:path";
2547
2667
  import { execFile as execFile6 } from "node:child_process";
2548
2668
  import { promisify as promisify6 } from "node:util";
2549
- import ts7 from "typescript";
2550
2669
  async function resourceFile(root, relative) {
2551
- if (typeof relative !== "string" || path19.isAbsolute(relative))
2670
+ if (typeof relative !== "string" || path20.isAbsolute(relative))
2552
2671
  throw new Error("Resource paths must be relative.");
2553
- const base = await fs6.realpath(root), file = await fs6.realpath(path19.resolve(base, relative));
2554
- if (!inside(path19.join(base, "resources"), file) || (await fs6.stat(file)).isDirectory())
2672
+ const base = await fs7.realpath(root), file = await fs7.realpath(path20.resolve(base, relative));
2673
+ if (!inside(path20.join(base, "resources"), file) || (await fs7.stat(file)).isDirectory())
2555
2674
  throw new Error("Resource files must stay inside resources/.");
2556
2675
  return file;
2557
2676
  }
2558
2677
  async function resourceFolder(root) {
2559
- const base = await fs6.realpath(root), folder = path19.join(base, "resources");
2560
- await fs6.mkdir(folder, { recursive: true });
2561
- if ((await fs6.lstat(folder)).isSymbolicLink())
2678
+ const base = await fs7.realpath(root), folder = path20.join(base, "resources");
2679
+ await fs7.mkdir(folder, { recursive: true });
2680
+ if ((await fs7.lstat(folder)).isSymbolicLink())
2562
2681
  throw new Error("resources/ must be a real directory.");
2563
2682
  return folder;
2564
2683
  }
2565
2684
  async function listProjectResources(root) {
2566
2685
  let entries;
2567
2686
  try {
2568
- entries = await fs6.readdir(path19.join(root, "resources"), {
2687
+ entries = await fs7.readdir(path20.join(root, "resources"), {
2569
2688
  withFileTypes: true
2570
2689
  });
2571
2690
  } catch (e) {
@@ -2577,12 +2696,12 @@ async function listProjectResources(root) {
2577
2696
  if (item.name.startsWith(".") || !item.isDirectory()) continue;
2578
2697
  try {
2579
2698
  const relative = `resources/${item.name}/resource.json`, manifestFile = await resourceFile(root, relative);
2580
- if ((await fs6.stat(manifestFile)).size > 1024 * 1024)
2699
+ if ((await fs7.stat(manifestFile)).size > 1024 * 1024)
2581
2700
  throw new Error("Manifest exceeds 1 MB");
2582
- const meta = JSON.parse(await fs6.readFile(manifestFile, "utf8"));
2701
+ const meta = JSON.parse(await fs7.readFile(manifestFile, "utf8"));
2583
2702
  const entry = `resources/${item.name}/${meta.entry}`, file = await resourceFile(root, entry);
2584
- const kind = meta.kind ?? (meta.category === "3d" ? "model" : meta.category) ?? kinds[path19.extname(file).toLowerCase()];
2585
- if (!["shader", "image", "video", "model", "scene"].includes(kind) || kinds[path19.extname(file).toLowerCase()] !== kind)
2703
+ const kind = meta.kind ?? (meta.category === "3d" ? "model" : meta.category) ?? kinds[path20.extname(file).toLowerCase()];
2704
+ if (!["shader", "image", "video", "model", "scene"].includes(kind) || kinds[path20.extname(file).toLowerCase()] !== kind)
2586
2705
  throw new Error("Unsupported resource type");
2587
2706
  let effect;
2588
2707
  if (kind === "shader" && meta.effect) {
@@ -2600,7 +2719,7 @@ async function listProjectResources(root) {
2600
2719
  const dimensions = Object.fromEntries(
2601
2720
  ["width", "height", "duration"].filter((k) => Number.isFinite(meta[k]) && meta[k] > 0).map((k) => [k, meta[k]])
2602
2721
  );
2603
- const info = await fs6.stat(file), preview = meta.preview ? `resources/${item.name}/${meta.preview}` : ["image", "video"].includes(kind) ? entry : null;
2722
+ const info = await fs7.stat(file), preview = meta.preview ? `resources/${item.name}/${meta.preview}` : ["image", "video"].includes(kind) ? entry : null;
2604
2723
  if (preview) await resourceFile(root, preview);
2605
2724
  resources.push({
2606
2725
  id: item.name,
@@ -2615,83 +2734,34 @@ async function listProjectResources(root) {
2615
2734
  } : {},
2616
2735
  preview: preview && (meta.preview || info.size < 12 * 1024 * 1024) ? preview : null,
2617
2736
  previewType: meta.preview ? "image" : kind,
2618
- stamp: `${info.mtimeMs}:${info.size}:${(await fs6.stat(manifestFile)).mtimeMs}`
2737
+ stamp: `${info.mtimeMs}:${info.size}:${(await fs7.stat(manifestFile)).mtimeMs}`
2619
2738
  });
2620
2739
  } catch (error) {
2621
2740
  if (error.code !== "ENOENT")
2622
2741
  errors.push(`${item.name}: ${error.message}`);
2623
2742
  }
2624
2743
  }
2625
- async function scenes(folder, depth = 0) {
2626
- if (depth > 16) return;
2627
- for (const item of await fs6.readdir(folder, { withFileTypes: true })) {
2628
- if (item.name.startsWith(".") || [
2629
- "node_modules",
2630
- "dist",
2631
- "target",
2632
- "references",
2633
- "video-references"
2634
- ].includes(item.name) || item.isSymbolicLink())
2635
- continue;
2636
- const file = path19.join(folder, item.name);
2637
- if (item.isDirectory()) {
2638
- await scenes(file, depth + 1);
2639
- continue;
2640
- }
2641
- if (!item.isFile() || !item.name.endsWith(".tsx") || (await fs6.stat(file)).size > 1024 * 1024)
2642
- continue;
2643
- const entry = path19.relative(root, file).split(path19.sep).join("/");
2644
- try {
2645
- const text = await fs6.readFile(file, "utf8"), ast = ts7.createSourceFile(
2646
- entry,
2647
- text,
2648
- ts7.ScriptTarget.Latest,
2649
- true,
2650
- ts7.ScriptKind.TSX
2651
- );
2652
- let node = ast.statements.find(ts7.isExportAssignment)?.expression;
2653
- while (node && ts7.isParenthesizedExpression(node))
2654
- node = node.expression;
2655
- const head = node && (ts7.isJsxElement(node) ? node.openingElement : ts7.isJsxSelfClosingElement(node) ? node : null);
2656
- if (head?.tagName.getText(ast) !== "Scene") continue;
2657
- const attrs = Object.fromEntries(
2658
- head.attributes.properties.filter(ts7.isJsxAttribute).map((a) => {
2659
- const value = ts7.isJsxExpression(a.initializer ?? {}) ? a.initializer.expression : a.initializer;
2660
- return [
2661
- // A namespaced name (a:b) has no text; nothing reads it.
2662
- a.name.text,
2663
- value && ts7.isNumericLiteral(value) ? Number(value.text) : void 0
2664
- ];
2665
- })
2666
- );
2667
- if (![attrs.width, attrs.height, attrs.duration].every(
2668
- (v) => Number.isFinite(v) && v > 0
2669
- ))
2670
- continue;
2671
- const existing = resources.find((r) => r.entry === entry);
2672
- const info = {
2673
- width: attrs.width,
2674
- height: attrs.height,
2675
- duration: attrs.duration,
2676
- fps: attrs.fps ?? 30
2677
- };
2678
- if (existing) Object.assign(existing, info);
2679
- else
2680
- resources.push({
2681
- id: `scene:${entry}`,
2682
- title: item.name === "scene.tsx" ? path19.basename(folder) : item.name.slice(0, -4),
2683
- kind: "scene",
2684
- entry,
2685
- ...info,
2686
- preview: null,
2687
- stamp: String((await fs6.stat(file)).mtimeMs)
2688
- });
2689
- } catch (error) {
2690
- errors.push(`${entry}: ${error.message}`);
2691
- }
2744
+ const catalog = await listProjectScenes(root);
2745
+ errors.push(...catalog.errors);
2746
+ for (const scene of catalog.scenes) {
2747
+ const { path: entry, width, height, duration, fps } = scene;
2748
+ if (!width || !height || !duration) continue;
2749
+ const existing = resources.find((resource) => resource.entry === entry);
2750
+ const info = { width, height, duration, fps: fps ?? 30 };
2751
+ if (existing) Object.assign(existing, info);
2752
+ else {
2753
+ const name = path20.basename(entry);
2754
+ resources.push({
2755
+ id: `scene:${entry}`,
2756
+ title: name === "scene.tsx" ? path20.basename(path20.dirname(path20.join(root, entry))) : name.slice(0, -4),
2757
+ kind: "scene",
2758
+ entry,
2759
+ ...info,
2760
+ preview: null,
2761
+ stamp: String((await fs7.stat(path20.join(root, entry))).mtimeMs)
2762
+ });
2692
2763
  }
2693
2764
  }
2694
- await scenes(root);
2695
2765
  return { resources, errors };
2696
2766
  }
2697
2767
  async function getProjectResource(root, id) {
@@ -2703,33 +2773,33 @@ async function getProjectResource(root, id) {
2703
2773
  async function resourcePreview(root, id) {
2704
2774
  const resource = await getProjectResource(root, id);
2705
2775
  if (!resource.preview) throw new Error("No preview for this resource");
2706
- const file = await resourceFile(root, resource.preview), type = mime[path19.extname(file).toLowerCase()];
2776
+ const file = await resourceFile(root, resource.preview), type = mime[path20.extname(file).toLowerCase()];
2707
2777
  if (!type) throw new Error("Unsupported preview type");
2708
- if ((await fs6.stat(file)).size > 12 * 1024 * 1024)
2778
+ if ((await fs7.stat(file)).size > 12 * 1024 * 1024)
2709
2779
  throw new Error("Preview is too large");
2710
- return { bytes: await fs6.readFile(file), type };
2780
+ return { bytes: await fs7.readFile(file), type };
2711
2781
  }
2712
2782
  async function installResourceExamples(root) {
2713
- const folder = await resourceFolder(root), source = path19.join(packageRoot, "src/resource-examples/");
2714
- for (const item of await fs6.readdir(source, { withFileTypes: true })) {
2783
+ const folder = await resourceFolder(root), source = path20.join(packageRoot, "src/resource-examples/");
2784
+ for (const item of await fs7.readdir(source, { withFileTypes: true })) {
2715
2785
  if (!item.isDirectory()) continue;
2716
- const destination = path19.join(folder, item.name);
2786
+ const destination = path20.join(folder, item.name);
2717
2787
  try {
2718
- await fs6.mkdir(destination);
2788
+ await fs7.mkdir(destination);
2719
2789
  } catch (e) {
2720
2790
  if (e.code === "EEXIST") continue;
2721
2791
  throw e;
2722
2792
  }
2723
2793
  try {
2724
- for (const file of (await fs6.readdir(path19.join(source, item.name))).sort(
2794
+ for (const file of (await fs7.readdir(path20.join(source, item.name))).sort(
2725
2795
  (a, b) => Number(a === "resource.json") - Number(b === "resource.json")
2726
2796
  ))
2727
- await fs6.copyFile(
2728
- path19.join(source, item.name, file),
2729
- path19.join(destination, file)
2797
+ await fs7.copyFile(
2798
+ path20.join(source, item.name, file),
2799
+ path20.join(destination, file)
2730
2800
  );
2731
2801
  } catch (error) {
2732
- await fs6.rm(destination, { recursive: true, force: true });
2802
+ await fs7.rm(destination, { recursive: true, force: true });
2733
2803
  throw error;
2734
2804
  }
2735
2805
  }
@@ -2739,6 +2809,7 @@ var exec6, kinds, mime, inside;
2739
2809
  var init_project_resources = __esm({
2740
2810
  "src/project-resources.ts"() {
2741
2811
  "use strict";
2812
+ init_project_scenes();
2742
2813
  init_runtime();
2743
2814
  exec6 = promisify6(execFile6);
2744
2815
  kinds = {
@@ -2765,8 +2836,8 @@ var init_project_resources = __esm({
2765
2836
  ".mov": "video/quicktime"
2766
2837
  };
2767
2838
  inside = (root, file) => {
2768
- const rel = path19.relative(root, file);
2769
- return rel !== ".." && !rel.startsWith(".." + path19.sep) && !path19.isAbsolute(rel);
2839
+ const rel = path20.relative(root, file);
2840
+ return rel !== ".." && !rel.startsWith(".." + path20.sep) && !path20.isAbsolute(rel);
2770
2841
  };
2771
2842
  }
2772
2843
  });
@@ -3005,6 +3076,86 @@ var init_resource_drop = __esm({
3005
3076
  }
3006
3077
  });
3007
3078
 
3079
+ // ../editor/src/duplicate-motion.ts
3080
+ import ts9 from "typescript";
3081
+ function duplicateMotionBindings(source) {
3082
+ const definitions = /* @__PURE__ */ new Map();
3083
+ const names = /* @__PURE__ */ new Set();
3084
+ function collect(node) {
3085
+ if (ts9.isIdentifier(node)) names.add(node.text);
3086
+ ts9.forEachChild(node, collect);
3087
+ }
3088
+ collect(source);
3089
+ for (const statement of source.statements) {
3090
+ if (!ts9.isVariableStatement(statement)) continue;
3091
+ for (const declaration of statement.declarationList.declarations) {
3092
+ const call2 = declaration.initializer;
3093
+ if (ts9.isIdentifier(declaration.name) && call2 && ts9.isCallExpression(call2) && ts9.isIdentifier(call2.expression) && call2.expression.text === "defineMotion")
3094
+ definitions.set(declaration.name.text, call2.getText(source));
3095
+ }
3096
+ }
3097
+ const copies = /* @__PURE__ */ new Map();
3098
+ let index = 1;
3099
+ return {
3100
+ copy(text) {
3101
+ const ast = ts9.createSourceFile(
3102
+ "copy.tsx",
3103
+ text,
3104
+ ts9.ScriptTarget.Latest,
3105
+ true,
3106
+ ts9.ScriptKind.TSX
3107
+ );
3108
+ const edits = [];
3109
+ function visit(node) {
3110
+ if (ts9.isPropertyAccessExpression(node) && ts9.isIdentifier(node.expression) && definitions.has(node.expression.text)) {
3111
+ const original = node.expression.text;
3112
+ let name = copies.get(original);
3113
+ if (!name) {
3114
+ do {
3115
+ name = `motionCopy${index++}`;
3116
+ } while (names.has(name));
3117
+ names.add(name);
3118
+ copies.set(original, name);
3119
+ }
3120
+ edits.push({
3121
+ start: node.expression.getStart(ast),
3122
+ end: node.expression.end,
3123
+ text: name
3124
+ });
3125
+ }
3126
+ ts9.forEachChild(node, visit);
3127
+ }
3128
+ visit(ast);
3129
+ for (const edit of edits.sort((a, b) => b.start - a.start))
3130
+ text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
3131
+ return text;
3132
+ },
3133
+ finish(text) {
3134
+ if (!copies.size) return text;
3135
+ const ast = ts9.createSourceFile(
3136
+ source.fileName,
3137
+ text,
3138
+ ts9.ScriptTarget.Latest,
3139
+ true,
3140
+ ts9.ScriptKind.TSX
3141
+ );
3142
+ const exported = ast.statements.find(ts9.isExportAssignment);
3143
+ if (!exported) throw new Error("Scene export no longer exists");
3144
+ const at = exported.getStart(ast);
3145
+ const declarations2 = [...copies].map(
3146
+ ([original, name]) => `const ${name} = ${definitions.get(original)};
3147
+ `
3148
+ ).join("");
3149
+ return text.slice(0, at) + declarations2 + text.slice(at);
3150
+ }
3151
+ };
3152
+ }
3153
+ var init_duplicate_motion = __esm({
3154
+ "../editor/src/duplicate-motion.ts"() {
3155
+ "use strict";
3156
+ }
3157
+ });
3158
+
3008
3159
  // ../editor/src/property-validation.ts
3009
3160
  function validProperty(name, value, spec) {
3010
3161
  if (name === "mask") {
@@ -3168,29 +3319,29 @@ var init_timing_model = __esm({
3168
3319
  });
3169
3320
 
3170
3321
  // ../editor/src/timeline.ts
3171
- import ts8 from "typescript";
3322
+ import ts10 from "typescript";
3172
3323
  function literal2(node) {
3173
3324
  const n = unwrap(node);
3174
3325
  if (!n) return { known: false };
3175
- if (ts8.isStringLiteral(n)) return { known: true, value: n.text };
3176
- if (ts8.isNumericLiteral(n)) return { known: true, value: Number(n.text) };
3177
- if (n.kind === ts8.SyntaxKind.TrueKeyword || n.kind === ts8.SyntaxKind.FalseKeyword)
3178
- return { known: true, value: n.kind === ts8.SyntaxKind.TrueKeyword };
3179
- if (ts8.isPrefixUnaryExpression(n) && [ts8.SyntaxKind.MinusToken, ts8.SyntaxKind.PlusToken].includes(n.operator)) {
3326
+ if (ts10.isStringLiteral(n)) return { known: true, value: n.text };
3327
+ if (ts10.isNumericLiteral(n)) return { known: true, value: Number(n.text) };
3328
+ if (n.kind === ts10.SyntaxKind.TrueKeyword || n.kind === ts10.SyntaxKind.FalseKeyword)
3329
+ return { known: true, value: n.kind === ts10.SyntaxKind.TrueKeyword };
3330
+ if (ts10.isPrefixUnaryExpression(n) && [ts10.SyntaxKind.MinusToken, ts10.SyntaxKind.PlusToken].includes(n.operator)) {
3180
3331
  const v = literal2(n.operand);
3181
3332
  return typeof v.value === "number" ? {
3182
3333
  known: true,
3183
- value: v.value * (n.operator === ts8.SyntaxKind.MinusToken ? -1 : 1)
3334
+ value: v.value * (n.operator === ts10.SyntaxKind.MinusToken ? -1 : 1)
3184
3335
  } : { known: false };
3185
3336
  }
3186
- if (ts8.isArrayLiteralExpression(n)) {
3337
+ if (ts10.isArrayLiteralExpression(n)) {
3187
3338
  const items = n.elements.map(literal2);
3188
3339
  return items.every((v) => v.known) ? { known: true, value: items.map((v) => v.value) } : { known: false };
3189
3340
  }
3190
- if (ts8.isObjectLiteralExpression(n)) {
3341
+ if (ts10.isObjectLiteralExpression(n)) {
3191
3342
  const entries = [];
3192
3343
  for (const p of n.properties) {
3193
- if (!ts8.isPropertyAssignment(p) || ts8.isComputedPropertyName(p.name))
3344
+ if (!ts10.isPropertyAssignment(p) || ts10.isComputedPropertyName(p.name))
3194
3345
  return { known: false };
3195
3346
  const v = literal2(p.initializer);
3196
3347
  if (!v.known) return { known: false };
@@ -3201,25 +3352,25 @@ function literal2(node) {
3201
3352
  return { known: false };
3202
3353
  }
3203
3354
  function opening(n) {
3204
- return ts8.isJsxElement(n) ? n.openingElement : ts8.isJsxSelfClosingElement(n) ? n : null;
3355
+ return ts10.isJsxElement(n) ? n.openingElement : ts10.isJsxSelfClosingElement(n) ? n : null;
3205
3356
  }
3206
3357
  function childCount(n) {
3207
3358
  return (n.children ?? []).reduce(
3208
- (count, child) => count + (ts8.isJsxFragment(child) || opening(child)?.tagName.getText() === "Fragment" ? childCount(child) : opening(child) ? 1 : 0),
3359
+ (count, child) => count + (ts10.isJsxFragment(child) || opening(child)?.tagName.getText() === "Fragment" ? childCount(child) : opening(child) ? 1 : 0),
3209
3360
  0
3210
3361
  );
3211
3362
  }
3212
3363
  function attributes(n) {
3213
3364
  return new Map(
3214
- opening(n).attributes.properties.filter(ts8.isJsxAttribute).map((p) => [p.name.text, p])
3365
+ opening(n).attributes.properties.filter(ts10.isJsxAttribute).map((p) => [p.name.text, p])
3215
3366
  );
3216
3367
  }
3217
3368
  function declarations(ast) {
3218
3369
  const result = {};
3219
3370
  for (const st of ast.statements)
3220
- if (ts8.isVariableStatement(st))
3371
+ if (ts10.isVariableStatement(st))
3221
3372
  for (const d of st.declarationList.declarations) {
3222
- if (d.initializer && ts8.isCallExpression(d.initializer) && d.initializer.expression.getText(ast) === "defineKnobs")
3373
+ if (d.initializer && ts10.isCallExpression(d.initializer) && d.initializer.expression.getText(ast) === "defineKnobs")
3223
3374
  result[d.name.getText(ast)] = literal2(d.initializer.arguments[0]).value ?? {};
3224
3375
  }
3225
3376
  return result;
@@ -3276,13 +3427,7 @@ function propertiesFor(type) {
3276
3427
  };
3277
3428
  }
3278
3429
  function describeGroup(text, filename2 = "animation.tsx") {
3279
- const ast = ts8.createSourceFile(
3280
- filename2,
3281
- text,
3282
- ts8.ScriptTarget.Latest,
3283
- true,
3284
- ts8.ScriptKind.TSX
3285
- ), n = unwrap(ast.statements.find(ts8.isExportAssignment)?.expression);
3430
+ const ast = parseModule(filename2, text), n = unwrap(ast.statements.find(ts10.isExportAssignment)?.expression);
3286
3431
  if (!n || !opening(n) || !["Scene", "Group"].includes(opening(n).tagName.getText(ast)))
3287
3432
  throw new Error("Reusable file must default-export a Scene or Group");
3288
3433
  const attrs = attributes(n), props = Object.fromEntries(
@@ -3294,7 +3439,7 @@ function describeGroup(text, filename2 = "animation.tsx") {
3294
3439
  const name = unwrap(attrs.get("knobs")?.initializer)?.getText(ast);
3295
3440
  const valueNode = unwrap(attrs.get("values")?.initializer);
3296
3441
  const values = Object.fromEntries(
3297
- valueNode && ts8.isObjectLiteralExpression(valueNode) ? valueNode.properties.filter(ts8.isPropertyAssignment).map((p) => [
3442
+ valueNode && ts10.isObjectLiteralExpression(valueNode) ? valueNode.properties.filter(ts10.isPropertyAssignment).map((p) => [
3298
3443
  p.name.text,
3299
3444
  {
3300
3445
  ...literal2(p.initializer),
@@ -3324,7 +3469,7 @@ function describeClip(n, ast, definitions, knobs) {
3324
3469
  const track = attrLiteral(attrs.get("track")).value;
3325
3470
  const schema = definition?.knobs ?? knobs[unwrap(attrs.get("knobs")?.initializer)?.getText(ast)] ?? {};
3326
3471
  const valueNode = unwrap(attrs.get("values")?.initializer), overrides = new Map(
3327
- valueNode && ts8.isObjectLiteralExpression(valueNode) ? valueNode.properties.filter(ts8.isPropertyAssignment).map((p) => [
3472
+ valueNode && ts10.isObjectLiteralExpression(valueNode) ? valueNode.properties.filter(ts10.isPropertyAssignment).map((p) => [
3328
3473
  p.name.text,
3329
3474
  p
3330
3475
  ]) : []
@@ -3357,8 +3502,8 @@ function describeClip(n, ast, definitions, knobs) {
3357
3502
  expression: v.expression
3358
3503
  };
3359
3504
  });
3360
- const words2 = tag === "Text" && ts8.isJsxElement(n) ? n.children.map(
3361
- (c) => ts8.isJsxText(c) ? c.text : ts8.isJsxExpression(c) ? literal2(c.expression).value ?? "" : ""
3505
+ const words2 = tag === "Text" && ts10.isJsxElement(n) ? n.children.map(
3506
+ (c) => ts10.isJsxText(c) ? c.text : ts10.isJsxExpression(c) ? literal2(c.expression).value ?? "" : ""
3362
3507
  ).join("").trim().replace(/\s+/g, " ").slice(0, 64) : "";
3363
3508
  return {
3364
3509
  id: id ?? `draft:${n.getStart(ast)}`,
@@ -3383,13 +3528,7 @@ function describeClip(n, ast, definitions, knobs) {
3383
3528
  };
3384
3529
  }
3385
3530
  function parse(text, definitions = {}, filename2 = "scene.tsx") {
3386
- const ast = ts8.createSourceFile(
3387
- filename2,
3388
- text,
3389
- ts8.ScriptTarget.Latest,
3390
- true,
3391
- ts8.ScriptKind.TSX
3392
- ), root = unwrap(ast.statements.find(ts8.isExportAssignment)?.expression);
3531
+ const ast = parseModule(filename2, text), root = unwrap(ast.statements.find(ts10.isExportAssignment)?.expression);
3393
3532
  if (ast.parseDiagnostics.length)
3394
3533
  throw new Error("Fix the TSX syntax before editing");
3395
3534
  if (!root || !opening(root) || opening(root).tagName.getText(ast) !== "Scene")
@@ -3409,7 +3548,7 @@ function parse(text, definitions = {}, filename2 = "scene.tsx") {
3409
3548
  throw new Error("Scene duration and fps must be literal numbers");
3410
3549
  const knobs = declarations(ast), clips = [];
3411
3550
  function visit(n) {
3412
- if (ts8.isJsxFragment(n) || opening(n)?.tagName.getText(ast) === "Fragment" && ts8.isJsxElement(n)) {
3551
+ if (ts10.isJsxFragment(n) || opening(n)?.tagName.getText(ast) === "Fragment" && ts10.isJsxElement(n)) {
3413
3552
  n.children.forEach(visit);
3414
3553
  return;
3415
3554
  }
@@ -3432,7 +3571,7 @@ function parse(text, definitions = {}, filename2 = "scene.tsx") {
3432
3571
  throw new Error("Clips must stay inside the scene.");
3433
3572
  clips.push({ ...clip, ...timing });
3434
3573
  }
3435
- if (ts8.isJsxElement(root)) root.children.forEach(visit);
3574
+ if (ts10.isJsxElement(root)) root.children.forEach(visit);
3436
3575
  const assigned = clips.filter((c) => c.track !== void 0);
3437
3576
  for (const clip of clips.filter((c) => c.track === void 0)) {
3438
3577
  clip.track = 0;
@@ -3497,28 +3636,28 @@ function setAttrs(text, node, values, ast) {
3497
3636
  }
3498
3637
  function copyClip(clip, ast, values, fresh) {
3499
3638
  let text = clip.node.getText(ast);
3500
- const subtree = ts8.createSourceFile(
3639
+ const subtree = ts10.createSourceFile(
3501
3640
  "copy.tsx",
3502
3641
  text,
3503
- ts8.ScriptTarget.Latest,
3642
+ ts10.ScriptTarget.Latest,
3504
3643
  true,
3505
- ts8.ScriptKind.TSX
3644
+ ts10.ScriptKind.TSX
3506
3645
  ), edits = [];
3507
3646
  const ids = /* @__PURE__ */ new Map();
3508
3647
  function collect(node2) {
3509
- if (ts8.isJsxAttribute(node2) && node2.name.text === "id")
3648
+ if (ts10.isJsxAttribute(node2) && node2.name.text === "id")
3510
3649
  ids.set(literal2(node2.initializer).value, fresh());
3511
- ts8.forEachChild(node2, collect);
3650
+ ts10.forEachChild(node2, collect);
3512
3651
  }
3513
3652
  collect(subtree);
3514
3653
  function replace(node2) {
3515
- if (ts8.isJsxAttribute(node2) && node2.name.text === "id")
3654
+ if (ts10.isJsxAttribute(node2) && node2.name.text === "id")
3516
3655
  edits.push({
3517
3656
  start: node2.getStart(subtree),
3518
3657
  end: node2.end,
3519
3658
  text: `id={${JSON.stringify(ids.get(literal2(node2.initializer).value))}}`
3520
3659
  });
3521
- if (ts8.isJsxAttribute(node2) && node2.name.text === "mask") {
3660
+ if (ts10.isJsxAttribute(node2) && node2.name.text === "mask") {
3522
3661
  const raw = literal2(node2.initializer).value, mask = readMask(raw);
3523
3662
  if (mask && ids.has(mask.source)) {
3524
3663
  const value = typeof raw === "string" ? ids.get(mask.source) : { ...raw, source: ids.get(mask.source) };
@@ -3529,16 +3668,16 @@ function copyClip(clip, ast, values, fresh) {
3529
3668
  });
3530
3669
  }
3531
3670
  }
3532
- ts8.forEachChild(node2, replace);
3671
+ ts10.forEachChild(node2, replace);
3533
3672
  }
3534
3673
  replace(subtree);
3535
3674
  text = patches(text, edits);
3536
- const copyAst = ts8.createSourceFile(
3675
+ const copyAst = ts10.createSourceFile(
3537
3676
  "copy.tsx",
3538
3677
  text,
3539
- ts8.ScriptTarget.Latest,
3678
+ ts10.ScriptTarget.Latest,
3540
3679
  true,
3541
- ts8.ScriptKind.TSX
3680
+ ts10.ScriptKind.TSX
3542
3681
  ), node = copyAst.statements[0].expression;
3543
3682
  return {
3544
3683
  text: setAttrs(text, node, values, copyAst),
@@ -3567,11 +3706,11 @@ function editClipValue(text, clip, command, ast) {
3567
3706
  if (!validKnob(knob.schema, command.value))
3568
3707
  throw new Error(`Invalid ${command.name}`);
3569
3708
  const attr = clip.attrs.get("values"), object = unwrap(attr?.initializer), rendered = JSON.stringify(command.value);
3570
- if (object && !ts8.isObjectLiteralExpression(object))
3709
+ if (object && !ts10.isObjectLiteralExpression(object))
3571
3710
  throw new Error("Edit the values binding in source.");
3572
3711
  if (object) {
3573
3712
  const prop = object.properties.find(
3574
- (p) => ts8.isPropertyAssignment(p) && p.name.text === command.name
3713
+ (p) => ts10.isPropertyAssignment(p) && p.name.text === command.name
3575
3714
  );
3576
3715
  if (prop)
3577
3716
  text = patches(text, [
@@ -3603,17 +3742,17 @@ function editClipValue(text, clip, command, ast) {
3603
3742
  }
3604
3743
  function editGroupSource(text, sourceStart, command, filename2 = "scene.tsx") {
3605
3744
  const changes = groupChanges(command);
3606
- const ast = ts8.createSourceFile(
3745
+ const ast = ts10.createSourceFile(
3607
3746
  filename2,
3608
3747
  text,
3609
- ts8.ScriptTarget.Latest,
3748
+ ts10.ScriptTarget.Latest,
3610
3749
  true,
3611
- ts8.ScriptKind.TSX
3750
+ ts10.ScriptKind.TSX
3612
3751
  );
3613
3752
  let target;
3614
3753
  function visit(node) {
3615
3754
  if (opening(node)?.getStart(ast) === sourceStart) target = node;
3616
- else ts8.forEachChild(node, visit);
3755
+ else ts10.forEachChild(node, visit);
3617
3756
  }
3618
3757
  visit(ast);
3619
3758
  if (!target) throw new Error("Group source no longer exists");
@@ -3638,7 +3777,7 @@ function applyTimelineCommand(text, command, {
3638
3777
  const v = literal2(attributes(n).get("id")?.initializer).value;
3639
3778
  if (v) ids.add(v);
3640
3779
  }
3641
- ts8.forEachChild(n, collect);
3780
+ ts10.forEachChild(n, collect);
3642
3781
  }
3643
3782
  collect(model.ast);
3644
3783
  const fresh = () => {
@@ -3688,6 +3827,7 @@ function applyTimelineCommand(text, command, {
3688
3827
  if (duplicate)
3689
3828
  next.tracks[command.track + 1] = { ...model.tracks[command.track] };
3690
3829
  const edits = [];
3830
+ const motions = duplicateMotionBindings(model.ast);
3691
3831
  for (const clip2 of model.clips) {
3692
3832
  const updated = next.clips.find((c) => c.id === clip2.id);
3693
3833
  if (!updated)
@@ -3714,12 +3854,13 @@ function applyTimelineCommand(text, command, {
3714
3854
  edits.push({
3715
3855
  start: clip2.node.end,
3716
3856
  end: clip2.node.end,
3717
- text: "\n " + copy.text
3857
+ text: "\n " + motions.copy(copy.text)
3718
3858
  });
3719
3859
  }
3720
3860
  }
3721
3861
  text = patches(text, edits);
3722
3862
  text = setAttrs(text, model.root, { tracks: next.tracks }, model.ast);
3863
+ text = motions.finish(text);
3723
3864
  return { text, selection: null, idMap: Object.fromEntries(idMap) };
3724
3865
  }
3725
3866
  if (command.type === "move" || command.type === "trim") {
@@ -3788,10 +3929,13 @@ function applyTimelineCommand(text, command, {
3788
3929
  }
3789
3930
  if (track >= 100) throw new Error("Maximum 100 tracks");
3790
3931
  const copy = copyClip(clip, model.ast, { start, track }, fresh);
3932
+ const motions = duplicateMotionBindings(model.ast);
3933
+ copy.text = motions.copy(copy.text);
3791
3934
  id = copy.id;
3792
3935
  text = patches(text, [
3793
3936
  { start: clip.node.end, end: clip.node.end, text: "\n " + copy.text }
3794
3937
  ]);
3938
+ text = motions.finish(text);
3795
3939
  } else if (command.type === "property") {
3796
3940
  text = editClipValue(text, clip, command, model.ast);
3797
3941
  } else if (command.type === "knob") {
@@ -3816,7 +3960,7 @@ function applyTimelineCommand(text, command, {
3816
3960
  ))
3817
3961
  track++;
3818
3962
  if (track >= 100) throw new Error("Maximum 100 tracks");
3819
- const imports = model.ast.statements.filter(ts8.isImportDeclaration), existing = imports.find(
3963
+ const imports = model.ast.statements.filter(ts10.isImportDeclaration), existing = imports.find(
3820
3964
  (st) => st.importClause?.name && (st.moduleSpecifier.text === importPath || definition.source && definitions[st.importClause.name.text]?.source === definition.source)
3821
3965
  );
3822
3966
  let tag = existing?.importClause.name.text;
@@ -3836,7 +3980,7 @@ function applyTimelineCommand(text, command, {
3836
3980
  model = parse(text, definitions, filename2);
3837
3981
  }
3838
3982
  const root = model.root;
3839
- if (ts8.isJsxSelfClosingElement(root))
3983
+ if (ts10.isJsxSelfClosingElement(root))
3840
3984
  text = patches(text, [
3841
3985
  {
3842
3986
  start: root.end - 2,
@@ -3875,18 +4019,20 @@ var unwrap, attrLiteral, propertySpecs, pick, transformNames, numberProp, mediaS
3875
4019
  var init_timeline = __esm({
3876
4020
  "../editor/src/timeline.ts"() {
3877
4021
  "use strict";
4022
+ init_duplicate_motion();
3878
4023
  init_mask();
3879
4024
  init_property_validation();
3880
4025
  init_clip();
3881
4026
  init_metadata();
3882
4027
  init_labels();
4028
+ init_module_parse();
3883
4029
  init_track_model();
3884
4030
  init_group_model();
3885
4031
  init_timing_model();
3886
4032
  init_clip_timing();
3887
4033
  init_clip_timing();
3888
4034
  unwrap = (n) => {
3889
- while (n && (ts8.isParenthesizedExpression(n) || ts8.isJsxExpression(n)))
4035
+ while (n && (ts10.isParenthesizedExpression(n) || ts10.isJsxExpression(n)))
3890
4036
  n = n.expression;
3891
4037
  return n;
3892
4038
  };
@@ -3967,20 +4113,20 @@ var init_timeline = __esm({
3967
4113
  });
3968
4114
 
3969
4115
  // ../editor/src/resource-source.ts
3970
- import ts9 from "typescript";
4116
+ import ts11 from "typescript";
3971
4117
  function addImports(text, names, declaration = "") {
3972
- const ast = ts9.createSourceFile(
4118
+ const ast = ts11.createSourceFile(
3973
4119
  "scene.tsx",
3974
4120
  text,
3975
- ts9.ScriptTarget.Latest,
4121
+ ts11.ScriptTarget.Latest,
3976
4122
  true,
3977
- ts9.ScriptKind.TSX
4123
+ ts11.ScriptKind.TSX
3978
4124
  );
3979
4125
  const existing = ast.statements.find(
3980
- (s) => ts9.isImportDeclaration(s) && s.moduleSpecifier.text === "@osmo/scene" && !s.importClause?.isTypeOnly
4126
+ (s) => ts11.isImportDeclaration(s) && s.moduleSpecifier.text === "@osmo/scene" && !s.importClause?.isTypeOnly
3981
4127
  );
3982
4128
  const bindings = existing?.importClause?.namedBindings;
3983
- if (bindings && ts9.isNamedImports(bindings)) {
4129
+ if (bindings && ts11.isNamedImports(bindings)) {
3984
4130
  const missing = names.filter(
3985
4131
  (name) => !bindings.elements.some((e) => e.name.text === name)
3986
4132
  );
@@ -3995,19 +4141,19 @@ function addImports(text, names, declaration = "") {
3995
4141
  } else text = `import {${names.join(", ")}} from '@osmo/scene';
3996
4142
  ${text}`;
3997
4143
  if (declaration) {
3998
- const next = ts9.createSourceFile(
4144
+ const next = ts11.createSourceFile(
3999
4145
  "scene.tsx",
4000
4146
  text,
4001
- ts9.ScriptTarget.Latest,
4147
+ ts11.ScriptTarget.Latest,
4002
4148
  true,
4003
- ts9.ScriptKind.TSX
4004
- ), at = next.statements.find(ts9.isExportAssignment).getStart(next);
4149
+ ts11.ScriptKind.TSX
4150
+ ), at = next.statements.find(ts11.isExportAssignment).getStart(next);
4005
4151
  text = patches2(text, [{ start: at, end: at, text: declaration + "\n" }]);
4006
4152
  }
4007
4153
  return text;
4008
4154
  }
4009
4155
  function insertChild(text, node, child, ast, first = false) {
4010
- if (ts9.isJsxSelfClosingElement(node))
4156
+ if (ts11.isJsxSelfClosingElement(node))
4011
4157
  return patches2(text, [
4012
4158
  {
4013
4159
  start: node.end - 2,
@@ -4026,7 +4172,7 @@ function findOpening(ast, start) {
4026
4172
  let found;
4027
4173
  function walk(n) {
4028
4174
  if (opening2(n)?.getStart(ast) === start) found = n;
4029
- else ts9.forEachChild(n, walk);
4175
+ else ts11.forEachChild(n, walk);
4030
4176
  }
4031
4177
  walk(ast);
4032
4178
  return found;
@@ -4058,7 +4204,7 @@ function applyResourceSource(text, plan, {
4058
4204
  }
4059
4205
  selection = newId();
4060
4206
  if (plan.resource.kind === "scene") {
4061
- const imports = model.ast.statements.filter(ts9.isImportDeclaration);
4207
+ const imports = model.ast.statements.filter(ts11.isImportDeclaration);
4062
4208
  const existing = imports.find(
4063
4209
  (st) => st.importClause?.name && (st.moduleSpecifier.text === src || definitions[st.importClause.name.text]?.source === plan.resource.entry)
4064
4210
  );
@@ -4092,12 +4238,12 @@ ${text}`;
4092
4238
  text = addImports(text, [plan.tag]);
4093
4239
  }
4094
4240
  } else {
4095
- const ast = ts9.createSourceFile(
4241
+ const ast = ts11.createSourceFile(
4096
4242
  filename2,
4097
4243
  text,
4098
- ts9.ScriptTarget.Latest,
4244
+ ts11.ScriptTarget.Latest,
4099
4245
  true,
4100
- ts9.ScriptKind.TSX
4246
+ ts11.ScriptKind.TSX
4101
4247
  );
4102
4248
  const node = findOpening(ast, plan.node.source.start);
4103
4249
  if (!node) throw new Error("The target layer changed. Try again.");
@@ -4107,7 +4253,7 @@ ${text}`;
4107
4253
  );
4108
4254
  const id = first && attributes2(first).get("id")?.initializer;
4109
4255
  const value = unwrap2(id);
4110
- const replace = first && ["Image", "Video"].includes(opening2(first).tagName.getText(ast)) && value && ts9.isStringLiteral(value) && value.text.startsWith("fill-");
4256
+ const replace = first && ["Image", "Video"].includes(opening2(first).tagName.getText(ast)) && value && ts11.isStringLiteral(value) && value.text.startsWith("fill-");
4111
4257
  const element = jsx(plan.tag, {
4112
4258
  id: replace ? value.text : `fill-${newId()}`,
4113
4259
  ...plan.props,
@@ -4127,7 +4273,7 @@ ${text}`;
4127
4273
  const attrs = attributes2(node), attr = attrs.get("effects"), array = unwrap2(attr?.initializer);
4128
4274
  const call2 = `${name}(${JSON.stringify(effect.values ?? {})})`;
4129
4275
  if (attr) {
4130
- if (!array || !ts9.isArrayLiteralExpression(array))
4276
+ if (!array || !ts11.isArrayLiteralExpression(array))
4131
4277
  throw new Error("Edit this effect stack in source.");
4132
4278
  const last = array.elements.at(-1), at = last ? last.end : array.getStart(ast) + 1;
4133
4279
  text = patches2(text, [
@@ -4167,11 +4313,11 @@ var init_resource_source = __esm({
4167
4313
  });
4168
4314
 
4169
4315
  // src/timeline-editor.ts
4170
- import fs7 from "node:fs/promises";
4316
+ import fs8 from "node:fs/promises";
4171
4317
  import { realpathSync } from "node:fs";
4172
- import path20 from "node:path";
4318
+ import path21 from "node:path";
4173
4319
  import { createHash as createHash6, randomUUID as randomUUID7 } from "node:crypto";
4174
- import ts10 from "typescript";
4320
+ import ts12 from "typescript";
4175
4321
  function createTimelineEditor(root, context, {
4176
4322
  nativeValidation = false,
4177
4323
  prepareCandidate,
@@ -4188,19 +4334,19 @@ function createTimelineEditor(root, context, {
4188
4334
  };
4189
4335
  async function read() {
4190
4336
  const { source: inputSource, scene } = context();
4191
- const source = await fs7.realpath(inputSource);
4192
- const text = await fs7.readFile(source, "utf8"), definitions = {}, inputs = /* @__PURE__ */ new Map([[source, text]]);
4337
+ const source = await fs8.realpath(inputSource);
4338
+ const text = await fs8.readFile(source, "utf8"), definitions = {}, inputs = /* @__PURE__ */ new Map([[source, text]]);
4193
4339
  async function dependencies(file, body3, depth = 0) {
4194
4340
  if (depth > 32) throw new Error("Too many imported animations");
4195
- const ast = ts10.createSourceFile(
4341
+ const ast = ts12.createSourceFile(
4196
4342
  file,
4197
4343
  body3,
4198
- ts10.ScriptTarget.Latest,
4344
+ ts12.ScriptTarget.Latest,
4199
4345
  true,
4200
- ts10.ScriptKind.TSX
4346
+ ts12.ScriptKind.TSX
4201
4347
  );
4202
4348
  for (const st of ast.statements)
4203
- if (ts10.isImportDeclaration(st) && st.importClause?.name && // A module specifier is a string literal.
4349
+ if (ts12.isImportDeclaration(st) && st.importClause?.name && // A module specifier is a string literal.
4204
4350
  st.moduleSpecifier.text.startsWith(".")) {
4205
4351
  const next = sceneInProject(
4206
4352
  root,
@@ -4208,7 +4354,7 @@ function createTimelineEditor(root, context, {
4208
4354
  );
4209
4355
  let code = inputs.get(next.source);
4210
4356
  if (code === void 0) {
4211
- code = await fs7.readFile(next.source, "utf8");
4357
+ code = await fs8.readFile(next.source, "utf8");
4212
4358
  inputs.set(next.source, code);
4213
4359
  await dependencies(next.source, code, depth + 1);
4214
4360
  }
@@ -4257,7 +4403,7 @@ function createTimelineEditor(root, context, {
4257
4403
  ...data,
4258
4404
  sourceFiles: Object.fromEntries(
4259
4405
  [...inputs].map(([file, body3]) => [
4260
- path20.relative(root, file).split(path20.sep).join("/"),
4406
+ path21.relative(root, file).split(path21.sep).join("/"),
4261
4407
  body3
4262
4408
  ])
4263
4409
  ),
@@ -4268,16 +4414,16 @@ function createTimelineEditor(root, context, {
4268
4414
  };
4269
4415
  }
4270
4416
  async function write(file, text) {
4271
- const temp = path20.join(
4272
- path20.dirname(file),
4417
+ const temp = path21.join(
4418
+ path21.dirname(file),
4273
4419
  `.osmo-edit-${randomUUID7()}.tmp`
4274
4420
  );
4275
4421
  try {
4276
- const mode = (await fs7.stat(file)).mode;
4277
- await fs7.writeFile(temp, text, { mode });
4278
- await fs7.rename(temp, file);
4422
+ const mode = (await fs8.stat(file)).mode;
4423
+ await fs8.writeFile(temp, text, { mode });
4424
+ await fs8.rename(temp, file);
4279
4425
  } finally {
4280
- await fs7.rm(temp, { force: true });
4426
+ await fs8.rm(temp, { force: true });
4281
4427
  }
4282
4428
  }
4283
4429
  return {
@@ -4290,27 +4436,27 @@ function createTimelineEditor(root, context, {
4290
4436
  resolveAdd: (cardId) => serial(async () => {
4291
4437
  const board = validateBoard(
4292
4438
  JSON.parse(
4293
- await fs7.readFile(
4294
- path20.join(root, "storyboard.canvas.json"),
4439
+ await fs8.readFile(
4440
+ path21.join(root, "storyboard.canvas.json"),
4295
4441
  "utf8"
4296
4442
  )
4297
4443
  )
4298
4444
  );
4299
4445
  const card = board.cards.find((c) => c.id === cardId);
4300
4446
  if (!card) throw new Error("Storyboard card no longer exists");
4301
- const located = sceneInProject(root, card.source), code = await fs7.readFile(located.source, "utf8");
4447
+ const located = sceneInProject(root, card.source), code = await fs8.readFile(located.source, "utf8");
4302
4448
  const document = await documentLoader.load(located.source, code);
4303
4449
  const sourceFiles = Object.fromEntries(
4304
4450
  await Promise.all(
4305
4451
  document.files.map(
4306
4452
  async (file) => [
4307
4453
  file,
4308
- await fs7.readFile(sceneInProject(root, file).source, "utf8")
4454
+ await fs8.readFile(sceneInProject(root, file).source, "utf8")
4309
4455
  ]
4310
4456
  )
4311
4457
  )
4312
4458
  );
4313
- let importPath = path20.relative(path20.dirname(context().source), located.source).split(path20.sep).join("/");
4459
+ let importPath = path21.relative(path21.dirname(context().source), located.source).split(path21.sep).join("/");
4314
4460
  if (!importPath.startsWith(".")) importPath = "./" + importPath;
4315
4461
  return {
4316
4462
  type: "add",
@@ -4350,7 +4496,7 @@ function createTimelineEditor(root, context, {
4350
4496
  if (typeof change.file !== "string" || !change.file.endsWith(".tsx") || typeof change.after !== "string" || change.after.length > 1024 * 1024)
4351
4497
  throw new Error("Invalid source patch");
4352
4498
  const file = sceneInProject(root, change.file).source;
4353
- const current2 = await fs7.readFile(file, "utf8");
4499
+ const current2 = await fs8.readFile(file, "utf8");
4354
4500
  if (current2 !== change.before)
4355
4501
  throw Object.assign(new Error(`Source changed: ${change.file}`), {
4356
4502
  status: 409
@@ -4384,7 +4530,7 @@ function createTimelineEditor(root, context, {
4384
4530
  status: 409
4385
4531
  });
4386
4532
  for (const change of changed)
4387
- if (await fs7.readFile(change.file, "utf8") !== change.before)
4533
+ if (await fs8.readFile(change.file, "utf8") !== change.before)
4388
4534
  throw Object.assign(new Error("Source changed while validating"), {
4389
4535
  status: 409
4390
4536
  });
@@ -4396,7 +4542,7 @@ function createTimelineEditor(root, context, {
4396
4542
  }
4397
4543
  } catch (error) {
4398
4544
  for (const change of written.reverse())
4399
- if (await fs7.readFile(change.file, "utf8") === change.after)
4545
+ if (await fs8.readFile(change.file, "utf8") === change.after)
4400
4546
  await write(change.file, change.before);
4401
4547
  throw error;
4402
4548
  }
@@ -4459,11 +4605,11 @@ function createTimelineEditor(root, context, {
4459
4605
  if (!before.inputs.has(file))
4460
4606
  throw new Error("Target source is not part of this scene");
4461
4607
  const relative = (value) => {
4462
- let p = path20.relative(path20.dirname(file), sceneInProject(root, value).source).split(path20.sep).join("/");
4608
+ let p = path21.relative(path21.dirname(file), sceneInProject(root, value).source).split(path21.sep).join("/");
4463
4609
  return p.startsWith(".") ? p : "./" + p;
4464
4610
  };
4465
4611
  result = applyResourceSource(before.inputs.get(file), plan, {
4466
- filename: path20.relative(root, file),
4612
+ filename: path21.relative(root, file),
4467
4613
  definitions: before.definitions,
4468
4614
  src: relative(resource.entry),
4469
4615
  effectSrc: resource.effect ? relative(resource.effect.src) : void 0,
@@ -4487,8 +4633,8 @@ function createTimelineEditor(root, context, {
4487
4633
  if (command.type === "add") {
4488
4634
  const board = validateBoard(
4489
4635
  JSON.parse(
4490
- await fs7.readFile(
4491
- path20.join(root, "storyboard.canvas.json"),
4636
+ await fs8.readFile(
4637
+ path21.join(root, "storyboard.canvas.json"),
4492
4638
  "utf8"
4493
4639
  )
4494
4640
  )
@@ -4497,12 +4643,12 @@ function createTimelineEditor(root, context, {
4497
4643
  const { source: groupSource, scene: groupScene } = sceneInProject(
4498
4644
  root,
4499
4645
  card.source
4500
- ), code = await fs7.readFile(groupSource, "utf8");
4646
+ ), code = await fs8.readFile(groupSource, "utf8");
4501
4647
  const definition = {
4502
4648
  ...describeGroup(code, groupScene),
4503
4649
  source: groupScene
4504
4650
  };
4505
- let importPath = path20.relative(path20.dirname(before.source), groupSource).split(path20.sep).join("/");
4651
+ let importPath = path21.relative(path21.dirname(before.source), groupSource).split(path21.sep).join("/");
4506
4652
  if (!importPath.startsWith(".")) importPath = "./" + importPath;
4507
4653
  resolved = {
4508
4654
  type: "add",
@@ -4614,8 +4760,8 @@ var init_timeline_editor = __esm({
4614
4760
  init_resource_source();
4615
4761
  init_timeline();
4616
4762
  init_group_model();
4617
- resolveImport = (source, spec) => path20.resolve(
4618
- path20.dirname(source),
4763
+ resolveImport = (source, spec) => path21.resolve(
4764
+ path21.dirname(source),
4619
4765
  spec.endsWith(".tsx") ? spec : spec.endsWith(".js") ? spec.slice(0, -3) + ".tsx" : spec + ".tsx"
4620
4766
  );
4621
4767
  hash2 = (value) => createHash6("sha256").update(value).digest("hex");
@@ -4624,12 +4770,12 @@ var init_timeline_editor = __esm({
4624
4770
 
4625
4771
  // src/studio-settings.ts
4626
4772
  import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile8, rename as rename3, rm as rm7 } from "node:fs/promises";
4627
- import path21 from "node:path";
4773
+ import path22 from "node:path";
4628
4774
  import { randomBytes } from "node:crypto";
4629
4775
  async function studioSettings(root) {
4630
- const directory = path21.join(root, ".osmo");
4776
+ const directory = path22.join(root, ".osmo");
4631
4777
  await mkdir8(directory, { recursive: true });
4632
- const lock = path21.join(directory, "studio.lock");
4778
+ const lock = path22.join(directory, "studio.lock");
4633
4779
  const deadline = Date.now() + 5e3;
4634
4780
  while (true) {
4635
4781
  try {
@@ -4646,7 +4792,7 @@ async function studioSettings(root) {
4646
4792
  }
4647
4793
  const release = () => rm7(lock, { recursive: true, force: true });
4648
4794
  try {
4649
- const file = path21.join(directory, "studio.json");
4795
+ const file = path22.join(directory, "studio.json");
4650
4796
  let settings, existing = false;
4651
4797
  try {
4652
4798
  settings = JSON.parse(await readFile8(file, "utf8"));
@@ -4658,7 +4804,7 @@ async function studioSettings(root) {
4658
4804
  throw new Error(
4659
4805
  "Cannot read .osmo/studio.json. Fix or remove it to reset the Studio address."
4660
4806
  );
4661
- const name = path21.basename(root).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48).replace(/-$/, "") || "project";
4807
+ const name = path22.basename(root).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48).replace(/-$/, "") || "project";
4662
4808
  settings = {
4663
4809
  version: 1,
4664
4810
  id: `${name}-${randomBytes(2).toString("hex")}`,
@@ -4666,7 +4812,7 @@ async function studioSettings(root) {
4666
4812
  token: randomBytes(24).toString("hex")
4667
4813
  };
4668
4814
  }
4669
- const ignorePath = path21.join(root, ".gitignore");
4815
+ const ignorePath = path22.join(root, ".gitignore");
4670
4816
  let ignore = "";
4671
4817
  try {
4672
4818
  ignore = await readFile8(ignorePath, "utf8");
@@ -4714,11 +4860,11 @@ var init_studio_settings = __esm({
4714
4860
  import { readFile as readFile9, writeFile as writeFile9, rename as rename4, mkdir as mkdir9 } from "node:fs/promises";
4715
4861
  import { userInfo } from "node:os";
4716
4862
  import { randomUUID as randomUUID8 } from "node:crypto";
4717
- import path22 from "node:path";
4863
+ import path23 from "node:path";
4718
4864
  async function commentProfile(root) {
4719
4865
  try {
4720
4866
  const profile = JSON.parse(
4721
- await readFile9(path22.join(root, ".osmo/comment-profile.json"), "utf8")
4867
+ await readFile9(path23.join(root, ".osmo/comment-profile.json"), "utf8")
4722
4868
  );
4723
4869
  if (!validAuthor(profile)) throw new Error("Invalid comment profile");
4724
4870
  return profile;
@@ -4737,11 +4883,11 @@ async function saveCommentProfile(root, input) {
4737
4883
  throw new Error(
4738
4884
  "Provide a name and a PNG, JPEG, or WebP avatar under 110 KB."
4739
4885
  );
4740
- const folder = path22.join(root, ".osmo");
4886
+ const folder = path23.join(root, ".osmo");
4741
4887
  await mkdir9(folder, { recursive: true });
4742
- const temp = path22.join(folder, `comment-profile.${randomUUID8()}.tmp`);
4888
+ const temp = path23.join(folder, `comment-profile.${randomUUID8()}.tmp`);
4743
4889
  await writeFile9(temp, JSON.stringify(profile) + "\n");
4744
- await rename4(temp, path22.join(folder, "comment-profile.json"));
4890
+ await rename4(temp, path23.join(folder, "comment-profile.json"));
4745
4891
  return profile;
4746
4892
  }
4747
4893
  var init_comment_profile = __esm({
@@ -4757,16 +4903,16 @@ __export(server_exports, {
4757
4903
  startStudio: () => startStudio
4758
4904
  });
4759
4905
  import http from "node:http";
4760
- import fs8 from "node:fs";
4906
+ import fs9 from "node:fs";
4761
4907
  import { readFile as readFile10, stat as stat4, realpath as realpath3, readdir as readdir3 } from "node:fs/promises";
4762
- import path23 from "node:path";
4908
+ import path24 from "node:path";
4763
4909
  import { createHash as createHash7, randomUUID as randomUUID9 } from "node:crypto";
4764
4910
  import { spawn as spawn3 } from "node:child_process";
4765
4911
  async function revision(paths2) {
4766
4912
  const hash3 = createHash7("sha256");
4767
4913
  for (const file of [...new Set(paths2)].sort()) {
4768
4914
  hash3.update(file);
4769
- for await (const chunk of fs8.createReadStream(file)) hash3.update(chunk);
4915
+ for await (const chunk of fs9.createReadStream(file)) hash3.update(chunk);
4770
4916
  }
4771
4917
  return `sha256:${hash3.digest("hex")}`;
4772
4918
  }
@@ -4783,7 +4929,7 @@ async function fingerprint(paths2) {
4783
4929
  ).then((v) => v.join("|"));
4784
4930
  }
4785
4931
  async function studioRevision() {
4786
- const files = (await readdir3(studioRoot, { recursive: true })).filter((file) => /\.(html|js|css|svg|woff2|wasm)$/.test(file)).sort().map((file) => path23.join(studioRoot, file));
4932
+ const files = (await readdir3(studioRoot, { recursive: true })).filter((file) => /\.(html|js|css|svg|woff2|ttf|wasm)$/.test(file)).sort().map((file) => path24.join(studioRoot, file));
4787
4933
  const stamp = await fingerprint(files);
4788
4934
  if (stamp !== studioStamp) {
4789
4935
  const digest = await revision(files);
@@ -4821,8 +4967,8 @@ async function startStudio({
4821
4967
  preview = {},
4822
4968
  open = true
4823
4969
  }) {
4824
- const studioBuild = path23.join(runtimeRoot, "packages/studio/tools/build.mjs");
4825
- if (fs8.existsSync(studioBuild))
4970
+ const studioBuild = path24.join(runtimeRoot, "packages/studio/tools/build.mjs");
4971
+ if (fs9.existsSync(studioBuild))
4826
4972
  await run(process.execPath, [studioBuild], { stdio: ["ignore", 2, 2] });
4827
4973
  root = await realpath3(root);
4828
4974
  source = await realpath3(source);
@@ -4859,7 +5005,7 @@ async function startStudio({
4859
5005
  scene,
4860
5006
  preview,
4861
5007
  version,
4862
- projectName: path23.basename(root),
5008
+ projectName: path24.basename(root),
4863
5009
  projectId: settings.id,
4864
5010
  loading: true,
4865
5011
  error: null,
@@ -5056,7 +5202,7 @@ async function startStudio({
5056
5202
  "Content-Type": "application/octet-stream",
5057
5203
  "X-Content-Type-Options": "nosniff"
5058
5204
  });
5059
- const stream = fs8.createReadStream(file2.sourcePath);
5205
+ const stream = fs9.createReadStream(file2.sourcePath);
5060
5206
  res.once("close", () => stream.destroy());
5061
5207
  stream.on("error", () => res.destroy()).pipe(res);
5062
5208
  return;
@@ -5112,6 +5258,8 @@ async function startStudio({
5112
5258
  types3[asset.extension] ?? "application/octet-stream"
5113
5259
  );
5114
5260
  }
5261
+ if (req.method === "GET" && url2.pathname === "/api/scenes")
5262
+ return json(res, 200, await listProjectScenes(root));
5115
5263
  if (req.method === "GET" && url2.pathname === "/api/resources")
5116
5264
  return json(
5117
5265
  res,
@@ -5301,14 +5449,14 @@ async function startStudio({
5301
5449
  geometry = {
5302
5450
  revision: rev,
5303
5451
  frame,
5304
- targets: result.targets.filter((t) => rows.has(t.path)).map(({ path: path25, ...t }, order) => ({
5452
+ targets: result.targets.filter((t) => rows.has(t.path)).map(({ path: path26, ...t }, order) => ({
5305
5453
  ...t,
5306
5454
  // Filtered to rows above.
5307
- id: rows.get(path25),
5455
+ id: rows.get(path26),
5308
5456
  order
5309
5457
  })),
5310
5458
  quads: Object.fromEntries(
5311
- Object.entries(result.quads).filter(([path25]) => rows.has(path25)).map(([path25, quad]) => [rows.get(path25), quad])
5459
+ Object.entries(result.quads).filter(([path26]) => rows.has(path26)).map(([path26, quad]) => [rows.get(path26), quad])
5312
5460
  )
5313
5461
  };
5314
5462
  geometryCache.set(key, geometry);
@@ -5451,7 +5599,7 @@ async function startStudio({
5451
5599
  "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(file2.name)}`,
5452
5600
  "X-Content-Type-Options": "nosniff"
5453
5601
  });
5454
- const stream = fs8.createReadStream(filename2);
5602
+ const stream = fs9.createReadStream(filename2);
5455
5603
  res.once("close", () => stream.destroy());
5456
5604
  stream.on("error", () => res.destroy()).pipe(res);
5457
5605
  return;
@@ -5550,15 +5698,15 @@ async function startStudio({
5550
5698
  if (!state.meta) throw new Error("Load a scene before exporting");
5551
5699
  const settings2 = validateExportSettings(input, state.meta);
5552
5700
  const format2 = exportFormats[settings2.format];
5553
- const filename2 = input.filename ?? `${path23.basename(scene, ".tsx")}-${Date.now()}.${format2.extension}`;
5701
+ const filename2 = input.filename ?? `${path24.basename(scene, ".tsx")}-${Date.now()}.${format2.extension}`;
5554
5702
  if (typeof filename2 !== "string" || !/^[\w .-]+$/.test(filename2) || !filename2.toLowerCase().endsWith(`.${format2.extension}`) || filename2.startsWith("."))
5555
5703
  throw new Error(`Use a filename ending in .${format2.extension}`);
5556
- const folder = path23.join(root, "exports");
5557
- await fs8.promises.mkdir(folder, { recursive: true });
5704
+ const folder = path24.join(root, "exports");
5705
+ await fs9.promises.mkdir(folder, { recursive: true });
5558
5706
  const canonicalFolder = await realpath3(folder);
5559
- if (path23.relative(root, canonicalFolder).startsWith(".."))
5707
+ if (path24.relative(root, canonicalFolder).startsWith(".."))
5560
5708
  throw new Error("Export folder must stay inside the project");
5561
- const destination = path23.join(canonicalFolder, filename2);
5709
+ const destination = path24.join(canonicalFolder, filename2);
5562
5710
  if (closed || project !== snapshot || state.loading || editing || dirty)
5563
5711
  throw Object.assign(
5564
5712
  new Error("The scene changed before export. Try again."),
@@ -5634,7 +5782,7 @@ async function startStudio({
5634
5782
  `${cookieName}=${token}; HttpOnly; SameSite=Strict; Path=${projectPath}; Max-Age=31536000`
5635
5783
  );
5636
5784
  const file = isPage ? "index.html" : url2.pathname.slice(1);
5637
- if (!/^(index\.html|public\/(osmo-studio-logo|pencil-simple|check-circle-filled)\.svg|src\/[\w/-]+\.(js|css|wasm|svg|woff2)|public\/comment-cursor\/(default|active-outer)\.svg|public\/fonts\/[\w-]+\.woff2)$/.test(
5785
+ if (!/^(index\.html|public\/(osmo-studio-logo|pencil-simple|check-circle-filled)\.svg|src\/[\w/-]+\.(js|css|wasm|svg|woff2|ttf)|public\/comment-cursor\/(default|active-outer)\.svg|public\/fonts\/[\w-]+\.woff2)$/.test(
5638
5786
  file
5639
5787
  ))
5640
5788
  return json(res, 404, { error: "Not found" });
@@ -5644,15 +5792,16 @@ async function startStudio({
5644
5792
  ".js": "text/javascript",
5645
5793
  ".css": "text/css",
5646
5794
  ".woff2": "font/woff2",
5795
+ ".ttf": "font/ttf",
5647
5796
  ".svg": "image/svg+xml"
5648
5797
  };
5649
- const bytes = await readFile10(path23.join(studioRoot, file));
5798
+ const bytes = await readFile10(path24.join(studioRoot, file));
5650
5799
  res.setHeader(
5651
5800
  "Content-Security-Policy",
5652
5801
  "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; media-src 'self' blob:; connect-src 'self' blob:; font-src 'self'; frame-ancestors 'none'"
5653
5802
  );
5654
5803
  res.writeHead(200, {
5655
- "Content-Type": types2[path23.extname(file)],
5804
+ "Content-Type": types2[path24.extname(file)],
5656
5805
  "Cache-Control": "no-cache"
5657
5806
  });
5658
5807
  res.end(bytes);
@@ -5768,16 +5917,16 @@ async function startStudio({
5768
5917
  const url = `http://127.0.0.1:${settings.port}${projectPath}`;
5769
5918
  openBrowser(url);
5770
5919
  try {
5771
- watcher = fs8.watch(root, { recursive: true }, async (event, filename2) => {
5920
+ watcher = fs9.watch(root, { recursive: true }, async (event, filename2) => {
5772
5921
  const name = String(filename2 ?? "");
5773
5922
  if (/(^|[/\\])\.osmo-edit-/.test(name)) return;
5774
5923
  if (name === "storyboard.canvas.json" || name.startsWith("storyboard.canvas.json."))
5775
5924
  return;
5776
5925
  if (/(^|[/\\])(node_modules|\.git|\.osmo|exports)([/\\]|$)/.test(name))
5777
5926
  return;
5778
- if ((name.startsWith("resources/") || name === "assets" || name.startsWith("assets/")) && !dependencies.includes(path23.join(root, name)))
5927
+ if ((name.startsWith("resources/") || name === "assets" || name.startsWith("assets/")) && !dependencies.includes(path24.join(root, name)))
5779
5928
  return;
5780
- const file = path23.join(root, name), before = watchedStamps.get(file);
5929
+ const file = path24.join(root, name), before = watchedStamps.get(file);
5781
5930
  const next = await fingerprint([file]);
5782
5931
  watchedStamps.set(file, next);
5783
5932
  if (closed || editing) return;
@@ -5804,6 +5953,7 @@ var studioStamp, studioDigest;
5804
5953
  var init_server = __esm({
5805
5954
  "src/server.ts"() {
5806
5955
  "use strict";
5956
+ init_project_scenes();
5807
5957
  init_asset_response();
5808
5958
  init_attachment_thumbnail();
5809
5959
  init_local_attachment_picker();
@@ -5827,7 +5977,7 @@ var init_server = __esm({
5827
5977
  init_check();
5828
5978
  import { existsSync as existsSync3, statSync } from "node:fs";
5829
5979
  import { mkdir as mkdir10, readFile as readFile11, rename as rename5, rm as rm8 } from "node:fs/promises";
5830
- import path24 from "node:path";
5980
+ import path25 from "node:path";
5831
5981
 
5832
5982
  // src/cloud.ts
5833
5983
  import { spawnSync as spawnSync2 } from "node:child_process";
@@ -8714,7 +8864,7 @@ async function checked(root, source) {
8714
8864
  const seen = /* @__PURE__ */ new Set();
8715
8865
  return issues.map((issue) => ({
8716
8866
  ...issue,
8717
- ...issue.file && path24.isAbsolute(issue.file) ? { file: path24.relative(root, issue.file) } : {}
8867
+ ...issue.file && path25.isAbsolute(issue.file) ? { file: path25.relative(root, issue.file) } : {}
8718
8868
  })).filter((issue) => {
8719
8869
  const key = JSON.stringify(issue);
8720
8870
  return !seen.has(key) && seen.add(key);
@@ -8800,7 +8950,7 @@ var STUDIO = {
8800
8950
  run: async ([project, folder]) => {
8801
8951
  const done = await clone(
8802
8952
  parseProject(project),
8803
- folder === void 0 ? void 0 : path24.resolve(folder)
8953
+ folder === void 0 ? void 0 : path25.resolve(folder)
8804
8954
  );
8805
8955
  return {
8806
8956
  status: "cloned",
@@ -8885,15 +9035,15 @@ ${r.status === "running" ? "Ctrl+C stops the server and renderer." : "Using the
8885
9035
  );
8886
9036
  const still = at !== void 0;
8887
9037
  const extension = still ? ".png" : ".mp4";
8888
- const name = path24.basename(scene, path24.extname(scene));
8889
- const out = path24.resolve(
8890
- flags.out ?? path24.join(
9038
+ const name = path25.basename(scene, path25.extname(scene));
9039
+ const out = path25.resolve(
9040
+ flags.out ?? path25.join(
8891
9041
  root,
8892
9042
  "exports",
8893
9043
  `${name}${still ? `-${at}s` : ""}${extension}`
8894
9044
  )
8895
9045
  );
8896
- if (path24.extname(out).toLowerCase() !== extension)
9046
+ if (path25.extname(out).toLowerCase() !== extension)
8897
9047
  throw new UsageError(
8898
9048
  still ? "With --at, --out names a .png file." : "--out names an .mp4 file. For one frame as a .png, add --at SECONDS.",
8899
9049
  "osmo studio render --help"
@@ -8927,10 +9077,10 @@ ${r.status === "running" ? "Ctrl+C stops the server and renderer." : "Using the
8927
9077
  "osmo studio render --help"
8928
9078
  );
8929
9079
  const frame = still ? Math.min(Math.round(at * fps), frames - 1) : void 0;
8930
- await mkdir10(path24.dirname(out), { recursive: true });
8931
- const partial = path24.join(
8932
- path24.dirname(out),
8933
- `.${path24.basename(out, extension)}.${process.pid}${extension}`
9080
+ await mkdir10(path25.dirname(out), { recursive: true });
9081
+ const partial = path25.join(
9082
+ path25.dirname(out),
9083
+ `.${path25.basename(out, extension)}.${process.pid}${extension}`
8934
9084
  );
8935
9085
  try {
8936
9086
  await runRenderer(