Skip to content

Commit 3664c2c

Browse files
committed
refactor(thumbnail): replace canvasFactory with split browser and Node exports
A bunch of issues were happening during the build due to the dynamic module importing. This commit completely separates browser and Node bundles for the `@nbw/thumbnail` package such that no Node-only dependencies slip into the client bundle.
1 parent 9d20ad8 commit 3664c2c

File tree

16 files changed

+210
-362
lines changed

16 files changed

+210
-362
lines changed

apps/backend/src/song/song-upload/song-upload.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
injectSongFileMetadata,
2323
obfuscateAndPackSong,
2424
} from '@nbw/song';
25-
import { drawToImage } from '@nbw/thumbnail';
25+
import { drawToImage } from '@nbw/thumbnail/node';
2626
import { FileService } from '@server/file/file.service';
2727
import { UserService } from '@server/user/user.service';
2828

apps/frontend/src/modules/song/components/client/ThumbnailRenderer.tsx

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,10 @@ import { useEffect, useRef, useState } from 'react';
44
import { UseFormReturn } from 'react-hook-form';
55

66
import { NoteQuadTree } from '@nbw/song';
7+
import { drawNotesOffscreen, swap } from '@nbw/thumbnail/browser';
78

89
import { UploadSongForm } from './SongForm.zod';
910

10-
// Dynamically import thumbnail functions to avoid SSR issues with HTMLCanvasElement
11-
const loadThumbnailFunctions = async () => {
12-
const thumbnail = await import('@nbw/thumbnail');
13-
return {
14-
drawNotesOffscreen: thumbnail.drawNotesOffscreen,
15-
swap: thumbnail.swap,
16-
};
17-
};
18-
1911
type ThumbnailRendererCanvasProps = {
2012
notes: NoteQuadTree;
2113
formMethods: UseFormReturn<UploadSongForm>;
@@ -28,11 +20,6 @@ export const ThumbnailRendererCanvas = ({
2820
const canvasRef = useRef<HTMLCanvasElement>(null);
2921
const drawRequest = useRef<number | null>(null);
3022
const [loading, setLoading] = useState(true);
31-
const [functionsLoaded, setFunctionsLoaded] = useState(false);
32-
const thumbnailFunctionsRef = useRef<{
33-
drawNotesOffscreen: typeof import('@nbw/thumbnail').drawNotesOffscreen;
34-
swap: typeof import('@nbw/thumbnail').swap;
35-
} | null>(null);
3623

3724
const [zoomLevel, startTick, startLayer, backgroundColor] = formMethods.watch(
3825
[
@@ -43,23 +30,6 @@ export const ThumbnailRendererCanvas = ({
4330
],
4431
);
4532

46-
// Load thumbnail functions on client side only
47-
useEffect(() => {
48-
if (typeof window === 'undefined') return;
49-
if (thumbnailFunctionsRef.current) return;
50-
51-
loadThumbnailFunctions()
52-
.then((funcs) => {
53-
thumbnailFunctionsRef.current = funcs;
54-
// Trigger a re-render to use the loaded functions
55-
setFunctionsLoaded(true);
56-
setLoading(false);
57-
})
58-
.catch((error) => {
59-
console.error('Failed to load thumbnail functions:', error);
60-
});
61-
}, []);
62-
6333
useEffect(() => {
6434
const canvas = canvasRef.current;
6535
if (!canvas) return;
@@ -76,12 +46,6 @@ export const ThumbnailRendererCanvas = ({
7646
}, []);
7747

7848
useEffect(() => {
79-
if (typeof window === 'undefined') return;
80-
if (!thumbnailFunctionsRef.current || !functionsLoaded) {
81-
setLoading(true);
82-
return;
83-
}
84-
8549
setLoading(true);
8650

8751
const canvas = canvasRef.current as HTMLCanvasElement | null;
@@ -92,11 +56,9 @@ export const ThumbnailRendererCanvas = ({
9256
cancelAnimationFrame(drawRequest.current);
9357
}
9458

95-
const { drawNotesOffscreen, swap } = thumbnailFunctionsRef.current;
96-
9759
drawRequest.current = requestAnimationFrame(async () => {
9860
try {
99-
const output = await drawNotesOffscreen({
61+
const output = (await drawNotesOffscreen({
10062
notes,
10163
startTick,
10264
startLayer,
@@ -105,7 +67,7 @@ export const ThumbnailRendererCanvas = ({
10567
canvasWidth: canvas.width,
10668
imgWidth: 1280,
10769
imgHeight: 768,
108-
});
70+
})) as OffscreenCanvas;
10971

11072
swap(output, canvas);
11173
setLoading(false);
@@ -114,14 +76,7 @@ export const ThumbnailRendererCanvas = ({
11476
setLoading(false);
11577
}
11678
});
117-
}, [
118-
notes,
119-
startTick,
120-
startLayer,
121-
zoomLevel,
122-
backgroundColor,
123-
functionsLoaded,
124-
]);
79+
}, [notes, startTick, startLayer, zoomLevel, backgroundColor]);
12580

12681
return (
12782
<div className='relative w-full'>

packages/thumbnail/package.json

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
{
22
"name": "@nbw/thumbnail",
3-
"main": "dist/index.node.js",
4-
"module": "dist/index.node.js",
5-
"types": "dist/index.d.ts",
63
"type": "module",
74
"private": true,
5+
"sideEffects": false,
86
"exports": {
9-
".": {
10-
"browser": "./dist/index.browser.js",
11-
"node": "./dist/index.node.js",
12-
"import": "./dist/index.node.js",
13-
"types": "./dist/index.d.ts",
14-
"development": "./src/index.ts"
7+
"./browser": {
8+
"import": "./dist/browser/index.js",
9+
"types": "./dist/browser/index.d.ts"
10+
},
11+
"./node": {
12+
"import": "./dist/node/index.js",
13+
"types": "./dist/node/index.d.ts"
1514
}
1615
},
1716
"scripts": {
18-
"build": "bun run clean && bun run build:node && bun run build:browser && bun run build:types",
19-
"build:node": "bun build src/index.ts --outdir dist --target node && mv dist/index.js dist/index.node.js",
20-
"build:browser": "bun build src/index.ts --outdir dist --target browser --external @napi-rs/canvas && mv dist/index.js dist/index.browser.js",
17+
"build": "bun run clean && bun run build:browser && bun run build:node && bun run build:types",
18+
"build:browser": "bun build src/browser/index.ts --outdir dist/browser --target browser --external @napi-rs/canvas",
19+
"build:node": "bun build src/node/index.ts --outdir dist/node --target node",
2120
"build:types": "tsc --project tsconfig.build.json",
2221
"clean": "rm -rf dist",
23-
"dev": "bun build src/index.ts --outdir dist --target node --watch",
22+
"dev:browser": "bun build src/browser/index.ts --outdir dist/browser --target node --watch",
23+
"dev:node": "bun build src/node/index.ts --outdir dist/node --target node --watch",
2424
"lint": "eslint \"src/**/*.ts\" --fix",
2525
"test": "jest"
2626
},
27+
"dependencies": {
28+
"@nbw/song": "workspace:*"
29+
},
30+
"optionalDependencies": {
31+
"@napi-rs/canvas": "^0.1.74"
32+
},
2733
"devDependencies": {
2834
"@types/bun": "latest",
2935
"typescript": "^5",
@@ -33,8 +39,7 @@
3339
"peerDependencies": {
3440
"typescript": "^5"
3541
},
36-
"dependencies": {
37-
"@napi-rs/canvas": "^0.1.74",
38-
"@nbw/song": "workspace:*"
42+
"engines": {
43+
"node": ">=18"
3944
}
4045
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { DrawingCanvas } from '../shared/drawTypes';
2+
3+
export const createCanvas = (w: number, h: number): DrawingCanvas =>
4+
new OffscreenCanvas(w, h) as unknown as DrawingCanvas;
5+
6+
export const loadImage = (src: string) =>
7+
new Promise<HTMLImageElement>((resolve, reject) => {
8+
const img = new Image();
9+
img.onload = () => resolve(img);
10+
img.onerror = reject;
11+
img.src = src;
12+
});
13+
14+
export const useFont = () => {
15+
const font = new FontFace('Lato', 'url(/fonts/Lato-Regular.ttf)');
16+
font.load().then((f) => document.fonts.add(f));
17+
};
18+
19+
export const noteBlockImage = loadImage('/img/note-block-grayscale.png');
20+
21+
export const saveToImage = () => {
22+
throw new Error('saveToImage not available in browser');
23+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { drawNotes } from '../shared/drawCore';
2+
import type { DrawParams } from '../shared/types';
3+
4+
import { createCanvas, noteBlockImage, useFont } from './canvas';
5+
6+
useFont();
7+
8+
export const drawNotesOffscreen = async (params: DrawParams) => {
9+
return await drawNotes(params, createCanvas, noteBlockImage);
10+
};
11+
12+
export const swap = (src: OffscreenCanvas, dst: HTMLCanvasElement) => {
13+
const ctx = dst.getContext('2d')!;
14+
ctx.drawImage(src, 0, 0);
15+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './canvas';
2+
export * from './draw';
3+
export * from '../shared/types';

packages/thumbnail/src/canvasFactory.ts

Lines changed: 0 additions & 178 deletions
This file was deleted.

0 commit comments

Comments
 (0)