egaki 0.9.0 → 0.10.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.
@@ -20,6 +20,7 @@ import type { Plugin, PluginOption } from 'vite'
20
20
  import { spiceflowPlugin } from 'spiceflow/vite'
21
21
  import react from '@vitejs/plugin-react'
22
22
  import tailwindcss from '@tailwindcss/vite'
23
+ import pc from 'picocolors'
23
24
  import { mdxParse } from 'safe-mdx/parse'
24
25
  import { collectServerImportSources } from './server-mdx.ts'
25
26
  import { parseFrontmatter } from './mdx-parse.ts'
@@ -44,6 +45,25 @@ const RESOLVED_MODULES = '\0' + VIRTUAL_MODULES
44
45
 
45
46
  const PKG_NAME = 'egaki'
46
47
 
48
+ // Packages that must resolve to a single physical copy (same pattern as
49
+ // spiceflow:dedupe-singleton). Remotion Player + useVideoConfig share React
50
+ // context; two copies → "No video config found". Motion patches need one
51
+ // JSAnimation class. resolve.dedupe alone fails under pnpm when the package
52
+ // is only a transitive dep of egaki (not hoisted to the consumer root).
53
+ const dedupePackages = new Set([
54
+ 'remotion',
55
+ '@remotion/player',
56
+ '@remotion/media',
57
+ '@remotion/web-renderer',
58
+ 'motion',
59
+ 'motion-dom',
60
+ 'motion-utils',
61
+ ])
62
+
63
+ // Fake importer anchored inside egaki's package so this.resolve() finds the
64
+ // same remotion/motion copy egaki itself depends on.
65
+ const dedupeImporter = path.join(__pkgRoot, '_dedupe_importer_.js')
66
+
47
67
  export interface VideoPluginOptions {
48
68
  /** Path to the default MDX entry file (relative to vite root or absolute).
49
69
  * When omitted, auto-discovers: video.mdx > index.mdx > first .mdx found.
@@ -258,7 +278,7 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
258
278
  const fullPath = path.join(dir, entry.name)
259
279
  if (entry.isDirectory()) {
260
280
  walkDir(fullPath)
261
- } else if (/\.(tsx?|jsx?|mdx?)$/.test(entry.name) && !/\.(test|spec|config)\./.test(entry.name)) {
281
+ } else if (/\.(tsx?|jsx?|mdx?)$/.test(entry.name) && !/\.(test|spec|config)\./.test(entry.name) && !/\.d\.ts$/.test(entry.name)) {
262
282
  // Entry MDX files are NOT skipped: they may be imported as
263
283
  // partials by other MDX files (e.g. `import Intro from './intro.mdx'`).
264
284
  // Self-import is a user error and harmless (safe-mdx handles it).
@@ -314,6 +334,52 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
314
334
  }
315
335
  },
316
336
 
337
+ // Print SDK usage instructions after Vite's startup banner.
338
+ // Agents read terminal output to learn how to interact with the
339
+ // running dev server via Playwriter + window.egakiSDK.
340
+ configureServer(server) {
341
+ server.httpServer?.on('listening', () => {
342
+ const log = server.config.logger
343
+ const p = (s: string) => log.info(s)
344
+ // Small delay so our block prints after Vite's "ready in" + URL lines
345
+ setTimeout(() => {
346
+ const title = ` ${pc.cyan(pc.bold('egaki SDK'))} ${pc.dim('—')} ${pc.white('programmatic control via')} ${pc.yellow('window.egakiSDK')}`
347
+ const sep = pc.dim(' ─'.padEnd(72, '─'))
348
+
349
+ p('')
350
+ p(title)
351
+ p(sep)
352
+ p('')
353
+ p(` ${pc.green('●')} ${pc.bold('Get info')} ${pc.dim('composition metadata, sections, fps')}`)
354
+ p(` ${pc.cyan(`playwriter -s 1 -e 'console.log(await state.page.evaluate(() => window.egakiSDK.getInfo()))'`)}`)
355
+ p('')
356
+ p(` ${pc.green('●')} ${pc.bold('Screenshot')} ${pc.dim('capture a single frame as PNG')}`)
357
+ p(` ${pc.cyan("playwriter -s 1 -e \"$(cat <<'JS'")}`)
358
+ p(` ${pc.cyan('const dataUrl = await state.page.evaluate(() => window.egakiSDK.screenshot({ frame: 60 }))')}`)
359
+ p(` ${pc.cyan('const buf = Buffer.from(await (await fetch(dataUrl)).arrayBuffer())')}`)
360
+ p(` ${pc.cyan('require("node:fs").writeFileSync("/tmp/frame-60.png", buf)')}`)
361
+ p(` ${pc.cyan('JS')}`)
362
+ p(` ${pc.cyan(')"')}`)
363
+ p('')
364
+ p(` ${pc.green('●')} ${pc.bold('Filmstrip')} ${pc.dim('grid of frames across scenes')}`)
365
+ p(` ${pc.cyan("playwriter -s 1 -e \"$(cat <<'JS'")}`)
366
+ p(` ${pc.cyan('const dataUrl = await state.page.evaluate(() =>')}`)
367
+ p(` ${pc.cyan(' window.egakiSDK.filmstrip({ scenes: [0, 1, 2], framesPerScene: 3 }))')}`)
368
+ p(` ${pc.cyan('const buf = Buffer.from(await (await fetch(dataUrl)).arrayBuffer())')}`)
369
+ p(` ${pc.cyan('require("node:fs").writeFileSync("/tmp/filmstrip.png", buf)')}`)
370
+ p(` ${pc.cyan('JS')}`)
371
+ p(` ${pc.cyan(')"')}`)
372
+ p('')
373
+ p(` ${pc.green('●')} ${pc.bold('Export video')} ${pc.dim('render MP4 via WebCodecs')}`)
374
+ p(` ${pc.cyan(`playwriter -s 1 -e 'await state.page.evaluate(() => window.egakiSDK.export({ path: "output.mp4" }))'`)}`)
375
+ p('')
376
+ p(` ${pc.green('●')} ${pc.bold('Seek')} ${pc.dim('jump to a specific frame')}`)
377
+ p(` ${pc.cyan(`playwriter -s 1 -e 'await state.page.evaluate(() => window.egakiSDK.seekTo(120))'`)}`)
378
+ p('')
379
+ }, 50)
380
+ })
381
+ },
382
+
317
383
  // HMR for file changes in the project.
318
384
  //
319
385
  // Entry MDX: the source string flows server → client through the RSC
@@ -440,16 +506,13 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
440
506
  arr.push(/^tweakpane/)
441
507
  config.resolve.noExternal = arr
442
508
 
443
- // Deduplicate motion packages so egaki's motion-timing virtual module
444
- // patches the same JSAnimation class that motion/react uses. Without
445
- // this, Vite can resolve motion-dom to different instances and
446
- // prototype patches won't affect the user's animations.
447
- // Always applied (not gated by hasMotion) because configEnvironment
448
- // runs before configResolved where hasMotion is set. Harmless when
449
- // motion isn't installed — dedupe on missing packages is a no-op.
509
+ // Baseline hint for Vite's built-in resolver (same list as the
510
+ // resolveId singleton plugin below). Real enforcement is the
511
+ // egaki:dedupe-singleton plugin — resolve.dedupe alone is not enough
512
+ // under pnpm when remotion is only a transitive dep of egaki.
450
513
  config.resolve.dedupe = mergeUnique(
451
514
  config.resolve.dedupe as string[] | undefined,
452
- ['motion', 'motion-dom', 'motion-utils'],
515
+ [...dedupePackages],
453
516
  )
454
517
 
455
518
  if (name === 'client') {
@@ -458,6 +521,14 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
458
521
  config.optimizeDeps.exclude,
459
522
  [PKG_NAME],
460
523
  )
524
+ // Prebundle safe-mdx as a single entry so its pure-CJS leaves
525
+ // (format via fault/micromark) stay internalized with CJS interop.
526
+ // Do NOT list format/fault as separate optimizeDeps entries — that
527
+ // splits them into named-only ESM chunks and the browser crashes with
528
+ // "does not provide an export named 'default'" (same bug Holocron hit
529
+ // when safe-mdx leaked to the client; see holocron vite CHANGELOG).
530
+ // Holocron's clean fix is keep safe-mdx server-only; egaki needs it on
531
+ // the client for MDX-in-browser, so correct prebundling is the fix.
461
532
  config.optimizeDeps.include = mergeUnique(
462
533
  config.optimizeDeps.include,
463
534
  [
@@ -465,6 +536,8 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
465
536
  `${PKG_NAME} > remotion`,
466
537
  `${PKG_NAME} > @remotion/player`,
467
538
  `${PKG_NAME} > safe-mdx`,
539
+ 'safe-mdx',
540
+ 'safe-mdx/parse',
468
541
  ],
469
542
  )
470
543
  }
@@ -479,9 +552,44 @@ export function video(options?: VideoPluginOptions): PluginOption[] {
479
552
  },
480
553
  }
481
554
 
555
+ // Force remotion / motion to a single copy across all importers, even when
556
+ // egaki is a transitive or file:-linked dep under pnpm. Mirrors
557
+ // spiceflow:dedupe-singleton (spiceflow/src/vite.tsx).
558
+ //
559
+ // Vite's resolve.dedupe sets basedir=root then walks node_modules/, but
560
+ // pnpm only symlinks direct deps at the root. Transitive deps stay buried
561
+ // in .pnpm/, so dedupe silently falls back to importer-based resolution
562
+ // and Player vs useVideoConfig load different remotion instances.
563
+ const dedupeSingletonPlugin: Plugin = {
564
+ name: 'egaki:dedupe-singleton',
565
+ enforce: 'pre',
566
+ async resolveId(id, _importer, options) {
567
+ if (
568
+ id.startsWith('.') ||
569
+ id.startsWith('/') ||
570
+ id.startsWith('\0') ||
571
+ id.includes('?')
572
+ ) {
573
+ return null
574
+ }
575
+
576
+ const pkgName = id.startsWith('@')
577
+ ? id.split('/').slice(0, 2).join('/')
578
+ : (id.split('/')[0] ?? id)
579
+
580
+ if (!dedupePackages.has(pkgName)) return null
581
+
582
+ return this.resolve(id, dedupeImporter, {
583
+ ...options,
584
+ skipSelf: true,
585
+ })
586
+ },
587
+ }
588
+
482
589
  return [
483
590
  videoPlugin,
484
591
  rscPackagePlugin,
592
+ dedupeSingletonPlugin,
485
593
  tailwindcss(),
486
594
  spiceflowPlugin({ entry: VIRTUAL_APP }),
487
595
  react(),