@1agh/maude 0.39.1 → 0.40.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/README.md +1 -1
- package/apps/studio/annotations-context-toolbar.tsx +44 -1
- package/apps/studio/annotations-layer.tsx +253 -3
- package/apps/studio/annotations-model.ts +107 -6
- package/apps/studio/api.ts +812 -64
- package/apps/studio/bin/_html-playwright.mjs +9 -1
- package/apps/studio/bin/_pdf-playwright.mjs +8 -1
- package/apps/studio/bin/_png-playwright.mjs +8 -1
- package/apps/studio/bin/_pw-launch.mjs +54 -0
- package/apps/studio/bin/_svg-playwright.mjs +8 -1
- package/apps/studio/bin/_video-playwright.mjs +452 -0
- package/apps/studio/bin/prep.sh +8 -1
- package/apps/studio/canvas-edit.ts +1885 -104
- package/apps/studio/canvas-lib.tsx +19 -0
- package/apps/studio/canvas-list-watch.ts +6 -2
- package/apps/studio/canvas-shell.tsx +27 -0
- package/apps/studio/client/app.jsx +1029 -30
- package/apps/studio/client/github.js +7 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +860 -0
- package/apps/studio/client/panels/timeline-parse.js +229 -0
- package/apps/studio/client/panels/timeline-snap.js +55 -0
- package/apps/studio/client/styles/3-shell-maude.css +107 -0
- package/apps/studio/config.schema.json +14 -0
- package/apps/studio/context-menu.tsx +1 -1
- package/apps/studio/context.ts +113 -1
- package/apps/studio/dist/client.bundle.js +88 -16
- package/apps/studio/dist/comment-mount.js +1 -1
- package/apps/studio/dist/runtime/.min-sizes.json +11 -1
- package/apps/studio/dist/runtime/@remotion_media.js +491 -0
- package/apps/studio/dist/runtime/@remotion_player.js +56 -0
- package/apps/studio/dist/runtime/@remotion_transitions.js +300 -0
- package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_fade.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_flip.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_none.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_slide.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_wipe.js +1 -0
- package/apps/studio/dist/runtime/REMOTION-LICENSE.md +28 -0
- package/apps/studio/dist/runtime/remotion.js +42 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/_browser-bundles.ts +117 -0
- package/apps/studio/exporters/_runtime.ts +69 -0
- package/apps/studio/exporters/html.ts +4 -3
- package/apps/studio/exporters/index.ts +28 -2
- package/apps/studio/exporters/pdf.ts +4 -3
- package/apps/studio/exporters/png.ts +5 -3
- package/apps/studio/exporters/pptx.ts +6 -4
- package/apps/studio/exporters/svg.ts +9 -5
- package/apps/studio/exporters/video-encode-lib.ts +200 -0
- package/apps/studio/exporters/video-render-lib.ts +108 -0
- package/apps/studio/exporters/video.ts +184 -0
- package/apps/studio/http.ts +535 -27
- package/apps/studio/input-router.tsx +7 -1
- package/apps/studio/runtime-bundle.ts +30 -0
- package/apps/studio/server.ts +34 -9
- package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
- package/apps/studio/test/canvas-create-api.test.ts +76 -0
- package/apps/studio/test/canvas-edit.test.ts +90 -0
- package/apps/studio/test/canvas-list-watch.test.ts +49 -0
- package/apps/studio/test/canvas-media-drop.test.ts +30 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +36 -0
- package/apps/studio/test/clip-addressing.test.ts +732 -0
- package/apps/studio/test/config-reload.test.ts +230 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +74 -0
- package/apps/studio/test/edit-persistence.test.ts +91 -0
- package/apps/studio/test/exporters/runtime.test.ts +59 -0
- package/apps/studio/test/file-lock.test.ts +84 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -0
- package/apps/studio/test/timeline-parse.test.ts +127 -0
- package/apps/studio/test/timeline-snap.test.ts +85 -0
- package/apps/studio/test/video-asset.test.ts +163 -0
- package/apps/studio/test/video-comp-fixture.test.ts +50 -0
- package/apps/studio/test/video-comp.test.ts +168 -0
- package/apps/studio/test/video-render-bridge.test.ts +149 -0
- package/apps/studio/use-annotation-resize.tsx +6 -3
- package/apps/studio/use-canvas-media-drop.tsx +66 -4
- package/apps/studio/video-comp.tsx +444 -0
- package/apps/studio/whats-new.json +27 -0
- package/apps/studio/ws.ts +5 -0
- package/package.json +8 -8
- package/plugins/design/templates/_shell.html +25 -2
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +1 -1
|
@@ -9,6 +9,8 @@ import { existsSync } from 'node:fs';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
|
+
import { DEV_SERVER_ROOT } from '../paths.ts';
|
|
13
|
+
|
|
12
14
|
interface CachedBundle {
|
|
13
15
|
path: string;
|
|
14
16
|
ready: Promise<string>;
|
|
@@ -68,6 +70,121 @@ async function buildIife(entry: string, globalName: string, cachePath: string):
|
|
|
68
70
|
return cachePath;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* DDR-148 — build the in-page video encoder (`video-encode-lib.ts`) to a
|
|
75
|
+
* self-contained browser ESM module cached under the OS temp dir. It imports
|
|
76
|
+
* mediabunny + gifenc (both INLINED — no externals) and assigns
|
|
77
|
+
* `window.__maudeEnc`; the capture shim injects it via
|
|
78
|
+
* `page.addScriptTag({ content, type: 'module' })`. Returns the cache path.
|
|
79
|
+
*
|
|
80
|
+
* Unlike getBrowserBundle this bundles a LOCAL source entry (not an npm
|
|
81
|
+
* package) and keeps ESM (the module's top-level `window.__maudeEnc = …` side
|
|
82
|
+
* effect runs on load — no IIFE export-hoisting needed).
|
|
83
|
+
*/
|
|
84
|
+
let encodeLibReady: Promise<string> | null = null;
|
|
85
|
+
export function getEncodeLibBundle(): Promise<string> {
|
|
86
|
+
if (encodeLibReady) return encodeLibReady;
|
|
87
|
+
// DDR-045: derive from DEV_SERVER_ROOT, never `import.meta.dir`. In a compiled
|
|
88
|
+
// binary the latter is the virtual `/$bunfs/root`, so `Bun.build` fails with
|
|
89
|
+
// "failed to open root directory: /$bunfs/root" (the mp4/gif export bug).
|
|
90
|
+
const entry = path.join(DEV_SERVER_ROOT, 'exporters', 'video-encode-lib.ts');
|
|
91
|
+
const cachePath = path.join(tmpdir(), 'maude-video-encode-lib.mjs');
|
|
92
|
+
encodeLibReady = (async () => {
|
|
93
|
+
const built = await Bun.build({
|
|
94
|
+
entrypoints: [entry],
|
|
95
|
+
target: 'browser',
|
|
96
|
+
format: 'esm',
|
|
97
|
+
minify: true,
|
|
98
|
+
conditions: ['browser', 'import'],
|
|
99
|
+
});
|
|
100
|
+
if (!built.success) {
|
|
101
|
+
throw new Error(`encode-lib bundle failed: ${built.logs.map((l) => l.message).join('; ')}`);
|
|
102
|
+
}
|
|
103
|
+
const first = built.outputs[0];
|
|
104
|
+
if (!first) throw new Error('encode-lib bundle produced no outputs');
|
|
105
|
+
await Bun.write(cachePath, await first.text());
|
|
106
|
+
return cachePath;
|
|
107
|
+
})();
|
|
108
|
+
return encodeLibReady;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* DDR-148 addendum (audio export) — build the in-page whole-comp renderer
|
|
113
|
+
* (`video-render-lib.ts`, wraps @remotion/web-renderer's renderMediaOnWeb) to a
|
|
114
|
+
* self-contained browser ESM module cached under the OS temp dir. Assigns
|
|
115
|
+
* `window.__maudeRenderVideo__`; the capture shim injects it via addScriptTag
|
|
116
|
+
* before calling `window.__maude_render_video__` (video-comp.tsx bridge).
|
|
117
|
+
*
|
|
118
|
+
* UNLIKE getEncodeLibBundle, this externalizes 'remotion' + '@remotion/media' —
|
|
119
|
+
* they must resolve to the SAME module instances the canvas's importmap already
|
|
120
|
+
* loaded (dual-package hazard: a second bundled copy would give
|
|
121
|
+
* renderMediaOnWeb's internal Composition/context providers a different
|
|
122
|
+
* `TimelineContext` than the one the passed `component`'s hooks read from,
|
|
123
|
+
* freezing every frame at 0 / silencing audio). The browser's importmap
|
|
124
|
+
* (declared in _shell.html) resolves these bare specifiers even for a
|
|
125
|
+
* dynamically injected `<script type=module>` tag.
|
|
126
|
+
*/
|
|
127
|
+
let webRendererLibReady: Promise<string> | null = null;
|
|
128
|
+
export function getWebRendererBundle(): Promise<string> {
|
|
129
|
+
if (webRendererLibReady) return webRendererLibReady;
|
|
130
|
+
const entry = path.join(DEV_SERVER_ROOT, 'exporters', 'video-render-lib.ts');
|
|
131
|
+
const cachePath = path.join(tmpdir(), 'maude-video-render-lib.mjs');
|
|
132
|
+
webRendererLibReady = (async () => {
|
|
133
|
+
// Bun's `external` field is package-name PREFIX matched (runtime-bundle.ts's
|
|
134
|
+
// comment on the same gotcha) — listing 'remotion' also externalizes deep
|
|
135
|
+
// subpaths like 'remotion/version' (a real import inside web-renderer's own
|
|
136
|
+
// dependency tree). The importmap only maps the bare 'remotion' specifier, so
|
|
137
|
+
// an externalized 'remotion/version' 404s in the browser with "Failed to
|
|
138
|
+
// resolve module specifier". Use an exact-match onResolve plugin (mirrors
|
|
139
|
+
// canvas-build.ts's `exact-externals`) so ONLY the specifiers the importmap
|
|
140
|
+
// actually covers are externalized; every subpath (version, no-react, etc.)
|
|
141
|
+
// gets bundled inline like any other dependency.
|
|
142
|
+
//
|
|
143
|
+
// react/react-dom MUST also be externalized here (not just remotion) — a
|
|
144
|
+
// second bundled React copy inside this module renders the SAME `component`
|
|
145
|
+
// reference the page's importmap-loaded React created, and two React
|
|
146
|
+
// instances in one page is the textbook "Invalid Hook Call" (minified React
|
|
147
|
+
// error #321): the component's hooks resolve against instance A's dispatcher
|
|
148
|
+
// while web-renderer's internal renderer/reconciler is instance B. Discovered
|
|
149
|
+
// empirically — the first working build threw exactly this error.
|
|
150
|
+
const EXTERNAL_EXACT = new Set([
|
|
151
|
+
'remotion',
|
|
152
|
+
'@remotion/media',
|
|
153
|
+
'react',
|
|
154
|
+
'react-dom',
|
|
155
|
+
'react-dom/client',
|
|
156
|
+
'react/jsx-runtime',
|
|
157
|
+
'react/jsx-dev-runtime',
|
|
158
|
+
]);
|
|
159
|
+
const built = await Bun.build({
|
|
160
|
+
entrypoints: [entry],
|
|
161
|
+
target: 'browser',
|
|
162
|
+
format: 'esm',
|
|
163
|
+
minify: true,
|
|
164
|
+
conditions: ['browser', 'import'],
|
|
165
|
+
plugins: [
|
|
166
|
+
{
|
|
167
|
+
name: 'exact-externals',
|
|
168
|
+
setup(builder) {
|
|
169
|
+
builder.onResolve({ filter: /.*/ }, (args) => {
|
|
170
|
+
if (EXTERNAL_EXACT.has(args.path)) return { path: args.path, external: true };
|
|
171
|
+
return null;
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
if (!built.success) {
|
|
178
|
+
throw new Error(`web-renderer bundle failed: ${built.logs.map((l) => l.message).join('; ')}`);
|
|
179
|
+
}
|
|
180
|
+
const first = built.outputs[0];
|
|
181
|
+
if (!first) throw new Error('web-renderer bundle produced no outputs');
|
|
182
|
+
await Bun.write(cachePath, await first.text());
|
|
183
|
+
return cachePath;
|
|
184
|
+
})();
|
|
185
|
+
return webRendererLibReady;
|
|
186
|
+
}
|
|
187
|
+
|
|
71
188
|
/**
|
|
72
189
|
* Returns the path to an IIFE bundle for the given npm package, attaching its
|
|
73
190
|
* exports under `window[globalName]`. Caches under the OS temp dir so a long-
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Shared runtime + path resolution for the external playwright render shims
|
|
2
|
+
// (`bin/_{png,pdf,svg,html,pptx,video}-playwright.mjs`).
|
|
3
|
+
//
|
|
4
|
+
// RCA: issue-desktop-export-failures. The render exporters were written for the
|
|
5
|
+
// dev-server runtime and never migrated for the compiled-Bun sidecar packaging,
|
|
6
|
+
// so they broke in two ways in every `bun --compile` standalone binary (the
|
|
7
|
+
// desktop app AND `npm i -g @1agh/maude`):
|
|
8
|
+
//
|
|
9
|
+
// 1. Shim paths computed from `import.meta.dir` → inside a compiled binary that
|
|
10
|
+
// is the virtual `/$bunfs/root`, not a real disk path, so `Bun.spawn` /
|
|
11
|
+
// `Bun.build` fail with "failed to open root directory: /$bunfs/root". This
|
|
12
|
+
// is the DDR-045 bug: filesystem-relative paths MUST derive from
|
|
13
|
+
// `paths.ts`, never a local `import.meta.dir`.
|
|
14
|
+
// 2. Spawning a hardcoded `'node'` → the desktop sidecar is a compiled Bun
|
|
15
|
+
// binary with no bundled `node`; a Finder-launched `.app` can't rely on one
|
|
16
|
+
// being on PATH, so the spawn fails with `posix_spawn 'node'` ENOENT.
|
|
17
|
+
//
|
|
18
|
+
// This module centralizes both concerns so all six adapters resolve identically.
|
|
19
|
+
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { DEV_SERVER_ROOT } from '../paths.ts';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Real-disk dir holding the render shims (`<DEV_SERVER_ROOT>/bin`).
|
|
26
|
+
*
|
|
27
|
+
* Derived from `DEV_SERVER_ROOT` (paths.ts) — NEVER `import.meta.dir`, which is
|
|
28
|
+
* the virtual `/$bunfs/root` inside a compiled binary (DDR-045). In dev this is
|
|
29
|
+
* the same `apps/studio/bin` the old `path.join(import.meta.dir, '..', 'bin')`
|
|
30
|
+
* resolved to; in the desktop bundle it resolves to `Resources/apps/studio/bin`
|
|
31
|
+
* (the shims ARE staged there — stage-resources ships the whole source tree).
|
|
32
|
+
*/
|
|
33
|
+
export const EXPORT_SHIM_DIR: string = path.join(DEV_SERVER_ROOT, 'bin');
|
|
34
|
+
|
|
35
|
+
/** Absolute path to a render shim by basename (e.g. `_png-playwright.mjs`). */
|
|
36
|
+
export function exportShimPath(basename: string): string {
|
|
37
|
+
return path.join(EXPORT_SHIM_DIR, basename);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a JS runtime able to execute the external `.mjs` render shims.
|
|
42
|
+
*
|
|
43
|
+
* Ladder (mirrors `acp/probe.ts` resolveAgentRuntime): explicit override → a
|
|
44
|
+
* real `node` on PATH → a standalone `bun` on PATH. The compiled maude binary
|
|
45
|
+
* (`process.execPath`) is deliberately NOT a fallback — a `bun --compile`
|
|
46
|
+
* standalone ignores a script argument and re-runs its own embedded entrypoint,
|
|
47
|
+
* so it can't run a shim. When nothing resolves (a packaged desktop app whose
|
|
48
|
+
* sidecar has no node/bun on PATH) we throw an ACTIONABLE error instead of the
|
|
49
|
+
* cryptic `posix_spawn 'node'` ENOENT the hardcoded `'node'` produced.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveExportRuntime(): string {
|
|
52
|
+
// Look up against the LIVE process PATH (the desktop sidecar sets it via env at
|
|
53
|
+
// spawn — DDR-128), not Bun's startup env snapshot, so a corrected PATH is
|
|
54
|
+
// honored and the lookup is deterministic/testable.
|
|
55
|
+
const pathEnv = process.env.PATH;
|
|
56
|
+
const runtime =
|
|
57
|
+
process.env.MAUDE_EXPORT_RUNTIME ||
|
|
58
|
+
Bun.which('node', { PATH: pathEnv }) ||
|
|
59
|
+
Bun.which('bun', { PATH: pathEnv });
|
|
60
|
+
if (!runtime) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'render export needs a JS runtime (node or bun) on PATH, but this build has none. ' +
|
|
63
|
+
'Run the export from the browser dev-server (`maude design serve`), install Node, ' +
|
|
64
|
+
'or set MAUDE_EXPORT_RUNTIME to a node/bun binary. ' +
|
|
65
|
+
'(Native desktop render-export backend is pending — see RCA issue-desktop-export-failures.)'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return runtime;
|
|
69
|
+
}
|
|
@@ -11,7 +11,7 @@ import { tmpdir } from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
|
|
13
13
|
import JSZip from 'jszip';
|
|
14
|
-
|
|
14
|
+
import { exportShimPath, resolveExportRuntime } from './_runtime.ts';
|
|
15
15
|
import {
|
|
16
16
|
canvasShellUrl,
|
|
17
17
|
type ExportContext,
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
} from './index.ts';
|
|
21
21
|
import type { Target } from './scope.ts';
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// DDR-045: resolve via DEV_SERVER_ROOT, never `import.meta.dir`. See _runtime.ts.
|
|
24
|
+
const HTML_PLAYWRIGHT = exportShimPath('_html-playwright.mjs');
|
|
24
25
|
|
|
25
26
|
async function captureHtml(
|
|
26
27
|
target: Extract<Target, { kind: 'element' }>,
|
|
@@ -45,7 +46,7 @@ async function captureHtml(
|
|
|
45
46
|
if (target.widen) args.push('--widen-to-artboard', '1');
|
|
46
47
|
args.push('--out', path.join(outDir, `${target.canvasSlug}.html`));
|
|
47
48
|
}
|
|
48
|
-
const proc = Bun.spawn([
|
|
49
|
+
const proc = Bun.spawn([resolveExportRuntime(), ...args], {
|
|
49
50
|
cwd: path.dirname(HTML_PLAYWRIGHT),
|
|
50
51
|
stdout: 'pipe',
|
|
51
52
|
stderr: 'pipe',
|
|
@@ -15,11 +15,34 @@ import * as png from './png.ts';
|
|
|
15
15
|
import * as pptx from './pptx.ts';
|
|
16
16
|
import { type ResolveScopeArgs, resolveScope, type Scope, type Target } from './scope.ts';
|
|
17
17
|
import * as svg from './svg.ts';
|
|
18
|
+
// DDR-148 — temporal formats (capture-spine encode). Named exports mp4/webm/gif.
|
|
19
|
+
import { gif, mp4, webm } from './video.ts';
|
|
18
20
|
import * as zip from './zip.ts';
|
|
19
21
|
|
|
20
|
-
export type Format =
|
|
22
|
+
export type Format =
|
|
23
|
+
| 'png'
|
|
24
|
+
| 'pdf'
|
|
25
|
+
| 'svg'
|
|
26
|
+
| 'html'
|
|
27
|
+
| 'pptx'
|
|
28
|
+
| 'canva'
|
|
29
|
+
| 'zip'
|
|
30
|
+
| 'mp4'
|
|
31
|
+
| 'webm'
|
|
32
|
+
| 'gif';
|
|
21
33
|
|
|
22
|
-
export const FORMATS: readonly Format[] = [
|
|
34
|
+
export const FORMATS: readonly Format[] = [
|
|
35
|
+
'png',
|
|
36
|
+
'pdf',
|
|
37
|
+
'svg',
|
|
38
|
+
'html',
|
|
39
|
+
'pptx',
|
|
40
|
+
'canva',
|
|
41
|
+
'zip',
|
|
42
|
+
'mp4',
|
|
43
|
+
'webm',
|
|
44
|
+
'gif',
|
|
45
|
+
];
|
|
23
46
|
|
|
24
47
|
/** Options bag forwarded to the adapter. Format-specific keys are validated by each adapter. */
|
|
25
48
|
export type ExportOptions = Record<string, unknown>;
|
|
@@ -63,6 +86,9 @@ const REGISTRY: Record<Format, Adapter> = {
|
|
|
63
86
|
pptx,
|
|
64
87
|
canva,
|
|
65
88
|
zip,
|
|
89
|
+
mp4,
|
|
90
|
+
webm,
|
|
91
|
+
gif,
|
|
66
92
|
};
|
|
67
93
|
|
|
68
94
|
export function getAdapter(format: Format): Adapter | null {
|
|
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
|
|
15
15
|
import { PDFDocument } from 'pdf-lib';
|
|
16
|
-
|
|
16
|
+
import { exportShimPath, resolveExportRuntime } from './_runtime.ts';
|
|
17
17
|
import {
|
|
18
18
|
canvasShellUrl,
|
|
19
19
|
type ExportContext,
|
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
} from './index.ts';
|
|
23
23
|
import type { Target } from './scope.ts';
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// DDR-045: resolve via DEV_SERVER_ROOT, never `import.meta.dir`. See _runtime.ts.
|
|
26
|
+
const PDF_PLAYWRIGHT = exportShimPath('_pdf-playwright.mjs');
|
|
26
27
|
|
|
27
28
|
async function capturePdf(
|
|
28
29
|
target: Extract<Target, { kind: 'element' }>,
|
|
@@ -48,7 +49,7 @@ async function capturePdf(
|
|
|
48
49
|
args.push('--out', path.join(outDir, `${target.canvasSlug}.pdf`));
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const proc = Bun.spawn([
|
|
52
|
+
const proc = Bun.spawn([resolveExportRuntime(), ...args], {
|
|
52
53
|
cwd: path.dirname(PDF_PLAYWRIGHT),
|
|
53
54
|
stdout: 'pipe',
|
|
54
55
|
stderr: 'pipe',
|
|
@@ -12,7 +12,7 @@ import { tmpdir } from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
14
|
import JSZip from 'jszip';
|
|
15
|
-
|
|
15
|
+
import { exportShimPath, resolveExportRuntime } from './_runtime.ts';
|
|
16
16
|
import {
|
|
17
17
|
canvasShellUrl,
|
|
18
18
|
type ExportContext,
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
} from './index.ts';
|
|
22
22
|
import type { Target } from './scope.ts';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// DDR-045: resolve via DEV_SERVER_ROOT, never `import.meta.dir` (→ /$bunfs/root
|
|
25
|
+
// in a compiled binary). See exporters/_runtime.ts.
|
|
26
|
+
const PNG_PLAYWRIGHT = exportShimPath('_png-playwright.mjs');
|
|
25
27
|
|
|
26
28
|
interface CaptureOptions {
|
|
27
29
|
scale?: 1 | 2 | 3;
|
|
@@ -66,7 +68,7 @@ async function captureElement(
|
|
|
66
68
|
if (target.widen) args.push('--widen-to-artboard', '1');
|
|
67
69
|
args.push('--out', path.join(outDir, `${target.canvasSlug}.png`));
|
|
68
70
|
}
|
|
69
|
-
const proc = Bun.spawn([
|
|
71
|
+
const proc = Bun.spawn([resolveExportRuntime(), ...args], {
|
|
70
72
|
cwd: path.dirname(PNG_PLAYWRIGHT),
|
|
71
73
|
stdout: 'pipe',
|
|
72
74
|
stderr: 'pipe',
|
|
@@ -27,6 +27,7 @@ import path from 'node:path';
|
|
|
27
27
|
import PptxGenJS from 'pptxgenjs';
|
|
28
28
|
|
|
29
29
|
import { getBrowserBundle } from './_browser-bundles.ts';
|
|
30
|
+
import { exportShimPath, resolveExportRuntime } from './_runtime.ts';
|
|
30
31
|
import {
|
|
31
32
|
canvasShellUrl,
|
|
32
33
|
type ExportContext,
|
|
@@ -35,8 +36,9 @@ import {
|
|
|
35
36
|
} from './index.ts';
|
|
36
37
|
import type { Target } from './scope.ts';
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
const
|
|
39
|
+
// DDR-045: resolve via DEV_SERVER_ROOT, never `import.meta.dir`. See _runtime.ts.
|
|
40
|
+
const SVG_PLAYWRIGHT = exportShimPath('_svg-playwright.mjs');
|
|
41
|
+
const PNG_PLAYWRIGHT = exportShimPath('_png-playwright.mjs');
|
|
40
42
|
const PPTX_MIME = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
41
43
|
const SLIDE_REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide';
|
|
42
44
|
const SLIDE_CT = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml';
|
|
@@ -49,9 +51,9 @@ function isElementTarget(t: Target): t is ElementTarget {
|
|
|
49
51
|
return t.kind === 'element';
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** Spawn a playwright shim via
|
|
54
|
+
/** Spawn a playwright shim via a resolved runtime, returning the written file paths (stdout). */
|
|
53
55
|
async function spawnShim(args: string[]): Promise<string[]> {
|
|
54
|
-
const proc = Bun.spawn([
|
|
56
|
+
const proc = Bun.spawn([resolveExportRuntime(), ...args], {
|
|
55
57
|
cwd: path.dirname(args[0]),
|
|
56
58
|
stdout: 'pipe',
|
|
57
59
|
stderr: 'pipe',
|
|
@@ -13,6 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import JSZip from 'jszip';
|
|
14
14
|
|
|
15
15
|
import { getBrowserBundle } from './_browser-bundles.ts';
|
|
16
|
+
import { exportShimPath, resolveExportRuntime } from './_runtime.ts';
|
|
16
17
|
import {
|
|
17
18
|
canvasShellUrl,
|
|
18
19
|
type ExportContext,
|
|
@@ -21,7 +22,8 @@ import {
|
|
|
21
22
|
} from './index.ts';
|
|
22
23
|
import type { Target } from './scope.ts';
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
// DDR-045: resolve via DEV_SERVER_ROOT, never `import.meta.dir`. See _runtime.ts.
|
|
26
|
+
const SVG_PLAYWRIGHT = exportShimPath('_svg-playwright.mjs');
|
|
25
27
|
|
|
26
28
|
async function captureSvg(
|
|
27
29
|
target: Extract<Target, { kind: 'element' }>,
|
|
@@ -50,10 +52,12 @@ async function captureSvg(
|
|
|
50
52
|
args.push('--out', path.join(outDir, `${target.canvasSlug}.svg`));
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
// Run via
|
|
54
|
-
// dev-server/node_modules (playwright is a devDep). `npm exec`
|
|
55
|
-
// bridge the module path for ESM imports — confirmed against npm 10.x.
|
|
56
|
-
|
|
55
|
+
// Run via a resolved node/bun runtime so the shim's `import 'playwright'`
|
|
56
|
+
// resolves against dev-server/node_modules (playwright is a devDep). `npm exec`
|
|
57
|
+
// doesn't bridge the module path for ESM imports — confirmed against npm 10.x.
|
|
58
|
+
// resolveExportRuntime() replaces a hardcoded `'node'` so a compiled binary
|
|
59
|
+
// without `node` on PATH surfaces an actionable error, not `posix_spawn 'node'`.
|
|
60
|
+
const proc = Bun.spawn([resolveExportRuntime(), ...args], {
|
|
57
61
|
cwd: path.dirname(SVG_PLAYWRIGHT),
|
|
58
62
|
stdout: 'pipe',
|
|
59
63
|
stderr: 'pipe',
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// video-encode-lib.ts — DDR-148 in-page MP4/WebM/GIF encoder.
|
|
2
|
+
//
|
|
3
|
+
// Runs INSIDE the capture Chromium (bundled to a self-contained module by
|
|
4
|
+
// _browser-bundles.getEncodeLibBundle, injected via page.addScriptTag). WebCodecs
|
|
5
|
+
// is guaranteed there — the user's shell browser / WKWebView never encodes.
|
|
6
|
+
//
|
|
7
|
+
// The Node shim (bin/_video-playwright.mjs) screenshots each frame, hands the
|
|
8
|
+
// PNG in as base64, and this lib decodes → draws to an offscreen canvas →
|
|
9
|
+
// encodes: mediabunny (Output + CanvasSource, H.264/avc in MP4, VP9/VP8 in WebM
|
|
10
|
+
// as the fallback), or gifenc (quantize → applyPalette). Video codec is chosen
|
|
11
|
+
// by mediabunny's own capability probe, so a browser without an H.264 encoder
|
|
12
|
+
// degrades to WebM instead of failing. Audio mixing is added in a follow-up
|
|
13
|
+
// slice (the comp's <Audio>/<Video> sources via OfflineAudioContext).
|
|
14
|
+
//
|
|
15
|
+
// The lib assigns a single `window.__maudeEnc` surface (no exports — it's a
|
|
16
|
+
// side-effecting module script).
|
|
17
|
+
|
|
18
|
+
import { applyPalette, GIFEncoder, quantize } from 'gifenc';
|
|
19
|
+
import {
|
|
20
|
+
BufferTarget,
|
|
21
|
+
CanvasSource,
|
|
22
|
+
getFirstEncodableVideoCodec,
|
|
23
|
+
Mp4OutputFormat,
|
|
24
|
+
Output,
|
|
25
|
+
QUALITY_HIGH,
|
|
26
|
+
type VideoCodec,
|
|
27
|
+
WebMOutputFormat,
|
|
28
|
+
} from 'mediabunny';
|
|
29
|
+
|
|
30
|
+
interface StartVideoOpts {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
fps: number;
|
|
34
|
+
format?: 'mp4' | 'webm';
|
|
35
|
+
}
|
|
36
|
+
interface StartGifOpts {
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
fps: number;
|
|
40
|
+
maxColors?: number;
|
|
41
|
+
}
|
|
42
|
+
interface EncodeResult {
|
|
43
|
+
b64: string;
|
|
44
|
+
bytes: number;
|
|
45
|
+
container: string;
|
|
46
|
+
codec: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface MaudeEnc {
|
|
50
|
+
startVideo(opts: StartVideoOpts): Promise<{ container: string; codec: string }>;
|
|
51
|
+
addVideoFrame(b64png: string): Promise<void>;
|
|
52
|
+
finishVideo(): Promise<EncodeResult>;
|
|
53
|
+
startGif(opts: StartGifOpts): void;
|
|
54
|
+
addGifFrame(b64png: string): Promise<void>;
|
|
55
|
+
finishGif(): EncodeResult;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function b64ToBytes(b64: string): Uint8Array {
|
|
59
|
+
const bin = atob(b64);
|
|
60
|
+
const out = new Uint8Array(bin.length);
|
|
61
|
+
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function bytesToB64(buf: ArrayBuffer | Uint8Array): string {
|
|
66
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
67
|
+
let bin = '';
|
|
68
|
+
const CHUNK = 0x8000;
|
|
69
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
70
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
71
|
+
}
|
|
72
|
+
return btoa(bin);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function decodeToBitmap(b64: string): Promise<ImageBitmap> {
|
|
76
|
+
return createImageBitmap(new Blob([b64ToBytes(b64)], { type: 'image/png' }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let vstate: {
|
|
80
|
+
output: Output;
|
|
81
|
+
source: CanvasSource;
|
|
82
|
+
canvas: OffscreenCanvas;
|
|
83
|
+
ctx: OffscreenCanvasRenderingContext2D;
|
|
84
|
+
fps: number;
|
|
85
|
+
frame: number;
|
|
86
|
+
container: string;
|
|
87
|
+
codec: string;
|
|
88
|
+
} | null = null;
|
|
89
|
+
|
|
90
|
+
let gstate: {
|
|
91
|
+
enc: ReturnType<typeof GIFEncoder>;
|
|
92
|
+
canvas: OffscreenCanvas;
|
|
93
|
+
ctx: OffscreenCanvasRenderingContext2D;
|
|
94
|
+
delay: number;
|
|
95
|
+
maxColors: number;
|
|
96
|
+
} | null = null;
|
|
97
|
+
|
|
98
|
+
const MP4_CODECS: VideoCodec[] = ['avc', 'hevc'];
|
|
99
|
+
const WEBM_CODECS: VideoCodec[] = ['vp9', 'vp8', 'av1'];
|
|
100
|
+
|
|
101
|
+
const api: MaudeEnc = {
|
|
102
|
+
async startVideo({ width, height, fps, format }) {
|
|
103
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
104
|
+
const ctx = canvas.getContext('2d');
|
|
105
|
+
if (!ctx) throw new Error('no 2d context for encode canvas');
|
|
106
|
+
|
|
107
|
+
// Prefer the requested container's codecs; fall back to WebM/VP9 so a
|
|
108
|
+
// browser without an H.264 encoder still produces a file.
|
|
109
|
+
const wantMp4 = format !== 'webm';
|
|
110
|
+
let outFormat: Mp4OutputFormat | WebMOutputFormat = wantMp4
|
|
111
|
+
? new Mp4OutputFormat()
|
|
112
|
+
: new WebMOutputFormat();
|
|
113
|
+
let codec = await getFirstEncodableVideoCodec(wantMp4 ? MP4_CODECS : WEBM_CODECS, {
|
|
114
|
+
width,
|
|
115
|
+
height,
|
|
116
|
+
});
|
|
117
|
+
if (!codec && wantMp4) {
|
|
118
|
+
outFormat = new WebMOutputFormat();
|
|
119
|
+
codec = await getFirstEncodableVideoCodec(WEBM_CODECS, { width, height });
|
|
120
|
+
}
|
|
121
|
+
if (!codec) throw new Error('no encodable video codec available in this browser');
|
|
122
|
+
|
|
123
|
+
const output = new Output({ format: outFormat, target: new BufferTarget() });
|
|
124
|
+
const source = new CanvasSource(canvas, { codec, bitrate: QUALITY_HIGH });
|
|
125
|
+
output.addVideoTrack(source, { frameRate: fps });
|
|
126
|
+
await output.start();
|
|
127
|
+
const container = outFormat instanceof Mp4OutputFormat ? 'mp4' : 'webm';
|
|
128
|
+
vstate = { output, source, canvas, ctx, fps, frame: 0, container, codec };
|
|
129
|
+
return { container, codec };
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
async addVideoFrame(b64png) {
|
|
133
|
+
if (!vstate) throw new Error('startVideo not called');
|
|
134
|
+
const bmp = await decodeToBitmap(b64png);
|
|
135
|
+
vstate.ctx.drawImage(bmp, 0, 0, vstate.canvas.width, vstate.canvas.height);
|
|
136
|
+
if ('close' in bmp) bmp.close();
|
|
137
|
+
await vstate.source.add(vstate.frame / vstate.fps, 1 / vstate.fps);
|
|
138
|
+
vstate.frame += 1;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async finishVideo() {
|
|
142
|
+
if (!vstate) throw new Error('startVideo not called');
|
|
143
|
+
await vstate.output.finalize();
|
|
144
|
+
const buf = vstate.output.target.buffer;
|
|
145
|
+
if (!buf) throw new Error('encode produced no buffer');
|
|
146
|
+
const res: EncodeResult = {
|
|
147
|
+
b64: bytesToB64(buf),
|
|
148
|
+
bytes: buf.byteLength,
|
|
149
|
+
container: vstate.container,
|
|
150
|
+
codec: vstate.codec,
|
|
151
|
+
};
|
|
152
|
+
vstate = null;
|
|
153
|
+
return res;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
startGif({ width, height, fps, maxColors }) {
|
|
157
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
158
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
159
|
+
if (!ctx) throw new Error('no 2d context for gif canvas');
|
|
160
|
+
gstate = {
|
|
161
|
+
enc: GIFEncoder(),
|
|
162
|
+
canvas,
|
|
163
|
+
ctx,
|
|
164
|
+
delay: Math.max(2, Math.round(100 / fps)) * 10, // gifenc delay is in ms
|
|
165
|
+
maxColors: Math.max(2, Math.min(256, maxColors ?? 256)),
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
async addGifFrame(b64png) {
|
|
170
|
+
if (!gstate) throw new Error('startGif not called');
|
|
171
|
+
const bmp = await decodeToBitmap(b64png);
|
|
172
|
+
gstate.ctx.drawImage(bmp, 0, 0, gstate.canvas.width, gstate.canvas.height);
|
|
173
|
+
if ('close' in bmp) bmp.close();
|
|
174
|
+
const { data, width, height } = gstate.ctx.getImageData(
|
|
175
|
+
0,
|
|
176
|
+
0,
|
|
177
|
+
gstate.canvas.width,
|
|
178
|
+
gstate.canvas.height
|
|
179
|
+
);
|
|
180
|
+
const palette = quantize(data, gstate.maxColors);
|
|
181
|
+
const index = applyPalette(data, palette);
|
|
182
|
+
gstate.enc.writeFrame(index, width, height, { palette, delay: gstate.delay });
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
finishGif() {
|
|
186
|
+
if (!gstate) throw new Error('startGif not called');
|
|
187
|
+
gstate.enc.finish();
|
|
188
|
+
const bytes = gstate.enc.bytes();
|
|
189
|
+
const res: EncodeResult = {
|
|
190
|
+
b64: bytesToB64(bytes),
|
|
191
|
+
bytes: bytes.length,
|
|
192
|
+
container: 'gif',
|
|
193
|
+
codec: 'gif',
|
|
194
|
+
};
|
|
195
|
+
gstate = null;
|
|
196
|
+
return res;
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
(globalThis as unknown as { __maudeEnc: MaudeEnc }).__maudeEnc = api;
|