@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,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: "
|
|
60
|
+
| { step: "idle"; error: string | null }
|
|
61
|
+
| { step: "starting"; startedAt: number }
|
|
62
62
|
| {
|
|
63
|
-
step: "
|
|
63
|
+
step: "streaming";
|
|
64
64
|
startedAt: number;
|
|
65
65
|
videoUrl: string;
|
|
66
66
|
scale: number;
|
|
67
|
-
downloadUrl?: string;
|
|
68
67
|
}
|
|
69
68
|
| {
|
|
70
|
-
step: "
|
|
69
|
+
step: "playing";
|
|
71
70
|
startedAt: number;
|
|
72
71
|
videoUrl: string;
|
|
73
72
|
scale: number;
|
|
74
73
|
playbackAt: number;
|
|
75
|
-
|
|
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: "
|
|
86
|
+
| { type: "stream"; videoUrl: string; scale: number }
|
|
81
87
|
| { type: "play" }
|
|
82
|
-
| { type: "
|
|
83
|
-
| { type: "
|
|
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: "
|
|
89
|
-
case "
|
|
90
|
-
if (state.step !== "
|
|
96
|
+
return { step: "starting", startedAt: Date.now() };
|
|
97
|
+
case "stream":
|
|
98
|
+
if (state.step !== "starting") return state;
|
|
91
99
|
return {
|
|
92
|
-
step: "
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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: "
|
|
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
|
-
|
|
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: "
|
|
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={
|
|
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 === "
|
|
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 === "
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
{
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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 && (
|
package/template/package.json
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"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
|
-
"@effing/annie-player": "^0.
|
|
16
|
-
"@effing/effie": "^0.
|
|
17
|
-
"@effing/effie-preview": "^0.
|
|
18
|
-
"@effing/ffs": "^0.
|
|
19
|
-
"@effing/satori": "^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.
|
|
22
|
-
"
|
|
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": "^
|
|
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
|
+
});
|
package/template/vite.config.ts
CHANGED
|
@@ -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
|
},
|