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.
Files changed (168) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/GLOSSARY.md +46 -2
  3. package/README.md +174 -106
  4. package/dist/bindings.d.ts +42 -0
  5. package/dist/bindings.d.ts.map +1 -0
  6. package/dist/bindings.js +144 -0
  7. package/dist/bindings.js.map +1 -0
  8. package/dist/cli.js +32 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/composition/adapter.d.ts +30 -0
  11. package/dist/composition/adapter.d.ts.map +1 -0
  12. package/dist/composition/adapter.js +2 -0
  13. package/dist/composition/adapter.js.map +1 -0
  14. package/dist/composition/animation.d.ts +19 -0
  15. package/dist/composition/animation.d.ts.map +1 -0
  16. package/dist/composition/animation.js +55 -0
  17. package/dist/composition/animation.js.map +1 -0
  18. package/dist/composition/attributes.d.ts +7 -0
  19. package/dist/composition/attributes.d.ts.map +1 -0
  20. package/dist/composition/attributes.js +24 -0
  21. package/dist/composition/attributes.js.map +1 -0
  22. package/dist/composition/descriptor.d.ts +30 -0
  23. package/dist/composition/descriptor.d.ts.map +1 -0
  24. package/dist/composition/descriptor.js +188 -0
  25. package/dist/composition/descriptor.js.map +1 -0
  26. package/dist/composition/diagnostics.d.ts +12 -0
  27. package/dist/composition/diagnostics.d.ts.map +1 -0
  28. package/dist/composition/diagnostics.js +38 -0
  29. package/dist/composition/diagnostics.js.map +1 -0
  30. package/dist/composition/index.d.ts +12 -0
  31. package/dist/composition/index.d.ts.map +1 -0
  32. package/dist/composition/index.js +7 -0
  33. package/dist/composition/index.js.map +1 -0
  34. package/dist/composition/media.d.ts +27 -0
  35. package/dist/composition/media.d.ts.map +1 -0
  36. package/dist/composition/media.js +162 -0
  37. package/dist/composition/media.js.map +1 -0
  38. package/dist/composition/paths.d.ts +5 -0
  39. package/dist/composition/paths.d.ts.map +1 -0
  40. package/dist/composition/paths.js +16 -0
  41. package/dist/composition/paths.js.map +1 -0
  42. package/dist/composition/transcripts.d.ts +19 -0
  43. package/dist/composition/transcripts.d.ts.map +1 -0
  44. package/dist/composition/transcripts.js +57 -0
  45. package/dist/composition/transcripts.js.map +1 -0
  46. package/dist/composition/walk.d.ts +10 -0
  47. package/dist/composition/walk.d.ts.map +1 -0
  48. package/dist/composition/walk.js +32 -0
  49. package/dist/composition/walk.js.map +1 -0
  50. package/dist/contracts.d.ts +75 -47
  51. package/dist/contracts.d.ts.map +1 -1
  52. package/dist/contracts.js +14 -159
  53. package/dist/contracts.js.map +1 -1
  54. package/dist/errors.d.ts +15 -4
  55. package/dist/errors.d.ts.map +1 -1
  56. package/dist/errors.js +8 -0
  57. package/dist/errors.js.map +1 -1
  58. package/dist/html/document-adapter.d.ts +5 -0
  59. package/dist/html/document-adapter.d.ts.map +1 -0
  60. package/dist/html/document-adapter.js +17 -0
  61. package/dist/html/document-adapter.js.map +1 -0
  62. package/dist/html/index.d.ts +5 -0
  63. package/dist/html/index.d.ts.map +1 -0
  64. package/dist/html/index.js +3 -0
  65. package/dist/html/index.js.map +1 -0
  66. package/dist/html/parse.d.ts +26 -0
  67. package/dist/html/parse.d.ts.map +1 -0
  68. package/dist/html/parse.js +80 -0
  69. package/dist/html/parse.js.map +1 -0
  70. package/dist/html/validate.d.ts +14 -0
  71. package/dist/html/validate.d.ts.map +1 -0
  72. package/dist/html/validate.js +648 -0
  73. package/dist/html/validate.js.map +1 -0
  74. package/dist/index.d.ts +2 -2
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/preview/main.d.ts +10 -3
  79. package/dist/preview/main.d.ts.map +1 -1
  80. package/dist/preview/main.js +189 -89
  81. package/dist/preview/main.js.map +1 -1
  82. package/dist/project.d.ts +2 -0
  83. package/dist/project.d.ts.map +1 -1
  84. package/dist/project.js +4 -0
  85. package/dist/project.js.map +1 -1
  86. package/dist/renderer/browser.d.ts +11 -1
  87. package/dist/renderer/browser.d.ts.map +1 -1
  88. package/dist/renderer/browser.js +93 -14
  89. package/dist/renderer/browser.js.map +1 -1
  90. package/dist/renderer/index.d.ts +25 -5
  91. package/dist/renderer/index.d.ts.map +1 -1
  92. package/dist/renderer/index.js +82 -16
  93. package/dist/renderer/index.js.map +1 -1
  94. package/dist/runtime/bindings.d.ts +11 -0
  95. package/dist/runtime/bindings.d.ts.map +1 -0
  96. package/dist/runtime/bindings.js +164 -0
  97. package/dist/runtime/bindings.js.map +1 -0
  98. package/dist/runtime/diagnostics.d.ts +4 -0
  99. package/dist/runtime/diagnostics.d.ts.map +1 -0
  100. package/dist/runtime/diagnostics.js +107 -0
  101. package/dist/runtime/diagnostics.js.map +1 -0
  102. package/dist/runtime/document-adapter.d.ts +4 -0
  103. package/dist/runtime/document-adapter.d.ts.map +1 -0
  104. package/dist/runtime/document-adapter.js +19 -0
  105. package/dist/runtime/document-adapter.js.map +1 -0
  106. package/dist/runtime/host.d.ts +14 -0
  107. package/dist/runtime/host.d.ts.map +1 -0
  108. package/dist/runtime/host.js +596 -0
  109. package/dist/runtime/host.js.map +1 -0
  110. package/dist/runtime/index.d.ts +0 -29
  111. package/dist/runtime/index.d.ts.map +1 -1
  112. package/dist/runtime/index.js +0 -360
  113. package/dist/runtime/index.js.map +1 -1
  114. package/dist/runtime/media.d.ts.map +1 -1
  115. package/dist/runtime/media.js +20 -51
  116. package/dist/runtime/media.js.map +1 -1
  117. package/dist/scaffold.d.ts.map +1 -1
  118. package/dist/scaffold.js +177 -84
  119. package/dist/scaffold.js.map +1 -1
  120. package/dist/server/composition-html.d.ts +20 -0
  121. package/dist/server/composition-html.d.ts.map +1 -0
  122. package/dist/server/composition-html.js +45 -0
  123. package/dist/server/composition-html.js.map +1 -0
  124. package/dist/server/preview-html.d.ts.map +1 -1
  125. package/dist/server/preview-html.js +19 -14
  126. package/dist/server/preview-html.js.map +1 -1
  127. package/dist/server/server.d.ts.map +1 -1
  128. package/dist/server/server.js +94 -6
  129. package/dist/server/server.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  133. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +7 -1
  134. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  135. package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
  136. package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
  137. package/docs/migration-0.4.md +279 -0
  138. package/docs/v0.1-design.md +78 -56
  139. package/examples/demo/README.md +8 -4
  140. package/examples/demo/compositions/demo.controller.js +120 -0
  141. package/examples/demo/compositions/demo.html +253 -0
  142. package/examples/demo/video.config.js +1 -1
  143. package/examples/video/README.md +61 -0
  144. package/examples/video/assets/clip.mp4 +0 -0
  145. package/examples/video/assets/clip.webm +0 -0
  146. package/examples/video/compositions/video-mp4.html +362 -0
  147. package/examples/video/compositions/video.html +362 -0
  148. package/examples/video/props/video.json +8 -0
  149. package/examples/video/scripts/make-clip.sh +109 -0
  150. package/examples/video/video.config.js +6 -0
  151. package/examples/webgl/README.md +30 -0
  152. package/examples/webgl/compositions/webgl.controller.js +200 -0
  153. package/examples/webgl/compositions/webgl.html +215 -0
  154. package/examples/webgl/props/webgl.json +6 -0
  155. package/examples/webgl/video.config.js +5 -0
  156. package/package.json +7 -3
  157. package/skills/frameyard/README.md +4 -0
  158. package/skills/frameyard/SKILL.md +40 -12
  159. package/skills/frameyard/references/audio-assets.md +25 -14
  160. package/skills/frameyard/references/bindings.md +73 -0
  161. package/skills/frameyard/references/common-mistakes.md +59 -0
  162. package/skills/frameyard/references/controllers.md +126 -0
  163. package/skills/frameyard/references/deterministic-authoring.md +143 -52
  164. package/skills/frameyard/references/diagnostics.md +90 -0
  165. package/skills/frameyard/references/failure-recovery.md +37 -14
  166. package/skills/frameyard/references/json-results.md +43 -9
  167. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  168. 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 entry modules. There is no directory scanning; a new composition is invisible until it is listed.
9
- - \`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).
10
- - \`compositions/\` contains native JavaScript ES modules. Import bare \`frameyard\` and \`frameyard/runtime\`; the preview server supplies the import map.
11
- - \`props/\` contains JSON props, merged shallowly over \`defaultProps\` and deep-frozen.
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
- \`width\`, \`height\`, \`fps\`, and \`durationInFrames\` are fixed composition metadata. Do not derive them from props.
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 source, and the props file before changing behavior.
19
- 2. Author the composition or props with native HTML, CSS, SVG, Canvas 2D, WebGL, embedded video, and JavaScript. Do not add a bundling step.
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> --out <file.mp4> [--concurrency <n>] --json\`.
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 --out hello.mp4 --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 --out hello.mp4
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 --out hello.mp4
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
- "video.config.js": `export default {\n compositions: {\n hello: "./compositions/hello.js",\n },\n};\n`,
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.js": `import { defineComposition } from "frameyard";
98
-
99
- let title;
100
- let sweep;
101
- let glow;
102
-
103
- export default defineComposition({
104
- width: 1280,
105
- height: 720,
106
- fps: 30,
107
- durationInFrames: 150,
108
- defaultProps: { title: "Hello, Frameyard", accent: "#b8f36b" },
109
- validateProps(props) {
110
- const errors = [];
111
- if (typeof props.title !== "string") errors.push("title must be a string");
112
- if (typeof props.accent !== "string") errors.push("accent must be a string");
113
- return errors;
114
- },
115
- mount({ container, props }) {
116
- container.style.setProperty("--accent", props.accent);
117
- container.innerHTML =
118
- '<style>' +
119
- '#hello { position: relative; width: 100%; height: 100%; display: grid; place-items: center; overflow: hidden; background: #0b0d10; color: #e8edf3; font-family: Inter, system-ui, sans-serif; }' +
120
- '#hello h1 { position: relative; margin: 0; font-size: 96px; letter-spacing: -0.04em; }' +
121
- '#hello p { position: relative; margin: 18px 0 0; color: #8a97a6; font-size: 26px; }' +
122
- '#glow { position: absolute; width: 520px; height: 520px; border-radius: 50%; background: var(--accent); filter: blur(120px); opacity: 0.16; }' +
123
- '#sweep { position: absolute; left: 0; bottom: 0; width: 0%; height: 4px; background: var(--accent); }' +
124
- '</style>' +
125
- '<div id="hello"><div id="glow"></div><div style="position: relative; text-align: center"><h1 id="title"></h1><p>Deterministic video, authored on the web.</p></div><div id="sweep"></div></div>';
126
- title = container.querySelector("#title");
127
- sweep = container.querySelector("#sweep");
128
- glow = container.querySelector("#glow");
129
- },
130
- update({ frame, props }) {
131
- title.textContent = props.title;
132
- const intro = Math.min(1, frame / 24);
133
- title.style.opacity = String(intro);
134
- title.style.transform = "translateY(" + (1 - intro) * 28 + "px)";
135
- glow.style.transform =
136
- "translate(" + Math.sin(frame / 26) * 120 + "px," + Math.cos(frame / 34) * 80 + "px)";
137
- sweep.style.width = (frame / 149) * 100 + "%";
138
- },
139
- dispose() {
140
- title = undefined;
141
- sweep = undefined;
142
- glow = undefined;
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 --out hello.mp4",
270
+ "npx frameyard render hello",
178
271
  ];
179
272
  //# sourceMappingURL=scaffold.js.map
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CA6DwB,OAAO,kFAAkF,OAAO;CAC5I,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BtB,CAAC;AAEF,MAAM,kBAAkB,GAA2B;IACjD,iBAAiB,EAAE,wFAAwF;IAC3G,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,uBAAuB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgD1B;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,4CAA4C;CAC7C,CAAC"}
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,CA6N1C"}
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 { transform-origin: top left; overflow: hidden; background: #000; }
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"><div id="stage" class="stage"></div></div>
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">&#9654;</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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqN7B,CAAC;AACT,CAAC"}
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":"AAMA,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAG1D,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;AA+JD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmG9F"}
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"}
@@ -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 "&amp;";
170
+ case "<":
171
+ return "&lt;";
172
+ case ">":
173
+ return "&gt;";
174
+ case '"':
175
+ return "&quot;";
176
+ default:
177
+ return "&#39;";
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
- pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
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, projectJson);
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) {