lovable-tagger
Version:
Vite plugins for component tagging and virtual file overrides with HMR support
347 lines (340 loc) • 16.2 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
componentTagger: () => lovableTagger
});
module.exports = __toCommonJS(src_exports);
// 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
var import_promises = __toESM(require("fs/promises"), 1);
var import_path = __toESM(require("path"), 1);
var esbuild = __toESM(require("esbuild"), 1);
var import_resolveConfig = __toESM(require("tailwindcss/resolveConfig.js"), 1);
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 = (0, import_resolveConfig.default)(userConfig.default);
await import_promises.default.writeFile(tailwindJsonOutfile, JSON.stringify(resolvedConfig, null, 2));
await import_promises.default.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 = import_path.default.resolve(projectRoot, "./tailwind.config.ts");
const tailwindIntermediateFile = import_path.default.resolve(projectRoot, "./.lov.tailwind.config.js");
const tailwindJsonOutfile = import_path.default.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 = import_path.default.resolve(projectRoot, "./tailwind.config.ts");
server.watcher.add(tailwindInputFile);
server.watcher.on("change", async (changedPath) => {
if (import_path.default.normalize(changedPath) === import_path.default.normalize(tailwindInputFile)) {
const tailwindIntermediateFile = import_path.default.resolve(projectRoot, "./.lov.tailwind.config.js");
const tailwindJsonOutfile = import_path.default.resolve(projectRoot, "./src/tailwind.config.lov.json");
await generateConfig(tailwindInputFile, tailwindIntermediateFile, tailwindJsonOutfile);
}
});
} catch (error) {
console.error("Error adding watcher:", error);
}
}
};
}
// src/features/virtualOverrides.ts
var import_path2 = __toESM(require("path"), 1);
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 (import_path2.default.isAbsolute(filePath)) {
return filePath;
}
return import_path2.default.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 module2 = server.moduleGraph.getModuleById(normalizedPath);
if (module2) {
log("Invalidating module:", normalizedPath);
server.moduleGraph.invalidateModule(module2);
if (server.ws) {
server.ws.send({
type: "update",
updates: [
{
type: module2.type === "css" ? "css-update" : "js-update",
path: module2.url,
acceptedPath: module2.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 = import_path2.default.isAbsolute(id) ? id : import_path2.default.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;
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
componentTagger
});