UNPKG

iles

Version:

Vite & Vue powered static site generator with partial hydration

458 lines (448 loc) 16.9 kB
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs"; import { t as ILES_APP_ENTRY } from "./constants-DUVbDEPQ.mjs"; import { c as NOT_FOUND_COMPONENT_PATH, d as USER_APP_REQUEST_PATH, f as USER_SITE_REQUEST_PATH, i as DEBUG_COMPONENT_PATH, n as APP_CONFIG_REQUEST_PATH, r as APP_PATH, t as APP_COMPONENT_PATH } from "./alias-Cyp8pcW6.mjs"; import { a as parseId, c as exists, d as uniq, i as wrapLayout, l as pascalCase, n as pathToHtmlFilename, o as parseImports, r as wrapIslandsInSFC, s as debug$1, t as resolveConfig, u as serialize } from "./config-BEGLROoQ.mjs"; import { existsSync, promises } from "fs"; import { basename, extname, relative, resolve } from "pathe"; import { createServer, mergeConfig, transformWithOxc } from "vite"; import MagicString from "magic-string"; import pc from "picocolors"; import creatDebugger from "debug"; import deepEqual from "deep-equal"; import glob from "fast-glob"; import micromatch from "micromatch"; //#region src/node/server.ts var server_exports = /* @__PURE__ */ __exportAll({ createServer: () => createServer$1 }); async function createServer$1(root = process.cwd(), serverOptions = {}) { const config = await resolveConfig(root); const viteConfig = mergeConfig(config.vite, { plugins: IslandsPlugins(config), server: serverOptions }); return { config, viteConfig, server: await createServer(viteConfig) }; } //#endregion //#region src/node/plugin/middleware.ts const supportedExtensions = new Set([ ".html", ".xml", ".json", ".rss", ".atom" ]); const debug = creatDebugger("iles:html-page-fallback"); function configureMiddleware(config, server, defaultLayoutPath) { restartOnConfigChanges(config, server); server.middlewares.use(function ilesHtmlPagesMiddleware(req, res, next) { let { url = "" } = req; url = pathToHtmlFilename(url); if (url.endsWith(".html")) { const filename = resolve(config.pagesDir, url.slice(1)); if (existsSync(filename)) { url = `/${relative(config.root, filename)}`; debug("Rewriting", req.method, req.url, "to", url); req.url = url; } } next(); }); return () => { server.middlewares.use(async (req, res, next) => { const url = req.url || ""; if (url.startsWith("/@fs/")) return next(); if (await exists(resolve(config.root, url.slice(1)))) return next(); if (url.includes(defaultLayoutPath)) { res.statusCode = 200; res.setHeader("content-type", "text/javascript"); res.end("export default false"); } else if (supportedExtensions.has(extname(url))) { res.statusCode = 200; res.setHeader("content-type", "text/html"); let html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <div id="app"></div> <script type="module" src="${ILES_APP_ENTRY}"><\/script> </body> </html>`; html = await server.transformIndexHtml(url, html, req.originalUrl); res.end(html); } else next(); }); }; } async function restartOnConfigChanges(config, server) { const restartIfConfigChanged = async (path) => { if (path === config.configPath) { server.config.logger.info(pc.green(`${relative(process.cwd(), config.configPath)} changed, restarting server...`), { clear: true, timestamp: true }); await server.close(); global.__vite_start_time = Date.now(); const { server: newServer } = await createServer$1(server.config.root, server.config.server); await newServer.listen(); } }; server.watcher.add(config.configPath); server.watcher.on("add", restartIfConfigChanged); server.watcher.on("change", restartIfConfigChanged); } //#endregion //#region src/node/plugin/site.ts function extendSite(code, config) { return `${code.replace("export default ", "let __site = ")} __site.url = '${config.siteUrl}${config.base.slice(0, config.base.length - 1)}' __site.canonical = '${config.siteUrl.split("//", 2)[1] ?? ""}' import { ref as _$ref } from 'vue' const __siteRef = _$ref(__site) __site = { ref: __siteRef } export { __site, __siteRef as default } if (import.meta.hot) import.meta.hot.accept(mod => { __site.ref.value = mod.__site.ref.value mod.__site.ref = __site.ref }) `; } //#endregion //#region src/node/plugin/markdown.ts let originalTags; function detectMDXComponents(code, config, server) { const mdxComponents = code.match(/\bmdxComponents\b(?:.*?){(.*?)}/s)?.[1]; if (!mdxComponents) return; const foundTags = Array.from(mdxComponents.matchAll(/\b['"]?(\w+)['"]?:/g)).map((m) => m[1]); if (!originalTags) originalTags = config.markdown.overrideElements ||= []; const dynamicElements = Array.from(new Set([...originalTags, ...foundTags])).sort(); if (!deepEqual(dynamicElements, config.markdown.overrideElements)) { config.markdown.overrideElements = dynamicElements; server?.moduleGraph.invalidateAll(); } } //#endregion //#region src/node/plugin/composables.ts const definitionRegex$1 = /(?:function|const|let|var)\s+(definePageComponent|use(?:Page|Route|Head|Documents)\b)/g; const composableUsageRegex = /\b(definePageComponent|use(?:Page|Route|Head|Documents))\s*\(/g; const composables = [ "definePageComponent", "useDocuments", "useHead", "usePage", "useRoute" ]; async function autoImportComposables(code, id) { const matches = Array.from(code.matchAll(composableUsageRegex)); if (matches.length === 0) return; const imports = await parseImports(code); const defined = new Set(Array.from(code.matchAll(definitionRegex$1)).map((a) => a[1])); const composables = uniq(matches.map((a) => a[1])).filter((composable) => !defined.has(composable) && !imports[composable]).join(", "); if (composables) return `${code}\nimport { ${composables} } from "iles"`; } function writeComposablesDTS(root) { promises.writeFile(resolve(root, "composables.d.ts"), `// generated by iles // We suggest you to commit this file into source control declare global { ${composables.map((fn) => ` const ${fn}: typeof import('iles')['${fn}']`).join("\n")} } export { } `, "utf-8"); } //#endregion //#region src/node/plugin/documents.ts const definitionRegex = /(function|const|let|var)[\s\n]+\buseDocuments\b/; const usageRegex = /\buseDocuments[\s\n]*\(([^)]+)\)/g; const fileCanUseDocuments = /(\.vue|\.[tj]sx?)$/; const DOCS_VIRTUAL_ID = "/@islands/documents"; function documentsPlugin(config) { const { root, drafts, namedPlugins: { pages } } = config; let server; const modulesById = Object.create(null); return { name: "iles:documents", configureServer(devServer) { server = devServer; }, resolveId(id) { if (id.startsWith(DOCS_VIRTUAL_ID)) return id; }, async load(id, options) { if (!id.startsWith(DOCS_VIRTUAL_ID)) return; const { query: { pattern: rawPath } } = parseId(id); const path = relative(root, await config.resolvePath(rawPath) || rawPath); const pattern = path.includes("*") ? path : `${path}/**/*.{md,mdx}`; if (server) modulesById[id] = { pattern, hasDocument: (path) => micromatch.isMatch(path, pattern) }; const files = await glob(pattern, { cwd: root }); debug$1.documents("%s %O", rawPath, { path, pattern, files }); let data = await Promise.all(files.map(async (file) => { const frontmatter = await pages.api.frontmatterForPageOrFile(file); frontmatter.meta.filename ||= file; return frontmatter; })); if (!drafts) data = data.filter((page) => !page.draft); debug$1.documents(`${files.length} files, ${data.length} documents, drafts: ${drafts}`); const documents = data.map(({ route: _, meta, layout, ...frontmatter }, index) => { return { ...meta, ...frontmatter, meta, frontmatter, component: `${index}_component` }; }); return ` import { shallowRef, defineAsyncComponent } from 'vue' export const documents = ${serialize(documents).replace(/component:"(\w+)"/g, (_, id) => { return `component: unwrapDefault(() => import('/${documents[id.split("_component")[0]].filename}'))`; })} .map(doc => ({ ...doc, ...defineAsyncComponent(doc.component) })) export default documents.ref = shallowRef(documents) function unwrapDefault (fn) { let cached return () => cached ||= fn().then(mod => mod.default) } if (import.meta.hot) { import.meta.hot.accept(mod => { const docs = documents.ref.value const oldDocsByFile = {} docs.forEach(doc => { oldDocsByFile[doc.filename] = doc }) documents.ref.value = mod.documents.map(newDoc => { const oldDoc = oldDocsByFile[newDoc.filename] if (!oldDoc) return newDoc const { meta, frontmatter } = newDoc return Object.assign(oldDoc, { ...meta, ...frontmatter, meta, frontmatter }) }) mod.documents.ref = documents.ref }) } `; }, async transform(code, id) { if (fileCanUseDocuments.test(id) && !definitionRegex.test(code)) { const paths = []; code = code.replace(usageRegex, (_, path) => { path = path.trim().slice(1, -1); const id = `_documents_${paths.length}`; paths.push([id, path]); return id; }); if (paths.length) { const imports = paths.map(([id, path]) => `import ${id} from '${DOCS_VIRTUAL_ID}?pattern=${path}'`); return `${code};${imports.join(";")}`; } } }, hotUpdate({ file, modules }) { const relFile = relative(root, file); const extra = []; for (const id in modulesById) if (modulesById[id].hasDocument(relFile)) { const mod = this.environment.moduleGraph.getModuleById(id); if (mod) extra.push(mod); } if (extra.length) return [...modules, ...extra]; } }; } //#endregion //#region src/node/plugin/plugin.ts function isMarkdown(path) { return path.endsWith(".mdx") || path.endsWith(".md"); } function isSFCMain(path, query) { return path.endsWith(".vue") && query.vue === void 0; } function isVueScript(path, query) { return path.endsWith(".vue") && (!query.type || query.type === "script"); } async function transformUserFile(path) { return await exists(path) ? await transformWithOxc(await promises.readFile(path, "utf-8"), path, { sourcemap: false }) : { code: "export default {}" }; } const templateLayoutRegex = /<template.*?\slayout=\s*['"](\w+)['"].*?>/; function IslandsPlugins(appConfig) { debug$1.config(appConfig); let base; let root; let isBuild; let server; const appPath = resolve(appConfig.srcDir, "app.ts"); const sitePath = resolve(appConfig.srcDir, "site.ts"); const layoutsRoot = `/${relative(appConfig.root, appConfig.layoutsDir)}`; const defaultLayoutPath = `${layoutsRoot}/default.vue`; const plugins = appConfig.namedPlugins; function isLayout(path) { return path.includes(appConfig.layoutsDir); } return [ { name: "iles", enforce: "pre", async configResolved(config) { if (base) return; base = config.base; root = config.root; isBuild = config.command === "build"; appConfig.resolvePath = config.createResolver(); writeComposablesDTS(root); detectMDXComponents((await transformUserFile(appPath)).code, appConfig, void 0); }, async resolveId(id) { if (id === "/@iles-entry") return APP_PATH; if (id === APP_CONFIG_REQUEST_PATH || id === USER_APP_REQUEST_PATH || id === USER_SITE_REQUEST_PATH) return id; if (id === "@islands/components/NotFound") return NOT_FOUND_COMPONENT_PATH; if (id === defaultLayoutPath) return resolve(root, id.slice(1)); }, async load(id) { if (id === APP_CONFIG_REQUEST_PATH) { const { base, debug, jsx, ssg: { sitemap }, siteUrl, markdown: { overrideElements = [] } } = appConfig; return `export default ${serialize({ base, debug, root, jsx, sitemap, siteUrl, overrideElements })}`; } const userFilename = id === USER_APP_REQUEST_PATH && appPath || id === USER_SITE_REQUEST_PATH && sitePath; if (userFilename) { this.addWatchFile(userFilename); const result = await transformUserFile(userFilename); if (id === USER_APP_REQUEST_PATH) detectMDXComponents(result.code, appConfig, server); if (id === USER_SITE_REQUEST_PATH) return extendSite(result.code, appConfig); return result; } if ((isBuild || process.env.VITEST) && id.includes(defaultLayoutPath) && !await exists(resolve(root, defaultLayoutPath.slice(1)))) return "<template><slot/></template>"; }, transform(code, id) { if (id === APP_COMPONENT_PATH && !isBuild && appConfig.debug) return code.replace("const DebugPanel = () => null", () => `import DebugPanel from '${DEBUG_COMPONENT_PATH}'`); }, hotUpdate({ file }) { if (file === appPath) return [this.environment.moduleGraph.getModuleById(USER_APP_REQUEST_PATH)]; if (file === sitePath) return [this.environment.moduleGraph.getModuleById(USER_SITE_REQUEST_PATH)]; }, configureServer(devServer) { server = devServer; return configureMiddleware(appConfig, server, defaultLayoutPath); } }, { name: "iles:detect-islands-in-vue", enforce: "pre", async transform(code, id) { const { path, query } = parseId(id); if (query.vue !== void 0 && query.type === "script-client") return "export default {}; if (import.meta.hot) import.meta.hot.accept()"; if (isSFCMain(path, query) && code.includes("client:") && code.includes("<template")) return wrapIslandsInSFC(appConfig, code, path); } }, { name: "iles:layouts", enforce: "pre", transform(code, id) { const { path, query } = parseId(id); if (!isSFCMain(path, query) || !isLayout(path)) return; const layoutName = code.match(templateLayoutRegex)?.[1] || false; if (String(layoutName) === "false") return; return wrapLayout(code, path); } }, plugins.vue, ...appConfig.vitePlugins, plugins.components, documentsPlugin(appConfig), { name: "iles:composables", enforce: "post", async transform(code, id) { if (!id.startsWith(appConfig.srcDir)) return; const { path, query } = parseId(id); if (isVueScript(path, query) || /\.[tj]sx?/.test(path)) return await autoImportComposables(code, id); } }, { name: "iles:page-data", enforce: "post", async transform(code, id, options) { const { path, query } = parseId(id); const isMdx = isMarkdown(path); if (!isMdx && !isVueScript(path, query)) return; const isLayoutFile = isLayout(path); const isPage = plugins.pages.api.isPage(path); if (!isMdx && !isLayoutFile && !isPage) return; const sfcIndex = indexOfVueComponentDefinition(code); if (!sfcIndex || sfcIndex === -1) return; const s = new MagicString(code); const appendToSfc = (key, value) => s.appendRight(sfcIndex, value ? `${key}:${value},` : `${key},`); if (isLayoutFile) { appendToSfc("name", `'${pascalCase(basename(path).replace(".vue", "Layout"))}'`); return s.toString(); } appendToSfc("inheritAttrs", serialize(false)); const { meta, layout = "default", route: _r, ...frontmatter } = await plugins.pages.api.frontmatterForPageOrFile(path, code); if (isMdx) { const keys = Object.keys(frontmatter); const bindings = Object.entries(frontmatter).map(([key, value]) => `${key} = ${serialize(value)}`); bindings.push(`meta = ${serialize(meta)}`); bindings.push(`frontmatter = { ${keys.length > 0 ? keys.join(", ") : ""} }`); s.prepend(`const ${bindings.join(", ")};`); appendToSfc("...meta, ...frontmatter, meta, frontmatter"); } else { s.prepend(`const _meta = ${serialize(meta)}, _frontmatter = ${serialize(frontmatter)};`); appendToSfc("..._meta, ..._frontmatter, meta: _meta, frontmatter: _frontmatter"); } if (isPage) { appendToSfc("layoutName", serialize(layout)); appendToSfc("layoutFn", String(layout) === "false" ? "false" : `() => import('${layoutsRoot}/${layout}.vue').then(m => m.default)`); } return s.toString(); } }, { name: "iles:page-hmr", apply: "serve", enforce: "post", async transform(code, id) { const { path } = parseId(id); if (isLayout(path) || plugins.pages.api.isPage(path)) return `${code} import.meta.hot?.accept('/${relative(root, path)}', (...args) => __ILES_PAGE_UPDATE__(args)) `; } }, appConfig.jsx === "preact" && { name: "iles:preact-jsx-config", config() { return { oxc: { jsx: { runtime: "automatic", importSource: "preact" }, include: /\.(tsx?|jsx)$/ } }; } } ]; } function indexOfVueComponentDefinition(code) { let sfcConstIndex = code.indexOf("const _sfc_main = "); if (sfcConstIndex === -1) sfcConstIndex = code.indexOf("export default "); if (sfcConstIndex === -1) return; const braceIndex = code.indexOf("{", sfcConstIndex); if (braceIndex === -1) return; return braceIndex + 1; } //#endregion export { server_exports as n, IslandsPlugins as t };