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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.10.0
6
+
7
+ 1. **New `egaki dev` command — zero-config MDX video dev server.** Serve an `.mdx` file with no project setup at all: no `package.json`, no `vite.config.ts`, no `npm install`. All dependencies (react, remotion, vite) come from the egaki CLI's own installation:
8
+
9
+ ```bash
10
+ # Serve a single MDX file, no project setup
11
+ egaki dev video.mdx
12
+
13
+ # Serve the current directory (auto-discovers video.mdx > index.mdx > first .mdx)
14
+ egaki dev
15
+
16
+ # Fixed port + open browser
17
+ egaki dev intro.mdx --port 5199 --open
18
+ ```
19
+
20
+ Uses a random free port by default; `--port`, `--host`, and `--open` are available. Under the hood a shim `node_modules` with symlinks to egaki's own dependencies is created in the folder, so Vite's dep optimizer, Tailwind scans, and even editor TypeScript resolution work in scratch folders. If the folder has a foreign `node_modules` without egaki, you get a clear error with guidance.
21
+
22
+ 2. **SDK usage instructions printed on dev server startup** — after Vite's ready banner, the dev server prints copy-pasteable `window.egakiSDK` examples (getInfo, screenshot, filmstrip, export, seekTo via Playwriter) so agents reading terminal output immediately know how to control the running player.
23
+
24
+ 3. **`<AngledScreen>` hexagonal iris bokeh + new `chromaticAberration` prop** — the depth-of-field kernel is now a sparse 6-blade hexagonal iris, so bright points form hard-edged aperture disks instead of smoothing into a gaussian. The new `chromaticAberration` prop (default `0.45`) adds a radial R/B channel split that scales with local blur, so purple/cyan fringing reads strongest in the bokeh like a real lens.
25
+
26
+ 4. **Fixed "No video config found" with duplicate remotion copies** — under pnpm, when remotion was only a transitive dep of egaki (or the app installed remotion directly while `file:`-linking egaki), two physical copies split the Player context from `useVideoConfig`. All `remotion`, `@remotion/*`, and `motion` imports now converge on a single copy.
27
+
28
+ 5. **Fixed browser crash `"does not provide an export named 'default'"`** on portable npm installs of egaki — nested pure-CJS packages under safe-mdx (format, fault, micromark chain) were split into named-only ESM chunks by the dep optimizer. They now stay internalized with correct CJS interop.
29
+
5
30
  ## 0.9.0
6
31
 
7
32
  1. **`<AngledScreen>` with true WebGL depth-of-field** — the 3D tilted screen component is now shader-based. Children DOM is captured per frame with Chromium's HTML-in-canvas API and rendered as a perspective-tilted plane in a WebGL2 fragment shader, giving true per-pixel depth. Depth drives bokeh blur (the exact Three.js BokehShader 41-tap ring kernel), fog toward `backgroundColor`, and subtle film grain. Available in MDX without imports:
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":";AAgGA,QAAA,MAAM,GAAG,yBAAgB,CAAA;AA2yCzB,OAAO,EAAE,GAAG,EAAE,CAAA"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":";AAgGA,QAAA,MAAM,GAAG,yBAAgB,CAAA;AA61CzB,OAAO,EAAE,GAAG,EAAE,CAAA"}
package/dist/cli/cli.js CHANGED
@@ -138,6 +138,45 @@ cli
138
138
  .action(async () => {
139
139
  await showUsage();
140
140
  });
141
+ // ─── dev command ─────────────────────────────────────────────────────────────
142
+ cli
143
+ .command('dev [entry]', dedent `
144
+ Start the MDX video dev server with zero setup.
145
+ Pass an .mdx file (or a directory containing one) — no package.json,
146
+ vite.config.ts, or npm install needed. All dependencies (react,
147
+ remotion, vite) come from the egaki CLI's own installation.
148
+ Uses a random free port unless --port is given.
149
+ `)
150
+ .option('-p, --port [port]', z.coerce.number().int().describe('Fixed port (default: random free port)'))
151
+ .option('--host [host]', z.string().describe('Host to bind (default: localhost)'))
152
+ .option('--open', 'Open the player in the default browser')
153
+ .example('# Serve a single MDX file, no project setup')
154
+ .example('egaki dev video.mdx')
155
+ .example('# Serve the current directory (auto-discovers video.mdx)')
156
+ .example('egaki dev')
157
+ .example('# Fixed port + open browser')
158
+ .example('egaki dev intro.mdx --port 5199 --open')
159
+ .action(async (entry, options) => {
160
+ const { startDevServer } = await import('./dev-server.js');
161
+ const result = await startDevServer({
162
+ entry,
163
+ port: options.port,
164
+ host: options.host,
165
+ });
166
+ if (result instanceof Error) {
167
+ console.error(pc.red(result.message));
168
+ process.exit(1);
169
+ }
170
+ console.log();
171
+ console.log(` ${pc.green('➜')} egaki dev server running`);
172
+ console.log(` ${pc.dim('root:')} ${result.root}`);
173
+ console.log(` ${pc.dim('url:')} ${pc.cyan(result.url)}`);
174
+ console.log();
175
+ if (options.open) {
176
+ const { openUrlInBrowser } = await import('./open-browser.js');
177
+ await openUrlInBrowser(result.url);
178
+ }
179
+ });
141
180
  // ─── image command ───────────────────────────────────────────────────────────
142
181
  cli
143
182
  .command('image <prompt>', dedent `
@@ -0,0 +1,30 @@
1
+ import type { ViteDevServer } from 'vite';
2
+ export interface StartDevServerOptions {
3
+ /** Path to an .mdx entry file, or a directory containing .mdx files.
4
+ * Defaults to the current working directory (auto-discovers video.mdx
5
+ * > index.mdx > first .mdx alphabetically). */
6
+ entry?: string;
7
+ /** Fixed port. When omitted, a random free port is chosen. */
8
+ port?: number;
9
+ /** Host to bind. Defaults to localhost. */
10
+ host?: string;
11
+ }
12
+ export interface RunningDevServer {
13
+ url: string;
14
+ port: number;
15
+ root: string;
16
+ server: ViteDevServer;
17
+ close: () => Promise<void>;
18
+ }
19
+ /** Find a random free TCP port by binding to port 0. */
20
+ export declare function findFreePort(host?: string): Promise<number>;
21
+ /** Ensure bare imports resolve from the project root.
22
+ *
23
+ * - egaki already resolvable from root → real project, do nothing
24
+ * - no node_modules (or a CLI-owned shim) → (re)create the shim
25
+ * - foreign node_modules without egaki → error with guidance
26
+ */
27
+ export declare function ensureDependencyShim(root: string): Error | 'normal' | 'shim';
28
+ /** Start the zero-config dev server. Returns Error | RunningDevServer. */
29
+ export declare function startDevServer(options?: StartDevServerOptions): Promise<Error | RunningDevServer>;
30
+ //# sourceMappingURL=dev-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/cli/dev-server.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAA;AAUzC,MAAM,WAAW,qBAAqB;IACpC;;oDAEgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,CAAA;IACrB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,wDAAwD;AACxD,wBAAsB,YAAY,CAAC,IAAI,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAStE;AAiDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CA4C5E;AAuFD,0EAA0E;AAC1E,wBAAsB,cAAc,CAClC,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,KAAK,GAAG,gBAAgB,CAAC,CA8CnC"}
@@ -0,0 +1,266 @@
1
+ // Zero-config dev server for egaki MDX videos.
2
+ //
3
+ // `egaki dev video.mdx` boots a Vite dev server with inline config
4
+ // (configFile: false) — no package.json, vite.config.ts, or npm install
5
+ // needed in the user's folder.
6
+ //
7
+ // Dependency resolution: when the folder has no usable node_modules, a
8
+ // shim node_modules is created containing symlinks to egaki itself and
9
+ // every egaki dependency (react, remotion, vite, ...) resolved from the
10
+ // CLI's own installation. A plain fallback resolveId plugin is NOT enough
11
+ // because Vite's dep optimizer resolves optimizeDeps.include entries with
12
+ // an internal resolver (alias + vite:resolve only) that skips user
13
+ // plugins — without real node_modules those includes fail and CJS deps
14
+ // like safe-mdx break in the browser. The shim makes standard node
15
+ // resolution work everywhere (vite:resolve, optimizer, tailwind scans).
16
+ // The shim dir contains a .egaki-shim marker so it can be refreshed
17
+ // idempotently and recognized as CLI-owned.
18
+ //
19
+ // Vite's cache dir is placed under ~/.cache/egaki/vite/<hash> so repeated
20
+ // runs reuse the prebundle without polluting the user's folder further.
21
+ import fs from 'node:fs';
22
+ import net from 'node:net';
23
+ import os from 'node:os';
24
+ import path from 'node:path';
25
+ import crypto from 'node:crypto';
26
+ import { createRequire } from 'node:module';
27
+ import { fileURLToPath } from 'node:url';
28
+ import { video } from "../vite/vite-plugin.js";
29
+ // Package root: this file runs from src/cli/ (dev) or dist/cli/ (published).
30
+ // Going up 2 levels reaches the egaki package root either way.
31
+ const __pkgRoot = path.resolve(fileURLToPath(new URL('.', import.meta.url)), '../..');
32
+ const egakiRequire = createRequire(path.join(__pkgRoot, 'package.json'));
33
+ const SHIM_MARKER = '.egaki-shim';
34
+ /** Find a random free TCP port by binding to port 0. */
35
+ export async function findFreePort(host = '127.0.0.1') {
36
+ return new Promise((resolve, reject) => {
37
+ const probe = net.createServer();
38
+ probe.once('error', reject);
39
+ probe.listen(0, host, () => {
40
+ const address = probe.address();
41
+ probe.close(() => resolve(address.port));
42
+ });
43
+ });
44
+ }
45
+ /** Locate the real directory of a package installed in egaki's own tree.
46
+ * Tries pkg/package.json first (works for most packages), then falls
47
+ * back to resolving the main entry and walking up to the package root
48
+ * (for packages whose exports map hides ./package.json). */
49
+ function packageDirFromEgaki(pkg) {
50
+ try {
51
+ return path.dirname(fs.realpathSync(egakiRequire.resolve(`${pkg}/package.json`)));
52
+ }
53
+ catch {
54
+ // Fall through to entry-based lookup
55
+ }
56
+ try {
57
+ let dir = path.dirname(fs.realpathSync(egakiRequire.resolve(pkg)));
58
+ while (true) {
59
+ const pkgJsonPath = path.join(dir, 'package.json');
60
+ if (fs.existsSync(pkgJsonPath)) {
61
+ const name = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')).name;
62
+ if (name === pkg)
63
+ return dir;
64
+ }
65
+ const parent = path.dirname(dir);
66
+ if (parent === dir)
67
+ return undefined;
68
+ dir = parent;
69
+ }
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ /** All packages the shim should link: egaki itself plus every dependency
76
+ * and (present) optional/peer dependency of the egaki package. */
77
+ function shimPackageDirs() {
78
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(__pkgRoot, 'package.json'), 'utf-8'));
79
+ const names = new Set([
80
+ ...Object.keys(pkgJson.dependencies ?? {}),
81
+ ...Object.keys(pkgJson.optionalDependencies ?? {}),
82
+ ...Object.keys(pkgJson.peerDependencies ?? {}),
83
+ ]);
84
+ const dirs = new Map();
85
+ dirs.set('egaki', fs.realpathSync(__pkgRoot));
86
+ for (const name of names) {
87
+ const dir = packageDirFromEgaki(name);
88
+ if (dir)
89
+ dirs.set(name, dir);
90
+ }
91
+ return dirs;
92
+ }
93
+ /** Ensure bare imports resolve from the project root.
94
+ *
95
+ * - egaki already resolvable from root → real project, do nothing
96
+ * - no node_modules (or a CLI-owned shim) → (re)create the shim
97
+ * - foreign node_modules without egaki → error with guidance
98
+ */
99
+ export function ensureDependencyShim(root) {
100
+ const nodeModulesDir = path.join(root, 'node_modules');
101
+ const isShim = fs.existsSync(path.join(nodeModulesDir, SHIM_MARKER));
102
+ if (!isShim) {
103
+ // Walk up from root looking for node_modules/egaki — a plain fs walk
104
+ // instead of createRequire().resolve() because test runners (tsx,
105
+ // vitest) patch Node's resolver and make it resolve 'egaki' from the
106
+ // wrong context, silently skipping shim creation.
107
+ let dir = root;
108
+ while (true) {
109
+ if (fs.existsSync(path.join(dir, 'node_modules', 'egaki', 'package.json'))) {
110
+ return 'normal';
111
+ }
112
+ const parent = path.dirname(dir);
113
+ if (parent === dir)
114
+ break;
115
+ dir = parent;
116
+ }
117
+ if (fs.existsSync(nodeModulesDir)) {
118
+ return new Error(`${nodeModulesDir} exists but 'egaki' is not installed there.\n` +
119
+ `Either run 'npm install egaki' in ${root}, or remove node_modules ` +
120
+ `to let 'egaki dev' manage dependencies automatically.`);
121
+ }
122
+ }
123
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
124
+ fs.writeFileSync(path.join(nodeModulesDir, SHIM_MARKER), 'This node_modules was generated by `egaki dev`. Safe to delete.\n');
125
+ for (const [name, targetDir] of shimPackageDirs()) {
126
+ const linkPath = path.join(nodeModulesDir, name);
127
+ fs.mkdirSync(path.dirname(linkPath), { recursive: true });
128
+ const existing = fs.lstatSync(linkPath, { throwIfNoEntry: false });
129
+ if (existing) {
130
+ if (!existing.isSymbolicLink())
131
+ continue;
132
+ if (fs.readlinkSync(linkPath) === targetDir)
133
+ continue;
134
+ fs.unlinkSync(linkPath);
135
+ }
136
+ fs.symlinkSync(targetDir, linkPath, 'dir');
137
+ }
138
+ return 'shim';
139
+ }
140
+ /** Walk up from a real path and collect the parent of every ancestor
141
+ * directory named node_modules. These parents are safe fs.allow roots
142
+ * that cover hoisted/pnpm-store dependency locations. */
143
+ function nodeModulesAncestorRoots(realPath) {
144
+ const roots = [];
145
+ let dir = realPath;
146
+ while (true) {
147
+ const parent = path.dirname(dir);
148
+ if (parent === dir)
149
+ break;
150
+ if (path.basename(dir) === 'node_modules')
151
+ roots.push(parent);
152
+ dir = parent;
153
+ }
154
+ return roots;
155
+ }
156
+ /** Compute fs.allow roots so Vite can serve framework + dependency files
157
+ * that live outside the user's project root (in the CLI's install tree). */
158
+ function computeFsAllow(root) {
159
+ const allow = new Set([root]);
160
+ const probes = [path.join(__pkgRoot, 'package.json')];
161
+ for (const id of ['react', 'react-dom', 'remotion', 'vite', 'spiceflow']) {
162
+ try {
163
+ probes.push(egakiRequire.resolve(id));
164
+ }
165
+ catch {
166
+ // Probe missing: skip
167
+ }
168
+ }
169
+ for (const probe of probes) {
170
+ try {
171
+ const real = fs.realpathSync(probe);
172
+ allow.add(path.dirname(real));
173
+ for (const ancestorRoot of nodeModulesAncestorRoots(real)) {
174
+ allow.add(ancestorRoot);
175
+ }
176
+ }
177
+ catch {
178
+ // Probe missing: skip
179
+ }
180
+ }
181
+ allow.add(fs.realpathSync(__pkgRoot));
182
+ return [...allow];
183
+ }
184
+ /** Resolve the entry option to { root, entryFile }.
185
+ * Accepts a file path, a directory, or nothing (cwd). */
186
+ function resolveEntry(entry) {
187
+ const target = path.resolve(process.cwd(), entry ?? '.');
188
+ if (!fs.existsSync(target)) {
189
+ return new Error(`entry not found: ${target}`);
190
+ }
191
+ const stat = fs.statSync(target);
192
+ if (stat.isDirectory()) {
193
+ const hasMdx = fs
194
+ .readdirSync(target)
195
+ .some((name) => name.endsWith('.mdx'));
196
+ if (!hasMdx) {
197
+ return new Error(`no .mdx files found in ${target}\nCreate a video.mdx file or pass one explicitly: egaki dev path/to/video.mdx`);
198
+ }
199
+ return { root: target };
200
+ }
201
+ if (!/\.mdx$/.test(target)) {
202
+ return new Error(`entry must be an .mdx file or a directory, got: ${target}`);
203
+ }
204
+ return { root: path.dirname(target), entryFile: target };
205
+ }
206
+ /** Warm up the server: the very first request can 500 transiently while
207
+ * the dep optimizer discovers new deps and reloads the ssr program (a
208
+ * browser recovers via auto-reload, but agents/tests hit the URL once).
209
+ * Retry until the page serves 200 so the returned URL is ready to use. */
210
+ async function warmUp(url, timeoutMs) {
211
+ const deadline = Date.now() + timeoutMs;
212
+ while (Date.now() < deadline) {
213
+ try {
214
+ const response = await fetch(url);
215
+ if (response.ok)
216
+ return;
217
+ }
218
+ catch {
219
+ // Server not accepting connections yet
220
+ }
221
+ await new Promise((resolve) => setTimeout(resolve, 300));
222
+ }
223
+ console.warn(`[egaki] dev server warmup did not reach 200 within ${timeoutMs}ms`);
224
+ }
225
+ /** Start the zero-config dev server. Returns Error | RunningDevServer. */
226
+ export async function startDevServer(options = {}) {
227
+ const entryResult = resolveEntry(options.entry);
228
+ if (entryResult instanceof Error)
229
+ return entryResult;
230
+ const { root, entryFile } = entryResult;
231
+ const shimResult = ensureDependencyShim(root);
232
+ if (shimResult instanceof Error)
233
+ return shimResult;
234
+ const host = options.host ?? 'localhost';
235
+ const port = options.port ?? (await findFreePort());
236
+ const rootHash = crypto.createHash('sha256').update(root).digest('hex').slice(0, 12);
237
+ const cacheDir = path.join(os.homedir(), '.cache', 'egaki', 'vite', rootHash);
238
+ const { createServer } = await import('vite');
239
+ const server = await createServer({
240
+ configFile: false,
241
+ root,
242
+ cacheDir,
243
+ plugins: [video(entryFile ? { entry: entryFile } : undefined)],
244
+ resolve: {
245
+ dedupe: ['react', 'react-dom', 'remotion', '@remotion/player', '@remotion/media'],
246
+ },
247
+ server: {
248
+ port,
249
+ strictPort: true,
250
+ host,
251
+ fs: {
252
+ allow: computeFsAllow(root),
253
+ },
254
+ },
255
+ });
256
+ await server.listen();
257
+ const url = `http://${host}:${port}/`;
258
+ await warmUp(url, 60_000);
259
+ return {
260
+ url,
261
+ port,
262
+ root,
263
+ server,
264
+ close: () => server.close(),
265
+ };
266
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dev-server.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-server.test.d.ts","sourceRoot":"","sources":["../../src/cli/dev-server.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,95 @@
1
+ // Tests for the zero-config dev server (egaki dev).
2
+ //
3
+ // Creates a temp directory containing ONLY a video.mdx file — no
4
+ // package.json, no vite.config.ts, no node_modules — and verifies the
5
+ // dev server boots, serves HTML at /, and resolves all framework deps
6
+ // from the CLI's own installation.
7
+ import { describe, test, expect, afterAll } from 'vitest';
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import dedent from 'string-dedent';
12
+ import { startDevServer, findFreePort } from "./dev-server.js";
13
+ const cleanups = [];
14
+ afterAll(async () => {
15
+ for (const cleanup of cleanups) {
16
+ await cleanup();
17
+ }
18
+ });
19
+ function makeScratchProject() {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'egaki-dev-test-'));
21
+ const MDX = dedent `
22
+ ---
23
+ width: 1920
24
+ height: 1080
25
+ fps: 30
26
+ ---
27
+
28
+ # Hello duration=2
29
+
30
+ Zero-config dev server test.
31
+ `;
32
+ fs.writeFileSync(path.join(dir, 'video.mdx'), MDX);
33
+ cleanups.push(async () => {
34
+ fs.rmSync(dir, { recursive: true, force: true });
35
+ });
36
+ return dir;
37
+ }
38
+ describe('findFreePort', () => {
39
+ test('returns a valid port number', async () => {
40
+ const port = await findFreePort();
41
+ expect(port).toBeGreaterThan(0);
42
+ expect(port).toBeLessThan(65536);
43
+ });
44
+ });
45
+ describe('startDevServer', () => {
46
+ test('returns Error for missing entry', async () => {
47
+ const result = await startDevServer({ entry: '/nonexistent/path/video.mdx' });
48
+ expect(result).toBeInstanceOf(Error);
49
+ if (result instanceof Error) {
50
+ expect(result.message).toContain('entry not found');
51
+ }
52
+ });
53
+ test('returns Error for directory without mdx files', async () => {
54
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'egaki-dev-empty-'));
55
+ cleanups.push(async () => {
56
+ fs.rmSync(dir, { recursive: true, force: true });
57
+ });
58
+ const result = await startDevServer({ entry: dir });
59
+ expect(result).toBeInstanceOf(Error);
60
+ if (result instanceof Error) {
61
+ expect(result.message).toContain('no .mdx files found');
62
+ }
63
+ });
64
+ test('returns Error for non-mdx file entry', async () => {
65
+ const dir = makeScratchProject();
66
+ const txtPath = path.join(dir, 'notes.txt');
67
+ fs.writeFileSync(txtPath, 'hi');
68
+ const result = await startDevServer({ entry: txtPath });
69
+ expect(result).toBeInstanceOf(Error);
70
+ if (result instanceof Error) {
71
+ expect(result.message).toContain('must be an .mdx file');
72
+ }
73
+ });
74
+ test('serves a scratch mdx file with no package.json or node_modules', async () => {
75
+ const dir = makeScratchProject();
76
+ const result = await startDevServer({
77
+ entry: path.join(dir, 'video.mdx'),
78
+ });
79
+ expect(result).not.toBeInstanceOf(Error);
80
+ const running = result;
81
+ cleanups.push(() => running.close());
82
+ expect(running.root).toBe(dir);
83
+ expect(running.url).toMatch(/^http:\/\/localhost:\d+\/$/);
84
+ const response = await fetch(running.url);
85
+ expect(response.status).toBe(200);
86
+ expect(response.headers.get('content-type')).toContain('text/html');
87
+ const html = await response.text();
88
+ // RSC streaming: the doctype shell arrives in a separate flush, so
89
+ // assert on stable player UI markup instead of '<html'.
90
+ expect(html).toContain('Export MP4');
91
+ // The dependency shim was created and is CLI-owned
92
+ expect(fs.existsSync(path.join(dir, 'node_modules', '.egaki-shim'))).toBe(true);
93
+ expect(fs.lstatSync(path.join(dir, 'node_modules', 'egaki')).isSymbolicLink()).toBe(true);
94
+ }, 120_000);
95
+ });
@@ -30,6 +30,11 @@ export interface AngledScreenProps {
30
30
  fog?: number;
31
31
  /** Film grain intensity. Default 0.02. */
32
32
  grainIntensity?: number;
33
+ /** Chromatic aberration strength. RGB channels separate along the radial
34
+ * axis from frame center, scaled up by local blur so fringing reads
35
+ * strongest in the bokeh (like real lens CA on the reference photos).
36
+ * 0 = off. Default 0.45. */
37
+ chromaticAberration?: number;
33
38
  /** Background color behind the tilted screen. Default '#000000'. */
34
39
  backgroundColor?: string;
35
40
  /** Width of the inner content wrapper. Default '80%'. */
@@ -1 +1 @@
1
- {"version":3,"file":"angled-screen-shader.d.ts","sourceRoot":"","sources":["../../src/vite/angled-screen-shader.tsx"],"names":[],"mappings":"AAyEA,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAKhG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAA;IAGnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;2EAEuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;mFAC+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;uEAEmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAA;IAGd,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAExB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAwND,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAuWpD"}
1
+ {"version":3,"file":"angled-screen-shader.d.ts","sourceRoot":"","sources":["../../src/vite/angled-screen-shader.tsx"],"names":[],"mappings":"AA4EA,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAKhG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAA;IAGnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;2EAEuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;mFAC+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;uEAEmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAA;IAGd,uEAAuE;IACvE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;iCAG6B;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAExB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IAEf,oDAAoD;IACpD,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAwPD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BA8WpD"}