egaki 0.6.0 → 0.7.1

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