Skip to content

Commit 4e2b116

Browse files
DeMoorJasperdanilowozcodesandbot
authored
feat: Add support for enabling service worker feature (#1127)
* feat: Add support for enabling service worker feature in sandpack-bundler * fix: actually set suffix correctly * fix * wip * wip * create story * refactor * docs * format * update * add example --------- Co-authored-by: Danilo Woznica <[email protected]> Co-authored-by: codesandbox-bot <[email protected]>
1 parent 0b09de6 commit 4e2b116

File tree

18 files changed

+268
-36
lines changed

18 files changed

+268
-36
lines changed

.codesandbox/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"command": "yarn workspace @codesandbox/sandpack-react build"
5252
},
5353
"dev:website-docs": {
54-
"name": "Documentation",
54+
"name": "Dev: website docs",
5555
"command": "yarn dev:docs"
5656
},
5757
"dev:sandpack-react": {

sandpack-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@codesandbox/nodebox": "0.1.8",
5959
"buffer": "^6.0.3",
6060
"dequal": "^2.0.2",
61+
"mime-db": "^1.52.0",
6162
"outvariant": "1.4.0",
6263
"static-browser-server": "1.0.3"
6364
},

sandpack-client/src/clients/node/client.utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ export const findStartScriptPackageJson = (
8585
}
8686

8787
if (item.type === TokenType.Command && commandNotFoundYet) {
88-
command = item.value;
88+
command = item.value!;
8989
}
9090

9191
if (
9292
item.type === TokenType.Argument ||
9393
(!commandNotFoundYet && item.type === TokenType.Command)
9494
) {
95-
args.push(item.value);
95+
args.push(item.value!);
9696
}
9797

9898
// TODO: support TokenType.AND, TokenType.OR, TokenType.PIPE

sandpack-client/src/clients/runtime/index.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,20 @@ import { SandpackClient } from "../base";
2323

2424
import Protocol from "./file-resolver-protocol";
2525
import { IFrameProtocol } from "./iframe-protocol";
26-
import type { SandpackRuntimeMessage } from "./types";
27-
import { getTemplate } from "./utils";
26+
import { EXTENSIONS_MAP } from "./mime";
27+
import type { IPreviewRequestMessage, IPreviewResponseMessage } from "./types";
28+
import { CHANNEL_NAME, type SandpackRuntimeMessage } from "./types";
29+
import { getExtension, getTemplate } from "./utils";
30+
31+
const SUFFIX_PLACEHOLDER = "-{{suffix}}";
2832

2933
const BUNDLER_URL =
3034
process.env.CODESANDBOX_ENV === "development"
3135
? "http://localhost:3000/"
3236
: `https://${process.env.PACKAGE_VERSION?.replace(
3337
/\./g,
3438
"-"
35-
)}-sandpack.codesandbox.io/`;
39+
)}${SUFFIX_PLACEHOLDER}-sandpack.codesandbox.io/`;
3640

3741
export class SandpackRuntime extends SandpackClient {
3842
fileResolverProtocol?: Protocol;
@@ -62,6 +66,15 @@ export class SandpackRuntime extends SandpackClient {
6266
`?cache=${Date.now()}`;
6367
}
6468

69+
const suffixes: string[] = [];
70+
if (options.experimental_enableServiceWorker) {
71+
suffixes.push(Math.random().toString(36).slice(4));
72+
}
73+
this.bundlerURL = this.bundlerURL.replace(
74+
SUFFIX_PLACEHOLDER,
75+
suffixes.length ? `-${suffixes.join("-")}` : ""
76+
);
77+
6578
this.bundlerState = undefined;
6679
this.errors = [];
6780
this.status = "initializing";
@@ -153,6 +166,89 @@ export class SandpackRuntime extends SandpackClient {
153166
}
154167
}
155168
);
169+
170+
if (options.experimental_enableServiceWorker) {
171+
this.serviceWorkerHandshake();
172+
}
173+
}
174+
175+
private serviceWorkerHandshake() {
176+
const channel = new MessageChannel();
177+
178+
const iframeContentWindow = this.iframe.contentWindow;
179+
if (!iframeContentWindow) {
180+
throw new Error("Could not get iframe contentWindow");
181+
}
182+
183+
const port = channel.port1;
184+
port.onmessage = (evt: MessageEvent) => {
185+
if (typeof evt.data === "object" && evt.data.$channel === CHANNEL_NAME) {
186+
switch (evt.data.$type) {
187+
case "preview/ready":
188+
// no op for now
189+
break;
190+
case "preview/request":
191+
this.handleWorkerRequest(evt.data, port);
192+
193+
break;
194+
}
195+
}
196+
};
197+
198+
this.iframe.onload = () => {
199+
const initMsg = {
200+
$channel: CHANNEL_NAME,
201+
$type: "preview/init",
202+
};
203+
204+
iframeContentWindow.postMessage(initMsg, "*", [channel.port2]);
205+
};
206+
}
207+
208+
private handleWorkerRequest(
209+
request: IPreviewRequestMessage,
210+
port: MessagePort
211+
) {
212+
try {
213+
const filepath = new URL(request.url, this.bundlerURL).pathname;
214+
215+
const headers: Record<string, string> = {};
216+
217+
const files = this.getFiles();
218+
const body = files[filepath].code;
219+
220+
if (!headers["Content-Type"]) {
221+
const extension = getExtension(filepath);
222+
const foundMimetype = EXTENSIONS_MAP.get(extension);
223+
if (foundMimetype) {
224+
headers["Content-Type"] = foundMimetype;
225+
}
226+
}
227+
228+
const responseMessage: IPreviewResponseMessage = {
229+
$channel: CHANNEL_NAME,
230+
$type: "preview/response",
231+
id: request.id,
232+
headers,
233+
status: 200,
234+
body,
235+
};
236+
237+
port.postMessage(responseMessage);
238+
} catch (err) {
239+
const responseMessage: IPreviewResponseMessage = {
240+
$channel: CHANNEL_NAME,
241+
$type: "preview/response",
242+
id: request.id,
243+
headers: {
244+
"Content-Type": "text/html; charset=utf-8",
245+
},
246+
status: 404,
247+
body: "File not found",
248+
};
249+
250+
port.postMessage(responseMessage);
251+
}
156252
}
157253

158254
public setLocationURLIntoIFrame(): void {
@@ -240,6 +336,8 @@ export class SandpackRuntime extends SandpackClient {
240336
hasFileResolver: Boolean(this.options.fileResolver),
241337
disableDependencyPreprocessing:
242338
this.sandboxSetup.disableDependencyPreprocessing,
339+
experimental_enableServiceWorker:
340+
this.options.experimental_enableServiceWorker,
243341
template:
244342
this.sandboxSetup.template ||
245343
getTemplate(packageJSON, normalizedModules),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import mimeDB from "mime-db";
2+
3+
const extensionMap = new Map<string, string>();
4+
const entries = Object.entries(mimeDB);
5+
for (const [mimetype, entry] of entries) {
6+
// eslint-disable-next-line
7+
// @ts-ignore
8+
if (!entry.extensions) {
9+
continue;
10+
}
11+
12+
// eslint-disable-next-line
13+
// @ts-ignore
14+
const extensions = entry.extensions as string[];
15+
if (extensions.length) {
16+
for (const ext of extensions) {
17+
extensionMap.set(ext, mimetype);
18+
}
19+
}
20+
}
21+
22+
export const EXTENSIONS_MAP = extensionMap;

sandpack-client/src/clients/runtime/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export type SandpackRuntimeMessage = BaseSandpackMessage &
6868
externalResources: string[];
6969
hasFileResolver: boolean;
7070
disableDependencyPreprocessing?: boolean;
71+
experimental_enableServiceWorker?: boolean;
7172
template?: string | SandpackTemplate;
7273
showOpenInCodeSandbox: boolean;
7374
showErrorScreen: boolean;
@@ -123,3 +124,21 @@ export type SandpackRuntimeMessage = BaseSandpackMessage &
123124
};
124125
}
125126
);
127+
export const CHANNEL_NAME = "$CSB_RELAY";
128+
129+
export interface IPreviewRequestMessage {
130+
$channel: typeof CHANNEL_NAME;
131+
$type: "preview/request";
132+
id: string;
133+
method: string;
134+
url: string;
135+
}
136+
137+
export interface IPreviewResponseMessage {
138+
$channel: typeof CHANNEL_NAME;
139+
$type: "preview/response";
140+
id: string;
141+
status: number;
142+
headers: Record<string, string>;
143+
body: string | Uint8Array;
144+
}

sandpack-client/src/clients/runtime/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,13 @@ export function getTemplate(
180180

181181
return undefined;
182182
}
183+
184+
export function getExtension(filepath: string): string {
185+
const parts = filepath.split(".");
186+
if (parts.length <= 1) {
187+
return "";
188+
} else {
189+
const ext = parts[parts.length - 1];
190+
return ext;
191+
}
192+
}

sandpack-client/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export interface ClientOptions {
7070
* and unlock a few capabilities
7171
*/
7272
teamId?: string;
73+
74+
/**
75+
* Enable the service worker feature for sandpack-bundler
76+
*/
77+
experimental_enableServiceWorker?: boolean;
7378
}
7479

7580
export interface SandboxSetup {

sandpack-react/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@
8888
"astring": "^1.8.4",
8989
"babel-loader": "^7.1.5",
9090
"rollup-plugin-filesize": "^10.0.0",
91-
"rollup-plugin-preserve-directives": "^0.4.0",
9291
"storybook": "^7.5.1",
9392
"typescript": "^5.2.2"
9493
},

sandpack-react/rollup.config.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ const commonjs = require("@rollup/plugin-commonjs");
33
const replace = require("@rollup/plugin-replace");
44
const typescript = require("@rollup/plugin-typescript");
55
const filesize = require("rollup-plugin-filesize");
6-
const { preserveDirectives } = require("rollup-plugin-preserve-directives");
76

87
const pkg = require("./package.json");
98
const generateUnstyledTypes = require("./scripts/rollup-generate-unstyled-types");
@@ -32,8 +31,7 @@ const configBase = [
3231
},
3332
}),
3433
typescript({ tsconfig: "./tsconfig.json" }),
35-
filesize(),
36-
preserveDirectives({ suppressPreserveModulesWarning: true })
34+
filesize()
3735
),
3836
output: [
3937
{

0 commit comments

Comments
 (0)