Skip to content

Commit efbebb5

Browse files
authored
docs: Using rolldown in translator app (#1611)
1 parent 54ce5f9 commit efbebb5

File tree

19 files changed

+837
-278
lines changed

19 files changed

+837
-278
lines changed

apps/typegpu-docs/astro.config.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,27 @@ const DEV = import.meta.env.DEV;
2727
export default defineConfig({
2828
site: 'https://docs.swmansion.com',
2929
base: 'TypeGPU',
30+
server: {
31+
// Required for '@rolldown/browser' to work in dev mode.
32+
// Since the service worker is hosted on the /TypeGPU path,
33+
// fetches from /@fs/ fail due to CORS. This fixes that.
34+
headers: {
35+
'Cross-Origin-Embedder-Policy': 'require-corp',
36+
'Cross-Origin-Opener-Policy': 'same-origin',
37+
},
38+
},
3039
markdown: {
3140
remarkPlugins: [remarkMath],
3241
rehypePlugins: [rehypeMathJax],
3342
},
3443
vite: {
44+
define: {
45+
// Required for '@rolldown/browser' to work.
46+
'process.env.NODE_DEBUG_NATIVE': '""',
47+
},
48+
optimizeDeps: {
49+
exclude: ['@rolldown/browser'],
50+
},
3551
// Allowing query params, for invalidation
3652
plugins: [
3753
wasm(),
@@ -48,6 +64,7 @@ export default defineConfig({
4864
ssr: {
4965
noExternal: [
5066
'wgsl-wasm-transpiler-bundler',
67+
'@rolldown/browser',
5168
],
5269
},
5370
},

apps/typegpu-docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@monaco-editor/react": "^4.7.0",
2323
"@radix-ui/react-select": "^2.2.5",
2424
"@radix-ui/react-slider": "^1.3.5",
25+
"@rolldown/browser": "1.0.0-beta.43",
2526
"@stackblitz/sdk": "^1.11.0",
2627
"@tailwindcss/vite": "^4.1.13",
2728
"@typegpu/color": "workspace:*",
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
2+
3+
// This service worker is responsible for intercepting fetch requests to
4+
// assets hosted on the same origin, and attaching CORS headers that
5+
// allow SharedArrayBuffer to function (required by @rolldown/browser).
6+
7+
let coepCredentialless = false;
8+
if (typeof window === 'undefined') {
9+
self.addEventListener('install', () => self.skipWaiting());
10+
self.addEventListener(
11+
'activate',
12+
(event) => event.waitUntil(self.clients.claim()),
13+
);
14+
15+
self.addEventListener('message', (ev) => {
16+
if (!ev.data) {
17+
return;
18+
}
19+
20+
if (ev.data.type === 'deregister') {
21+
self.registration
22+
.unregister()
23+
.then(() => {
24+
return self.clients.matchAll();
25+
})
26+
.then((clients) => {
27+
for (const client of clients) {
28+
client.navigate(client.url);
29+
}
30+
});
31+
} else if (ev.data.type === 'coepCredentialless') {
32+
coepCredentialless = ev.data.value;
33+
}
34+
});
35+
36+
self.addEventListener('fetch', (event) => {
37+
const r = event.request;
38+
if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') {
39+
return;
40+
}
41+
42+
const request = (coepCredentialless && r.mode === 'no-cors')
43+
? new Request(r, {
44+
credentials: 'omit',
45+
})
46+
: r;
47+
event.respondWith(
48+
fetch(request)
49+
.then((response) => {
50+
if (response.status === 0) {
51+
return response;
52+
}
53+
54+
const newHeaders = new Headers(response.headers);
55+
newHeaders.set(
56+
'Cross-Origin-Embedder-Policy',
57+
coepCredentialless ? 'credentialless' : 'require-corp',
58+
);
59+
if (!coepCredentialless) {
60+
newHeaders.set('Cross-Origin-Resource-Policy', 'cross-origin');
61+
}
62+
newHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
63+
64+
return new Response(response.body, {
65+
status: response.status,
66+
statusText: response.statusText,
67+
headers: newHeaders,
68+
});
69+
})
70+
.catch((e) => console.error(e)),
71+
);
72+
});
73+
} else {
74+
(() => {
75+
const reloadedBySelf = window.sessionStorage.getItem('coiReloadedBySelf');
76+
window.sessionStorage.removeItem('coiReloadedBySelf');
77+
const coepDegrading = reloadedBySelf === 'coepdegrade';
78+
79+
// You can customize the behavior of this script through a global `coi` variable.
80+
const coi = {
81+
shouldRegister: () => !reloadedBySelf,
82+
shouldDeregister: () => false,
83+
coepCredentialless: () => true,
84+
coepDegrade: () => true,
85+
doReload: () => window.location.reload(),
86+
quiet: false,
87+
...window.coi,
88+
};
89+
90+
const n = navigator;
91+
const controlling = !!n.serviceWorker && !!n.serviceWorker.controller;
92+
93+
// Record the failure if the page is served by serviceWorker.
94+
if (controlling && !window.crossOriginIsolated) {
95+
window.sessionStorage.setItem('coiCoepHasFailed', 'true');
96+
}
97+
const coepHasFailed = window.sessionStorage.getItem('coiCoepHasFailed');
98+
99+
if (controlling) {
100+
// Reload only on the first failure.
101+
const reloadToDegrade = coi.coepDegrade() && !(
102+
coepDegrading || window.crossOriginIsolated
103+
);
104+
n.serviceWorker.controller.postMessage({
105+
type: 'coepCredentialless',
106+
value: (reloadToDegrade || coepHasFailed && coi.coepDegrade())
107+
? false
108+
: coi.coepCredentialless(),
109+
});
110+
if (reloadToDegrade) {
111+
!coi.quiet && console.log('Reloading page to degrade COEP.');
112+
window.sessionStorage.setItem('coiReloadedBySelf', 'coepdegrade');
113+
coi.doReload('coepdegrade');
114+
}
115+
116+
if (coi.shouldDeregister()) {
117+
n.serviceWorker.controller.postMessage({ type: 'deregister' });
118+
}
119+
}
120+
121+
// If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
122+
// already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
123+
if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
124+
125+
if (!window.isSecureContext) {
126+
!coi.quiet &&
127+
console.log(
128+
'COOP/COEP Service Worker not registered, a secure context is required.',
129+
);
130+
return;
131+
}
132+
133+
// In some environments (e.g. Firefox private mode) this won't be available
134+
if (!n.serviceWorker) {
135+
!coi.quiet &&
136+
console.error(
137+
'COOP/COEP Service Worker not registered, perhaps due to private mode.',
138+
);
139+
return;
140+
}
141+
142+
n.serviceWorker.register(window.document.currentScript.src).then(
143+
(registration) => {
144+
!coi.quiet &&
145+
console.log(
146+
'COOP/COEP Service Worker registered',
147+
registration.scope,
148+
);
149+
150+
registration.addEventListener('updatefound', () => {
151+
!coi.quiet &&
152+
console.log(
153+
'Reloading page to make use of updated COOP/COEP Service Worker.',
154+
);
155+
window.sessionStorage.setItem('coiReloadedBySelf', 'updatefound');
156+
coi.doReload();
157+
});
158+
159+
// If the registration is active, but it's not controlling the page
160+
if (registration.active && !n.serviceWorker.controller) {
161+
!coi.quiet &&
162+
console.log(
163+
'Reloading page to make use of COOP/COEP Service Worker.',
164+
);
165+
window.sessionStorage.setItem('coiReloadedBySelf', 'notcontrolling');
166+
coi.doReload();
167+
}
168+
},
169+
(err) => {
170+
!coi.quiet &&
171+
console.error('COOP/COEP Service Worker failed to register:', err);
172+
},
173+
);
174+
})();
175+
}

apps/typegpu-docs/src/components/CodeEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function handleEditorWillMount(monaco: Monaco) {
1717
entries(SANDBOX_MODULES),
1818
map(([key, moduleDef]) => {
1919
if ('reroute' in moduleDef.typeDef) {
20-
return [key, moduleDef.typeDef.reroute] as const;
20+
return [key, [moduleDef.typeDef.reroute]] as [string, string[]];
2121
}
2222
return null;
2323
}),

apps/typegpu-docs/src/components/SearchableExampleList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ function ExamplesGrid({ examples }: { examples: Example[] }) {
1212
);
1313
}
1414

15-
const DEV = globalThis.process.env.NODE_ENV === 'development';
16-
const TEST = globalThis.process.env.NODE_ENV === 'test';
15+
const DEV = process.env.NODE_ENV === 'development';
16+
const TEST = process.env.NODE_ENV === 'test';
1717

1818
export function SearchableExampleList(
1919
{ excludeTags = [], scrollContainerRef }: {

apps/typegpu-docs/src/components/translator/lib/constants.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ fn fs_main() -> @location(0) vec4<f32> {
2323

2424
export const DEFAULT_TGSL = `import tgpu from 'typegpu';
2525
import * as d from 'typegpu/data';
26-
import * as std from 'typegpu/std';
2726
2827
const Particle = d.struct({
2928
position: d.vec3f,
@@ -42,14 +41,9 @@ const layout = tgpu.bindGroupLayout({
4241
4342
export const updateParicle = tgpu.fn([Particle, d.vec3f, d.f32], Particle)(
4443
(particle, gravity, deltaTime) => {
45-
const newVelocity = std.mul(
46-
particle.velocity,
47-
std.mul(gravity, deltaTime),
48-
);
49-
const newPosition = std.add(
50-
particle.position,
51-
std.mul(newVelocity, deltaTime),
52-
);
44+
const newVelocity = particle.velocity.mul(gravity).mul(deltaTime);
45+
const newPosition = particle.position.add(newVelocity.mul(deltaTime));
46+
5347
return Particle({
5448
position: newPosition,
5549
velocity: newVelocity,

apps/typegpu-docs/src/components/translator/lib/editorConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function setupMonacoEditor(monaco: Monaco) {
4444
entries(SANDBOX_MODULES),
4545
map(([key, moduleDef]) => {
4646
if ('reroute' in moduleDef.typeDef) {
47-
return [key, moduleDef.typeDef.reroute] as const;
47+
return [key, [moduleDef.typeDef.reroute]] as [string, string[]];
4848
}
4949
return null;
5050
}),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { InputOptions, OutputOptions } from '@rolldown/browser';
2+
import { join } from 'pathe';
3+
4+
export interface BundleResult {
5+
output: Record<string, string | Uint8Array>;
6+
warnings?: string[] | undefined;
7+
}
8+
9+
export interface SourceFile {
10+
filename: string;
11+
code: string;
12+
isEntry?: boolean;
13+
}
14+
15+
export type FileMap = Record<
16+
string,
17+
{
18+
content: string;
19+
} | {
20+
reroute: string;
21+
} | undefined
22+
>;
23+
24+
export async function bundle(
25+
files: FileMap,
26+
entries: string[],
27+
config: InputOptions & { output?: OutputOptions | undefined } = {},
28+
): Promise<BundleResult> {
29+
const rolldown = await import('@rolldown/browser');
30+
31+
const warnings: string[] = [];
32+
33+
const inputOptions: InputOptions = {
34+
input: entries,
35+
cwd: '/',
36+
onLog(level, log, logger) {
37+
if (level === 'warn') {
38+
warnings.push(String(log));
39+
} else {
40+
logger(level, log);
41+
}
42+
},
43+
...config,
44+
plugins: [
45+
// Virtual file system plugin
46+
{
47+
name: 'virtual-fs',
48+
resolveId(source, importer) {
49+
const id = source[0] === '.'
50+
? join(importer || '/', '..', source)
51+
: source;
52+
53+
if (files[id] && 'reroute' in files[id]) {
54+
// Rerouting
55+
return files[id].reroute;
56+
}
57+
58+
return id;
59+
},
60+
load(id) {
61+
if (!files[id]) {
62+
return;
63+
}
64+
65+
if ('reroute' in files[id]) {
66+
// Reroutes are supposed to be resolved in `resolveId`
67+
throw new Error(`Unresolved reroute for ${id}`);
68+
}
69+
70+
return files[id].content;
71+
},
72+
},
73+
...(Array.isArray(config?.plugins)
74+
? config.plugins
75+
: [config?.plugins].filter(Boolean)),
76+
],
77+
};
78+
79+
const outputOptions: OutputOptions = {
80+
format: 'esm',
81+
...config?.output,
82+
};
83+
84+
const bundle = await rolldown.rolldown(inputOptions);
85+
const result = await bundle.generate(outputOptions);
86+
87+
const output = Object.fromEntries(
88+
result.output.map((chunk) =>
89+
chunk.type === 'chunk'
90+
? [chunk.fileName, chunk.code]
91+
: [chunk.fileName, chunk.source]
92+
),
93+
);
94+
95+
return {
96+
output,
97+
warnings,
98+
};
99+
}

0 commit comments

Comments
 (0)