UNPKG

microsite

Version:
243 lines (240 loc) 9.79 kB
import { startDevServer } from "snowpack"; import arg from "arg"; import { join, resolve, extname } from "path"; import { green, dim } from "kleur/colors"; import polka from "polka"; import { openInBrowser } from "../utils/open.js"; import { readDir } from "../utils/fs.js"; import { promises as fsp } from "fs"; import { loadConfiguration } from "../utils/command.js"; const pageScript = (page, props) => `import { csr } from '/web_modules/microsite/client/csr.js'; import Page from '${page}'; csr("${page.replace(/\/src\/pages/, "").replace(/\.js$/, "")}", ${props ? `Page, ${JSON.stringify(props)}` : `Page`});`; const errorScript = (props) => `import { h, render } from '/web_modules/preact.js'; import Page from '/web_modules/microsite/error.js'; const root = document.querySelector('#__microsite'); render(h(Page, ${JSON.stringify(props)}, null), root);`; const globalScript = () => `(async () => { try { await import('/src/global/index.css.proxy.js'); } catch (e) {} try { const global = await import('/src/global/index.js').then(mod => mod.default); if (global) global(); } catch (e) {} document.documentElement.style.removeProperty('visibility'); })()`; const doc = (page, props) => `<!doctype html>\n<!-- Generated by microsite -->\n<html lang="en" dir="ltr" style="visibility:hidden">\n\t<head></head>\n\t<body> \t\t<div id="__microsite"></div> \t\t<script data-csr="true">window.HMR_WEBSOCKET_URL = 'ws://localhost:3333';</script> \t\t<script type="module" src="/__snowpack__/hmr-client.js"></script> \t\t<script type="module" data-microsite="page">${pageScript(page, props)}</script> \t\t<script type="module" data-microsite="global">${globalScript()}</script> </body>\n</html>`; const errorPage = (props) => `<!doctype html>\n<!-- Generated by microsite -->\n<html lang="en" dir="ltr">\n\t<head></head>\n\t<body> \t\t<div id="__microsite"></div> \t\t<script data-csr="true">window.HMR_WEBSOCKET_URL = 'ws://localhost:3333';</script> \t\t<script type="module" src="/__snowpack__/hmr-client.js"></script> \t\t<script type="module" data-microsite="page">${errorScript(props)}</script> \t\t<script type="module" data-microsite="global">${globalScript()}</script>`; const EXTS = [".js", ".jsx", ".ts", ".tsx", ".mjs"]; function parseArgs(argv) { return arg({ "--port": Number, // Aliases "-p": "--port", }, { permissive: true, argv }); } export default async function dev(argv) { var _a; const cwd = process.cwd(); const args = parseArgs(argv); let PORT = (_a = args["--port"]) !== null && _a !== void 0 ? _a : 8888; const [errs, config] = await loadConfiguration('dev'); if (errs) { errs.forEach((err) => console.error(err)); return; } const snowpack = await startDevServer({ cwd: process.cwd(), config, lockfile: null, }); const loadErrorPage = async (contentType, props) => { try { const url = `/src/pages/_error.js`; const result = await snowpack.loadUrl(url); if (!result) throw new Error(); if (contentType === "text/html") return doc("/src/pages/_error.js", props); if (contentType === "application/javascript") return result.contents.toString(); } catch (e) { } try { const url = `/web_modules/microsite/error.js`; const result = await snowpack.loadUrl(url); if (!result) throw new Error(); if (contentType === "text/html") return errorPage(props); if (contentType === "application/javascript") return result.contents.toString(); } catch (e) { } return null; }; const sendErr = async (res, props) => { var _a; const contents = await loadErrorPage("text/html", props); res.writeHead((_a = props.statusCode) !== null && _a !== void 0 ? _a : 500, { "Content-Type": "text/html", }); res.end(contents); }; const server = polka() .use(async (req, res, next) => { var _a; if ((_a = req.url) === null || _a === void 0 ? void 0 : _a.endsWith(".js")) { res.setHeader("Content-Type", "application/javascript"); } next(); }) .use(async (req, res, next) => { var _a, _b; if (req.url === "/") return next(); const clean = /(\.html|index\.html|index|\/)$/; if (clean.test((_a = req.url) !== null && _a !== void 0 ? _a : "")) { res.writeHead(302, { Location: (_b = req.url) === null || _b === void 0 ? void 0 : _b.replace(clean, ""), }); res.end(); } next(); }) .use(async (req, res, next) => { var _a; if (((_a = req.url) === null || _a === void 0 ? void 0 : _a.indexOf("microsite")) > -1) { if (req.url.endsWith("_error.js")) { const contents = await loadErrorPage("application/javascript"); res.setHeader("Content-Type", "application/javascript"); res.end(contents); } else { req.url.replace("_microsite", "microsite"); } } next(); }) .use(async (req, res, next) => { if (!(req.url.endsWith(".html") || req.url.indexOf(".") === -1)) return next(); let base = req.url.slice(1); if (base.endsWith(".html")) base = base.slice(0, ".html".length * -1); if (base === "") base = "index"; const loadAndSSR = async (base) => { try { const url = `/src/pages/${base}.js`; const result = await snowpack.loadUrl(url, { isSSR: true }); if (!result) throw new Error(); res.setHeader("Content-Type", "text/html"); res.end(doc(url)); return true; } catch (err) { // console.error(err); } return false; }; const findPotentialMatch = async (base) => { const baseParts = [...base.split("/"), "index"]; const pages = join(cwd, "src", "pages"); let files = await readDir(pages); files = files .filter((file) => EXTS.includes(extname(file))) .map((file) => file.slice(pages.length, extname(file).length * -1)) .filter((file) => { if (file.indexOf("[") === -1) return false; const parts = file.slice(1).split("/"); if (parts.length === baseParts.length - 1) return parts.every((part, i) => part.indexOf("[") > -1 ? true : part === baseParts[i]); if (parts.length === baseParts.length) return parts.every((part, i) => part.indexOf("[") > -1 ? true : part === baseParts[i]); if (file.indexOf("[[") > -1) return parts.every((part, i) => { if (part.indexOf("[[")) return i === parts.length - 1; if (part.indexOf("[")) return true; return part === baseParts[i]; }); }); if (files.length === 0) return null; if (files.length === 1) return files[0].slice(1); if (files.length > 1) { // TODO: rank direct matches above catch-all routes // console.log(files); return files[0]; } }; const direct = await loadAndSSR(base); if (direct) { return next(); } const index = await loadAndSSR(`${base}/index`); if (index) { return next(); } const dynamic = await findPotentialMatch(base); if (dynamic) { await loadAndSSR(dynamic); } next(); }) .use(async (req, res, next) => { try { // Respond directly if asset is found const result = await snowpack.loadUrl(req.url); if (result.contentType) res.setHeader("Content-Type", result.contentType); if (req.url.indexOf("/web_modules/microsite") === -1) { result.contents = result.contents .toString() .replace(/preact\/hooks/, "microsite/client/hooks"); } return res.end(result.contents); } catch (err) { } next(); }) .use(async (req, res, next) => { try { let localPath = resolve(cwd, `.${req.url}`); const stats = await fsp.stat(localPath); if (stats.isDirectory()) { let contents = await readDir(localPath); contents = contents.map((path) => path.slice(localPath.length)); res.setHeader("Content-Type", "application/json"); return res.end(JSON.stringify(contents)); } } catch (err) { } next(); }) .get("*", (_req, res) => sendErr(res, { statusCode: 404 })); await new Promise((resolve) => server.listen(PORT, (err) => { if (err) throw err; resolve(); })); let protocol = "http:"; let hostname = "localhost"; await openInBrowser(protocol, hostname, PORT, "chrome"); console.log(`${dim("[microsite]")} ${green("✔")} Microsite started on ${green(`${protocol}//${hostname}:${PORT}`)}\n`); }