@effing/create 0.15.0 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effing/create",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Create a new Effing project",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.js",
@@ -1,3 +1,8 @@
1
+ import path from "node:path";
1
2
  import { createSatoriPool } from "@effing/satori/pool";
2
3
 
3
- export const satoriPool = createSatoriPool();
4
+ export const satoriPool = createSatoriPool(
5
+ process.env.NODE_ENV === "production"
6
+ ? { workerFile: path.resolve("build/satori.mjs") }
7
+ : undefined,
8
+ );
@@ -57,52 +57,72 @@ type ActionResult =
57
57
  | { intent: "reload"; success: false; error: string };
58
58
 
59
59
  type RenderState =
60
- | { step: "idle" }
61
- | { step: "started"; startedAt: number }
60
+ | { step: "idle"; error: string | null }
61
+ | { step: "starting"; startedAt: number }
62
62
  | {
63
- step: "ready";
63
+ step: "streaming";
64
64
  startedAt: number;
65
65
  videoUrl: string;
66
66
  scale: number;
67
- downloadUrl?: string;
68
67
  }
69
68
  | {
70
- step: "done";
69
+ step: "playing";
71
70
  startedAt: number;
72
71
  videoUrl: string;
73
72
  scale: number;
74
73
  playbackAt: number;
75
- downloadUrl?: string;
74
+ }
75
+ | {
76
+ step: "done";
77
+ startedAt: number;
78
+ videoUrl: string;
79
+ scale: number;
80
+ playbackAt: number | null;
81
+ downloadUrl: string;
76
82
  };
77
83
 
78
84
  type RenderAction =
79
85
  | { type: "start" }
80
- | { type: "ready"; videoUrl: string; scale: number }
86
+ | { type: "stream"; videoUrl: string; scale: number }
81
87
  | { type: "play" }
82
- | { type: "error" }
83
- | { type: "buffered"; downloadUrl: string };
88
+ | { type: "finish"; downloadUrl: string }
89
+ | { type: "error"; error?: string };
90
+
91
+ const INITIAL_RENDER_STATE: RenderState = { step: "idle", error: null };
84
92
 
85
93
  function renderReducer(state: RenderState, action: RenderAction): RenderState {
86
94
  switch (action.type) {
87
95
  case "start":
88
- return { step: "started", startedAt: Date.now() };
89
- case "ready":
90
- if (state.step !== "started") return state;
96
+ return { step: "starting", startedAt: Date.now() };
97
+ case "stream":
98
+ if (state.step !== "starting") return state;
91
99
  return {
92
- step: "ready",
100
+ step: "streaming",
93
101
  startedAt: state.startedAt,
94
102
  videoUrl: action.videoUrl,
95
103
  scale: action.scale,
96
104
  };
97
105
  case "play":
98
- if (state.step !== "ready") return state;
99
- return { ...state, step: "done", playbackAt: Date.now() };
100
- case "buffered":
101
- if (state.step === "ready" || state.step === "done")
106
+ if (state.step === "streaming")
107
+ return { ...state, step: "playing", playbackAt: Date.now() };
108
+ if (state.step === "done" && state.playbackAt === null)
109
+ return { ...state, playbackAt: Date.now() };
110
+ return state;
111
+ case "finish":
112
+ if (state.step === "streaming")
113
+ return {
114
+ ...state,
115
+ step: "done",
116
+ playbackAt: null,
117
+ downloadUrl: action.downloadUrl,
118
+ };
119
+ if (state.step === "playing")
120
+ return { ...state, step: "done", downloadUrl: action.downloadUrl };
121
+ if (state.step === "done")
102
122
  return { ...state, downloadUrl: action.downloadUrl };
103
123
  return state;
104
124
  case "error":
105
- return { step: "idle" };
125
+ return { step: "idle", error: action.error ?? "Render failed" };
106
126
  }
107
127
  }
108
128
 
@@ -296,25 +316,32 @@ export default function EffiePreviewPage() {
296
316
  const actionData = useActionData<typeof action>();
297
317
  const navigation = useNavigation();
298
318
 
299
- const [render, dispatch] = useReducer(renderReducer, { step: "idle" });
319
+ const [render, dispatch] = useReducer(renderReducer, INITIAL_RENDER_STATE);
300
320
  const [elapsedToPlay, setElapsedToPlay] = useState<number | null>(null);
301
321
  const prevDownloadUrlRef = useRef<string | null>(null);
302
322
 
303
323
  // Update elapsed time while rendering is in progress
304
324
  useEffect(() => {
305
- if (render.step === "idle") {
306
- setElapsedToPlay(null);
307
- return;
308
- }
309
- if (render.step === "done") {
310
- setElapsedToPlay((render.playbackAt - render.startedAt) / 1000);
311
- return;
325
+ switch (render.step) {
326
+ case "idle":
327
+ setElapsedToPlay(null);
328
+ return;
329
+ case "playing":
330
+ setElapsedToPlay((render.playbackAt - render.startedAt) / 1000);
331
+ return;
332
+ case "done":
333
+ if (render.playbackAt !== null)
334
+ setElapsedToPlay((render.playbackAt - render.startedAt) / 1000);
335
+ return;
336
+ case "starting":
337
+ case "streaming": {
338
+ const update = () =>
339
+ setElapsedToPlay((Date.now() - render.startedAt) / 1000);
340
+ update();
341
+ const interval = setInterval(update, 100);
342
+ return () => clearInterval(interval);
343
+ }
312
344
  }
313
- const update = () =>
314
- setElapsedToPlay((Date.now() - render.startedAt) / 1000);
315
- update();
316
- const interval = setInterval(update, 100);
317
- return () => clearInterval(interval);
318
345
  }, [render]);
319
346
 
320
347
  // Connect to SSE progress when render action completes
@@ -328,15 +355,23 @@ export default function EffiePreviewPage() {
328
355
  eventSource.addEventListener("ready", (e) => {
329
356
  try {
330
357
  const { videoUrl } = JSON.parse(e.data);
331
- dispatch({ type: "ready", videoUrl, scale: renderScale });
358
+ dispatch({ type: "stream", videoUrl, scale: renderScale });
332
359
  } catch {
333
360
  // Ignore parse errors
334
361
  }
335
362
  eventSource.close();
336
363
  });
337
364
 
338
- eventSource.addEventListener("error", () => {
339
- dispatch({ type: "error" });
365
+ eventSource.addEventListener("error", (e) => {
366
+ let error = "Render failed";
367
+ if (e instanceof MessageEvent) {
368
+ try {
369
+ error = JSON.parse(e.data).message;
370
+ } catch {
371
+ // use default
372
+ }
373
+ }
374
+ dispatch({ type: "error", error });
340
375
  eventSource.close();
341
376
  });
342
377
 
@@ -406,7 +441,7 @@ export default function EffiePreviewPage() {
406
441
  }
407
442
  const downloadUrl = URL.createObjectURL(blob);
408
443
  prevDownloadUrlRef.current = downloadUrl;
409
- dispatch({ type: "buffered", downloadUrl });
444
+ dispatch({ type: "finish", downloadUrl });
410
445
  };
411
446
 
412
447
  const formatSourceUrl = (url: string, maxLen = 70) => {
@@ -485,7 +520,13 @@ export default function EffiePreviewPage() {
485
520
  <EffieCoverPreview
486
521
  cover={effie.cover}
487
522
  resolution={coverResolution}
488
- video={"videoUrl" in render ? render.videoUrl : null}
523
+ video={
524
+ render.step === "streaming" ||
525
+ render.step === "playing" ||
526
+ render.step === "done"
527
+ ? render.videoUrl
528
+ : null
529
+ }
489
530
  onPlay={handleVideoPlay}
490
531
  onFullyBuffered={handleFullyBuffered}
491
532
  style={{
@@ -620,9 +661,12 @@ export default function EffiePreviewPage() {
620
661
  issues={actionData.issues}
621
662
  />
622
663
  )}
664
+ {render.step === "idle" && render.error && (
665
+ <div style={{ color: "#E44444" }}>{render.error}</div>
666
+ )}
623
667
 
624
668
  {/* Render Progress */}
625
- {(render.step === "started" || render.step === "ready") &&
669
+ {(render.step === "starting" || render.step === "streaming") &&
626
670
  elapsedToPlay !== null && (
627
671
  <div style={{ color: "#4CAE4C" }}>
628
672
  Rendering... {elapsedToPlay.toFixed(1)}s
@@ -630,41 +674,40 @@ export default function EffiePreviewPage() {
630
674
  )}
631
675
 
632
676
  {/* Render Success */}
633
- {(render.step === "ready" || render.step === "done") &&
634
- elapsedToPlay !== null && (
635
- <div
636
- style={{
637
- display: "flex",
638
- flexDirection: "column",
639
- alignItems: "flex-start",
640
- gap: "1rem",
641
- }}
642
- >
643
- {render.step === "done" && (
644
- <span style={{ color: "#4CAE4C" }}>
645
- Started playing after {elapsedToPlay.toFixed(1)}s (at{" "}
646
- {Math.round(render.scale * 100)}%)
647
- </span>
648
- )}
649
- {render.downloadUrl && (
650
- <a
651
- href={render.downloadUrl}
652
- download={`${effieId}-${width}x${height}.mp4`}
653
- style={{
654
- padding: "0.4rem 0.75rem",
655
- backgroundColor: "#fff",
656
- color: "#4CAE4C",
657
- border: "1px solid #4CAE4C",
658
- borderRadius: 4,
659
- fontSize: "14px",
660
- textDecoration: "none",
661
- }}
662
- >
663
- Download video
664
- </a>
665
- )}
666
- </div>
667
- )}
677
+ {(render.step === "playing" || render.step === "done") && (
678
+ <div
679
+ style={{
680
+ display: "flex",
681
+ flexDirection: "column",
682
+ alignItems: "flex-start",
683
+ gap: "1rem",
684
+ }}
685
+ >
686
+ {render.playbackAt !== null && elapsedToPlay !== null && (
687
+ <span style={{ color: "#4CAE4C" }}>
688
+ Started playing after {elapsedToPlay.toFixed(1)}s (at{" "}
689
+ {Math.round(render.scale * 100)}%)
690
+ </span>
691
+ )}
692
+ {render.step === "done" && (
693
+ <a
694
+ href={render.downloadUrl}
695
+ download={`${effieId}-${width}x${height}.mp4`}
696
+ style={{
697
+ padding: "0.4rem 0.75rem",
698
+ backgroundColor: "#fff",
699
+ color: "#4CAE4C",
700
+ border: "1px solid #4CAE4C",
701
+ borderRadius: 4,
702
+ fontSize: "14px",
703
+ textDecoration: "none",
704
+ }}
705
+ >
706
+ Download video
707
+ </a>
708
+ )}
709
+ </div>
710
+ )}
668
711
 
669
712
  {/* Downloading Status */}
670
713
  {warmupDownloadingItems.length > 0 && (
@@ -3,7 +3,9 @@
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
5
  "scripts": {
6
- "build": "react-router build",
6
+ "build": "run-s build:app build:satori",
7
+ "build:app": "react-router build",
8
+ "build:satori": "vite build -c vite.config.satori.ts",
7
9
  "dev": "run-p dev:*",
8
10
  "dev:app": "react-router dev",
9
11
  "dev:ffs": "ffs",
@@ -11,15 +13,16 @@
11
13
  "typecheck": "react-router typegen && tsc"
12
14
  },
13
15
  "dependencies": {
14
- "@effing/annie": "^0.15.0",
15
- "@effing/annie-player": "^0.15.0",
16
- "@effing/effie": "^0.15.0",
17
- "@effing/effie-preview": "^0.15.0",
18
- "@effing/ffs": "^0.15.0",
19
- "@effing/satori": "^0.15.0",
16
+ "@effing/annie": "^0.16.0",
17
+ "@effing/annie-player": "^0.16.0",
18
+ "@effing/effie": "^0.16.0",
19
+ "@effing/effie-preview": "^0.16.0",
20
+ "@effing/ffs": "^0.16.0",
21
+ "@effing/satori": "^0.16.0",
20
22
  "@resvg/resvg-js": "^2.6.2",
21
- "@effing/serde": "^0.15.0",
22
- "@effing/tween": "^0.15.0",
23
+ "@effing/serde": "^0.16.0",
24
+ "satori": "^0.19.2",
25
+ "@effing/tween": "^0.16.0",
23
26
  "@react-router/node": "^7.0.0",
24
27
  "@react-router/serve": "^7.0.0",
25
28
  "cross-env": "^7.0.3",
@@ -38,7 +41,7 @@
38
41
  "@types/react-dom": "^19.0.0",
39
42
  "npm-run-all": "^4.1.5",
40
43
  "typescript": "^5.9.3",
41
- "vite": "^6.0.0",
44
+ "vite": "^7.0.0",
42
45
  "vite-tsconfig-paths": "^4.3.2"
43
46
  },
44
47
  "engines": {
@@ -0,0 +1,34 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { defineConfig } from "vite";
3
+
4
+ const workerEntry = fileURLToPath(import.meta.resolve("@effing/satori/worker"));
5
+
6
+ // Second Vite build that bundles the satori worker into a single `build/satori.mjs`.
7
+ // Referenced by `app/pool.server.ts` as the worker file in production.
8
+ //
9
+ // Why bundle: The satori pool spawns worker_threads via bare `import()`. Each
10
+ // worker thread independently resolves and parses the full dependency tree on
11
+ // startup (no shared module cache with the main thread). Bundling all pure-JS
12
+ // deps into one file avoids that overhead.
13
+ //
14
+ // Why `@resvg/resvg-js` stays external: It uses a CJS runtime shim to load
15
+ // platform-specific `.node` native binaries — can't be statically bundled.
16
+ //
17
+ // Why `emptyOutDir: false`: The main app build already placed files in `build/`.
18
+ export default defineConfig({
19
+ build: {
20
+ ssr: true,
21
+ outDir: "build",
22
+ rollupOptions: {
23
+ input: workerEntry,
24
+ external: [/\.node$/, "@resvg/resvg-js"],
25
+ output: {
26
+ entryFileNames: "satori.mjs",
27
+ },
28
+ },
29
+ emptyOutDir: false,
30
+ },
31
+ ssr: {
32
+ noExternal: true,
33
+ },
34
+ });
@@ -5,10 +5,6 @@ import tsconfigPaths from "vite-tsconfig-paths";
5
5
  export default defineConfig({
6
6
  server: { port: 3839 }, // 3839 = 0xEFF, how effing cool is that? ʘ‿ʘ
7
7
  plugins: [reactRouter(), tsconfigPaths()],
8
- ssr: {
9
- noExternal: ["yoga-wasm-web", "yoga-wasm-web/asm", "satori", "emoji-regex"],
10
- external: ["@resvg/resvg-js"],
11
- },
12
8
  optimizeDeps: {
13
9
  exclude: ["@resvg/resvg-js"],
14
10
  },