UNPKG

lovable-tagger

Version:

Vite plugins for component tagging and virtual file overrides with HMR support

310 lines (305 loc) 14.2 kB
// src/devRuntimeCode.txt var devRuntimeCode_default = 'import * as React from "react";\nimport * as ReactJSXDevRuntime from "react/jsx-dev-runtime";\n\nconst _jsxDEV = ReactJSXDevRuntime.jsxDEV;\nexport const Fragment = ReactJSXDevRuntime.Fragment;\n\nconst SOURCE_KEY = Symbol.for("__jsxSource__");\n\nconst cleanFileName = (fileName) => {\n if (!fileName) return "";\n if (fileName.includes("dev_server")) {\n fileName = fileName.split("dev_server")[1].slice(1);\n }\n if (fileName.includes("sandbox-scheduler/sandbox")) {\n const sandboxPart = fileName.split("sandbox-scheduler/")[1];\n fileName = sandboxPart.split("/").slice(1).join("/");\n }\n return fileName.replace(/^\\/dev-server\\//, "");\n};\n\nconst sourceElementMap = new Map();\nwindow.sourceElementMap = sourceElementMap;\n\nfunction getSourceKey(sourceInfo) {\n return `${cleanFileName(sourceInfo.fileName)}:${sourceInfo.lineNumber}:${sourceInfo.columnNumber}`;\n}\n\nfunction unregisterElement(node, sourceInfo) {\n const key = getSourceKey(sourceInfo);\n const refs = sourceElementMap.get(key);\n if (refs) {\n for (const ref of refs) {\n if (ref.deref() === node) {\n refs.delete(ref);\n break;\n }\n }\n if (refs.size === 0) {\n sourceElementMap.delete(key);\n }\n }\n}\n\nfunction registerElement(node, sourceInfo) {\n const key = getSourceKey(sourceInfo);\n if (!sourceElementMap.has(key)) {\n sourceElementMap.set(key, new Set());\n }\n sourceElementMap.get(key).add(new WeakRef(node));\n}\n\nfunction getTypeName(type) {\n if (typeof type === "string") return type;\n if (typeof type === "function") return type.displayName || type.name || "Unknown";\n if (typeof type === "object" && type !== null) {\n return type.displayName || type.render?.displayName || type.render?.name || "Unknown";\n }\n return "Unknown";\n}\n\nexport function jsxDEV(type, props, key, isStatic, source, self) {\n // For custom components (like <Icon />, <Button />), tag their rendered output\n // This captures the JSX element name for library components that don\'t have source info\n if (source?.fileName && typeof type !== "string" && type !== Fragment) {\n const typeName = getTypeName(type);\n const jsxSourceInfo = {\n fileName: cleanFileName(source.fileName),\n lineNumber: source.lineNumber,\n columnNumber: source.columnNumber,\n displayName: typeName,\n };\n\n const originalRef = props?.ref;\n const enhancedProps = {\n ...props,\n ref: (node) => {\n if (node) {\n // Only tag if this element doesn\'t already have source info\n // (library components won\'t have it, user components will)\n if (!node[SOURCE_KEY]) {\n node[SOURCE_KEY] = jsxSourceInfo;\n registerElement(node, jsxSourceInfo);\n }\n }\n if (typeof originalRef === "function") {\n originalRef(node);\n } else if (originalRef && typeof originalRef === "object") {\n originalRef.current = node;\n }\n },\n };\n\n return _jsxDEV(type, enhancedProps, key, isStatic, source, self);\n }\n\n // For host elements (div, span, etc.), tag with component context\n if (source?.fileName && typeof type === "string") {\n const sourceInfo = {\n fileName: cleanFileName(source.fileName),\n lineNumber: source.lineNumber,\n columnNumber: source.columnNumber,\n displayName: type,\n };\n\n const originalRef = props?.ref;\n\n const enhancedProps = {\n ...props,\n ref: (node) => {\n if (node) {\n const existingSource = node[SOURCE_KEY];\n if (existingSource) {\n if (getSourceKey(existingSource) !== getSourceKey(sourceInfo)) {\n unregisterElement(node, existingSource);\n node[SOURCE_KEY] = sourceInfo;\n registerElement(node, sourceInfo);\n }\n } else {\n node[SOURCE_KEY] = sourceInfo;\n registerElement(node, sourceInfo);\n }\n }\n if (typeof originalRef === "function") {\n originalRef(node);\n } else if (originalRef && typeof originalRef === "object") {\n originalRef.current = node;\n }\n },\n };\n return _jsxDEV(type, enhancedProps, key, isStatic, source, self);\n }\n\n return _jsxDEV(type, props, key, isStatic, source, self);\n}\n'; // src/features/jsxSource.ts function createJsxTaggerFeature() { return { resolveId(id, importer) { if (id === "react/jsx-dev-runtime" && !importer?.includes("\0jsx-source")) { return "\0jsx-source/jsx-dev-runtime"; } return null; }, load(id) { if (id === "\0jsx-source/jsx-dev-runtime") { return devRuntimeCode_default; } return null; } }; } // src/features/tailwindConfig.ts import fs from "fs/promises"; import path from "path"; import * as esbuild from "esbuild"; import resolveConfig from "tailwindcss/resolveConfig.js"; function createTailwindConfigFeature() { let projectRoot = ""; const generateConfig = async (tailwindInputFile, tailwindIntermediateFile, tailwindJsonOutfile) => { try { await esbuild.build({ entryPoints: [tailwindInputFile], outfile: tailwindIntermediateFile, bundle: true, format: "esm", banner: { js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);' } }); try { const userConfig = await import(tailwindIntermediateFile + "?update=" + Date.now()); if (!userConfig || !userConfig.default) { console.error("Invalid Tailwind config structure:", userConfig); throw new Error("Invalid Tailwind config structure"); } const resolvedConfig = resolveConfig(userConfig.default); await fs.writeFile(tailwindJsonOutfile, JSON.stringify(resolvedConfig, null, 2)); await fs.unlink(tailwindIntermediateFile).catch(() => { }); } catch (error) { console.error("Error processing config:", error); throw error; } } catch (error) { console.error("Error in generateConfig:", error); throw error; } }; return { onConfigResolved(config) { projectRoot = config.root; }, async onBuildStart() { try { const tailwindInputFile = path.resolve(projectRoot, "./tailwind.config.ts"); const tailwindIntermediateFile = path.resolve(projectRoot, "./.lov.tailwind.config.js"); const tailwindJsonOutfile = path.resolve(projectRoot, "./src/tailwind.config.lov.json"); await generateConfig(tailwindInputFile, tailwindIntermediateFile, tailwindJsonOutfile); } catch (error) { console.error("Error generating tailwind.config.lov.json:", error); } }, onConfigureServer(server) { try { const tailwindInputFile = path.resolve(projectRoot, "./tailwind.config.ts"); server.watcher.add(tailwindInputFile); server.watcher.on("change", async (changedPath) => { if (path.normalize(changedPath) === path.normalize(tailwindInputFile)) { const tailwindIntermediateFile = path.resolve(projectRoot, "./.lov.tailwind.config.js"); const tailwindJsonOutfile = path.resolve(projectRoot, "./src/tailwind.config.lov.json"); await generateConfig(tailwindInputFile, tailwindIntermediateFile, tailwindJsonOutfile); } }); } catch (error) { console.error("Error adding watcher:", error); } } }; } // src/features/virtualOverrides.ts import path2 from "path"; function createVirtualOverridesFeature(context) { const overrides = /* @__PURE__ */ new Map(); let server = null; let projectRoot = ""; const log = (...args) => { if (context.debug) { console.log("[virtual-overrides]", ...args); } }; const normalizeFilePath = (filePath) => { if (path2.isAbsolute(filePath)) { return filePath; } return path2.resolve(projectRoot, filePath); }; const findModulesByPath = (filePath) => { if (!server) return []; const modules = []; const normalizedPath = normalizeFilePath(filePath); for (const mod of server.moduleGraph.idToModuleMap.values()) { if (mod.file === normalizedPath || mod.id === normalizedPath) { modules.push(mod); } } return modules; }; const invalidateModule = async (filePath) => { if (!server) return; const normalizedPath = normalizeFilePath(filePath); const module = server.moduleGraph.getModuleById(normalizedPath); if (module) { log("Invalidating module:", normalizedPath); server.moduleGraph.invalidateModule(module); if (server.ws) { server.ws.send({ type: "update", updates: [ { type: module.type === "css" ? "css-update" : "js-update", path: module.url, acceptedPath: module.url, timestamp: Date.now() } ] }); } } else { log("Module not found for invalidation:", normalizedPath); const modules = findModulesByPath(normalizedPath); for (const mod of modules) { log("Invalidating found module:", mod.url); server.moduleGraph.invalidateModule(mod); if (server.ws) { server.ws.send({ type: "update", updates: [ { type: mod.type === "css" ? "css-update" : "js-update", path: mod.url, acceptedPath: mod.url, timestamp: Date.now() } ] }); } } } }; const handleOverride = async (data) => { const normalizedPath = normalizeFilePath(data.path); log("Setting override for:", normalizedPath); overrides.set(normalizedPath, data.content); await invalidateModule(data.path); }; const handleClearOverride = async (data) => { const normalizedPath = normalizeFilePath(data.path); log("Clearing override for:", normalizedPath); overrides.delete(normalizedPath); await invalidateModule(data.path); }; const handleClearAllOverrides = async () => { log("Clearing all overrides, count:", overrides.size); const paths = Array.from(overrides.keys()); overrides.clear(); for (const filePath of paths) { await invalidateModule(filePath); } }; return { onConfigResolved(config) { projectRoot = config.root; log("Project root:", projectRoot); }, onConfigureServer(devServer) { server = devServer; log("Configuring server, setting up WebSocket listeners"); devServer.ws.on("lovable:override", async function(data) { try { await handleOverride(data); } catch (error) { console.error("[virtual-overrides] Error handling override:", error); } }); devServer.ws.on("lovable:clear-override", async function(data) { try { await handleClearOverride(data); } catch (error) { console.error("[virtual-overrides] Error clearing override:", error); } }); devServer.ws.on("lovable:clear-all-overrides", async function() { try { await handleClearAllOverrides(); } catch (error) { console.error("[virtual-overrides] Error clearing all overrides:", error); } }); devServer.ws.on("message", function(message) { try { const parsed = JSON.parse(message); if (parsed.type === "custom") { switch (parsed.event) { case "lovable:override": void handleOverride(parsed.data); break; case "lovable:clear-override": void handleClearOverride(parsed.data); break; case "lovable:clear-all-overrides": void handleClearAllOverrides(); break; } } } catch { } }); }, load(id) { const normalizedId = path2.isAbsolute(id) ? id : path2.resolve(projectRoot, id); const override = overrides.get(normalizedId); if (override !== void 0) { log("Returning override for:", normalizedId); return override; } const idWithoutQuery = normalizedId.split("?")[0]; const overrideWithoutQuery = overrides.get(idWithoutQuery); if (overrideWithoutQuery !== void 0) { log("Returning override (without query) for:", idWithoutQuery); return overrideWithoutQuery; } return null; } }; } // src/plugin.ts var isSandbox = process.env.LOVABLE_DEV_SERVER === "true"; function lovableTagger({ jsxSource = isSandbox, tailwindConfig = isSandbox, virtualOverrides = isSandbox, debug = false } = {}) { const context = { debug }; const features = []; if (jsxSource) { features.push(createJsxTaggerFeature()); } if (tailwindConfig) { features.push(createTailwindConfigFeature()); } if (virtualOverrides) { features.push(createVirtualOverridesFeature(context)); } return { name: "lovable-plugin", enforce: "pre", configResolved(config) { for (const feature of features) { feature.onConfigResolved?.(config); } }, async buildStart() { for (const feature of features) { await feature.onBuildStart?.(); } }, configureServer(server) { for (const feature of features) { feature.onConfigureServer?.(server); } }, resolveId(id, importer) { for (const feature of features) { const result = feature.resolveId?.(id, importer); if (result !== null && result !== void 0) { return result; } } return null; }, load(id) { for (const feature of features) { const result = feature.load?.(id); if (result !== null && result !== void 0) { return result; } } return null; } }; } export { lovableTagger as componentTagger };