frameyard 0.3.0 → 0.5.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/CHANGELOG.md +117 -0
- package/GLOSSARY.md +46 -2
- package/README.md +174 -106
- package/dist/bindings.d.ts +42 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +144 -0
- package/dist/bindings.js.map +1 -0
- package/dist/cli.js +32 -21
- package/dist/cli.js.map +1 -1
- package/dist/composition/adapter.d.ts +30 -0
- package/dist/composition/adapter.d.ts.map +1 -0
- package/dist/composition/adapter.js +2 -0
- package/dist/composition/adapter.js.map +1 -0
- package/dist/composition/animation.d.ts +19 -0
- package/dist/composition/animation.d.ts.map +1 -0
- package/dist/composition/animation.js +55 -0
- package/dist/composition/animation.js.map +1 -0
- package/dist/composition/attributes.d.ts +7 -0
- package/dist/composition/attributes.d.ts.map +1 -0
- package/dist/composition/attributes.js +24 -0
- package/dist/composition/attributes.js.map +1 -0
- package/dist/composition/descriptor.d.ts +30 -0
- package/dist/composition/descriptor.d.ts.map +1 -0
- package/dist/composition/descriptor.js +188 -0
- package/dist/composition/descriptor.js.map +1 -0
- package/dist/composition/diagnostics.d.ts +12 -0
- package/dist/composition/diagnostics.d.ts.map +1 -0
- package/dist/composition/diagnostics.js +38 -0
- package/dist/composition/diagnostics.js.map +1 -0
- package/dist/composition/index.d.ts +12 -0
- package/dist/composition/index.d.ts.map +1 -0
- package/dist/composition/index.js +7 -0
- package/dist/composition/index.js.map +1 -0
- package/dist/composition/media.d.ts +27 -0
- package/dist/composition/media.d.ts.map +1 -0
- package/dist/composition/media.js +162 -0
- package/dist/composition/media.js.map +1 -0
- package/dist/composition/paths.d.ts +5 -0
- package/dist/composition/paths.d.ts.map +1 -0
- package/dist/composition/paths.js +16 -0
- package/dist/composition/paths.js.map +1 -0
- package/dist/composition/transcripts.d.ts +19 -0
- package/dist/composition/transcripts.d.ts.map +1 -0
- package/dist/composition/transcripts.js +57 -0
- package/dist/composition/transcripts.js.map +1 -0
- package/dist/composition/walk.d.ts +10 -0
- package/dist/composition/walk.d.ts.map +1 -0
- package/dist/composition/walk.js +32 -0
- package/dist/composition/walk.js.map +1 -0
- package/dist/contracts.d.ts +75 -47
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +14 -159
- package/dist/contracts.js.map +1 -1
- package/dist/errors.d.ts +15 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/html/document-adapter.d.ts +5 -0
- package/dist/html/document-adapter.d.ts.map +1 -0
- package/dist/html/document-adapter.js +17 -0
- package/dist/html/document-adapter.js.map +1 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -0
- package/dist/html/index.js +3 -0
- package/dist/html/index.js.map +1 -0
- package/dist/html/parse.d.ts +26 -0
- package/dist/html/parse.d.ts.map +1 -0
- package/dist/html/parse.js +80 -0
- package/dist/html/parse.js.map +1 -0
- package/dist/html/validate.d.ts +14 -0
- package/dist/html/validate.d.ts.map +1 -0
- package/dist/html/validate.js +648 -0
- package/dist/html/validate.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/preview/main.d.ts +10 -3
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +189 -89
- package/dist/preview/main.js.map +1 -1
- package/dist/project.d.ts +2 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +4 -0
- package/dist/project.js.map +1 -1
- package/dist/renderer/browser.d.ts +11 -1
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +93 -14
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/index.d.ts +25 -5
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +82 -16
- package/dist/renderer/index.js.map +1 -1
- package/dist/runtime/bindings.d.ts +11 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +164 -0
- package/dist/runtime/bindings.js.map +1 -0
- package/dist/runtime/diagnostics.d.ts +4 -0
- package/dist/runtime/diagnostics.d.ts.map +1 -0
- package/dist/runtime/diagnostics.js +107 -0
- package/dist/runtime/diagnostics.js.map +1 -0
- package/dist/runtime/document-adapter.d.ts +4 -0
- package/dist/runtime/document-adapter.d.ts.map +1 -0
- package/dist/runtime/document-adapter.js +19 -0
- package/dist/runtime/document-adapter.js.map +1 -0
- package/dist/runtime/host.d.ts +14 -0
- package/dist/runtime/host.d.ts.map +1 -0
- package/dist/runtime/host.js +596 -0
- package/dist/runtime/host.js.map +1 -0
- package/dist/runtime/index.d.ts +0 -29
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +0 -360
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/media.d.ts.map +1 -1
- package/dist/runtime/media.js +20 -51
- package/dist/runtime/media.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +177 -84
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +20 -0
- package/dist/server/composition-html.d.ts.map +1 -0
- package/dist/server/composition-html.js +45 -0
- package/dist/server/composition-html.js.map +1 -0
- package/dist/server/preview-html.d.ts.map +1 -1
- package/dist/server/preview-html.js +19 -14
- package/dist/server/preview-html.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +94 -6
- package/dist/server/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
- package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +7 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
- package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
- package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
- package/docs/migration-0.4.md +279 -0
- package/docs/v0.1-design.md +78 -56
- package/examples/demo/README.md +8 -4
- package/examples/demo/compositions/demo.controller.js +120 -0
- package/examples/demo/compositions/demo.html +253 -0
- package/examples/demo/video.config.js +1 -1
- package/examples/video/README.md +61 -0
- package/examples/video/assets/clip.mp4 +0 -0
- package/examples/video/assets/clip.webm +0 -0
- package/examples/video/compositions/video-mp4.html +362 -0
- package/examples/video/compositions/video.html +362 -0
- package/examples/video/props/video.json +8 -0
- package/examples/video/scripts/make-clip.sh +109 -0
- package/examples/video/video.config.js +6 -0
- package/examples/webgl/README.md +30 -0
- package/examples/webgl/compositions/webgl.controller.js +200 -0
- package/examples/webgl/compositions/webgl.html +215 -0
- package/examples/webgl/props/webgl.json +6 -0
- package/examples/webgl/video.config.js +5 -0
- package/package.json +7 -3
- package/skills/frameyard/README.md +4 -0
- package/skills/frameyard/SKILL.md +40 -12
- package/skills/frameyard/references/audio-assets.md +25 -14
- package/skills/frameyard/references/bindings.md +73 -0
- package/skills/frameyard/references/common-mistakes.md +59 -0
- package/skills/frameyard/references/controllers.md +126 -0
- package/skills/frameyard/references/deterministic-authoring.md +143 -52
- package/skills/frameyard/references/diagnostics.md +90 -0
- package/skills/frameyard/references/failure-recovery.md +37 -14
- package/skills/frameyard/references/json-results.md +43 -9
- package/skills/frameyard/references/subtitles-karaoke.md +10 -18
- package/examples/demo/compositions/demo.js +0 -206
package/dist/scaffold.js
CHANGED
|
@@ -5,22 +5,101 @@ This project produces deterministic, parameterized video with Frameyard. Follow
|
|
|
5
5
|
|
|
6
6
|
## Project layout
|
|
7
7
|
|
|
8
|
-
- \`video.config.js\` explicitly maps composition IDs to
|
|
9
|
-
- \`
|
|
10
|
-
- \`
|
|
11
|
-
- \`
|
|
8
|
+
- \`video.config.js\` explicitly maps composition IDs to \`.html\` composition documents. There is no directory scanning; a new composition is invisible until it is listed.
|
|
9
|
+
- \`compositions/\` contains HTML/CSS compositions. An optional controller lives beside the document as a \`.controller.js\` module; import \`frameyard/runtime\` for \`random("stable:key")\` and other browser APIs.
|
|
10
|
+
- \`props/\` contains JSON props, merged shallowly over the composition's \`data-fy="props"\` defaults and deep-frozen.
|
|
11
|
+
- \`out/\` contains rendered MP4s and is gitignored. \`render\` writes \`out/<composition-id>.mp4\` by default.
|
|
12
12
|
- \`assets/\` (create it when needed) contains audio, images, fonts, and generated assets.
|
|
13
|
+
- \`package.json\` pins the CLI version in \`dev\`, \`check\`, \`render\`, and \`still\` scripts. Run \`dev\` with \`npm run dev\` or \`pnpm dev\`; for the others, pass the composition id: \`npm run check -- hello\` (npm) or \`pnpm check hello\` (pnpm).
|
|
14
|
+
|
|
15
|
+
\`width\`, \`height\`, \`fps\`, and \`duration-in-frames\` are fixed on \`<fy-composition>\`. Do not derive them from props.
|
|
16
|
+
|
|
17
|
+
## Composition skeleton
|
|
18
|
+
|
|
19
|
+
\`\`\`html
|
|
20
|
+
<!doctype html>
|
|
21
|
+
<html>
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="utf-8">
|
|
24
|
+
<script type="application/json" data-fy="props">{ "title": "Hello" }</script>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<fy-composition width="1280" height="720" fps="30" duration-in-frames="150">
|
|
28
|
+
<h1 data-fy-text="title"></h1>
|
|
29
|
+
</fy-composition>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
## Bindings
|
|
35
|
+
|
|
36
|
+
- \`data-fy-text="path"\` sets text content.
|
|
37
|
+
- \`data-fy-attr-src="path"\` (and other \`data-fy-attr-*\` names) sets an attribute.
|
|
38
|
+
- \`data-fy-class-active="path"\` toggles a class.
|
|
39
|
+
- \`data-fy-if="path"\` removes the element when the value is false.
|
|
40
|
+
- \`<template data-fy-repeat="items">\` clones its content for each item; use \`item\` and \`index\`, and style stagger with \`--fy-index\`.
|
|
41
|
+
- Paths are dot-separated (\`user.name\`, \`items.0.label\`). Missing paths warn and resolve to an empty value.
|
|
42
|
+
- Primitive top-level props are also available as \`--fy-prop-<name>\` CSS custom properties.
|
|
43
|
+
|
|
44
|
+
## Animation
|
|
45
|
+
|
|
46
|
+
- CSS \`@keyframes\` and Web Animations API animations are paused and seeked from the frame clock.
|
|
47
|
+
- \`data-fy-anim-start\`, \`data-fy-anim-duration\`, and \`data-fy-anim-easing\` schedule one animation per element in composition frames. The CSS \`animation\` shorthand supplies keyframes and fill mode.
|
|
48
|
+
- Frame-state variables are set on \`:root\`: \`--fy-frame\`, \`--fy-time\`, \`--fy-progress\`, \`--fy-fps\`, \`--fy-duration\`, and \`--fy-duration-frames\`.
|
|
49
|
+
- Use \`--fy-progress\` for progress bars and \`--fy-frame\`/\`--fy-time\` in \`calc()\` for simple computed motion.
|
|
50
|
+
- CSS transitions are not deterministic; do not use them.
|
|
51
|
+
|
|
52
|
+
## Controller
|
|
13
53
|
|
|
14
|
-
|
|
54
|
+
Exactly one optional controller is allowed, inline or external:
|
|
55
|
+
|
|
56
|
+
\`\`\`html
|
|
57
|
+
<script type="text/fy-controller">
|
|
58
|
+
export default {
|
|
59
|
+
mount({ root, props }) {},
|
|
60
|
+
update({ frame, props, root }) {},
|
|
61
|
+
};
|
|
62
|
+
</script>
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
\`\`\`html
|
|
66
|
+
<script type="text/fy-controller" src="./hello.controller.js"></script>
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
- \`mount\` may be asynchronous; \`update\` and \`validateProps\` are synchronous and seek-safe; \`dispose\` releases resources.
|
|
70
|
+
- Use inline for small, single-use controllers; extract to an external module for Canvas 2D, WebGL, karaoke, reuse, tests, or a large controller.
|
|
71
|
+
- A controller may be validation-only and export just \`validateProps(props)\`.
|
|
72
|
+
- The controller is the only executable code. Other scripts and \`on*\` handlers are rejected.
|
|
73
|
+
- Controller-created DOM is not re-bound; declarative bindings apply once before \`mount\`.
|
|
74
|
+
|
|
75
|
+
## Determinism rules
|
|
76
|
+
|
|
77
|
+
- A frame may depend only on its integer \`frame\`, immutable \`props\`, and assets loaded before frame production.
|
|
78
|
+
- Never use \`Math.random()\`, \`Date.now()\`, \`performance.now()\`, timers, independent \`requestAnimationFrame()\` loops, CSS transitions, or state left from a previously visited frame as animation inputs.
|
|
79
|
+
- Use \`random("stable:key")\` from \`frameyard/runtime\` for randomness.
|
|
80
|
+
- Load remote data, fonts, and images before frame production. Network requests are blocked during capture; a \`fetch()\` from \`update()\` is a bug.
|
|
81
|
+
- Redraw Canvas 2D and WebGL from the current frame. Production pins software WebGL for reproducible pixels.
|
|
82
|
+
- Embedded video and audio use local project files and \`data-fy-start\` (frames), \`data-fy-trim\` (seconds), optional \`data-fy-duration\` (seconds), and \`data-fy-volume\` (0-1). Let the runtime seek video; do not call \`play()\`, \`pause()\`, or set \`currentTime\` in \`update()\`.
|
|
83
|
+
- Media and transcript \`src\` values resolve from the project root. The controller \`src\` and ordinary HTML asset URLs resolve relative to the composition document.
|
|
84
|
+
|
|
85
|
+
## Audio and transcripts
|
|
86
|
+
|
|
87
|
+
- Audio clips are \`<audio>\` elements placed by composition frames and trimmed by seconds. Frameyard does not time-stretch audio or change composition duration to fit it.
|
|
88
|
+
- An unmuted embedded video contributes its audio track to the same mix; use \`muted\` or \`data-fy-audio="off"\` for a visual-only video.
|
|
89
|
+
- Declare required transcripts with \`<fy-transcript src="assets/voice.transcript.json" words></fy-transcript>\`.
|
|
90
|
+
- Produce speech with \`npx frameyard tts\` and transcripts with \`npx frameyard transcribe\`; never call \`qwen-tts\` or \`whisper-cli\` directly, and never run inference inside a controller.
|
|
91
|
+
- Do not overwrite or regenerate generated assets without explicit approval.
|
|
92
|
+
- \`words\` requires measured word timings; never interpolate segment timings.
|
|
93
|
+
- Commit \`frameyard.assets.json\` when present; do not edit hashes or manifest entries by hand.
|
|
15
94
|
|
|
16
95
|
## Agent workflow
|
|
17
96
|
|
|
18
|
-
1. Read \`video.config.js\`, the composition
|
|
19
|
-
2. Author the composition
|
|
20
|
-
3. Validate every frame: \`npx frameyard check <id> --json\`. Use \`--props <file>\` to validate a specific props file.
|
|
97
|
+
1. Read \`video.config.js\`, the composition HTML, the optional controller, and the props file before changing behavior.
|
|
98
|
+
2. Author the composition with native HTML, CSS, SVG, Canvas 2D, WebGL, embedded video, and an optional controller. Do not add a bundling step.
|
|
99
|
+
3. Validate every frame: \`npx frameyard check <id> --json\`. Static validation runs before Chromium and reports stable diagnostic codes with source locations. Use \`--props <file>\` to validate a specific props file.
|
|
21
100
|
4. Inspect a representative frame: \`npx frameyard still <id> --frame <n> --out <file.png> --json\`, then view the PNG when the host supports images. State explicitly when visual inspection is not possible.
|
|
22
|
-
5. Render: \`npx frameyard render <id> --props <file>
|
|
23
|
-
6. Report the composition, props, validation result, still path and inspection status, video path, and any failure. Never claim success when \`check\` fails, a still is missing, or a render exits nonzero.
|
|
101
|
+
5. Render: \`npx frameyard render <id> --props <file> [--concurrency <n>] --json\`. The video is written to \`out/<id>.mp4\` unless \`--out\` is passed.
|
|
102
|
+
6. Report the composition, controller, props, validation result, diagnostics, still path and inspection status, video path, and any failure. Never claim success when \`check\` fails, a still is missing, or a render exits nonzero.
|
|
24
103
|
|
|
25
104
|
## Commands
|
|
26
105
|
|
|
@@ -29,35 +108,15 @@ npx frameyard dev
|
|
|
29
108
|
npx frameyard doctor --json
|
|
30
109
|
npx frameyard check hello --json
|
|
31
110
|
npx frameyard still hello --frame 42 --out hello-42.png --json
|
|
32
|
-
npx frameyard render hello --props props/hello.json --
|
|
111
|
+
npx frameyard render hello --props props/hello.json --json
|
|
33
112
|
\`\`\`
|
|
34
113
|
|
|
35
114
|
- Use \`--json\` for non-interactive commands. Parse stdout as the structured result; treat stderr as progress and diagnostics.
|
|
36
115
|
- Use \`--project <dir>\` when running outside the project root.
|
|
116
|
+
- \`render\` writes to \`out/<id>.mp4\` in the project unless \`--out\` is passed. \`out/\` is gitignored.
|
|
37
117
|
- Use \`--concurrency <n>\` for faster local rendering when useful; the default is \`1\` and the maximum is \`16\`. It changes capture scheduling only, not composition behavior or output frame order.
|
|
38
118
|
- \`still\` and \`render\` replace existing outputs only with \`--overwrite\`. Generated assets are replaced only with \`--force\`, and only with user approval.
|
|
39
119
|
|
|
40
|
-
## Determinism rules
|
|
41
|
-
|
|
42
|
-
- A frame may depend only on its integer \`frame\`, immutable \`props\`, and assets loaded before frame production.
|
|
43
|
-
- \`mount()\` may await loading work. \`update({ frame, props })\` is synchronous and seek-safe. \`dispose()\` releases resources.
|
|
44
|
-
- Keep mutable state in module-scope variables, not on \`this\`.
|
|
45
|
-
- Use \`random("stable:key")\` from \`frameyard/runtime\` for randomness.
|
|
46
|
-
- Never use \`Math.random()\`, \`Date.now()\`, \`performance.now()\`, timers, independent \`requestAnimationFrame()\` loops, CSS transitions, or state left from a previously visited frame as animation inputs.
|
|
47
|
-
- CSS \`@keyframes\` and the Web Animations API are valid; the runtime seeks them.
|
|
48
|
-
- Load remote data, fonts, and images during \`mount()\` or before the first frame. Network requests are blocked during capture; a \`fetch()\` from \`update()\` is a bug.
|
|
49
|
-
- Redraw Canvas 2D and WebGL from the current frame. Production pins software WebGL for reproducible pixels.
|
|
50
|
-
- Embedded video must use a local project file in the composition container's light DOM and be created, with its \`data-frameyard-*\` attributes, in \`mount()\`. Let the runtime seek it; do not call \`play()\`, \`pause()\`, or set \`currentTime\` in \`update()\`. Use \`data-frameyard-start\` (frames), \`data-frameyard-trim\` (seconds), optional \`data-frameyard-duration\` (seconds), and optional \`data-frameyard-volume\` (0-1).
|
|
51
|
-
|
|
52
|
-
## Audio and transcripts
|
|
53
|
-
|
|
54
|
-
- Audio clips are placed by composition frames and trimmed by seconds. Frameyard does not time-stretch audio or change composition duration to fit it.
|
|
55
|
-
- An unmuted embedded video contributes its audio track to the same mix; use \`muted\` or \`data-frameyard-audio="off"\` for a visual-only video.
|
|
56
|
-
- Produce speech with \`npx frameyard tts\` and transcripts with \`npx frameyard transcribe\`; never call \`qwen-tts\` or \`whisper-cli\` directly, and never run inference inside \`mount()\` or \`update()\`.
|
|
57
|
-
- Do not overwrite or regenerate generated assets without explicit approval.
|
|
58
|
-
- \`words: true\` requires measured word timings; never interpolate segment timings.
|
|
59
|
-
- Commit \`frameyard.assets.json\` when present; do not edit hashes or manifest entries by hand.
|
|
60
|
-
|
|
61
120
|
The official \`frameyard\` agent skill contains the full host-neutral contract. Install it with \`npx skills add vladiantio/frameyard\` or see the \`skills/frameyard/\` directory in the \`frameyard\` npm package. This file is the project-local summary.
|
|
62
121
|
|
|
63
122
|
This project was scaffolded with frameyard ${VERSION}. \`npx frameyard\` resolves the latest published version; run \`npx frameyard@${VERSION}\` when you need the CLI version that scaffolded the project.
|
|
@@ -66,6 +125,8 @@ const PROJECT_README = `# My Frameyard project
|
|
|
66
125
|
|
|
67
126
|
Coding agents should read \`AGENTS.md\` for the project rules and the check, still, and render workflow.
|
|
68
127
|
|
|
128
|
+
The composition is a plain HTML document at \`compositions/hello.html\`. Its metadata, default props, markup, CSS animation, and bindings live in that one file.
|
|
129
|
+
|
|
69
130
|
Start the preview:
|
|
70
131
|
|
|
71
132
|
\`\`\`sh
|
|
@@ -77,71 +138,103 @@ Validate and render:
|
|
|
77
138
|
\`\`\`sh
|
|
78
139
|
npx frameyard doctor
|
|
79
140
|
npx frameyard check hello
|
|
80
|
-
npx frameyard render hello --props props/hello.json
|
|
141
|
+
npx frameyard render hello --props props/hello.json
|
|
81
142
|
\`\`\`
|
|
82
143
|
|
|
144
|
+
The video is written to \`out/hello.mp4\` in the project. Pass \`--out <file.mp4>\` to choose another path.
|
|
145
|
+
|
|
83
146
|
Or run the pinned scripts from \`package.json\`:
|
|
84
147
|
|
|
85
148
|
\`\`\`sh
|
|
86
149
|
npm run dev
|
|
87
150
|
npm run check -- hello
|
|
88
|
-
npm run render -- hello --props props/hello.json
|
|
151
|
+
npm run render -- hello --props props/hello.json
|
|
89
152
|
npm run still -- hello --frame 42 --out hello-42.png
|
|
90
153
|
\`\`\`
|
|
91
154
|
`;
|
|
92
155
|
const SCAFFOLD_TEMPLATES = {
|
|
93
|
-
"
|
|
156
|
+
".gitignore": "out/\n",
|
|
157
|
+
"video.config.js": `export default {\n compositions: {\n hello: "./compositions/hello.html",\n },\n};\n`,
|
|
94
158
|
"props/hello.json": `${JSON.stringify({ title: "Hello, Frameyard", accent: "#b8f36b" }, null, 2)}\n`,
|
|
95
159
|
"AGENTS.md": AGENTS_GUIDE,
|
|
96
160
|
"README.md": PROJECT_README,
|
|
97
|
-
"compositions/hello.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
"compositions/hello.html": `<!doctype html>
|
|
162
|
+
<html>
|
|
163
|
+
<head>
|
|
164
|
+
<meta charset="utf-8">
|
|
165
|
+
<script type="application/json" data-fy="props">
|
|
166
|
+
{ "title": "Hello, Frameyard", "accent": "#b8f36b" }
|
|
167
|
+
</script>
|
|
168
|
+
<style>
|
|
169
|
+
@keyframes fade-up {
|
|
170
|
+
from { opacity: 0; transform: translateY(28px); }
|
|
171
|
+
to { opacity: 1; transform: translateY(0); }
|
|
172
|
+
}
|
|
173
|
+
@keyframes drift {
|
|
174
|
+
from { transform: translate(-120px, -80px); }
|
|
175
|
+
to { transform: translate(120px, 80px); }
|
|
176
|
+
}
|
|
177
|
+
#hello {
|
|
178
|
+
position: relative;
|
|
179
|
+
width: 100%;
|
|
180
|
+
height: 100%;
|
|
181
|
+
display: grid;
|
|
182
|
+
place-items: center;
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
background: #0b0d10;
|
|
185
|
+
color: #e8edf3;
|
|
186
|
+
font-family: Inter, system-ui, sans-serif;
|
|
187
|
+
}
|
|
188
|
+
#hello h1 {
|
|
189
|
+
position: relative;
|
|
190
|
+
margin: 0;
|
|
191
|
+
font-size: 96px;
|
|
192
|
+
letter-spacing: -0.04em;
|
|
193
|
+
opacity: 0;
|
|
194
|
+
animation: fade-up 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
|
|
195
|
+
}
|
|
196
|
+
#hello p {
|
|
197
|
+
position: relative;
|
|
198
|
+
margin: 18px 0 0;
|
|
199
|
+
color: #8a97a6;
|
|
200
|
+
font-size: 26px;
|
|
201
|
+
}
|
|
202
|
+
#glow {
|
|
203
|
+
position: absolute;
|
|
204
|
+
left: 50%;
|
|
205
|
+
top: 50%;
|
|
206
|
+
width: 520px;
|
|
207
|
+
height: 520px;
|
|
208
|
+
margin: -260px 0 0 -260px;
|
|
209
|
+
border-radius: 50%;
|
|
210
|
+
background: var(--fy-prop-accent);
|
|
211
|
+
filter: blur(120px);
|
|
212
|
+
opacity: 0.16;
|
|
213
|
+
animation: drift 12s ease-in-out infinite alternate;
|
|
214
|
+
}
|
|
215
|
+
#sweep {
|
|
216
|
+
position: absolute;
|
|
217
|
+
left: 0;
|
|
218
|
+
bottom: 0;
|
|
219
|
+
width: calc(var(--fy-progress) * 100%);
|
|
220
|
+
height: 4px;
|
|
221
|
+
background: var(--fy-prop-accent);
|
|
222
|
+
}
|
|
223
|
+
</style>
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<fy-composition width="1280" height="720" fps="30" duration-in-frames="150">
|
|
227
|
+
<div id="hello">
|
|
228
|
+
<div id="glow"></div>
|
|
229
|
+
<div style="position: relative; text-align: center">
|
|
230
|
+
<h1 id="title" data-fy-text="title" data-fy-anim-start="0" data-fy-anim-duration="24"></h1>
|
|
231
|
+
<p>Deterministic video, authored on the web.</p>
|
|
232
|
+
</div>
|
|
233
|
+
<div id="sweep"></div>
|
|
234
|
+
</div>
|
|
235
|
+
</fy-composition>
|
|
236
|
+
</body>
|
|
237
|
+
</html>
|
|
145
238
|
`,
|
|
146
239
|
};
|
|
147
240
|
// Keep the generated package name valid for npm and pnpm.
|
|
@@ -174,6 +267,6 @@ export const SCAFFOLD_NEXT = [
|
|
|
174
267
|
"npx frameyard doctor",
|
|
175
268
|
"npx frameyard dev",
|
|
176
269
|
"npx frameyard check hello",
|
|
177
|
-
"npx frameyard render hello
|
|
270
|
+
"npx frameyard render hello",
|
|
178
271
|
];
|
|
179
272
|
//# sourceMappingURL=scaffold.js.map
|
package/dist/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,YAAY,GAAG
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAwHwB,OAAO,kFAAkF,OAAO;CAC5I,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BtB,CAAC;AAEF,MAAM,kBAAkB,GAA2B;IACjD,YAAY,EAAE,QAAQ;IACtB,iBAAiB,EAAE,0FAA0F;IAC7G,kBAAkB,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI;IACpG,WAAW,EAAE,YAAY;IACzB,WAAW,EAAE,cAAc;IAC3B,yBAAyB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6E5B;CACA,CAAC;AAEF,0DAA0D;AAC1D,SAAS,WAAW,CAAC,WAAmB;IACtC,MAAM,UAAU,GAAG,WAAW;SAC3B,WAAW,EAAE;SACb,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC;SAC9B,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;SACb,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC3B,OAAO,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,UAAU,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,WAAmB;IAC/C,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,CAAC,SAAS,CAC/B;YACE,IAAI,EAAE,WAAW,CAAC,WAAW,CAAC;YAC9B,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE;gBACP,GAAG,EAAE,uBAAuB,OAAO,MAAM;gBACzC,KAAK,EAAE,uBAAuB,OAAO,QAAQ;gBAC7C,MAAM,EAAE,uBAAuB,OAAO,SAAS;gBAC/C,KAAK,EAAE,uBAAuB,OAAO,QAAQ;aAC9C;SACF,EACD,IAAI,EACJ,CAAC,CACF,IAAI;QACL,GAAG,kBAAkB;KACtB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,sBAAsB;IACtB,mBAAmB;IACnB,2BAA2B;IAC3B,4BAA4B;CAC7B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const FRAMEWORK_IMPORT_MAP: {
|
|
2
|
+
readonly frameyard: "/__frameyard/framework/index.js";
|
|
3
|
+
readonly "frameyard/runtime": "/__frameyard/framework/runtime/index.js";
|
|
4
|
+
};
|
|
5
|
+
export declare const COMPOSITION_HOST_SCRIPT_URL = "/__frameyard/framework/runtime/host.js";
|
|
6
|
+
export declare const COMPOSITION_HOST_STYLE = "html, body { margin: 0; width: 100%; height: 100%; } fy-composition { display: block; width: 100%; height: 100%; } fy-composition[data-fy-pending] { visibility: hidden !important; }";
|
|
7
|
+
export interface CompositionHostInjectionOptions {
|
|
8
|
+
html: string;
|
|
9
|
+
importMap?: Record<string, string>;
|
|
10
|
+
hostScriptUrl?: string;
|
|
11
|
+
styleText?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Injects the framework import map, sizing style, and composition host into a
|
|
15
|
+
* composition document. The host script is a deferred module, so it runs after
|
|
16
|
+
* the document is parsed; author executable scripts are rejected by static
|
|
17
|
+
* validation before this function is called.
|
|
18
|
+
*/
|
|
19
|
+
export declare function injectCompositionHost(options: CompositionHostInjectionOptions): string;
|
|
20
|
+
//# sourceMappingURL=composition-html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition-html.d.ts","sourceRoot":"","sources":["../../src/server/composition-html.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;CAGvB,CAAC;AAEX,eAAO,MAAM,2BAA2B,2CAA2C,CAAC;AAEpF,eAAO,MAAM,sBAAsB,0LACsJ,CAAC;AAE1L,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,+BAA+B,GAAG,MAAM,CA4BtF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defaultTreeAdapter, html, parse, serialize } from "parse5";
|
|
2
|
+
import { getElementByTagName } from "../html/parse.js";
|
|
3
|
+
export const FRAMEWORK_IMPORT_MAP = {
|
|
4
|
+
frameyard: "/__frameyard/framework/index.js",
|
|
5
|
+
"frameyard/runtime": "/__frameyard/framework/runtime/index.js",
|
|
6
|
+
};
|
|
7
|
+
export const COMPOSITION_HOST_SCRIPT_URL = "/__frameyard/framework/runtime/host.js";
|
|
8
|
+
export const COMPOSITION_HOST_STYLE = "html, body { margin: 0; width: 100%; height: 100%; } fy-composition { display: block; width: 100%; height: 100%; } fy-composition[data-fy-pending] { visibility: hidden !important; }";
|
|
9
|
+
/**
|
|
10
|
+
* Injects the framework import map, sizing style, and composition host into a
|
|
11
|
+
* composition document. The host script is a deferred module, so it runs after
|
|
12
|
+
* the document is parsed; author executable scripts are rejected by static
|
|
13
|
+
* validation before this function is called.
|
|
14
|
+
*/
|
|
15
|
+
export function injectCompositionHost(options) {
|
|
16
|
+
const document = parse(options.html);
|
|
17
|
+
const head = getElementByTagName(document, "head");
|
|
18
|
+
if (!head) {
|
|
19
|
+
throw new Error("Composition document has no <head> element.");
|
|
20
|
+
}
|
|
21
|
+
const importMap = createElement("script", [
|
|
22
|
+
{ name: "type", value: "importmap" },
|
|
23
|
+
{ name: "data-fy-host", value: "" },
|
|
24
|
+
]);
|
|
25
|
+
appendText(importMap, JSON.stringify({ imports: options.importMap ?? FRAMEWORK_IMPORT_MAP }));
|
|
26
|
+
const style = createElement("style", [{ name: "data-fy-host-style", value: "" }]);
|
|
27
|
+
appendText(style, options.styleText ?? COMPOSITION_HOST_STYLE);
|
|
28
|
+
const host = createElement("script", [
|
|
29
|
+
{ name: "type", value: "module" },
|
|
30
|
+
{ name: "src", value: options.hostScriptUrl ?? COMPOSITION_HOST_SCRIPT_URL },
|
|
31
|
+
{ name: "data-fy-host", value: "" },
|
|
32
|
+
]);
|
|
33
|
+
head.childNodes.unshift(importMap, style, host);
|
|
34
|
+
for (const child of [importMap, style, host]) {
|
|
35
|
+
child.parentNode = head;
|
|
36
|
+
}
|
|
37
|
+
return serialize(document);
|
|
38
|
+
}
|
|
39
|
+
function createElement(tagName, attrs) {
|
|
40
|
+
return defaultTreeAdapter.createElement(tagName, html.NS.HTML, attrs);
|
|
41
|
+
}
|
|
42
|
+
function appendText(element, value) {
|
|
43
|
+
defaultTreeAdapter.appendChild(element, defaultTreeAdapter.createTextNode(value));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=composition-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition-html.js","sourceRoot":"","sources":["../../src/server/composition-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAoB,MAAM,kBAAkB,CAAC;AAEzE,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,SAAS,EAAE,iCAAiC;IAC5C,mBAAmB,EAAE,yCAAyC;CACtD,CAAC;AAEX,MAAM,CAAC,MAAM,2BAA2B,GAAG,wCAAwC,CAAC;AAEpF,MAAM,CAAC,MAAM,sBAAsB,GACjC,uLAAuL,CAAC;AAS1L;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAwC;IAC5E,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACnD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,SAAS,GAAG,aAAa,CAAC,QAAQ,EAAE;QACxC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE;QACpC,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,EAAE;KACpC,CAAC,CAAC;IACH,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,IAAI,oBAAoB,EAAE,CAAC,CAAC,CAAC;IAE9F,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAClF,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,IAAI,sBAAsB,CAAC,CAAC;IAE/D,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,EAAE;QACnC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE;QACjC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,aAAa,IAAI,2BAA2B,EAAE;QAC5E,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,EAAE;KACpC,CAAC,CAAC;IAEH,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAChD,KAAK,MAAM,KAAK,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;QAC7C,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED,OAAO,SAAS,CAAC,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,aAAa,CAAC,OAAe,EAAE,KAA6C;IACnF,OAAO,kBAAkB,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,UAAU,CAAC,OAAoB,EAAE,KAAa;IACrD,kBAAkB,CAAC,WAAW,CAAC,OAAO,EAAE,kBAAkB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;AACpF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview-html.d.ts","sourceRoot":"","sources":["../../src/server/preview-html.ts"],"names":[],"mappings":"AAWA,wBAAgB,iBAAiB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"preview-html.d.ts","sourceRoot":"","sources":["../../src/server/preview-html.ts"],"names":[],"mappings":"AAWA,wBAAgB,iBAAiB,IAAI,MAAM,CAkO1C"}
|
|
@@ -148,7 +148,21 @@ export function renderPreviewHtml() {
|
|
|
148
148
|
repeating-conic-gradient(#0d1014 0% 25%, #0b0d10 0% 50%) 50% / 24px 24px;
|
|
149
149
|
}
|
|
150
150
|
.stage-wrap { position: relative; flex: 0 0 auto; }
|
|
151
|
-
.stage {
|
|
151
|
+
.stage-frame { position: absolute; top: 0; left: 0; border: 0; transform-origin: top left; background: #000; }
|
|
152
|
+
.diagnostics {
|
|
153
|
+
border: 1px solid var(--line);
|
|
154
|
+
background: var(--panel-2);
|
|
155
|
+
border-radius: 7px;
|
|
156
|
+
padding: 8px 10px;
|
|
157
|
+
max-height: 26vh;
|
|
158
|
+
overflow: auto;
|
|
159
|
+
font-family: var(--mono);
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
line-height: 1.5;
|
|
162
|
+
}
|
|
163
|
+
.diagnostics .diagnostic { display: block; white-space: pre-wrap; }
|
|
164
|
+
.diagnostics .warning { color: #e8c76a; }
|
|
165
|
+
.diagnostics .error { color: var(--danger); }
|
|
152
166
|
.transport {
|
|
153
167
|
grid-row: 3;
|
|
154
168
|
grid-column: 2;
|
|
@@ -170,18 +184,6 @@ export function renderPreviewHtml() {
|
|
|
170
184
|
.audio { display: flex; align-items: center; gap: 7px; color: var(--muted); font-family: var(--mono); font-size: 11px; }
|
|
171
185
|
.audio input[type="range"] { width: 72px; accent-color: var(--accent); }
|
|
172
186
|
|
|
173
|
-
body.render #app { display: block; height: auto; }
|
|
174
|
-
body.render .topbar, body.render .panel, body.render .transport { display: none; }
|
|
175
|
-
body.render .viewport {
|
|
176
|
-
position: fixed;
|
|
177
|
-
inset: 0;
|
|
178
|
-
display: block;
|
|
179
|
-
padding: 0;
|
|
180
|
-
overflow: hidden;
|
|
181
|
-
background: #000;
|
|
182
|
-
}
|
|
183
|
-
body.render .stage-wrap { position: absolute; inset: 0; width: auto !important; height: auto !important; }
|
|
184
|
-
body.render .stage { transform: none !important; }
|
|
185
187
|
</style>
|
|
186
188
|
</head>
|
|
187
189
|
<body>
|
|
@@ -201,13 +203,16 @@ export function renderPreviewHtml() {
|
|
|
201
203
|
</div>
|
|
202
204
|
<button id="apply" class="primary" type="button">Apply props</button>
|
|
203
205
|
<div id="error" class="error" role="alert" hidden></div>
|
|
206
|
+
<div id="diagnostics" class="diagnostics" hidden></div>
|
|
204
207
|
<div class="hint">
|
|
205
208
|
Frames are derived from <code>frame</code> and <code>props</code> only.
|
|
206
209
|
Wall-clock animation and unseeded randomness are not deterministic.
|
|
207
210
|
</div>
|
|
208
211
|
</aside>
|
|
209
212
|
<main id="viewport" class="viewport">
|
|
210
|
-
<div id="stage-wrap" class="stage-wrap"
|
|
213
|
+
<div id="stage-wrap" class="stage-wrap">
|
|
214
|
+
<iframe id="stage-frame" class="stage-frame" title="Composition" hidden></iframe>
|
|
215
|
+
</div>
|
|
211
216
|
</main>
|
|
212
217
|
<footer class="transport">
|
|
213
218
|
<button id="play" class="play" type="button" aria-label="Play">▶</button>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview-html.js","sourceRoot":"","sources":["../../src/server/preview-html.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAC/B;IACE,OAAO,EAAE;QACP,SAAS,EAAE,iCAAiC;QAC5C,mBAAmB,EAAE,yCAAyC;KAC/D;CACF,EACD,IAAI,EACJ,CAAC,CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;2BAMkB,UAAU
|
|
1
|
+
{"version":3,"file":"preview-html.js","sourceRoot":"","sources":["../../src/server/preview-html.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAC/B;IACE,OAAO,EAAE;QACP,SAAS,EAAE,iCAAiC;QAC5C,mBAAmB,EAAE,yCAAyC;KAC/D;CACF,EACD,IAAI,EACJ,CAAC,CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;2BAMkB,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0N7B,CAAC;AACT,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server/server.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,OAAO,EAA2B,MAAM,eAAe,CAAC;AAInF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAyOD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CA+H9F"}
|
package/dist/server/server.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
|
-
import { stat } from "node:fs/promises";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { FrameyardError } from "../errors.js";
|
|
7
|
+
import { validateHtmlComposition } from "../html/validate.js";
|
|
7
8
|
import { loadProject } from "../project.js";
|
|
9
|
+
import { injectCompositionHost } from "./composition-html.js";
|
|
8
10
|
import { renderPreviewHtml } from "./preview-html.js";
|
|
9
11
|
const MIME_TYPES = {
|
|
10
12
|
".avif": "image/avif",
|
|
@@ -144,14 +146,76 @@ async function serveFile(request, response, base, relativePath) {
|
|
|
144
146
|
.on("error", () => response.destroy())
|
|
145
147
|
.pipe(response);
|
|
146
148
|
}
|
|
149
|
+
function sendHtml(response, status, body, headOnly = false) {
|
|
150
|
+
response.writeHead(status, {
|
|
151
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
152
|
+
"Content-Length": Buffer.byteLength(body),
|
|
153
|
+
"Cache-Control": "no-store",
|
|
154
|
+
});
|
|
155
|
+
response.end(headOnly ? undefined : body);
|
|
156
|
+
}
|
|
157
|
+
function sendJavaScript(response, status, body, headOnly = false) {
|
|
158
|
+
response.writeHead(status, {
|
|
159
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
160
|
+
"Content-Length": Buffer.byteLength(body),
|
|
161
|
+
"Cache-Control": "no-store",
|
|
162
|
+
});
|
|
163
|
+
response.end(headOnly ? undefined : body);
|
|
164
|
+
}
|
|
165
|
+
function escapeHtml(value) {
|
|
166
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
167
|
+
switch (character) {
|
|
168
|
+
case "&":
|
|
169
|
+
return "&";
|
|
170
|
+
case "<":
|
|
171
|
+
return "<";
|
|
172
|
+
case ">":
|
|
173
|
+
return ">";
|
|
174
|
+
case '"':
|
|
175
|
+
return """;
|
|
176
|
+
default:
|
|
177
|
+
return "'";
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function renderCompositionError(entry, diagnostics) {
|
|
182
|
+
const items = diagnostics
|
|
183
|
+
.map((diagnostic) => {
|
|
184
|
+
const location = diagnostic.details?.line ? ` at line ${diagnostic.details.line}` : "";
|
|
185
|
+
return `<li><code>${escapeHtml(diagnostic.code)}</code> ${escapeHtml(diagnostic.message)}${escapeHtml(location)}</li>`;
|
|
186
|
+
})
|
|
187
|
+
.join("");
|
|
188
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Frameyard composition error</title></head><body><h1>Composition error</h1><p>${escapeHtml(entry)}</p><ul>${items}</ul></body></html>`;
|
|
189
|
+
}
|
|
190
|
+
function publicController(controller) {
|
|
191
|
+
if (!controller) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return controller.kind === "inline" ? { kind: "inline" } : { kind: "external", src: controller.src ?? "" };
|
|
195
|
+
}
|
|
196
|
+
async function buildProjectJson(project) {
|
|
197
|
+
const compositions = [];
|
|
198
|
+
for (const composition of project.compositions.values()) {
|
|
199
|
+
const result = await validateHtmlComposition({ projectRoot: project.root, entry: composition.entry });
|
|
200
|
+
compositions.push({
|
|
201
|
+
id: composition.id,
|
|
202
|
+
entry: composition.entry,
|
|
203
|
+
kind: "html",
|
|
204
|
+
metadata: result.info?.metadata ?? null,
|
|
205
|
+
controller: publicController(result.info?.controller ?? null),
|
|
206
|
+
diagnostics: result.diagnostics,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return { compositions };
|
|
210
|
+
}
|
|
211
|
+
function findCompositionForPath(project, pathname) {
|
|
212
|
+
return [...project.compositions.values()].find((composition) => `/${composition.entry}` === pathname);
|
|
213
|
+
}
|
|
147
214
|
export async function startPreviewServer(options) {
|
|
148
215
|
const project = await loadProject(options.projectRoot);
|
|
149
216
|
const frameworkDir = options.frameworkDir ?? path.join(fileURLToPath(new URL("../../", import.meta.url)), "dist");
|
|
150
217
|
const host = options.host ?? "127.0.0.1";
|
|
151
218
|
const html = renderPreviewHtml();
|
|
152
|
-
const projectJson = {
|
|
153
|
-
compositions: Array.from(project.compositions.values(), ({ id, entry }) => ({ id, entry })),
|
|
154
|
-
};
|
|
155
219
|
const server = createServer((request, response) => {
|
|
156
220
|
void (async () => {
|
|
157
221
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
@@ -159,9 +223,11 @@ export async function startPreviewServer(options) {
|
|
|
159
223
|
response.end();
|
|
160
224
|
return;
|
|
161
225
|
}
|
|
226
|
+
let url;
|
|
162
227
|
let pathname;
|
|
163
228
|
try {
|
|
164
|
-
|
|
229
|
+
url = new URL(request.url ?? "/", "http://localhost");
|
|
230
|
+
pathname = decodeURIComponent(url.pathname);
|
|
165
231
|
}
|
|
166
232
|
catch {
|
|
167
233
|
sendJson(response, 400, { error: "Invalid URL" });
|
|
@@ -187,13 +253,35 @@ export async function startPreviewServer(options) {
|
|
|
187
253
|
return;
|
|
188
254
|
}
|
|
189
255
|
if (pathname === "/__frameyard/project.json") {
|
|
190
|
-
sendJson(response, 200,
|
|
256
|
+
sendJson(response, 200, await buildProjectJson(project));
|
|
191
257
|
return;
|
|
192
258
|
}
|
|
193
259
|
if (pathname.startsWith("/__frameyard/framework/")) {
|
|
194
260
|
await serveFile(request, response, frameworkDir, pathname.slice("/__frameyard/framework/".length));
|
|
195
261
|
return;
|
|
196
262
|
}
|
|
263
|
+
const composition = findCompositionForPath(project, pathname);
|
|
264
|
+
if (composition) {
|
|
265
|
+
const result = await validateHtmlComposition({ projectRoot: project.root, entry: composition.entry });
|
|
266
|
+
if (url.searchParams.has("__fy-controller")) {
|
|
267
|
+
const controller = result.info?.controller;
|
|
268
|
+
if (controller?.kind === "inline" && controller.source !== undefined) {
|
|
269
|
+
sendJavaScript(response, 200, controller.source, request.method === "HEAD");
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
sendJson(response, 404, { error: "Composition has no inline controller." });
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
277
|
+
if (errors.length > 0) {
|
|
278
|
+
sendHtml(response, 500, renderCompositionError(composition.entry, result.diagnostics), request.method === "HEAD");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const source = await readFile(composition.absoluteEntry, "utf8");
|
|
282
|
+
sendHtml(response, 200, injectCompositionHost({ html: source }), request.method === "HEAD");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
197
285
|
await serveFile(request, response, project.root, pathname.slice(1));
|
|
198
286
|
})().catch((error) => {
|
|
199
287
|
if (!response.headersSent) {
|