UNPKG

@codesandbox/sandpack-client

Version:

<img style="width:100%" src="https://user-images.githubusercontent.com/4838076/143581035-ebee5ba2-9cb1-4fe8-a05b-2f44bd69bb4b.gif" alt="Component toolkit for live running code editing experiences" />

653 lines (646 loc) 30.1 kB
import { _ as __awaiter, a as __generator, n as nullthrows, c as createError, g as __extends, h as __assign } from '../../utils-52664384.mjs'; import { INJECT_MESSAGE_TYPE, Nodebox, PREVIEW_LOADED_MESSAGE_TYPE } from '@codesandbox/nodebox'; import { S as SandpackClient } from '../../base-80a1f760.mjs'; import { c as consoleHook, f as fromBundlerFilesToFS, r as readBuffer, w as writeBuffer, g as generateRandomId, E as EventEmitter, a as getMessageFromError, b as findStartScriptPackageJson } from '../../consoleHook-59e792cb.mjs'; import 'outvariant'; import 'dequal'; function loadPreviewIframe(iframe, url) { return __awaiter(this, void 0, void 0, function () { var contentWindow, TIME_OUT, MAX_MANY_TIRES, tries, timeout; return __generator(this, function (_a) { contentWindow = iframe.contentWindow; nullthrows(contentWindow, "Failed to await preview iframe: no content window found"); TIME_OUT = 90000; MAX_MANY_TIRES = 20; tries = 0; return [2 /*return*/, new Promise(function (resolve, reject) { var triesToSetUrl = function () { var onLoadPage = function () { clearTimeout(timeout); tries = MAX_MANY_TIRES; resolve(); iframe.removeEventListener("load", onLoadPage); }; if (tries >= MAX_MANY_TIRES) { reject(createError("Could not able to connect to preview.")); return; } iframe.setAttribute("src", url); timeout = setTimeout(function () { triesToSetUrl(); iframe.removeEventListener("load", onLoadPage); }, TIME_OUT); tries = tries + 1; iframe.addEventListener("load", onLoadPage); }; iframe.addEventListener("error", function () { return reject(new Error("Iframe error")); }); iframe.addEventListener("abort", function () { return reject(new Error("Aborted")); }); triesToSetUrl(); })]; }); }); } var setPreviewIframeProperties = function (iframe, options) { iframe.style.border = "0"; iframe.style.width = options.width || "100%"; iframe.style.height = options.height || "100%"; iframe.style.overflow = "hidden"; iframe.allow = "cross-origin-isolated"; }; /* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/explicit-function-return-type, no-restricted-globals, @typescript-eslint/no-explicit-any */ function setupHistoryListeners(_a) { var scope = _a.scope; // @ts-ignore var origHistoryProto = window.history.__proto__; var historyList = []; var historyPosition = 0; var dispatchMessage = function (url) { parent.postMessage({ type: "urlchange", url: url, back: historyPosition > 0, forward: historyPosition < historyList.length - 1, channelId: scope.channelId, }, "*"); }; function pushHistory(url, state) { // remove "future" locations historyList.splice(historyPosition + 1); historyList.push({ url: url, state: state }); historyPosition = historyList.length - 1; } Object.assign(window.history, { go: function (delta) { var newPos = historyPosition + delta; if (newPos >= 0 && newPos <= historyList.length - 1) { historyPosition = newPos; var _a = historyList[historyPosition], url = _a.url, state = _a.state; origHistoryProto.replaceState.call(window.history, state, "", url); var newURL = document.location.href; dispatchMessage(newURL); window.dispatchEvent(new PopStateEvent("popstate", { state: state })); } }, back: function () { window.history.go(-1); }, forward: function () { window.history.go(1); }, pushState: function (state, title, url) { origHistoryProto.replaceState.call(window.history, state, title, url); pushHistory(url, state); dispatchMessage(document.location.href); }, replaceState: function (state, title, url) { origHistoryProto.replaceState.call(window.history, state, title, url); historyList[historyPosition] = { state: state, url: url }; dispatchMessage(document.location.href); }, }); function handleMessage(_a) { var data = _a.data; if (data.type === "urlback") { history.back(); } else if (data.type === "urlforward") { history.forward(); } else if (data.type === "refresh") { document.location.reload(); } } window.addEventListener("message", handleMessage); } /* eslint-disable @typescript-eslint/no-explicit-any */ function watchResize(_a) { var scope = _a.scope; var lastHeight = 0; function getDocumentHeight() { if (typeof window === "undefined") return 0; var body = document.body; var html = document.documentElement; return Math.max(body.scrollHeight, body.offsetHeight, html.offsetHeight); } function sendResizeEvent() { var height = getDocumentHeight(); if (lastHeight !== height) { window.parent.postMessage({ type: "resize", height: height, codesandbox: true, channelId: scope.channelId, }, "*"); } lastHeight = height; } sendResizeEvent(); var throttle; var observer = new MutationObserver(function () { if (throttle === undefined) { sendResizeEvent(); throttle = setTimeout(function () { throttle = undefined; }, 300); } }); observer.observe(document, { attributes: true, childList: true, subtree: true, }); /** * Ideally we should only use a `MutationObserver` to trigger a resize event, * however, we noted that it's not 100% reliable, so we went for polling strategy as well */ setInterval(sendResizeEvent, 300); } /* eslint-disable @typescript-eslint/ban-ts-comment */ var scripts = [ { code: setupHistoryListeners.toString(), id: "historyListener" }, { code: "function consoleHook({ scope }) {" + consoleHook + "\n};", id: "consoleHook", }, { code: watchResize.toString(), id: "watchResize" }, ]; var injectScriptToIframe = function (iframe, channelId) { scripts.forEach(function (_a) { var _b; var code = _a.code, id = _a.id; var message = { uid: id, type: INJECT_MESSAGE_TYPE, code: "exports.activate = ".concat(code), scope: { channelId: channelId }, }; (_b = iframe.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, "*"); }); }; /* eslint-disable no-console,@typescript-eslint/no-explicit-any,prefer-rest-params,@typescript-eslint/explicit-module-boundary-types */ var SandpackNode = /** @class */ (function (_super) { __extends(SandpackNode, _super); function SandpackNode(selector, sandboxInfo, options) { if (options === void 0) { options = {}; } var _this = _super.call(this, selector, sandboxInfo, __assign(__assign({}, options), { bundlerURL: options.bundlerURL })) || this; _this._modulesCache = new Map(); _this.messageChannelId = generateRandomId(); _this._initPromise = null; _this.emitter = new EventEmitter(); // Assign iframes _this.manageIframes(selector); // Init emulator _this.emulator = new Nodebox({ iframe: _this.emulatorIframe, runtimeUrl: _this.options.bundlerURL, }); // Trigger initial compile _this.updateSandbox(sandboxInfo); return _this; } // Initialize nodebox, should only ever be called once SandpackNode.prototype._init = function (files) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.emulator.connect()]; case 1: _a.sent(); // 2. Setup return [4 /*yield*/, this.emulator.fs.init(files)]; case 2: // 2. Setup _a.sent(); // 2.1 Other dependencies return [4 /*yield*/, this.globalListeners()]; case 3: // 2.1 Other dependencies _a.sent(); return [2 /*return*/]; } }); }); }; /** * It initializes the emulator and provide it with files, template and script to run */ SandpackNode.prototype.compile = function (files) { return __awaiter(this, void 0, void 0, function () { var shellId, err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 5, , 6]); // 1. Init this.status = "initializing"; this.dispatch({ type: "start", firstLoad: true }); if (!this._initPromise) { this._initPromise = this._init(files); } return [4 /*yield*/, this._initPromise]; case 1: _a.sent(); this.dispatch({ type: "connected" }); return [4 /*yield*/, this.createShellProcessFromTask(files)]; case 2: shellId = (_a.sent()).id; // 4. Launch Preview return [4 /*yield*/, this.createPreviewURLFromId(shellId)]; case 3: // 4. Launch Preview _a.sent(); return [4 /*yield*/, this.setLocationURLIntoIFrame()]; case 4: _a.sent(); // 5. Returns to consumer this.dispatchDoneMessage(); return [3 /*break*/, 6]; case 5: err_1 = _a.sent(); this.dispatch({ type: "action", action: "notification", notificationType: "error", title: getMessageFromError(err_1), }); this.dispatch({ type: "done", compilatonError: true }); return [3 /*break*/, 6]; case 6: return [2 /*return*/]; } }); }); }; /** * It creates a new shell and run the starting task */ SandpackNode.prototype.createShellProcessFromTask = function (files) { return __awaiter(this, void 0, void 0, function () { var packageJsonContent; var _a; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: packageJsonContent = readBuffer(files["/package.json"]); this.emulatorCommand = findStartScriptPackageJson(packageJsonContent); this.emulatorShellProcess = this.emulator.shell.create(); // Shell listeners return [4 /*yield*/, this.emulatorShellProcess.on("exit", function (exitCode) { _this.dispatch({ type: "action", action: "notification", notificationType: "error", title: createError("Error: process.exit(".concat(exitCode, ") called.")), }); })]; case 1: // Shell listeners _b.sent(); return [4 /*yield*/, this.emulatorShellProcess.on("progress", function (data) { var _a, _b; if (data.state === "command_running" || data.state === "starting_command") { _this.dispatch({ type: "shell/progress", data: __assign(__assign({}, data), { command: [ (_a = _this.emulatorCommand) === null || _a === void 0 ? void 0 : _a[0], (_b = _this.emulatorCommand) === null || _b === void 0 ? void 0 : _b[1].join(" "), ].join(" ") }), }); _this.status = "installing-dependencies"; return; } _this.dispatch({ type: "shell/progress", data: data }); })]; case 2: _b.sent(); this.emulatorShellProcess.stdout.on("data", function (data) { _this.dispatch({ type: "stdout", payload: { data: data, type: "out" } }); }); this.emulatorShellProcess.stderr.on("data", function (data) { _this.dispatch({ type: "stdout", payload: { data: data, type: "err" } }); }); return [4 /*yield*/, (_a = this.emulatorShellProcess).runCommand.apply(_a, this.emulatorCommand)]; case 3: return [2 /*return*/, _b.sent()]; } }); }); }; SandpackNode.prototype.createPreviewURLFromId = function (id) { var _a; return __awaiter(this, void 0, void 0, function () { var url; return __generator(this, function (_b) { switch (_b.label) { case 0: this.iframePreviewUrl = undefined; return [4 /*yield*/, this.emulator.preview.getByShellId(id)]; case 1: url = (_b.sent()).url; this.iframePreviewUrl = url + ((_a = this.options.startRoute) !== null && _a !== void 0 ? _a : ""); return [2 /*return*/]; } }); }); }; /** * Nodebox needs to handle two types of iframes at the same time: * * 1. Runtime iframe: where the emulator process runs, which is responsible * for creating the other iframes (hidden); * 2. Preview iframes: any other node process that contains a PORT (public); */ SandpackNode.prototype.manageIframes = function (selector) { var _a; /** * Pick the preview iframe */ if (typeof selector === "string") { var element = document.querySelector(selector); nullthrows(element, "The element '".concat(selector, "' was not found")); this.iframe = document.createElement("iframe"); element === null || element === void 0 ? void 0 : element.appendChild(this.iframe); } else { this.iframe = selector; } // Set preview iframe styles setPreviewIframeProperties(this.iframe, this.options); nullthrows(this.iframe.parentNode, "The given iframe does not have a parent."); /** * Create the runtime iframe, which is hidden sibling * from the preview one */ this.emulatorIframe = document.createElement("iframe"); this.emulatorIframe.classList.add("sp-bridge-frame"); (_a = this.iframe.parentNode) === null || _a === void 0 ? void 0 : _a.appendChild(this.emulatorIframe); }; SandpackNode.prototype.setLocationURLIntoIFrame = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.iframePreviewUrl) return [3 /*break*/, 2]; return [4 /*yield*/, loadPreviewIframe(this.iframe, this.iframePreviewUrl)]; case 1: _a.sent(); _a.label = 2; case 2: return [2 /*return*/]; } }); }); }; /** * Send all messages and events to tell to the * consumer that the bundler is ready without any error */ SandpackNode.prototype.dispatchDoneMessage = function () { this.status = "done"; this.dispatch({ type: "done", compilatonError: false }); if (this.iframePreviewUrl) { this.dispatch({ type: "urlchange", url: this.iframePreviewUrl, back: false, forward: false, }); } }; SandpackNode.prototype.globalListeners = function () { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: window.addEventListener("message", function (event) { if (event.data.type === PREVIEW_LOADED_MESSAGE_TYPE) { injectScriptToIframe(_this.iframe, _this.messageChannelId); } if (event.data.type === "urlchange" && event.data.channelId === _this.messageChannelId) { _this.dispatch({ type: "urlchange", url: event.data.url, back: event.data.back, forward: event.data.forward, }); } else if (event.data.channelId === _this.messageChannelId) { _this.dispatch(event.data); } }); return [4 /*yield*/, this.emulator.fs.watch(["*"], [ ".next", "node_modules", "build", "dist", "vendor", ".config", ".vuepress", ], function (message) { return __awaiter(_this, void 0, void 0, function () { var event, path, type, _a, content, newContent, err_2; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!message) return [2 /*return*/]; event = message; path = "newPath" in event ? event.newPath : "path" in event ? event.path : ""; return [4 /*yield*/, this.emulator.fs.stat(path)]; case 1: type = (_b.sent()).type; if (type !== "file") return [2 /*return*/, null]; _b.label = 2; case 2: _b.trys.push([2, 10, , 11]); _a = event.type; switch (_a) { case "change": return [3 /*break*/, 3]; case "create": return [3 /*break*/, 3]; case "remove": return [3 /*break*/, 5]; case "rename": return [3 /*break*/, 6]; case "close": return [3 /*break*/, 8]; } return [3 /*break*/, 9]; case 3: return [4 /*yield*/, this.emulator.fs.readFile(event.path, "utf8")]; case 4: content = _b.sent(); this.dispatch({ type: "fs/change", path: event.path, content: content, }); this._modulesCache.set(event.path, writeBuffer(content)); return [3 /*break*/, 9]; case 5: this.dispatch({ type: "fs/remove", path: event.path, }); this._modulesCache.delete(event.path); return [3 /*break*/, 9]; case 6: this.dispatch({ type: "fs/remove", path: event.oldPath, }); this._modulesCache.delete(event.oldPath); return [4 /*yield*/, this.emulator.fs.readFile(event.newPath, "utf8")]; case 7: newContent = _b.sent(); this.dispatch({ type: "fs/change", path: event.newPath, content: newContent, }); this._modulesCache.set(event.newPath, writeBuffer(newContent)); return [3 /*break*/, 9]; case 8: return [3 /*break*/, 9]; case 9: return [3 /*break*/, 11]; case 10: err_2 = _b.sent(); this.dispatch({ type: "action", action: "notification", notificationType: "error", title: getMessageFromError(err_2), }); return [3 /*break*/, 11]; case 11: return [2 /*return*/]; } }); }); })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; /** * PUBLIC Methods */ SandpackNode.prototype.restartShellProcess = function () { var _a; return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_b) { switch (_b.label) { case 0: if (!(this.emulatorShellProcess && this.emulatorCommand)) return [3 /*break*/, 3]; // 1. Set the loading state and clean the URL this.dispatch({ type: "start", firstLoad: true }); this.status = "initializing"; // 2. Exit shell return [4 /*yield*/, this.emulatorShellProcess.kill()]; case 1: // 2. Exit shell _b.sent(); (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.removeAttribute("attr"); this.emulator.fs.rm("/node_modules/.vite", { recursive: true, force: true, }); // 3 Run command again return [4 /*yield*/, this.compile(Object.fromEntries(this._modulesCache))]; case 2: // 3 Run command again _b.sent(); _b.label = 3; case 3: return [2 /*return*/]; } }); }); }; SandpackNode.prototype.updateSandbox = function (setup) { var _this = this; var _a; var modules = fromBundlerFilesToFS(setup.files); /** * Update file changes */ if (((_a = this.emulatorShellProcess) === null || _a === void 0 ? void 0 : _a.state) === "running") { Object.entries(modules).forEach(function (_a) { var key = _a[0], value = _a[1]; if (!_this._modulesCache.get(key) || readBuffer(value) !== readBuffer(_this._modulesCache.get(key))) { _this.emulator.fs.writeFile(key, value, { recursive: true }); } }); return; } /** * Pass init files to the bundler */ this.dispatch({ codesandbox: true, modules: modules, template: setup.template, type: "compile", }); /** * Add modules to cache, this will ensure uniqueness changes * * Keep it after the compile action, in order to update the cache at the right moment */ Object.entries(modules).forEach(function (_a) { var key = _a[0], value = _a[1]; _this._modulesCache.set(key, writeBuffer(value)); }); }; SandpackNode.prototype.dispatch = function (message) { var _a, _b; return __awaiter(this, void 0, void 0, function () { var _c; return __generator(this, function (_d) { switch (_d.label) { case 0: _c = message.type; switch (_c) { case "compile": return [3 /*break*/, 1]; case "refresh": return [3 /*break*/, 2]; case "urlback": return [3 /*break*/, 4]; case "urlforward": return [3 /*break*/, 4]; case "shell/restart": return [3 /*break*/, 5]; case "shell/openPreview": return [3 /*break*/, 6]; } return [3 /*break*/, 7]; case 1: this.compile(message.modules); return [3 /*break*/, 8]; case 2: return [4 /*yield*/, this.setLocationURLIntoIFrame()]; case 3: _d.sent(); return [3 /*break*/, 8]; case 4: (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, "*"); return [3 /*break*/, 8]; case 5: this.restartShellProcess(); return [3 /*break*/, 8]; case 6: window.open(this.iframePreviewUrl, "_blank"); return [3 /*break*/, 8]; case 7: this.emitter.dispatch(message); _d.label = 8; case 8: return [2 /*return*/]; } }); }); }; SandpackNode.prototype.listen = function (listener) { return this.emitter.listener(listener); }; SandpackNode.prototype.destroy = function () { this.emulatorIframe.remove(); this.emitter.cleanup(); }; return SandpackNode; }(SandpackClient)); export { SandpackNode };