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