Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): refactor iFrame fs protocol #483

Merged
merged 4 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions sandpack-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,17 +177,19 @@ export class SandpackClient {

if (this.options.fileResolver) {
this.fileResolverProtocol = new Protocol(
"file-resolver",
async (data: { m: "isFile" | "readFile"; p: string }) => {
if (data.m === "isFile") {
"fs",
async (data) => {
if (data.method === "isFile") {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.options.fileResolver!.isFile(data.p);
return this.options.fileResolver!.isFile(data.params[0]);
} else if (data.method === "readFile") {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.options.fileResolver!.readFile(data.params[0]);
} else {
throw new Error("Method not supported");
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.options.fileResolver!.readFile(data.p);
},
this.iframe.contentWindow
this.iframeProtocol
);
}

Expand Down
147 changes: 37 additions & 110 deletions sandpack-client/src/file-resolver-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,127 +5,54 @@
* an abstraction over the actions that can be dispatched between the bundler and the iframe.
*/

const generateId = () =>
// Such a random ID
Math.floor(Math.random() * 1000000 + Math.random() * 1000000);

const getConstructorName = (x: any) => {
try {
return x.constructor.name;
} catch (e) {
return "";
}
};
import { IFrameProtocol } from "./iframe-protocol";
import type {
UnsubscribeFunction,
ProtocolRequestMessage,
ProtocolResultMessage,
ProtocolErrorMessage,
} from "./types";

export default class Protocol {
private outgoingMessages: Set<number> = new Set();
private internalId: number;
private isWorker: boolean;
private _disposeMessageListener: UnsubscribeFunction;

constructor(
private type: string,
private handleMessage: (message: any) => any,
private target: Worker | Window
private handleMessage: (message: ProtocolRequestMessage) => any,
private protocol: IFrameProtocol
) {
this.createConnection();
this.internalId = generateId();
this.isWorker = getConstructorName(target) === "Worker";
this._disposeMessageListener = this.protocol.channelListen(
async (msg: any) => {
if (msg.type === this.getTypeId() && msg.method) {
const message = msg as ProtocolRequestMessage;
try {
const result = await this.handleMessage(message);
const response: ProtocolResultMessage = {
type: this.getTypeId(),
msgId: message.msgId,
result: result,
};
this.protocol.dispatch(response as any);
} catch (err: any) {
const response: ProtocolErrorMessage = {
type: this.getTypeId(),
msgId: message.msgId,
error: {
message: err.message,
},
};
this.protocol.dispatch(response as any);
}
}
}
);
}

getTypeId() {
return `p-${this.type}`;
}

createConnection() {
self.addEventListener("message", this._messageListener);
return `protocol-${this.type}`;
}

public dispose() {
self.removeEventListener("message", this._messageListener);
}

sendMessage<PromiseType>(data: any): Promise<PromiseType> {
return new Promise((resolve) => {
const messageId = generateId();

const message = {
$originId: this.internalId,
$type: this.getTypeId(),
$data: data,
$id: messageId,
};

this.outgoingMessages.add(messageId);

const listenFunction = (e: MessageEvent) => {
const { data } = e;

if (
data.$type === this.getTypeId() &&
data.$id === messageId &&
data.$originId !== this.internalId
) {
resolve(data.$data);

self.removeEventListener("message", listenFunction);
}
};

self.addEventListener("message", listenFunction);

this._postMessage(message);
});
}

private _messageListener = async (e: MessageEvent) => {
const { data } = e;

if (data.$type !== this.getTypeId()) {
return;
}

// We are getting a response to the message
if (this.outgoingMessages.has(data.$id)) {
return;
}

// any is fine for now... gotta refactor this later...
let returnMessage: any = {
$originId: this.internalId,
$type: this.getTypeId(),
$id: data.$id,
};

try {
const result = await this.handleMessage(data.$data);
returnMessage.$data = result;
} catch (err: any) {
if (!err.message) {
console.error(err);
}
returnMessage.$error = { message: err.message ?? "Unknown error" };
}

if (e.source) {
// @ts-ignore
e.source.postMessage(returnMessage, "*");
} else {
this._postMessage(returnMessage);
}
};

private _postMessage(m: any) {
if (
this.isWorker ||
// @ts-ignore Unknown to TS
(typeof DedicatedWorkerGlobalScope !== "undefined" &&
// @ts-ignore Unknown to TS
this.target instanceof DedicatedWorkerGlobalScope)
) {
// @ts-ignore
this.target.postMessage(m);
} else {
(this.target as Window).postMessage(m, "*");
}
this._disposeMessageListener();
}
}
22 changes: 22 additions & 0 deletions sandpack-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,28 @@ export interface BaseSandpackMessage {
codesandbox?: boolean;
}

export interface BaseProtocolMessage {
type: string;
msgId: string;
}

export interface ProtocolErrorMessage extends BaseProtocolMessage {
error: {
message: string;
};
}

export interface ProtocolResultMessage extends BaseProtocolMessage {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result: any;
}

export interface ProtocolRequestMessage extends BaseProtocolMessage {
method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any[];
}

export type SandpackMessage = BaseSandpackMessage &
(
| {
Expand Down
19 changes: 5 additions & 14 deletions sandpack-react/src/Playground.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { Sandpack } from "./";

export default {
title: "Intro/Playground",
};

export const Main = (): JSX.Element => {
return (
<Sandpack
files={{
"./baz": {
code: "",
},
}}
options={{
activeFile: "./baz",
visibleFiles: ["./baz", "/src/App.vue"],
}}
template="vue"
/>
);
return <Sandpack />;
};
95 changes: 95 additions & 0 deletions sandpack-react/src/presets/Sandpack.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,98 @@ export const ShowLineNumber: React.FC = () => (
export const wrapContent: React.FC = () => (
<Sandpack options={{ wrapContent: true }} template="vanilla" />
);

const defaultFiles = {
"/styles.css": `body {
font-family: sans-serif;
-webkit-font-smoothing: auto;
-moz-font-smoothing: auto;
-moz-osx-font-smoothing: grayscale;
font-smoothing: auto;
text-rendering: optimizeLegibility;
font-smooth: always;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}

h1 {
font-size: 1.5rem;
}`,
"/index.js": `import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);`,
"/package.json": `{
"name": "test-sandbox",
"main": "/index.js",
"private": true,
"scripts": {},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "^4.0.0"
}
}
`,
};

const filesA = {
"/App.js": `import "./styles.css";

export default function App() {
return <h1>File A</h1>
}`,
};

const filesB = {
"/App.js": `import "./styles.css";

export default function App() {
return <h1>File B</h1>
}`,
};

export const FileResolver = (): JSX.Element => {
return (
<>
<Sandpack
customSetup={{
environment: "create-react-app",
entry: "/index.js",
}}
files={defaultFiles}
options={{
bundlerURL: "https://1ad528b9.sandpack-bundler.pages.dev",
fileResolver: {
isFile: async (fileName): Promise<boolean> =>
new Promise((resolve) => resolve(!!filesA[fileName])),
readFile: async (fileName): Promise<string> =>
new Promise((resolve) => resolve(filesA[fileName])),
},
}}
/>

<Sandpack
customSetup={{
environment: "create-react-app",
entry: "/index.js",
}}
files={defaultFiles}
options={{
bundlerURL: "https://1ad528b9.sandpack-bundler.pages.dev",
fileResolver: {
isFile: async (fileName): Promise<boolean> =>
new Promise((resolve) => resolve(!!filesB[fileName])),
readFile: async (fileName): Promise<string> =>
new Promise((resolve) => resolve(filesB[fileName])),
},
}}
/>
</>
);
};