egaki 0.6.0 → 0.7.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 (271) hide show
  1. package/CHANGELOG.md +140 -9
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/{cli.js → cli/cli.js} +626 -600
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/cli/credentials.d.ts.map +1 -0
  22. package/dist/{credentials.js → cli/credentials.js} +20 -0
  23. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  24. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  25. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  26. package/dist/cli/generate.d.ts +140 -0
  27. package/dist/cli/generate.d.ts.map +1 -0
  28. package/dist/cli/generate.js +604 -0
  29. package/dist/cli/generate.test.d.ts +2 -0
  30. package/dist/cli/generate.test.d.ts.map +1 -0
  31. package/dist/cli/generate.test.js +131 -0
  32. package/dist/cli/login.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts +3 -0
  34. package/dist/cli/main.d.ts.map +1 -0
  35. package/dist/cli/main.js +7 -0
  36. package/dist/{model-catalog.d.ts → cli/model-catalog.d.ts} +6 -0
  37. package/dist/cli/model-catalog.d.ts.map +1 -0
  38. package/dist/{model-catalog.js → cli/model-catalog.js} +714 -4
  39. package/dist/{models.d.ts → cli/models.d.ts} +12 -5
  40. package/dist/cli/models.d.ts.map +1 -0
  41. package/dist/{models.js → cli/models.js} +58 -57
  42. package/dist/cli/open-browser.d.ts.map +1 -0
  43. package/dist/cli/openai-speech-provider.d.ts +3 -0
  44. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.js +28 -0
  46. package/dist/cli/separation-generate.d.ts +34 -0
  47. package/dist/cli/separation-generate.d.ts.map +1 -0
  48. package/dist/cli/separation-generate.js +117 -0
  49. package/dist/cli/speech-catalog.d.ts +35 -0
  50. package/dist/cli/speech-catalog.d.ts.map +1 -0
  51. package/dist/cli/speech-catalog.js +195 -0
  52. package/dist/cli/speech-generate.d.ts +86 -0
  53. package/dist/cli/speech-generate.d.ts.map +1 -0
  54. package/dist/cli/speech-generate.js +156 -0
  55. package/dist/cli/speech-models.d.ts +8 -0
  56. package/dist/cli/speech-models.d.ts.map +1 -0
  57. package/dist/cli/speech-models.js +109 -0
  58. package/dist/cli/speech-provider.test.d.ts +2 -0
  59. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  60. package/dist/cli/speech-provider.test.js +197 -0
  61. package/dist/cli/subscription.d.ts.map +1 -0
  62. package/dist/cli/transcription-catalog.d.ts +35 -0
  63. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.js +192 -0
  65. package/dist/cli/transcription-generate.d.ts +62 -0
  66. package/dist/cli/transcription-generate.d.ts.map +1 -0
  67. package/dist/cli/transcription-generate.js +144 -0
  68. package/dist/cli/transcription-models.d.ts +9 -0
  69. package/dist/cli/transcription-models.d.ts.map +1 -0
  70. package/dist/cli/transcription-models.js +100 -0
  71. package/dist/cli/transcription-providers.d.ts +22 -0
  72. package/dist/cli/transcription-providers.d.ts.map +1 -0
  73. package/dist/cli/transcription-providers.js +290 -0
  74. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  75. package/dist/cli/xai-auth.d.ts.map +1 -0
  76. package/dist/{xai-auth.js → cli/xai-auth.js} +1 -1
  77. package/dist/vite/angled-screen.d.ts +57 -0
  78. package/dist/vite/angled-screen.d.ts.map +1 -0
  79. package/dist/vite/angled-screen.js +115 -0
  80. package/dist/vite/app.d.ts +51 -0
  81. package/dist/vite/app.d.ts.map +1 -0
  82. package/dist/vite/app.js +150 -0
  83. package/dist/vite/bands-shader.d.ts +47 -0
  84. package/dist/vite/bands-shader.d.ts.map +1 -0
  85. package/dist/vite/bands-shader.js +267 -0
  86. package/dist/vite/code-block.d.ts +107 -0
  87. package/dist/vite/code-block.d.ts.map +1 -0
  88. package/dist/vite/code-block.js +641 -0
  89. package/dist/vite/components.d.ts +36 -0
  90. package/dist/vite/components.d.ts.map +1 -0
  91. package/dist/vite/components.js +113 -0
  92. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  93. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  94. package/dist/vite/dispersion-rings-shader.js +282 -0
  95. package/dist/vite/easing-curves.d.ts +129 -0
  96. package/dist/vite/easing-curves.d.ts.map +1 -0
  97. package/dist/vite/easing-curves.js +584 -0
  98. package/dist/vite/easing-curves.test.d.ts +2 -0
  99. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  100. package/dist/vite/easing-curves.test.js +200 -0
  101. package/dist/vite/generated-media-client.d.ts +35 -0
  102. package/dist/vite/generated-media-client.d.ts.map +1 -0
  103. package/dist/vite/generated-media-client.js +76 -0
  104. package/dist/vite/keyframes.d.ts +129 -0
  105. package/dist/vite/keyframes.d.ts.map +1 -0
  106. package/dist/vite/keyframes.js +194 -0
  107. package/dist/vite/layout-editor.d.ts +45 -0
  108. package/dist/vite/layout-editor.d.ts.map +1 -0
  109. package/dist/vite/layout-editor.js +587 -0
  110. package/dist/vite/layout-transition.d.ts +136 -0
  111. package/dist/vite/layout-transition.d.ts.map +1 -0
  112. package/dist/vite/layout-transition.js +581 -0
  113. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  114. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  115. package/dist/vite/liquid-gradient-shader.js +413 -0
  116. package/dist/vite/mdx-client.d.ts +15 -0
  117. package/dist/vite/mdx-client.d.ts.map +1 -0
  118. package/dist/vite/mdx-client.js +265 -0
  119. package/dist/vite/mdx-parse.d.ts +102 -0
  120. package/dist/vite/mdx-parse.d.ts.map +1 -0
  121. package/dist/vite/mdx-parse.js +251 -0
  122. package/dist/vite/mdx-provided-components.d.ts +18 -0
  123. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  124. package/dist/vite/mdx-provided-components.js +14 -0
  125. package/dist/vite/mdx-video.d.ts +327 -0
  126. package/dist/vite/mdx-video.d.ts.map +1 -0
  127. package/dist/vite/mdx-video.js +596 -0
  128. package/dist/vite/mdx-video.test.d.ts +9 -0
  129. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  130. package/dist/vite/mdx-video.test.js +1945 -0
  131. package/dist/vite/media-components.d.ts +45 -0
  132. package/dist/vite/media-components.d.ts.map +1 -0
  133. package/dist/vite/media-components.js +355 -0
  134. package/dist/vite/media-duration-store.d.ts +66 -0
  135. package/dist/vite/media-duration-store.d.ts.map +1 -0
  136. package/dist/vite/media-duration-store.js +204 -0
  137. package/dist/vite/player-page.d.ts +35 -0
  138. package/dist/vite/player-page.d.ts.map +1 -0
  139. package/dist/vite/player-page.js +720 -0
  140. package/dist/vite/render-client.d.ts +18 -0
  141. package/dist/vite/render-client.d.ts.map +1 -0
  142. package/dist/vite/render-client.js +67 -0
  143. package/dist/vite/sdk.d.ts +221 -0
  144. package/dist/vite/sdk.d.ts.map +1 -0
  145. package/dist/vite/sdk.js +355 -0
  146. package/dist/vite/server-components.d.ts +55 -0
  147. package/dist/vite/server-components.d.ts.map +1 -0
  148. package/dist/vite/server-components.js +156 -0
  149. package/dist/vite/server-mdx.d.ts +74 -0
  150. package/dist/vite/server-mdx.d.ts.map +1 -0
  151. package/dist/vite/server-mdx.js +259 -0
  152. package/dist/vite/shader-renderer.d.ts +48 -0
  153. package/dist/vite/shader-renderer.d.ts.map +1 -0
  154. package/dist/vite/shader-renderer.js +411 -0
  155. package/dist/vite/store-hooks.d.ts +12 -0
  156. package/dist/vite/store-hooks.d.ts.map +1 -0
  157. package/dist/vite/store-hooks.js +93 -0
  158. package/dist/vite/store.d.ts +75 -0
  159. package/dist/vite/store.d.ts.map +1 -0
  160. package/dist/vite/store.js +52 -0
  161. package/dist/vite/store.test.d.ts +6 -0
  162. package/dist/vite/store.test.d.ts.map +1 -0
  163. package/dist/vite/store.test.js +172 -0
  164. package/dist/vite/tweakpane-hook.d.ts +94 -0
  165. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  166. package/dist/vite/tweakpane-hook.js +450 -0
  167. package/dist/vite/vite-plugin.d.ts +28 -0
  168. package/dist/vite/vite-plugin.d.ts.map +1 -0
  169. package/dist/vite/vite-plugin.js +538 -0
  170. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  171. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  172. package/dist/vite/wave-gradient-shader.js +207 -0
  173. package/package.json +101 -24
  174. package/skills/egaki/SKILL.md +162 -1
  175. package/src/cli/cache-utils.ts +169 -0
  176. package/src/cli/cached-generate.test.ts +531 -0
  177. package/src/cli/cached-generate.ts +348 -0
  178. package/src/cli/cartesia-provider.ts +249 -0
  179. package/src/{cli.ts → cli/cli.ts} +836 -884
  180. package/src/cli/clone-generate.ts +202 -0
  181. package/src/{credentials.ts → cli/credentials.ts} +20 -0
  182. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  183. package/src/cli/generate.test.ts +166 -0
  184. package/src/cli/generate.ts +912 -0
  185. package/src/cli/main.ts +8 -0
  186. package/src/{model-catalog.ts → cli/model-catalog.ts} +787 -58
  187. package/src/{models.ts → cli/models.ts} +65 -86
  188. package/src/cli/openai-speech-provider.ts +34 -0
  189. package/src/cli/separation-generate.ts +187 -0
  190. package/src/cli/speech-catalog.ts +253 -0
  191. package/src/cli/speech-generate.ts +275 -0
  192. package/src/cli/speech-models.ts +146 -0
  193. package/src/cli/speech-provider.test.ts +226 -0
  194. package/src/cli/transcription-catalog.ts +250 -0
  195. package/src/cli/transcription-generate.ts +214 -0
  196. package/src/cli/transcription-models.ts +136 -0
  197. package/src/cli/transcription-providers.ts +427 -0
  198. package/src/{xai-auth.ts → cli/xai-auth.ts} +1 -1
  199. package/src/vite/angled-screen.tsx +214 -0
  200. package/src/vite/app.tsx +197 -0
  201. package/src/vite/bands-shader.tsx +319 -0
  202. package/src/vite/code-block.tsx +939 -0
  203. package/src/vite/components.tsx +278 -0
  204. package/src/vite/dispersion-rings-shader.tsx +331 -0
  205. package/src/vite/easing-curves.test.ts +232 -0
  206. package/src/vite/easing-curves.ts +743 -0
  207. package/src/vite/generated-media-client.tsx +121 -0
  208. package/src/vite/keyframes.tsx +347 -0
  209. package/src/vite/layout-editor.tsx +709 -0
  210. package/src/vite/layout-transition.tsx +706 -0
  211. package/src/vite/liquid-gradient-shader.tsx +474 -0
  212. package/src/vite/mdx-client.tsx +420 -0
  213. package/src/vite/mdx-parse.ts +343 -0
  214. package/src/vite/mdx-provided-components.ts +19 -0
  215. package/src/vite/mdx-video.test.tsx +2219 -0
  216. package/src/vite/mdx-video.tsx +976 -0
  217. package/src/vite/media-components.tsx +473 -0
  218. package/src/vite/media-duration-store.ts +225 -0
  219. package/src/vite/motion-timing.ts +194 -0
  220. package/src/vite/player-page.tsx +1107 -0
  221. package/src/vite/render-client.ts +84 -0
  222. package/src/vite/sdk.ts +598 -0
  223. package/src/vite/server-components.tsx +241 -0
  224. package/src/vite/server-mdx.ts +285 -0
  225. package/src/vite/shader-renderer.tsx +535 -0
  226. package/src/vite/store-hooks.ts +117 -0
  227. package/src/vite/store.test.ts +202 -0
  228. package/src/vite/store.ts +115 -0
  229. package/src/vite/styles.css +21 -0
  230. package/src/vite/tweakpane-hook.tsx +593 -0
  231. package/src/vite/virtual-modules.d.ts +30 -0
  232. package/src/vite/vite-plugin.ts +584 -0
  233. package/src/vite/wave-gradient-shader.tsx +253 -0
  234. package/README.md +0 -370
  235. package/dist/antigravity-auth.d.ts +0 -15
  236. package/dist/antigravity-auth.d.ts.map +0 -1
  237. package/dist/antigravity-auth.js +0 -544
  238. package/dist/antigravity-image.d.ts +0 -11
  239. package/dist/antigravity-image.d.ts.map +0 -1
  240. package/dist/antigravity-image.js +0 -285
  241. package/dist/chatgpt-auth.d.ts.map +0 -1
  242. package/dist/cli.d.ts +0 -3
  243. package/dist/cli.d.ts.map +0 -1
  244. package/dist/credentials.d.ts.map +0 -1
  245. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  246. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  247. package/dist/gateway-model-candidates.generated.js +0 -64
  248. package/dist/login.d.ts.map +0 -1
  249. package/dist/model-catalog.d.ts.map +0 -1
  250. package/dist/models.d.ts.map +0 -1
  251. package/dist/open-browser.d.ts.map +0 -1
  252. package/dist/subscription.d.ts.map +0 -1
  253. package/dist/video-model-catalog.d.ts.map +0 -1
  254. package/dist/xai-auth.d.ts.map +0 -1
  255. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  256. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  257. /package/dist/{credentials.d.ts → cli/credentials.d.ts} +0 -0
  258. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  259. /package/dist/{login.js → cli/login.js} +0 -0
  260. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  261. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  262. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  263. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  264. /package/dist/{video-model-catalog.d.ts → cli/video-model-catalog.d.ts} +0 -0
  265. /package/dist/{video-model-catalog.js → cli/video-model-catalog.js} +0 -0
  266. /package/dist/{xai-auth.d.ts → cli/xai-auth.d.ts} +0 -0
  267. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  268. /package/src/{login.ts → cli/login.ts} +0 -0
  269. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  270. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
  271. /package/src/{video-model-catalog.ts → cli/video-model-catalog.ts} +0 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Vite plugin for the video framework.
3
+ *
4
+ * Discovers MDX files in the project root and serves each as a page route.
5
+ * The default entry (video.mdx or index.mdx) is served at /; others at
6
+ * /<relative-path-without-extension>. Auto-injects spiceflow + react plugins.
7
+ *
8
+ * Usage in vite.config.ts:
9
+ * import { video } from 'egaki/vite'
10
+ * export default defineConfig({ plugins: [video()] })
11
+ * // or with explicit default entry:
12
+ * export default defineConfig({ plugins: [video({ entry: './video.mdx' })] })
13
+ */
14
+
15
+ import path from 'node:path'
16
+ import fs from 'node:fs'
17
+ import { createRequire } from 'node:module'
18
+ import { fileURLToPath } from 'node:url'
19
+ import type { Plugin, PluginOption } from 'vite'
20
+ import { spiceflowPlugin } from 'spiceflow/vite'
21
+ import react from '@vitejs/plugin-react'
22
+ import tailwindcss from '@tailwindcss/vite'
23
+ import { mdxParse } from 'safe-mdx/parse'
24
+ import { collectServerImportSources } from './server-mdx.ts'
25
+ import { parseFrontmatter } from './mdx-parse.ts'
26
+
27
+ // Resolve the package src/ directory from this file's location.
28
+ // Used for resolve.alias so the RSC module runner can resolve relative
29
+ // imports from app.tsx (same pattern as egaki/vite).
30
+ const __srcDir = fileURLToPath(new URL('.', import.meta.url))
31
+ const APP_SRC_PATH = path.join(__srcDir, 'app.tsx')
32
+
33
+ const VIRTUAL_APP = 'virtual:egaki-app'
34
+ const RESOLVED_APP = '\0' + VIRTUAL_APP
35
+
36
+ const VIRTUAL_MDX = 'virtual:egaki-mdx'
37
+ const RESOLVED_MDX = '\0' + VIRTUAL_MDX
38
+
39
+ const VIRTUAL_MODULES = 'virtual:egaki-modules'
40
+ const RESOLVED_MODULES = '\0' + VIRTUAL_MODULES
41
+
42
+
43
+
44
+ const PKG_NAME = 'egaki'
45
+
46
+ export interface VideoPluginOptions {
47
+ /** Path to the default MDX entry file (relative to vite root or absolute).
48
+ * When omitted, auto-discovers: video.mdx > index.mdx > first .mdx found.
49
+ * All other .mdx files in the project become additional routes. */
50
+ entry?: string
51
+ }
52
+
53
+ /** Resolve a relative MDX import source against the project root,
54
+ * probing common extensions. Returns the absolute path or undefined. */
55
+ function resolveSourceToFile(root: string, source: string): string | undefined {
56
+ const base = path.resolve(root, source)
57
+ for (const ext of ['', '.tsx', '.ts', '.jsx', '.js', '.mdx', '.md']) {
58
+ const candidate = base + ext
59
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
60
+ return candidate.replace(/\\/g, '/')
61
+ }
62
+ }
63
+ return undefined
64
+ }
65
+
66
+ /**
67
+ * Extract raw source text ranges per section directly from the mdast.
68
+ * Works with heading nodes' positions to capture the FULL raw heading
69
+ * text including props like `duration=3s` and `transition=20` that
70
+ * `splitIntoSections` strips during parsing. Each section spans from
71
+ * its heading's start offset to the next heading's start (or EOF).
72
+ */
73
+ function sectionRawTexts(source: string, mdast: any): string[] {
74
+ // Collect the start offsets of each heading node
75
+ const headingStarts: number[] = []
76
+ for (const node of mdast.children) {
77
+ if (node.type === 'heading' && node.position?.start?.offset != null) {
78
+ headingStarts.push(node.position.start.offset)
79
+ }
80
+ }
81
+ // Slice source between consecutive headings, trimming trailing whitespace
82
+ // so boundary changes (adding/removing a following section) don't cause
83
+ // false positives on the preceding section.
84
+ return headingStarts.map((start, i) => {
85
+ const end = i + 1 < headingStarts.length ? headingStarts[i + 1]! : source.length
86
+ return source.slice(start, end).trimEnd()
87
+ })
88
+ }
89
+
90
+ /**
91
+ * Compare old and new MDX sources section-by-section. Returns the index
92
+ * of the first added or edited section, or null if nothing changed (or
93
+ * only deletions occurred, which we skip per design).
94
+ */
95
+ export function findChangedSectionIndex(oldSource: string, newSource: string): number | null {
96
+ try {
97
+ const oldTexts = sectionRawTexts(oldSource, mdxParse(oldSource))
98
+ const newTexts = sectionRawTexts(newSource, mdxParse(newSource))
99
+
100
+ const maxLen = Math.max(oldTexts.length, newTexts.length)
101
+ for (let i = 0; i < maxLen; i++) {
102
+ if (i >= oldTexts.length) return i // new section added
103
+ if (i >= newTexts.length) return null // section deleted, skip
104
+ if (oldTexts[i] !== newTexts[i]) {
105
+ // Distinguish edit from deletion: if the next old section matches
106
+ // the current new section, a section was deleted (shifted down).
107
+ // Look ahead to detect deletions at any position, not just the tail.
108
+ if (newTexts.length < oldTexts.length && oldTexts[i + 1] === newTexts[i]) {
109
+ return null // deletion, skip
110
+ }
111
+ return i // content changed
112
+ }
113
+ }
114
+ return null // no scene-level change (frontmatter/preamble only)
115
+ } catch {
116
+ return null // parse error, don't seek
117
+ }
118
+ }
119
+
120
+ /** Discover .mdx files in the project root directory (non-recursive).
121
+ * Only root-level files become entries to avoid import resolution issues
122
+ * with nested paths. Returns a map of routePath → absolutePath. */
123
+ function discoverMdxEntries(root: string): Map<string, string> {
124
+ const entries = new Map<string, string>()
125
+ if (!fs.existsSync(root)) return entries
126
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
127
+ if (entry.isDirectory()) continue
128
+ if (!/\.mdx$/.test(entry.name)) continue
129
+ const fullPath = path.join(root, entry.name)
130
+ const routePath = entry.name.replace(/\.mdx$/, '')
131
+ entries.set(routePath, fullPath.replace(/\\/g, '/'))
132
+ }
133
+ return entries
134
+ }
135
+
136
+ /** Pick the default entry route from discovered entries.
137
+ * Priority: explicit option > video.mdx > index.mdx > first alphabetically. */
138
+ function resolveDefaultRoute(entries: Map<string, string>, explicitEntry?: string, root?: string): string {
139
+ if (explicitEntry && root) {
140
+ const absEntry = path.isAbsolute(explicitEntry)
141
+ ? explicitEntry.replace(/\\/g, '/')
142
+ : path.resolve(root, explicitEntry).replace(/\\/g, '/')
143
+ for (const [route, absPath] of entries) {
144
+ if (absPath === absEntry) return route
145
+ }
146
+ }
147
+ if (entries.has('video')) return 'video'
148
+ if (entries.has('index')) return 'index'
149
+ // First alphabetically
150
+ const sorted = [...entries.keys()].sort()
151
+ return sorted[0] ?? 'video'
152
+ }
153
+
154
+ export function video(options?: VideoPluginOptions): PluginOption[] {
155
+ let root: string
156
+ /** All discovered MDX entry files: routePath → absolutePath */
157
+ let mdxEntries: Map<string, string> = new Map()
158
+ /** Route path of the default entry (served at /) */
159
+ let defaultRoute: string = ''
160
+ /** Whether the user's project has `motion` (framer-motion) installed. */
161
+ let hasMotion = false
162
+ /** Cached previous MDX sources for section-level diff detection, keyed by abs path. */
163
+ const previousMdxSources: Map<string, string> = new Map()
164
+ /** Set of absolute paths of all entry MDX files for quick lookup. */
165
+ let entryPathSet: Set<string> = new Set()
166
+
167
+ /** Is this file referenced inside a <Server> block of any entry MDX?
168
+ * Parsed on demand (no cache — file changes are rare and parsing is
169
+ * milliseconds). Used to decide which edits need an rsc:update. */
170
+ const isServerImportedFile = (file: string): boolean => {
171
+ try {
172
+ for (const absPath of entryPathSet) {
173
+ const sources = collectServerImportSources(mdxParse(fs.readFileSync(absPath, 'utf-8')))
174
+ if (sources.some((source) => resolveSourceToFile(root, source) === file)) return true
175
+ }
176
+ return false
177
+ } catch {
178
+ return false
179
+ }
180
+ }
181
+
182
+ const videoPlugin: Plugin = {
183
+ name: 'egaki:core',
184
+
185
+ configResolved(config) {
186
+ root = config.root
187
+
188
+ // Discover all MDX entries in the project root.
189
+ mdxEntries = discoverMdxEntries(root)
190
+
191
+ // If explicit entry is provided but not found, create a minimal entry
192
+ // in the map (will error below if file doesn't exist).
193
+ if (options?.entry) {
194
+ const absEntry = (path.isAbsolute(options.entry)
195
+ ? options.entry
196
+ : path.resolve(root, options.entry)).replace(/\\/g, '/')
197
+ if (!fs.existsSync(absEntry)) {
198
+ throw new Error(
199
+ `[egaki] entry file not found: ${absEntry}\n` +
200
+ `Set entry to a path relative to the vite root.`,
201
+ )
202
+ }
203
+ // Ensure explicit entry is in the map
204
+ const relPath = path.relative(root, absEntry).replace(/\\/g, '/')
205
+ const routePath = relPath.replace(/\.mdx$/, '')
206
+ mdxEntries.set(routePath, absEntry)
207
+ }
208
+
209
+ if (mdxEntries.size === 0) {
210
+ throw new Error(
211
+ `[egaki] no .mdx files found in ${root}\n` +
212
+ `Create a video.mdx file or set entry explicitly.`,
213
+ )
214
+ }
215
+
216
+ defaultRoute = resolveDefaultRoute(mdxEntries, options?.entry, root)
217
+ entryPathSet = new Set(mdxEntries.values())
218
+
219
+ // Seed previous sources for section-level diff detection.
220
+ for (const [, absPath] of mdxEntries) {
221
+ try {
222
+ previousMdxSources.set(absPath, fs.readFileSync(absPath, 'utf-8'))
223
+ } catch { /* ignore read errors */ }
224
+ }
225
+
226
+ // Auto-generate egaki-env.d.ts so MDX LSP knows about built-in
227
+ // components via the global MDXProvidedComponents type. Same
228
+ // pattern Vite uses for vite-env.d.ts.
229
+ const envDtsPath = path.join(root, 'egaki-env.d.ts')
230
+ const envDtsContent = 'import \'egaki/mdx-components\'\n'
231
+ try {
232
+ const existing = fs.existsSync(envDtsPath) ? fs.readFileSync(envDtsPath, 'utf-8') : ''
233
+ if (existing !== envDtsContent) {
234
+ fs.writeFileSync(envDtsPath, envDtsContent)
235
+ }
236
+ } catch {
237
+ // Non-fatal: LSP autocomplete just won't work
238
+ }
239
+
240
+ // Detect if the user has `motion` (framer-motion) installed.
241
+ // When present, we inject timing patches so motion.div animations
242
+ // sync with Remotion's frame-based rendering.
243
+ try {
244
+ createRequire(root + '/').resolve('motion-dom')
245
+ hasMotion = true
246
+ } catch {
247
+ hasMotion = false
248
+ }
249
+ },
250
+
251
+ resolveId(id) {
252
+ if (id === VIRTUAL_APP) return RESOLVED_APP
253
+ if (id === VIRTUAL_MDX) return RESOLVED_MDX
254
+ if (id === VIRTUAL_MODULES) return RESOLVED_MODULES
255
+ },
256
+
257
+ load(id) {
258
+ if (id === RESOLVED_MDX) {
259
+ // Import all entry MDX files as raw strings (?raw for HMR tracking).
260
+ // Export an entries map so app.tsx can serve each at its route.
261
+ const defaultAbsPath = mdxEntries.get(defaultRoute)!
262
+ // Parse frontmatter from the default entry for composition dimensions.
263
+ let fm: ReturnType<typeof parseFrontmatter>
264
+ try {
265
+ const mdxContent = fs.readFileSync(defaultAbsPath, 'utf-8')
266
+ fm = parseFrontmatter(mdxParse(mdxContent))
267
+ } catch (e) {
268
+ console.error('[egaki] frontmatter parse error:', e)
269
+ fm = { fps: 30, bpm: 120, width: 1920, height: 1080, scale: 1 }
270
+ }
271
+ const folderName = path.basename(root)
272
+
273
+ const imports: string[] = []
274
+ const entriesObj: string[] = []
275
+ const pathsObj: string[] = []
276
+ let i = 0
277
+ for (const [routePath, absPath] of mdxEntries) {
278
+ const varName = `__entry${i++}`
279
+ imports.push(`import ${varName} from ${JSON.stringify(absPath + '?raw')}`)
280
+ entriesObj.push(` ${JSON.stringify(routePath)}: ${varName}`)
281
+ pathsObj.push(` ${JSON.stringify(routePath)}: ${JSON.stringify(absPath)}`)
282
+ }
283
+
284
+ return [
285
+ ...imports,
286
+ `export const entries = {`,
287
+ entriesObj.join(',\n'),
288
+ `}`,
289
+ `export const entryPaths = {`,
290
+ pathsObj.join(',\n'),
291
+ `}`,
292
+ `export const defaultRoute = ${JSON.stringify(defaultRoute)}`,
293
+ `export const projectRoot = ${JSON.stringify(root.replace(/\\/g, '/'))}`,
294
+ `export const compositionWidth = ${fm.width}`,
295
+ `export const compositionHeight = ${fm.height}`,
296
+ `export const folderName = ${JSON.stringify(folderName)}`,
297
+ // Backward compat: default export is the default entry source
298
+ `export default entries[${JSON.stringify(defaultRoute)}]`,
299
+ `export const entryPath = entryPaths[${JSON.stringify(defaultRoute)}]`,
300
+ ].join('\n')
301
+ }
302
+
303
+ if (id === RESOLVED_MODULES) {
304
+ // Build an eager module map for all user files in the project
305
+ // root. Each file is imported statically so modules are available
306
+ // synchronously — no async resolution, no loading state.
307
+ //
308
+ // This map is only imported by the client (and ssr) — the rsc env
309
+ // resolves <Server> slot modules via dynamic imports in app.tsx.
310
+ // *.server.{ts,tsx} files are excluded: that postfix is the hard
311
+ // "never bundle to the browser" guarantee for files with API keys
312
+ // or node-only imports.
313
+ const imports: string[] = []
314
+ const entries: string[] = []
315
+ let i = 0
316
+ const walkDir = (dir: string) => {
317
+ if (!fs.existsSync(dir)) return
318
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
319
+ if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'e2e' || entry.name === 'test-results' || entry.name.startsWith('.')) continue
320
+ const fullPath = path.join(dir, entry.name)
321
+ if (entry.isDirectory()) {
322
+ walkDir(fullPath)
323
+ } else if (/\.(tsx?|jsx?|mdx?)$/.test(entry.name) && !/\.(test|spec|config)\./.test(entry.name)) {
324
+ // Entry MDX files are NOT skipped: they may be imported as
325
+ // partials by other MDX files (e.g. `import Intro from './intro.mdx'`).
326
+ // Self-import is a user error and harmless (safe-mdx handles it).
327
+ if (/\.server\.[jt]sx?$/.test(entry.name)) continue
328
+ const isMdx = /\.mdx?$/.test(entry.name)
329
+ const relPath = './' + path.relative(root, fullPath).replace(/\\/g, '/')
330
+ const absPath = fullPath.replace(/\\/g, '/')
331
+ const varName = `__mod${i++}`
332
+ if (isMdx) {
333
+ // MDX/MD files loaded as raw strings for client rendering
334
+ imports.push(`import ${varName} from ${JSON.stringify(absPath + '?raw')}`)
335
+ entries.push(` ${JSON.stringify(relPath)}: { default: ${varName} }`)
336
+ } else {
337
+ imports.push(`import * as ${varName} from ${JSON.stringify(absPath)}`)
338
+ entries.push(` ${JSON.stringify(relPath)}: ${varName}`)
339
+ }
340
+ }
341
+ }
342
+ }
343
+ walkDir(root)
344
+
345
+ // No self-accept here: mdx-client.tsx accepts updates of this
346
+ // module via import.meta.hot.accept('virtual:egaki-modules', cb).
347
+ // When a user file changes, HMR propagates through this module to
348
+ // that boundary, re-executing this module with fresh imports and
349
+ // handing the new map to the callback. Self-accepting here would
350
+ // make THIS module the boundary and the importer callback would
351
+ // never fire.
352
+ // When motion is installed, import motion-timing.ts as a
353
+ // side-effect so JSAnimation patches run before any user component.
354
+ // resolve.dedupe (set in configEnvironment) ensures motion-dom
355
+ // resolves to the same instance that motion/react uses.
356
+ const motionTimingPath = path.join(__srcDir, 'motion-timing.ts').replace(/\\/g, '/')
357
+ const motionImport = hasMotion ? [`import ${JSON.stringify(motionTimingPath)}`] : []
358
+
359
+ return [
360
+ ...motionImport,
361
+ ...imports,
362
+ `export const eagerModules = {`,
363
+ entries.join(',\n'),
364
+ `}`,
365
+ ].join('\n')
366
+ }
367
+
368
+ if (id === RESOLVED_APP) {
369
+ // Spiceflow entry: import the framework's app from its absolute
370
+ // source path so the RSC module runner resolves relative imports
371
+ // (./mdx-parse.ts etc.) from the correct filesystem directory.
372
+ return [
373
+ `import { app } from ${JSON.stringify(APP_SRC_PATH)}`,
374
+ `export { app }`,
375
+ ].join('\n')
376
+ }
377
+ },
378
+
379
+ // HMR for file changes in the project.
380
+ //
381
+ // Entry MDX: the source string flows server → client through the RSC
382
+ // flight payload, so invalidate the virtual modules in all envs and
383
+ // send rsc:update to re-fetch the flight.
384
+ //
385
+ // User .tsx/.ts/.mdx/.css files: handled in the client module graph
386
+ // (Fast Refresh for components, dep-accept in mdx-client.tsx for the
387
+ // rest) AND via rsc:update, because <Server> slots are rendered in the
388
+ // rsc env from the same files — the flight refetch delivers fresh
389
+ // slots. On rsc/ssr envs we invalidate the changed modules manually
390
+ // and return [] to suppress default HMR, which would trigger an SSR
391
+ // "program reload" → full page reload.
392
+ //
393
+ // File create/delete: the generated module list changed and no accept
394
+ // chain exists for new files, so invalidate everything + full reload.
395
+ hotUpdate(ctx) {
396
+ const normalizedFile = ctx.file.replace(/\\/g, '/')
397
+ const isEntryMdx = entryPathSet.has(normalizedFile)
398
+ const isImportedMdx = /\.mdx?$/.test(ctx.file)
399
+ && !isEntryMdx
400
+ && !ctx.file.includes('node_modules')
401
+ && ctx.file.startsWith(root)
402
+ const isUserFile = /\.[jt]sx?$/.test(ctx.file)
403
+ && !ctx.file.includes('node_modules')
404
+ && ctx.file.startsWith(root)
405
+ const isCss = /\.css$/.test(ctx.file)
406
+ && !ctx.file.includes('node_modules')
407
+ && ctx.file.startsWith(root)
408
+
409
+ if (!isEntryMdx && !isImportedMdx && !isUserFile && !isCss) return
410
+
411
+ const invalidateVirtual = (ids: string[]) => {
412
+ for (const env of Object.values(ctx.server.environments)) {
413
+ for (const resolvedId of ids) {
414
+ const mod = env.moduleGraph.getModuleById(resolvedId)
415
+ if (mod) {
416
+ env.moduleGraph.invalidateModule(mod)
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ // Create/delete: regenerate module list and entry map, full reload.
423
+ if (ctx.type !== 'update') {
424
+ // Re-discover entries in case a new MDX file was added or removed.
425
+ mdxEntries = discoverMdxEntries(root)
426
+ if (options?.entry) {
427
+ const absEntry = (path.isAbsolute(options.entry)
428
+ ? options.entry
429
+ : path.resolve(root, options.entry)).replace(/\\/g, '/')
430
+ const relPath = path.relative(root, absEntry).replace(/\\/g, '/')
431
+ mdxEntries.set(relPath.replace(/\.mdx$/, ''), absEntry)
432
+ }
433
+ defaultRoute = resolveDefaultRoute(mdxEntries, options?.entry, root)
434
+ const nextEntryPathSet = new Set(mdxEntries.values())
435
+
436
+ // Sync previousMdxSources: seed new entries, prune removed ones.
437
+ for (const file of previousMdxSources.keys()) {
438
+ if (!nextEntryPathSet.has(file)) previousMdxSources.delete(file)
439
+ }
440
+ for (const file of nextEntryPathSet) {
441
+ if (!previousMdxSources.has(file)) {
442
+ try { previousMdxSources.set(file, fs.readFileSync(file, 'utf-8')) }
443
+ catch { /* file may be mid-write */ }
444
+ }
445
+ }
446
+ entryPathSet = nextEntryPathSet
447
+
448
+ invalidateVirtual([RESOLVED_APP, RESOLVED_MDX, RESOLVED_MODULES])
449
+ if (this.environment.name === 'client') {
450
+ ctx.server.environments.client?.hot.send({ type: 'full-reload' })
451
+ }
452
+ return []
453
+ }
454
+
455
+ if (isEntryMdx) {
456
+ invalidateVirtual([RESOLVED_APP, RESOLVED_MDX])
457
+
458
+ if (this.environment.name === 'client') {
459
+ // Section-level diff detection for the changed entry.
460
+ const newMdxSource = fs.readFileSync(normalizedFile, 'utf-8')
461
+ const prevSource = previousMdxSources.get(normalizedFile)
462
+ const changedSection = prevSource != null
463
+ ? findChangedSectionIndex(prevSource, newMdxSource)
464
+ : null
465
+ previousMdxSources.set(normalizedFile, newMdxSource)
466
+
467
+ ctx.server.environments.client?.hot.send({
468
+ type: 'custom',
469
+ event: 'rsc:update',
470
+ data: { file: ctx.file },
471
+ })
472
+ if (changedSection != null) {
473
+ ctx.server.environments.client?.hot.send({
474
+ type: 'custom',
475
+ event: 'egaki:scene-changed',
476
+ data: { sectionIndex: changedSection, file: normalizedFile },
477
+ })
478
+ }
479
+ }
480
+ return []
481
+ }
482
+
483
+ // User file / imported MDX / CSS updates.
484
+ // Client env: let default HMR run (Fast Refresh for components,
485
+ // dep-accept propagation through virtual:egaki-modules for the rest).
486
+ if (this.environment.name === 'client') {
487
+ return
488
+ }
489
+
490
+ // rsc/ssr envs: keep graphs fresh for the next render, but suppress
491
+ // default HMR (would cause a full program reload).
492
+ invalidateVirtual([RESOLVED_APP, RESOLVED_MODULES])
493
+ for (const mod of ctx.modules) {
494
+ this.environment.moduleGraph.invalidateModule(mod)
495
+ }
496
+
497
+ // Edits to files referenced inside <Server> (or *.server.* postfix)
498
+ // send rsc:update: <Server> slots render in the rsc env, so the
499
+ // flight must be refetched for fresh slot content. Sent from the
500
+ // rsc branch AFTER invalidation so the browser's refetch cannot
501
+ // race a stale rsc module graph. The refetch remounts the client
502
+ // tree (spiceflow payload swap resets the Player to frame 0), so it
503
+ // must NOT fire for regular files — those are covered by
504
+ // client-graph HMR which preserves player state.
505
+ if (this.environment.name === 'rsc') {
506
+ const file = ctx.file.replace(/\\/g, '/')
507
+ if (/\.server\.[jt]sx?$/.test(file) || isServerImportedFile(file)) {
508
+ ctx.server.environments.client?.hot.send({
509
+ type: 'custom',
510
+ event: 'rsc:update',
511
+ data: { file: ctx.file },
512
+ })
513
+ }
514
+ }
515
+ return []
516
+ },
517
+ }
518
+
519
+ // Keep the video package inside the RSC/SSR transform pipeline
520
+ const rscPackagePlugin: Plugin = {
521
+ name: 'egaki:rsc-package',
522
+ configEnvironment(name, config) {
523
+ // noExternal: keep package in transform pipeline for all environments
524
+ config.resolve ??= {}
525
+ const existing = config.resolve.noExternal
526
+ if (existing === true) return
527
+ const arr = Array.isArray(existing) ? existing : existing ? [existing] : []
528
+ arr.push(new RegExp(`^${PKG_NAME}`))
529
+ arr.push(/^tweakpane/)
530
+ config.resolve.noExternal = arr
531
+
532
+ // Deduplicate motion packages so egaki's motion-timing virtual module
533
+ // patches the same JSAnimation class that motion/react uses. Without
534
+ // this, Vite can resolve motion-dom to different instances and
535
+ // prototype patches won't affect the user's animations.
536
+ // Always applied (not gated by hasMotion) because configEnvironment
537
+ // runs before configResolved where hasMotion is set. Harmless when
538
+ // motion isn't installed — dedupe on missing packages is a no-op.
539
+ config.resolve.dedupe = mergeUnique(
540
+ config.resolve.dedupe as string[] | undefined,
541
+ ['motion', 'motion-dom', 'motion-utils'],
542
+ )
543
+
544
+ if (name === 'client') {
545
+ config.optimizeDeps ??= {}
546
+ config.optimizeDeps.exclude = mergeUnique(
547
+ config.optimizeDeps.exclude,
548
+ [PKG_NAME],
549
+ )
550
+ config.optimizeDeps.include = mergeUnique(
551
+ config.optimizeDeps.include,
552
+ [
553
+ `${PKG_NAME} > spiceflow > @vitejs/plugin-rsc/vendor/react-server-dom/client.browser`,
554
+ `${PKG_NAME} > remotion`,
555
+ `${PKG_NAME} > @remotion/player`,
556
+ `${PKG_NAME} > safe-mdx`,
557
+ ],
558
+ )
559
+ }
560
+
561
+ if (name === 'rsc' || name === 'ssr') {
562
+ config.optimizeDeps ??= {}
563
+ config.optimizeDeps.exclude = mergeUnique(
564
+ config.optimizeDeps.exclude,
565
+ ['spiceflow'],
566
+ )
567
+ }
568
+ },
569
+ }
570
+
571
+ return [
572
+ videoPlugin,
573
+ rscPackagePlugin,
574
+ tailwindcss(),
575
+ spiceflowPlugin({ entry: VIRTUAL_APP }),
576
+ react(),
577
+ ]
578
+ }
579
+
580
+ function mergeUnique(existing: string[] | undefined, items: string[]): string[] {
581
+ const set = new Set(existing ?? [])
582
+ for (const item of items) set.add(item)
583
+ return [...set]
584
+ }