UNPKG

iles

Version:

Vite & Vue powered static site generator with partial hydration

152 lines (149 loc) 6.38 kB
import { parseExports, parseImports } from "./chunk-6FAOHHHS.js"; import { debug, isString, pascalCase } from "./chunk-ROUSHGC2.js"; import { MagicString } from "./chunk-Y4KSH55M.js"; // src/node/plugin/wrap.ts import { parse } from "vue/compiler-sfc"; var unresolvedIslandKey = "__viteIslandComponent"; async function wrapLayout(code, filename) { const { descriptor: { template }, errors } = parse(code, { filename }); if (errors.length > 0 || !template || !isString(template.attrs.layout)) return; const s = new MagicString(code); const nodes = template.ast?.children; if (!nodes?.length) { return; } const Layout = `${pascalCase(template.attrs.layout)}Layout`; debug.layout(`${template.attrs.layout} ${filename}`); s.appendLeft(nodes[0].loc.start.offset, `<${Layout}>`); s.appendRight(nodes[nodes.length - 1].loc.end.offset, `</${Layout}>`); return { code: s.toString(), map: s.generateMap({ hires: true }) }; } var scriptClientRE = /<script\b([^>]*\bclient:[^>]*)>([^]*?)<\/script>/; async function wrapIslandsInSFC(config, code, filename) { code = code.replace(scriptClientRE, (_, attrs, content) => `<script-client${attrs}>${content}</script-client>`); const { descriptor: { template, script, scriptSetup, customBlocks }, errors } = parse(code, { filename }); const scriptClientIndex = customBlocks.findIndex((b) => b.type === "script-client"); const scriptClient = scriptClientIndex > -1 && customBlocks[scriptClientIndex]; if (errors.length > 0) return; if (scriptClient && "setup" in scriptClient.attrs || scriptSetup && Object.keys(scriptSetup.attrs).some((attr) => attr.startsWith("client:"))) throw new Error("Incorrect usage of hydration strategy in script setup.\nSee https://iles-docs.netlify.app/guide/client-scripts#client-script-block"); if (!template?.ast?.children.length) { if (scriptClient) { throw new Error(`Vue components with <script client:...> must define a template containing at least one tag. No valid template found in ${filename}`); } return; } const sfcRootNode = template.ast; const s = new MagicString(code); const components = config.namedPlugins.components.api; if (scriptClient) { await injectClientScript(sfcRootNode, s, filename, scriptClientIndex, scriptClient); } const jsCode = scriptSetup?.loc?.source || script?.loc?.source; const imports = jsCode ? await parseImports(jsCode) : {}; let componentCounter = 0; let injectionOffset = scriptSetup?.loc?.start?.offset; const elements = sfcRootNode.children.filter((n) => n.tag); for (const child of elements) { await visitSFCNode(child, s, resolveComponentImport); } if (!scriptSetup && injectionOffset === 0) s.appendRight(0, "\n</script>\n"); return { code: s.toString(), map: s.generateMap({ hires: true }) }; async function resolveComponentImport(strategy, tagName) { debug.detect(`<${tagName} ${strategy}>`); if (imports[tagName]) return await resolveImportPath(config, imports[tagName], filename); const info = await resolveComponent(components, tagName, filename, componentCounter++); if (strategy !== "client:only") injectComponentImport(info); return info; } function injectComponentImport(info) { if (injectionOffset === void 0) { const opening = `<script setup lang="${script?.attrs?.lang || "ts"}">`; s.prepend(opening); injectionOffset = 0; } s.appendRight(injectionOffset, ` ${components.stringifyImport(info)};`); } } async function visitSFCNode(node, s, resolveComponentImport) { const strategy = "props" in node && node.props.find((prop) => prop.name.startsWith("client:"))?.name; if (strategy) { const { tag, loc: { start, end } } = node; const importMeta = await resolveComponentImport(strategy, tag); const componentProps = ` :component="${strategy === "client:only" ? null : importMeta.as}" componentName="${tag}" importName="${importMeta.name}" importFrom="${importMeta.from}" `; s.overwrite( start.offset + 1, start.offset + 1 + tag.length, `Island ${componentProps.replace(/\n\s*/g, " ")}`, { contentOnly: true } ); if (!node.isSelfClosing) s.overwrite(end.offset - 1 - tag.length, end.offset - 1, "Island", { contentOnly: true }); } if ("children" in node) { for (const child of node.children) await visitSFCNode(child, s, resolveComponentImport); } } async function resolveComponent(components, tag, filename, counter) { const info = await components.findComponent(pascalCase(tag), filename); if (!info) throw new Error(`Could not resolve ${tag} in ${filename}. Make sure to import it explicitly, or add a component resolver.`); return { name: "default", ...info, as: `__ile_components_${counter}` }; } async function resolveImportPath(config, info, importer) { info.from = await config.resolvePath(info.from, importer) || info.from; return info; } async function injectClientScript(node, s, filename, index, block) { const { attrs, content, loc: { end } } = block; const { lang = "ts", ...props } = attrs; const importFrom = `${filename}?vue&index=${index}&clientScript=true&lang.${lang}`; const exported = await parseExports(content); if (!exported.includes("onLoad")) { if (attrs["client:load"] || attrs["client:only"]) { s.appendLeft(end.offset, "\nexport const onLoad = undefined\n"); } else { const prettyFilename = filename.slice(Math.max(0, filename.indexOf("src/"))); throw new Error(`Client script in ${prettyFilename} does not export 'onLoad'. Should be a function to execute when the strategy condition is met.`); } } const elements = node.children.filter((n) => n.tag); if (elements.length === 1) { const el = elements[0]; if (!el.props.some((prop) => prop.name === "bind" && prop.loc.source.includes("$attrs"))) s.appendRight(el.loc.start.offset + 1 + el.tag.length, ' v-bind="$attrs"'); } const lastTemplateChildNode = elements[elements.length - 1]; s.appendRight(lastTemplateChildNode.loc.end.offset, ` <Island v-bind='${JSON.stringify({ ...props, component: {}, componentName: "clientScript", importName: "onLoad", using: "vanilla", importFrom })}'/>`); } export { unresolvedIslandKey, wrapLayout, wrapIslandsInSFC, resolveComponent, resolveImportPath };