UNPKG

@sanity/ui-workshop

Version:

An environment for designing, reviewing, and quality-testing React components.

386 lines (379 loc) 10.4 kB
import cpx from "cpx"; import { writeFile, readFile } from "fs/promises"; import path from "path"; import rimraf from "rimraf"; import { build as build$1, createServer } from "vite"; import { createRequire } from "node:module"; import fs, { accessSync } from "fs"; import globby from "globby"; import mkdirp from "mkdirp"; import react from "@vitejs/plugin-react"; import chokidar from "chokidar"; import express from "express"; function _fileExists(file) { try { return accessSync(file), !0; } catch { return !1; } } const RUNTIME_FILE_NAMES = ["workshop.runtime.js", "workshop.runtime.jsx", "workshop.runtime.mjs", "workshop.runtime.cjs", "workshop.runtime.ts", "workshop.runtime.tsx"]; function _findRuntimeFile(options) { const { packagePath } = options; for (const f of RUNTIME_FILE_NAMES) { const file = path.resolve(packagePath, f); if (_fileExists(file)) return file; } } const require2 = createRequire(import.meta.url); async function _loadRuntime(options) { const { packagePath } = options, configPath = _findRuntimeFile({ packagePath }); if (!configPath) return; const { register } = await import("esbuild-register/dist/node"), eslintOptions = { // eslint options jsx: "automatic", jsxFactory: "createElement", jsxFragment: "Fragment", jsxImportSource: "react", logLevel: "silent" }, { unregister } = globalThis.__DEV__ ? { unregister: () => { } } : register(eslintOptions), config = require2(configPath); return unregister(), config?.default || config; } const DEFAULT_PATTERN = ["src/**/__workshop__/index.js", "src/**/__workshop__/index.jsx", "src/**/__workshop__/index.ts", "src/**/__workshop__/index.tsx"]; function _getFiles(options) { const { cwd, pattern } = options; return { subscribe(observer) { return globby(pattern, { cwd }).then((files) => { observer.next(files.map((f) => path.resolve(cwd, f))); }), { unsubscribe() { } }; } }; } function _sanitizeModulePath(modulePath) { return modulePath.replace(/\.[^/.]+$/, "").replace(/\/index$/, ""); } function _compileModule(paths) { if (paths.length === 0) return `// THIS FILE IS AUTO-GENERATED export const scopes = [] `; const sortedPaths = paths.sort(), imports = sortedPaths.map((p, idx) => `import _${idx} from '${_sanitizeModulePath(p)}'`).join(` `), exports = sortedPaths.map((_p, idx) => ` _${idx}`).join(`, `); return ["// THIS FILE IS AUTO-GENERATED", imports, `export const scopes = [ ${exports}, ]`].join(` `) + ` `; } const HTML$1 = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover" /> <style> html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; -webkit-font-smoothing: antialiased; } html, body, #root { height: 100%; margin: 0; } </style> </head> <body> <div id="root"></div> <script> if (window.parent !== window) { window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__ } <\/script> <script type="module" src="/.workshop/frame/main.tsx"><\/script> </body> </html> `; async function _writeFrameHTML(options) { await writeFile(path.resolve(options.outDir, "frame/index.html"), HTML$1); } const SCRIPT$1 = `// THIS FILE IS AUTO-GENERATED import {mountFrame} from '@sanity/ui-workshop' import {scopes} from '../scopes' import config from '../../workshop.config' mountFrame({ config: {...config, scopes}, element: document.getElementById('root'), }) `; async function _writeFrameScript(options) { await writeFile(path.resolve(options.outDir, "frame/main.tsx"), SCRIPT$1); } const HTML = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover" /> <style> html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; -webkit-font-smoothing: antialiased; } html, body, #root { height: 100%; margin: 0; } </style> </head> <body> <div id="root"></div> <script type="module" src="/.workshop/main.tsx"><\/script> </body> </html> `; async function _writeHTML(options) { await writeFile(path.resolve(options.outDir, "index.html"), HTML); } const SCRIPT = `// THIS FILE IS AUTO-GENERATED import {mount} from '@sanity/ui-workshop' import {scopes} from './scopes' import config from '../workshop.config' mount({ config: {...config, scopes}, element: document.getElementById('root'), }) `; async function _writeScript(options) { await writeFile(path.resolve(options.outDir, "main.tsx"), SCRIPT); } async function buildStaticFiles(options) { const { runtimeDir } = options; await mkdirp(runtimeDir), await _writeHTML({ outDir: runtimeDir }), await _writeScript({ outDir: runtimeDir }), await mkdirp(path.resolve(runtimeDir, "frame")), await _writeFrameHTML({ outDir: runtimeDir }), await _writeFrameScript({ outDir: runtimeDir }); } function createViteConfig(options) { const { cwd, outDir, runtimeDir } = options; return { build: { outDir, rollupOptions: { input: { main: path.resolve(runtimeDir, "index.html"), frame: path.resolve(runtimeDir, "frame/index.html") } } }, optimizeDeps: { esbuildOptions: { jsx: "automatic" } }, plugins: [react({ babel: { plugins: [["babel-plugin-react-compiler", { target: "19" }]] } })], root: cwd }; } function getScopes(options) { return new Promise((resolve) => { _getFiles(options).subscribe({ next: resolve }); }); } async function build(options) { const { cwd } = options, runtime = await _loadRuntime({ packagePath: cwd }), runtimeDir = path.resolve(cwd, ".workshop"), outDir = runtime?.build?.outDir || path.resolve(cwd, "dist"); await buildStaticFiles({ runtimeDir }); const relativeScopes = (await getScopes({ cwd, pattern: runtime?.pattern || DEFAULT_PATTERN })).map((f) => path.relative(outDir, f)), code = _compileModule(relativeScopes); await writeFile(path.resolve(runtimeDir, "scopes.ts"), code); const baseViteConfig = createViteConfig({ cwd, outDir, runtimeDir }); let viteConfig = runtime?.vite?.(baseViteConfig) || baseViteConfig; typeof viteConfig == "object" && "then" in viteConfig && (viteConfig = await viteConfig), await build$1(viteConfig), cpx.copySync(path.resolve(outDir, ".workshop", "**/*"), outDir), await rimraf(path.resolve(outDir, ".workshop")); } function _watchFiles(options) { const { cwd, pattern } = options; return { subscribe(observer) { const watcher = chokidar.watch(pattern, { cwd, ignoreInitial: !0 }); return watcher.on("all", (event, file) => { observer.next({ type: event, file: path.resolve(cwd, file) }); }), { unsubscribe() { watcher.close(); } }; } }; } function _watchScopes(options) { return { subscribe(observer) { const initialFiles$ = _getFiles(options), fileEvent$ = _watchFiles(options); let files; initialFiles$.subscribe({ next(initialFiles) { files = initialFiles, observer.next(files); } }); const fileEventSub = fileEvent$.subscribe({ next(event) { if (event.type === "add" && (files.push(event.file), observer.next(files)), event.type === "unlink") { const idx = files.indexOf(event.file); idx && (files.splice(idx, 1), observer.next(files)); } } }); return { unsubscribe() { fileEventSub.unsubscribe(); } }; } }; } async function createDevServer(options) { const { cwd, outDir, runtime, runtimeDir } = options, baseViteConfig = { ...createViteConfig({ cwd, outDir, runtimeDir }), appType: "custom", // don't include HTML middlewares configFile: !1, logLevel: "info", server: { middlewareMode: !0 } }; let viteConfig = runtime?.vite?.(baseViteConfig) || baseViteConfig; typeof viteConfig == "object" && "then" in viteConfig && (viteConfig = await viteConfig); const vite = await createServer(viteConfig), app = express(); return app.use(vite.middlewares), app.use("*all", async (req, res) => { const url = req.originalUrl; let htmlPath = "index.html"; url.includes("/frame/") && (htmlPath = "frame/index.html"); try { const template = await readFile(path.resolve(runtimeDir, htmlPath), "utf-8"), html = await vite.transformIndexHtml(url, template); res.status(200).set({ "Content-Type": "text/html" }).send(html); } catch (e) { e instanceof Error ? (vite.ssrFixStacktrace(e), console.log(e.stack), res.status(500).end(e.stack)) : res.status(500).end(String(e)); } }), app; } async function dev(options) { const { cwd } = options, runtime = await _loadRuntime({ packagePath: cwd }), runtimeDir = path.resolve(cwd, ".workshop"), outDir = path.resolve(cwd, "dist"); await buildStaticFiles({ runtimeDir }); const scopesSub = _watchScopes({ cwd, pattern: runtime?.pattern || DEFAULT_PATTERN }).subscribe({ next: (scopes) => { const relativeScopes = scopes.map((f) => path.relative(runtimeDir, f)), code = _compileModule(relativeScopes); fs.writeFileSync(path.resolve(runtimeDir, "scopes.ts"), code); } }), app = await createDevServer({ cwd, outDir, runtime, runtimeDir }), port = runtime?.server?.port || 1337; app.listen(port, () => { console.log(`listening on http://localhost:${port}`); }).on("close", () => { console.log("server closed"), scopesSub.unsubscribe(), process.exit(1); }); } export { build, dev }; //# sourceMappingURL=runtime.js.map