import { addBuildPlugin, addImports, addTemplate, addTypeTemplate, createResolver, defineNuxtModule, useNitro } from "@nuxt/kit"; import { Buffer } from "node:buffer"; import { EventEmitter } from "node:events"; import { mkdir, open, readFile, writeFile } from "node:fs/promises"; import { watch } from "chokidar"; import { dirname, isAbsolute, join, relative } from "pathe"; import { genExport, genImport, genInlineTypeImport, genObjectKey } from "knitwork"; import { ElementTypes, NodeTypes, parse } from "@vue/compiler-dom"; import MagicString from "magic-string"; import { createUnplugin } from "unplugin"; //#region package.json var name = "@dxup/nuxt"; //#endregion //#region src/event/client.ts const responseRE = /^```json \{(?.*)\}\n(?[\s\S]*?)\n```$/; async function createEventClient(nuxt) { const path = join(nuxt.options.buildDir, "dxup/events.md"); await mkdir(dirname(path), { recursive: true }); await writeFile(path, ""); const fd = await open(path, "r"); const watcher = watch(path, { ignoreInitial: true }); nuxt.hook("close", async () => { await fd.close(); await watcher.close(); }); const client = new EventEmitter(); let offset = 0; watcher.on("change", async (path, stats) => { if (!stats || stats.size <= offset) return; const pos = offset; offset = stats.size; const buffer = Buffer.alloc(offset - pos); await fd.read(buffer, 0, buffer.length, pos); const match = buffer.toString("utf-8").trim().match(responseRE); if (match) { const { key, value } = match.groups; client.emit(key, JSON.parse(value)); } }); return client; } //#endregion //#region src/module/events.ts const uppercaseRE = /[A-Z]/; async function onComponentsRename(nuxt, { fileName, references }) { const component = Object.values(nuxt.apps).flatMap((app) => app.components).find((c) => c.filePath === fileName); if (!component) return; const tasks = Object.entries(references).map(async ([fileName, references]) => { const code = await readFile(fileName, "utf-8"); const chunks = []; let offset = 0; for (const { textSpan, lazy } of references) { const start = textSpan.start; const end = start + textSpan.length; const oldName = code.slice(start, end); const newName = uppercaseRE.test(oldName) ? lazy ? "Lazy" + component.pascalName : component.pascalName : lazy ? "lazy-" + component.kebabName : component.kebabName; chunks.push(code.slice(offset, start), newName); offset = end; } chunks.push(code.slice(offset)); await writeFile(fileName, chunks.join("")); }); await Promise.all(tasks); } //#endregion //#region src/module/named-layout-slots/utils.ts const vueRE = /[?&]vue(?:&|$)/; const typeRE = /[?&]type=[^&]*/; function isVue(id) { const index = id.indexOf("?"); const query = index !== -1 ? id.slice(index) : void 0; if (query === void 0) return id.endsWith(".vue"); if (query === "?macro=true") return true; if (!vueRE.test(query)) return false; if (typeRE.test(query)) return false; return true; } function isInDir(path, dir) { const rel = relative(dir, path); return rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel); } function parseSFC(code) { const sfc = parse(code, { parseMode: "sfc" }); let scriptSetup; let template; for (const node of sfc.children) { if (node.type !== NodeTypes.ELEMENT) continue; if (node.tag === "script" && node.props.some((prop) => prop.type === NodeTypes.ATTRIBUTE && (prop.name === "setup" || prop.name === "vapor"))) scriptSetup = node; else if (node.tag === "template") template = node; } return { scriptSetup, template }; } function* forEachElementNode(node) { if (node.type === NodeTypes.ROOT || node.type === NodeTypes.FOR || node.type === NodeTypes.IF_BRANCH) for (const child of node.children) yield* forEachElementNode(child); else if (node.type === NodeTypes.ELEMENT) { yield node; for (const child of node.children) yield* forEachElementNode(child); } else if (node.type === NodeTypes.IF) for (const branch of node.branches) yield* forEachElementNode(branch); } //#endregion //#region src/module/named-layout-slots/transform.ts const TransformPlugins = (options) => createUnplugin(() => [{ name: name + ":transform-layout", enforce: "pre", transformInclude: isVue, transform(code, id) { if (!options.layoutDirs.some((dir) => isInDir(id, dir))) return; const { scriptSetup, template } = parseSFC(code); if (!template) return; const slots = []; for (const node of forEachElementNode(template)) if (node.tagType === ElementTypes.SLOT && node.props.length && node.props.every((prop) => prop.name !== "name" || prop.type !== NodeTypes.ATTRIBUTE || prop.value && prop.value.content !== "default")) slots.push(node); if (!slots.length) return; const s = new MagicString(code); const imports = genImport("#build/dxup/layouts.mjs", ["LayoutSlot"]); if (scriptSetup) { const start = scriptSetup.innerLoc.start.offset; s.appendLeft(start, `\n${imports}\n`); } else s.prepend(`