iles
Version:
Vite & Vue powered static site generator with partial hydration
458 lines (448 loc) • 16.9 kB
JavaScript
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 };