1 | import { getTemplate } from "codesandbox-import-utils/lib/create-sandbox/templates";
|
2 | import isEqual from "lodash.isequal";
|
3 |
|
4 |
|
5 |
|
6 | import { version } from "../package.json";
|
7 | import Protocol from "./file-resolver-protocol";
|
8 | import { IFrameProtocol } from "./iframe-protocol";
|
9 | import { createPackageJSON, addPackageJSONIfNeeded, extractErrorDetails, } from "./utils";
|
10 | const BUNDLER_URL = process.env.CODESANDBOX_ENV === "development"
|
11 | ? "http://localhost:3000/"
|
12 | : `https://${version.replace(/\./g, "-")}-sandpack.codesandbox.io/`;
|
13 | export class SandpackClient {
|
14 | constructor(selector, sandboxInfo, options = {}) {
|
15 | this.getTranspilerContext = () => new Promise((resolve) => {
|
16 | const unsubscribe = this.listen((message) => {
|
17 | if (message.type === "transpiler-context") {
|
18 | resolve(message.data);
|
19 | unsubscribe();
|
20 | }
|
21 | });
|
22 | this.dispatch({ type: "get-transpiler-context" });
|
23 | });
|
24 | this.options = options;
|
25 | this.sandboxInfo = sandboxInfo;
|
26 | this.bundlerURL = options.bundlerURL || BUNDLER_URL;
|
27 | this.bundlerState = undefined;
|
28 | this.errors = [];
|
29 | this.status = "initializing";
|
30 | if (typeof selector === "string") {
|
31 | this.selector = selector;
|
32 | const element = document.querySelector(selector);
|
33 | if (!element) {
|
34 | throw new Error(`No element found for selector '${selector}'`);
|
35 | }
|
36 | this.element = element;
|
37 | this.iframe = document.createElement("iframe");
|
38 | this.initializeElement();
|
39 | }
|
40 | else {
|
41 | this.element = selector;
|
42 | this.iframe = selector;
|
43 | }
|
44 | if (!this.iframe.getAttribute("sandbox")) {
|
45 | this.iframe.setAttribute("sandbox", "allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts");
|
46 | }
|
47 | if (!this.iframe.getAttribute("allow")) {
|
48 | this.iframe.setAttribute("allow", "accelerometer; ambient-light-sensor; autoplay; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking");
|
49 | }
|
50 | this.iframe.src = options.startRoute
|
51 | ? new URL(options.startRoute, this.bundlerURL).toString()
|
52 | : this.bundlerURL;
|
53 | this.iframeProtocol = new IFrameProtocol(this.iframe, this.bundlerURL);
|
54 | this.unsubscribeGlobalListener = this.iframeProtocol.globalListen((mes) => {
|
55 | if (mes.type !== "initialized" || !this.iframe.contentWindow) {
|
56 | return;
|
57 | }
|
58 | this.iframeProtocol.register();
|
59 | if (this.options.fileResolver) {
|
60 |
|
61 | this.fileResolverProtocol = new Protocol("file-resolver", async (data) => {
|
62 | if (data.m === "isFile") {
|
63 | return this.options.fileResolver.isFile(data.p);
|
64 | }
|
65 | return this.options.fileResolver.readFile(data.p);
|
66 | }, this.iframe.contentWindow);
|
67 | }
|
68 | this.updatePreview(this.sandboxInfo, true);
|
69 | });
|
70 | this.unsubscribeChannelListener = this.iframeProtocol.channelListen((mes) => {
|
71 | switch (mes.type) {
|
72 | case "start": {
|
73 | this.errors = [];
|
74 | break;
|
75 | }
|
76 | case "status": {
|
77 | this.status = mes.status;
|
78 | break;
|
79 | }
|
80 | case "action": {
|
81 | if (mes.action === "show-error") {
|
82 | this.errors = [...this.errors, extractErrorDetails(mes)];
|
83 | }
|
84 | break;
|
85 | }
|
86 | case "state": {
|
87 | this.bundlerState = mes.state;
|
88 | break;
|
89 | }
|
90 | }
|
91 | });
|
92 | }
|
93 | cleanup() {
|
94 | this.unsubscribeChannelListener();
|
95 | this.unsubscribeGlobalListener();
|
96 | this.iframeProtocol.cleanup();
|
97 | }
|
98 | updateOptions(options) {
|
99 | if (!isEqual(this.options, options)) {
|
100 | this.options = options;
|
101 | this.updatePreview();
|
102 | }
|
103 | }
|
104 | updatePreview(sandboxInfo = this.sandboxInfo, isInitializationCompile) {
|
105 | var _a, _b, _c;
|
106 | this.sandboxInfo = sandboxInfo;
|
107 | const files = this.getFiles();
|
108 | const modules = Object.keys(files).reduce((prev, next) => ({
|
109 | ...prev,
|
110 | [next]: {
|
111 | code: files[next].code,
|
112 | path: next,
|
113 | },
|
114 | }), {});
|
115 | let packageJSON = JSON.parse(createPackageJSON(this.sandboxInfo.dependencies, this.sandboxInfo.entry));
|
116 | try {
|
117 | packageJSON = JSON.parse(files["/package.json"].code);
|
118 | }
|
119 | catch (e) {
|
120 | console.error("Could not parse package.json file: " + e.message);
|
121 | }
|
122 |
|
123 | const normalizedModules = Object.keys(files).reduce((prev, next) => ({
|
124 | ...prev,
|
125 | [next]: {
|
126 | content: files[next].code,
|
127 | path: next,
|
128 | },
|
129 | }), {});
|
130 | this.dispatch({
|
131 | type: "compile",
|
132 | codesandbox: true,
|
133 | version: 3,
|
134 | isInitializationCompile,
|
135 | modules,
|
136 | externalResources: [],
|
137 | hasFileResolver: Boolean(this.options.fileResolver),
|
138 | disableDependencyPreprocessing: this.sandboxInfo
|
139 | .disableDependencyPreprocessing,
|
140 | template: this.sandboxInfo.template ||
|
141 | getTemplate(packageJSON, normalizedModules),
|
142 | showOpenInCodeSandbox: (_a = this.options.showOpenInCodeSandbox) !== null && _a !== void 0 ? _a : true,
|
143 | showErrorScreen: (_b = this.options.showErrorScreen) !== null && _b !== void 0 ? _b : true,
|
144 | showLoadingScreen: (_c = this.options.showLoadingScreen) !== null && _c !== void 0 ? _c : true,
|
145 | skipEval: this.options.skipEval || false,
|
146 | clearConsoleDisabled: !this.options.clearConsoleOnFirstCompile,
|
147 | });
|
148 | }
|
149 | dispatch(message) {
|
150 | this.iframeProtocol.dispatch(message);
|
151 | }
|
152 | listen(listener) {
|
153 | return this.iframeProtocol.channelListen(listener);
|
154 | }
|
155 | |
156 |
|
157 |
|
158 | getCodeSandboxURL() {
|
159 | const files = this.getFiles();
|
160 | const paramFiles = Object.keys(files).reduce((prev, next) => ({
|
161 | ...prev,
|
162 | [next.replace("/", "")]: {
|
163 | content: files[next].code,
|
164 | isBinary: false,
|
165 | },
|
166 | }), {});
|
167 | return fetch("https://codesandbox.io/api/v1/sandboxes/define?json=1", {
|
168 | method: "POST",
|
169 | body: JSON.stringify({ files: paramFiles }),
|
170 | headers: {
|
171 | Accept: "application/json",
|
172 | "Content-Type": "application/json",
|
173 | },
|
174 | })
|
175 | .then((x) => x.json())
|
176 | .then((res) => ({
|
177 | sandboxId: res.sandbox_id,
|
178 | editorUrl: `https://codesandbox.io/s/${res.sandbox_id}`,
|
179 | embedUrl: `https://codesandbox.io/embed/${res.sandbox_id}`,
|
180 | }));
|
181 | }
|
182 | getFiles() {
|
183 | const { sandboxInfo } = this;
|
184 | if (sandboxInfo.files["/package.json"] === undefined) {
|
185 | return addPackageJSONIfNeeded(sandboxInfo.files, sandboxInfo.dependencies, sandboxInfo.entry);
|
186 | }
|
187 | return this.sandboxInfo.files;
|
188 | }
|
189 | initializeElement() {
|
190 | this.iframe.style.border = "0";
|
191 | this.iframe.style.width = this.options.width || "100%";
|
192 | this.iframe.style.height = this.options.height || "100%";
|
193 | this.iframe.style.overflow = "hidden";
|
194 | if (!this.element.parentNode) {
|
195 |
|
196 | throw new Error("Given element does not have a parent.");
|
197 | }
|
198 | this.element.parentNode.replaceChild(this.iframe, this.element);
|
199 | }
|
200 | }
|
201 |
|
\ | No newline at end of file |