@lalalic/markcut 1.0.0

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.
Files changed (98) hide show
  1. package/.env.example +27 -0
  2. package/.github/user-steer.md +9 -0
  3. package/.vscode/settings.json +3 -0
  4. package/AGENTS.md +271 -0
  5. package/README.md +219 -0
  6. package/SKILL.md +209 -0
  7. package/docs/dynamic-components.md +191 -0
  8. package/docs/edit-mode.md +220 -0
  9. package/docs/json-descriptive.md +539 -0
  10. package/docs/label-mode.md +110 -0
  11. package/docs/markdown-descriptive.md +751 -0
  12. package/docs/templates.md +52 -0
  13. package/package.json +64 -0
  14. package/remotion.config.ts +5 -0
  15. package/scripts/artlist-dl.mjs +190 -0
  16. package/scripts/build-pipeline.sh +19 -0
  17. package/scripts/build-player.sh +20 -0
  18. package/src/Root.tsx +55 -0
  19. package/src/config.mjs +88 -0
  20. package/src/context/EventContext.tsx +168 -0
  21. package/src/context/index.tsx +42 -0
  22. package/src/descriptive/compiler.test.ts +1135 -0
  23. package/src/descriptive/compiler.ts +1230 -0
  24. package/src/descriptive/dsl.ts +455 -0
  25. package/src/descriptive/markdown.test.ts +866 -0
  26. package/src/descriptive/markdown.ts +674 -0
  27. package/src/descriptive/resolve.test.ts +951 -0
  28. package/src/descriptive/resolve.ts +891 -0
  29. package/src/entry.tsx +163 -0
  30. package/src/index.ts +4 -0
  31. package/src/player/browser.tsx +356 -0
  32. package/src/player/bundle/player.js +60259 -0
  33. package/src/player/bundler.mjs +269 -0
  34. package/src/player/label-server.mjs +599 -0
  35. package/src/player/pipeline.mjs +11123 -0
  36. package/src/player/pipeline.ts +117 -0
  37. package/src/player/server-shared.mjs +144 -0
  38. package/src/player/server.mjs +1006 -0
  39. package/src/render/cli-tools.ts +177 -0
  40. package/src/render/cli.mjs +628 -0
  41. package/src/schema/index.ts +259 -0
  42. package/src/types/Audio.tsx +56 -0
  43. package/src/types/Component.tsx +135 -0
  44. package/src/types/Effect.tsx +88 -0
  45. package/src/types/Folder.tsx +180 -0
  46. package/src/types/FrameSyncStyle.tsx +51 -0
  47. package/src/types/Image.tsx +51 -0
  48. package/src/types/Include.tsx +394 -0
  49. package/src/types/Map.tsx +252 -0
  50. package/src/types/Rhythm.tsx +58 -0
  51. package/src/types/Scene.tsx +42 -0
  52. package/src/types/Subtitle.tsx +218 -0
  53. package/src/types/Video.tsx +70 -0
  54. package/src/types/keyframes.ts +454 -0
  55. package/src/utils/__tests__/vtt.test.ts +129 -0
  56. package/src/utils/index.ts +168 -0
  57. package/src/utils/tween.ts +118 -0
  58. package/src/vision/cli.mjs +1187 -0
  59. package/src/vision/vision_prompts.md +67 -0
  60. package/tests/dsl.test.ts +317 -0
  61. package/tests/fixtures/audio.json +25 -0
  62. package/tests/fixtures/basic.json +27 -0
  63. package/tests/fixtures/component-all.json +38 -0
  64. package/tests/fixtures/components.json +38 -0
  65. package/tests/fixtures/effects.json +64 -0
  66. package/tests/fixtures/full.json +51 -0
  67. package/tests/fixtures/map.json +23 -0
  68. package/tests/fixtures/md/all-nodes.md +28 -0
  69. package/tests/fixtures/md/basic.md +6 -0
  70. package/tests/fixtures/md/component-imports.md +20 -0
  71. package/tests/fixtures/md/edge-cases.md +33 -0
  72. package/tests/fixtures/md/effects.md +17 -0
  73. package/tests/fixtures/md/frontmatter.md +20 -0
  74. package/tests/fixtures/md/full-feature.md +58 -0
  75. package/tests/fixtures/md/imports-block.md +19 -0
  76. package/tests/fixtures/md/include-main.md +11 -0
  77. package/tests/fixtures/md/include-sub.md +25 -0
  78. package/tests/fixtures/md/jsx-code-fence.md +21 -0
  79. package/tests/fixtures/md/map.md +11 -0
  80. package/tests/fixtures/md/nested-scenes.md +25 -0
  81. package/tests/fixtures/md/rhythm.md +17 -0
  82. package/tests/fixtures/md/scenes.md +16 -0
  83. package/tests/fixtures/md/tween.md +11 -0
  84. package/tests/fixtures/md/vars-test.md +6 -0
  85. package/tests/fixtures/scenes.json +40 -0
  86. package/tests/fixtures/subtitle.json +59 -0
  87. package/tests/fixtures/subvideo.json +59 -0
  88. package/tests/fixtures/templates/courseware.md +351 -0
  89. package/tests/fixtures/tween-visual.json +28 -0
  90. package/tests/fixtures/video-series.json +54 -0
  91. package/tests/md-descriptive.test.ts +742 -0
  92. package/tests/render.test.ts +985 -0
  93. package/tests/schema.test.ts +68 -0
  94. package/tests/server.test.ts +308 -0
  95. package/tests/utils.ts +391 -0
  96. package/tests/vitest.config.ts +18 -0
  97. package/tests/vitest.integration.config.ts +16 -0
  98. package/tsconfig.json +20 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Component bundler — creates a temp npm project from template imports,
3
+ * installs dependencies, and bundles into a single ESM file.
4
+ *
5
+ * Supports:
6
+ * - npm re-exports: "export { X } from "npm:pkg" → installs pkg, re-exports X
7
+ * - inline functions: "export function X() {...}" → writes to file, re-exports
8
+ * - @remotion/* packages auto-pinned to the host markcut version
9
+ *
10
+ * Cached by content hash — re-bundling only when dependencies change.
11
+ */
12
+
13
+ import { createHash } from "node:crypto";
14
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
15
+ import { execSync } from "node:child_process";
16
+ import { join, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = join(__dirname, "..", "..");
21
+ const CACHE_DIR = join(ROOT, "public", ".component-cache");
22
+
23
+ /** Cached bundles: hash → { url, exports } */
24
+ const BUNDLED = new Map();
25
+
26
+ /** Cache for host package.json dependency map. */
27
+ let _hostDeps = null;
28
+ function getHostDeps() {
29
+ if (_hostDeps) return _hostDeps;
30
+ const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
31
+ _hostDeps = { ...pkg.dependencies, ...pkg.devDependencies };
32
+ return _hostDeps;
33
+ }
34
+
35
+ /**
36
+ * Parse an npm specifier into package name and full import specifier.
37
+ *
38
+ * Examples:
39
+ * "npm:recharts" → { pkgName: "recharts", importSpecifier: "recharts" }
40
+ * "npm:react-markdown" → { pkgName: "react-markdown", importSpecifier: "react-markdown" }
41
+ * "npm:@remotion/effects" → { pkgName: "@remotion/effects", importSpecifier: "@remotion/effects" }
42
+ * "npm:@remotion/effects/checkerboard" → { pkgName: "@remotion/effects", importSpecifier: "@remotion/effects/checkerboard" }
43
+ * "npm:package/sub/path" → { pkgName: "package", importSpecifier: "package/sub/path" }
44
+ *
45
+ * The pkgName is used for package.json dependencies (npm install).
46
+ * The importSpecifier is used in import/export statements (esbuild resolves it
47
+ * via the installed package's exports map).
48
+ */
49
+ function parseNpmSpec(spec) {
50
+ const raw = spec.startsWith("npm:") ? spec.slice(4) : spec;
51
+ // Scoped package: @scope/name[/subpath]
52
+ if (raw.startsWith("@")) {
53
+ const parts = raw.split("/");
54
+ // @scope/name is the package (first two slash-delimited parts)
55
+ const pkgName = parts.slice(0, 2).join("/");
56
+ return { pkgName, importSpecifier: raw };
57
+ }
58
+ // Unscoped package: package[/subpath]
59
+ const slashIdx = raw.indexOf("/");
60
+ if (slashIdx === -1) {
61
+ return { pkgName: raw, importSpecifier: raw };
62
+ }
63
+ return { pkgName: raw.slice(0, slashIdx), importSpecifier: raw };
64
+ }
65
+
66
+ /**
67
+ * Resolve the best version for a package name.
68
+ * If the host (markcut) already depends on it, use that exact version
69
+ * (e.g. @remotion/player → "4.0.469"). Otherwise use "latest".
70
+ *
71
+ * Special handling for @remotion/* packages: they must use the SAME version
72
+ * as markcut's own `remotion` dependency to avoid React context mismatches
73
+ * and duplicate module instances at runtime.
74
+ */
75
+ function resolveVersion(pkgName) {
76
+ // All @remotion/* packages must match markcut's remotion version
77
+ if (pkgName.startsWith("@remotion/") || pkgName === "remotion") {
78
+ const hostDeps = getHostDeps();
79
+ const remotionVersion = hostDeps["remotion"];
80
+ if (remotionVersion) {
81
+ // Strip semver range (^/~) for exact pinning
82
+ return remotionVersion.replace(/^[\^~]/, "");
83
+ }
84
+ }
85
+ const hostDeps = getHostDeps();
86
+ return hostDeps[pkgName] || "latest";
87
+ }
88
+
89
+ /**
90
+ * Determine which packages should be externalized from the component bundle.
91
+ * These are packages already bundled in the main player.js that must be shared
92
+ * via import map to avoid duplicate React/Remotion instances (which would break
93
+ * context sharing and create multiple copies of singletons).
94
+ *
95
+ * Must stay in sync with the import map entries created in browser.tsx's
96
+ * `__remotionShared` registry. Only packages listed here AND registered in
97
+ * `__remotionShared` get proper import map resolution.
98
+ *
99
+ * Returns an array of esbuild --external arguments.
100
+ */
101
+ function getSharedExternals() {
102
+ return [
103
+ // Core React — always needed
104
+ "react",
105
+ "react/jsx-runtime",
106
+ "react-dom",
107
+ // Remotion core — must be shared for context identity
108
+ "remotion",
109
+ // Commonly used @remotion/* sub-packages
110
+ "@remotion/player",
111
+ ];
112
+ }
113
+
114
+ /**
115
+ * Build a component registry bundle from parsed import entries.
116
+ *
117
+ * @param {Array<{name:string, from?:string, jsx?:string, exports?:string}>} entries — export entries (component registrations)
118
+ * @param {string[]} [extraSpecs] — additional dependency specs from import statements (e.g. ["react", "npm:lodash"])
119
+ * @param {string} [rawSource] — optional raw imports block source. If provided, used as the bundle entry
120
+ * instead of rebuilding from entries. Entries/extraSpecs are still used for dependency resolution.
121
+ * @param {string} [cacheDir] — optional cache directory for component bundles. Defaults to public/.component-cache.
122
+ * @returns {Promise<{url:string|null, exports:string[]}>}
123
+ */
124
+ export async function bundleFromEntries(entries, extraSpecs = [], rawSource = null, cacheDir = null) {
125
+ const CACHE_DIR = cacheDir || join(ROOT, "public", ".component-cache");
126
+ const hasEntries = entries && entries.length > 0;
127
+ const hasRaw = rawSource && rawSource.trim();
128
+
129
+ // Separate npm imports from inline function definitions
130
+ const npmDeps = [];
131
+
132
+ // Strip npm: prefixes from the raw source so esbuild can resolve them
133
+ if (hasRaw) {
134
+ rawSource = rawSource.replace(/from\s+["'`]npm:([^"'`]+)["'`]/g, 'from "$1"');
135
+
136
+ // Extract deps from raw source using the same pattern as extractDependencySpecs
137
+ const fromRe = /from\s+["'`](.+?)["'`]\s*;?\s*$/gm;
138
+ let m;
139
+ while ((m = fromRe.exec(rawSource)) !== null) {
140
+ extraSpecs.push(m[1]);
141
+ }
142
+ // Also catch bare side-effect imports: import "spec"
143
+ const bareRe = /^import\s+["'`](.+?)["'`]\s*;?\s*$/gm;
144
+ while ((m = bareRe.exec(rawSource)) !== null) {
145
+ extraSpecs.push(m[1]);
146
+ }
147
+ }
148
+
149
+ if (hasEntries) {
150
+ for (const entry of entries) {
151
+ if (entry.from) {
152
+ const { pkgName, importSpecifier } = parseNpmSpec(entry.from);
153
+ const exportName = entry.exports || "default";
154
+ npmDeps.push({ name: entry.name, pkgName, importSpecifier, exportName });
155
+ }
156
+ // entry.name-only entries (inline function defs) are handled by rawSource — no separate inline file needed
157
+ }
158
+ }
159
+
160
+ // Add extra dependency specs that don't overlap with existing npmDeps
161
+ const existingPkgs = new Set(npmDeps.map(d => d.pkgName));
162
+ for (const spec of extraSpecs) {
163
+ const { pkgName, importSpecifier } = parseNpmSpec(spec);
164
+ if (!existingPkgs.has(pkgName)) {
165
+ existingPkgs.add(pkgName);
166
+ npmDeps.push({ name: importSpecifier, pkgName, importSpecifier, exportName: null }); // no re-export needed
167
+ }
168
+ }
169
+
170
+ if (npmDeps.length === 0 && !hasRaw) return { url: null, exports: [] };
171
+
172
+ // Create a stable hash from sorted inputs
173
+ const hashInput = hasRaw
174
+ ? rawSource
175
+ : sortedDeps.map(d => d.name + "=npm:" + d.importSpecifier + "+" + d.exportName).join(",");
176
+ const hash = createHash("md5").update(hashInput).digest("hex").slice(0, 8);
177
+
178
+ if (BUNDLED.has(hash)) return BUNDLED.get(hash);
179
+
180
+ const dir = join(CACHE_DIR, hash);
181
+ mkdirSync(dir, { recursive: true });
182
+
183
+ // Build package.json with all dependencies
184
+ const pkgJson = { type: "module", private: true, dependencies: {} };
185
+ for (const dep of npmDeps) {
186
+ pkgJson.dependencies[dep.pkgName] = resolveVersion(dep.pkgName);
187
+ }
188
+ writeFileSync(join(dir, "package.json"), JSON.stringify(pkgJson, null, 2));
189
+
190
+ // Build index.jsx — always use rawSource (the original imports block content)
191
+ // Name it .jsx so esbuild enables JSX parsing automatically
192
+ const lines = hasRaw ? [rawSource] : [];
193
+ const entryFile = "index.jsx";
194
+ writeFileSync(join(dir, entryFile), lines.join("\n\n") + "\n");
195
+
196
+ // npm install (skip if node_modules already exists)
197
+ if (!existsSync(join(dir, "node_modules"))) {
198
+ const depNames = npmDeps.map(d => d.pkgName);
199
+ console.log(" \ud83d\udce6 Installing components: " + depNames.join(", "));
200
+ try {
201
+ execSync("npm install --no-audit --no-fund --prefer-offline --loglevel=error", {
202
+ cwd: dir,
203
+ stdio: "pipe",
204
+ timeout: 60000,
205
+ });
206
+ } catch (e) {
207
+ console.error(" \u26a0\ufe0f npm install failed:", e.stderr?.toString().slice(0, 300));
208
+ throw new Error("Failed to install dependencies: " + (e.stderr?.toString().slice(0, 200) || e.message));
209
+ }
210
+ } else {
211
+ console.log(" \ud83d\udce6 Dependencies cached");
212
+ }
213
+
214
+ // Bundle with esbuild
215
+ const outFile = join(CACHE_DIR, hash + ".js");
216
+ const externals = getSharedExternals();
217
+ const externalArgs = [
218
+ ...externals.map(p => `--external:${p}`),
219
+ "--external:node:*",
220
+ ].join(" ");
221
+ if (!existsSync(outFile)) {
222
+ console.log(" \ud83d\udd27 Bundling components \u2192 " + hash + ".js");
223
+ try {
224
+ execSync(
225
+ 'npx esbuild ' + entryFile + ' --bundle --format=esm --outfile="' + outFile + '" ' +
226
+ externalArgs + " " +
227
+ "--platform=browser --target=es2020",
228
+ { cwd: dir, stdio: "pipe", timeout: 30000 }
229
+ );
230
+ } catch (e) {
231
+ console.error(" \u26a0\ufe0f esbuild failed:", e.stderr?.toString().slice(0, 300));
232
+ throw new Error("Failed to bundle components: " + (e.stderr?.toString().slice(0, 200) || e.message));
233
+ }
234
+ } else {
235
+ console.log(" \ud83d\udce6 Components cached \u2192 " + hash + ".js");
236
+ }
237
+
238
+ // Extract export names from raw source for the log message
239
+ let exportNames;
240
+ if (hasRaw) {
241
+ exportNames = [];
242
+ const exportRe = /export\s+(?:\{([^}]+)\}|function\s+(\w+)|default\s+(\w+))/g;
243
+ let m;
244
+ while ((m = exportRe.exec(rawSource)) !== null) {
245
+ if (m[1]) {
246
+ // export { Name1, Name2 } or export { Name } from "..."
247
+ m[1].split(",").forEach(p => {
248
+ const trimmed = p.trim();
249
+ if (trimmed) exportNames.push(trimmed);
250
+ });
251
+ } else if (m[2]) {
252
+ // export function Name(...)
253
+ exportNames.push(m[2]);
254
+ } else if (m[3]) {
255
+ // export default Name
256
+ exportNames.push(m[3]);
257
+ }
258
+ }
259
+ } else {
260
+ exportNames = (entries || []).map(e => e.name).filter(Boolean);
261
+ }
262
+
263
+ const result = {
264
+ url: outFile,
265
+ exports: exportNames,
266
+ };
267
+ BUNDLED.set(hash, result);
268
+ return result;
269
+ }