@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.
- package/.env.example +27 -0
- package/.github/user-steer.md +9 -0
- package/.vscode/settings.json +3 -0
- package/AGENTS.md +271 -0
- package/README.md +219 -0
- package/SKILL.md +209 -0
- package/docs/dynamic-components.md +191 -0
- package/docs/edit-mode.md +220 -0
- package/docs/json-descriptive.md +539 -0
- package/docs/label-mode.md +110 -0
- package/docs/markdown-descriptive.md +751 -0
- package/docs/templates.md +52 -0
- package/package.json +64 -0
- package/remotion.config.ts +5 -0
- package/scripts/artlist-dl.mjs +190 -0
- package/scripts/build-pipeline.sh +19 -0
- package/scripts/build-player.sh +20 -0
- package/src/Root.tsx +55 -0
- package/src/config.mjs +88 -0
- package/src/context/EventContext.tsx +168 -0
- package/src/context/index.tsx +42 -0
- package/src/descriptive/compiler.test.ts +1135 -0
- package/src/descriptive/compiler.ts +1230 -0
- package/src/descriptive/dsl.ts +455 -0
- package/src/descriptive/markdown.test.ts +866 -0
- package/src/descriptive/markdown.ts +674 -0
- package/src/descriptive/resolve.test.ts +951 -0
- package/src/descriptive/resolve.ts +891 -0
- package/src/entry.tsx +163 -0
- package/src/index.ts +4 -0
- package/src/player/browser.tsx +356 -0
- package/src/player/bundle/player.js +60259 -0
- package/src/player/bundler.mjs +269 -0
- package/src/player/label-server.mjs +599 -0
- package/src/player/pipeline.mjs +11123 -0
- package/src/player/pipeline.ts +117 -0
- package/src/player/server-shared.mjs +144 -0
- package/src/player/server.mjs +1006 -0
- package/src/render/cli-tools.ts +177 -0
- package/src/render/cli.mjs +628 -0
- package/src/schema/index.ts +259 -0
- package/src/types/Audio.tsx +56 -0
- package/src/types/Component.tsx +135 -0
- package/src/types/Effect.tsx +88 -0
- package/src/types/Folder.tsx +180 -0
- package/src/types/FrameSyncStyle.tsx +51 -0
- package/src/types/Image.tsx +51 -0
- package/src/types/Include.tsx +394 -0
- package/src/types/Map.tsx +252 -0
- package/src/types/Rhythm.tsx +58 -0
- package/src/types/Scene.tsx +42 -0
- package/src/types/Subtitle.tsx +218 -0
- package/src/types/Video.tsx +70 -0
- package/src/types/keyframes.ts +454 -0
- package/src/utils/__tests__/vtt.test.ts +129 -0
- package/src/utils/index.ts +168 -0
- package/src/utils/tween.ts +118 -0
- package/src/vision/cli.mjs +1187 -0
- package/src/vision/vision_prompts.md +67 -0
- package/tests/dsl.test.ts +317 -0
- package/tests/fixtures/audio.json +25 -0
- package/tests/fixtures/basic.json +27 -0
- package/tests/fixtures/component-all.json +38 -0
- package/tests/fixtures/components.json +38 -0
- package/tests/fixtures/effects.json +64 -0
- package/tests/fixtures/full.json +51 -0
- package/tests/fixtures/map.json +23 -0
- package/tests/fixtures/md/all-nodes.md +28 -0
- package/tests/fixtures/md/basic.md +6 -0
- package/tests/fixtures/md/component-imports.md +20 -0
- package/tests/fixtures/md/edge-cases.md +33 -0
- package/tests/fixtures/md/effects.md +17 -0
- package/tests/fixtures/md/frontmatter.md +20 -0
- package/tests/fixtures/md/full-feature.md +58 -0
- package/tests/fixtures/md/imports-block.md +19 -0
- package/tests/fixtures/md/include-main.md +11 -0
- package/tests/fixtures/md/include-sub.md +25 -0
- package/tests/fixtures/md/jsx-code-fence.md +21 -0
- package/tests/fixtures/md/map.md +11 -0
- package/tests/fixtures/md/nested-scenes.md +25 -0
- package/tests/fixtures/md/rhythm.md +17 -0
- package/tests/fixtures/md/scenes.md +16 -0
- package/tests/fixtures/md/tween.md +11 -0
- package/tests/fixtures/md/vars-test.md +6 -0
- package/tests/fixtures/scenes.json +40 -0
- package/tests/fixtures/subtitle.json +59 -0
- package/tests/fixtures/subvideo.json +59 -0
- package/tests/fixtures/templates/courseware.md +351 -0
- package/tests/fixtures/tween-visual.json +28 -0
- package/tests/fixtures/video-series.json +54 -0
- package/tests/md-descriptive.test.ts +742 -0
- package/tests/render.test.ts +985 -0
- package/tests/schema.test.ts +68 -0
- package/tests/server.test.ts +308 -0
- package/tests/utils.ts +391 -0
- package/tests/vitest.config.ts +18 -0
- package/tests/vitest.integration.config.ts +16 -0
- 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
|
+
}
|