UNPKG

@pandabox/unplugin

Version:

Panda CSS as a Vite/Rollup/Webpack/Esbuild plugin

719 lines (698 loc) 24.6 kB
// src/plugin/core.ts import { loadConfig } from "@pandacss/config"; import { createFilter } from "@rollup/pluginutils"; import "unplugin"; import { writeFile } from "node:fs/promises"; import { codegen as codegen2 } from "@pandacss/node"; // src/plugin/create-context.ts import { removeUnusedCssVars, removeUnusedKeyframes } from "@pandabox/postcss-plugins"; import { PandaContext, codegen } from "@pandacss/node"; import { createCss, createMergeCss } from "@pandacss/shared"; import postcss from "postcss"; // src/plugin/ensure-absolute.ts import { isAbsolute, resolve } from "path"; var ensureAbsolute = (path2, root) => path2 ? isAbsolute(path2) ? path2 : resolve(root, path2) : root; // src/plugin/create-context.ts var createContext = (options) => { const { conf } = options; let panda = new PandaContext(conf); const root = ensureAbsolute("", options.root); const files = /* @__PURE__ */ new Map(); const toCss = async (sheet, opts) => { panda.appendLayerParams(sheet); panda.appendBaselineCss(sheet); panda.appendParserCss(sheet); let css2 = panda.getCss(sheet); if (opts.optimizeCss) { css2 = postcss([removeUnusedCssVars, removeUnusedKeyframes]).process(css2).toString(); } if (opts.minifyCss) { const { transform } = await import("esbuild"); if (transform) { const { code } = await transform(css2, { loader: "css", minify: true }); css2 = code; } } return css2; }; const css = createCss(panda.baseSheetContext); const mergeFns = createMergeCss(panda.baseSheetContext); const mergeCss = mergeFns.mergeCss; return { // So that we can mutate the `panda` variable and it's still reflected outside get panda() { return panda; }, css, mergeCss, reloadContext: async () => { const affecteds = await panda.diff.reloadConfigAndRefreshContext((conf2) => { panda = new PandaContext(conf2); }); await panda.hooks["config:change"]?.({ config: panda.config, changes: affecteds }); if (options.codegen) { await codegen(panda, Array.from(affecteds.artifacts)); } return panda; }, root, files, toCss, paths: { root } }; }; // src/plugin/transform.ts import { box, extractCallExpressionArguments, unbox } from "@pandacss/extractor"; import MagicString from "magic-string"; import { Node as Node3 } from "ts-morph"; // src/plugin/create-cva.ts import { compact } from "@pandacss/shared"; var defaults = (conf) => ({ base: {}, variants: {}, defaultVariants: {}, compoundVariants: [], ...conf }); var createCva = (config, mergeCss) => { const { base, variants, defaultVariants, compoundVariants } = defaults(config); function resolve2(props = {}) { const computedVariants = { ...defaultVariants, ...compact(props) }; let variantCss = { ...base }; for (const [key, value] of Object.entries(computedVariants)) { const variantStyleObj = variants[key]?.[value]; if (variantStyleObj) { variantCss = mergeCss(variantCss, variantStyleObj); } } const compoundVariantCss = getCompoundVariantCss(compoundVariants, computedVariants, mergeCss); return mergeCss(variantCss, compoundVariantCss); } return resolve2; }; function getCompoundVariantCss(compoundVariants, variantMap, mergeCss) { let result = {}; compoundVariants.forEach((compoundVariant) => { const isMatching = Object.entries(compoundVariant).every(([key, value]) => { if (key === "css") return true; const values = Array.isArray(value) ? value : [value]; return values.some((value2) => variantMap[key] === value2); }); if (isMatching) { result = mergeCss(result, compoundVariant.css); } }); return result; } var transformCva = (name, config, css) => { const { base, variants, defaultVariants, compoundVariants } = defaults(config); return `(function () { const base = ${JSON.stringify(css(base))} const variantStyles = ${JSON.stringify( Object.fromEntries( Object.entries(variants).map(([variantKey, variantMap]) => [ variantKey, Object.fromEntries( Object.entries(variantMap).map(([valueKey, variantStyle]) => [valueKey, css(variantStyle)]) ) ]) ), null, 2 )} const defaultVariants = ${JSON.stringify(defaultVariants)} return function ${name}(variants) { ${compoundVariants.length > 0 ? ` const classList = [inlineCva(base, defaultVariants, variantStyles, variants)] const compoundVariants = ${JSON.stringify(compoundVariants)} addCompoundVariantCss(compoundVariants, variantProps, classList) return classList.join(' ')` : `return inlineCva(base, defaultVariants, variantStyles, variants)`} } })()`; }; // src/plugin/get-cva-var-name.ts import { Node } from "ts-morph"; var getVariableName = (node) => { const parent = node.getParent(); if (!Node.isVariableDeclaration(parent)) return; const name = parent.getName(); return name; }; // src/plugin/get-import-declarations.ts import { resolveTsPathPattern } from "@pandacss/config/ts-path"; // src/plugin/get-module-specifier-value.ts var getModuleSpecifierValue = (node) => { try { return node.getModuleSpecifierValue(); } catch { return; } }; // src/plugin/has-macro-attribute.ts import { Node as Node2 } from "ts-morph"; var getMacroAttribute = (node, attrName = "type") => { const attrs = node.getAttributes(); if (!attrs) return null; const elements = attrs.getElements(); if (!elements.length) return null; let withAttr = null; elements.some((n) => { const name = n.getName(); if (name !== attrName) return; const value = n.getValue(); if (!Node2.isStringLiteral(value)) return; withAttr = value.getLiteralText(); return true; }); return withAttr; }; // src/plugin/get-import-declarations.ts function getImportDeclarations(context, sourceFile, onlyMacroImports = false) { const { imports, tsOptions } = context; const importDeclarations = []; sourceFile.getImportDeclarations().forEach((node) => { const mod = getModuleSpecifierValue(node); if (!mod) return; const withAttr = getMacroAttribute(node); node.getNamedImports().forEach((specifier) => { const name = specifier.getNameNode().getText(); const alias = specifier.getAliasNode()?.getText() || name; const result = { name, alias, mod, kind: "named", withAttr }; const found = imports.match(result, (mod2) => { if (!tsOptions?.pathMappings) return; return resolveTsPathPattern(tsOptions.pathMappings, mod2); }); if (!found) return; importDeclarations.push(result); }); const namespace = node.getNamespaceImport(); if (namespace) { const name = namespace.getText(); const result = { name, alias: name, mod, kind: "namespace", withAttr }; const found = imports.match(result, (mod2) => { if (!tsOptions?.pathMappings) return; return resolveTsPathPattern(tsOptions.pathMappings, mod2); }); if (!found) return; importDeclarations.push(result); } }); return importDeclarations; } // src/plugin/unbox-combine-result.ts var combineResult = (unboxed) => { return [...unboxed.conditions, unboxed.raw, ...unboxed.spreadConditions]; }; // src/plugin/transform.ts var tranformPanda = (ctx, options) => { const { code, optimizeJs, sourceFile, parserResult } = options; if (!parserResult) return null; const { panda, css, mergeCss } = ctx; const factoryName = panda.jsx.factoryName || "styled"; const s = new MagicString(code); const onlyMacroImports = optimizeJs === "macro"; const importDeclarations = getImportDeclarations(panda.parserOptions, sourceFile, onlyMacroImports); const file = panda.imports.file(importDeclarations); const jsxPatternKeys = panda.patterns.details.map((d) => d.jsxName); const isJsxPatternImported = file["createMatch"](file["importMap"].jsx, jsxPatternKeys); const cvaNames = collectCvaNames(parserResult); const cvaUsages = extractCvaUsages(sourceFile, cvaNames); const cvaConfigs = /* @__PURE__ */ new Map(); let needInlineCvaImport = false; let needCompoundVariantsImport = false; parserResult.all.forEach((result) => { const fnName = result.name; if (!fnName) return; if (!result.box) return; if (result.type === "jsx" || result.type === "jsx-recipe") return; const node = result.box.getNode(); let shouldOnlyTransformMacroImport = onlyMacroImports; if (result.type && !shouldOnlyTransformMacroImport && typeof optimizeJs === "object" && result.type !== "sva" && optimizeJs[result.type] === "macro") { shouldOnlyTransformMacroImport = true; } const importedIdentifier = getImportedIdentifier(node); if (!importedIdentifier) return; const importedName = fnName.split(".")[0]; const importDecl = importDeclarations.find( (imp) => imp.name === importedName && imp.alias === importedIdentifier ); if (!importDecl || importDecl.withAttr === "runtime" || shouldOnlyTransformMacroImport && importDecl.withAttr !== "macro") { return; } if (result.type?.includes("jsx")) { const isJsx = Node3.isJsxOpeningElement(node) || Node3.isJsxSelfClosingElement(node); if (!isJsx) return; const tagName = node.getTagNameNode().getText(); const isJsxPattern = panda.patterns.details.find((node2) => node2.jsxName === tagName); if (isJsxPattern && !isJsxPatternImported(tagName)) return; const isPandaComponent = file.isPandaComponent(tagName); if (!isPandaComponent) return; if (result.type === "jsx-factory" && !tagName.includes(factoryName + ".")) { return; } const styleProps = new Set(result.data.flatMap((data) => Object.keys(data))); const styleObjects2 = result.type === "jsx-pattern" ? result.data.map((data) => panda.patterns.transform(panda.patterns.find(fnName), data)) : result.data; const merged2 = mergeCss(...styleObjects2); const className2 = css(merged2); const otherProps = node.getAttributes().filter((n) => { if (Node3.isJsxAttribute(n)) { return !styleProps.has(n.getNameNode().getText()); } return true; }); let tag; if (tagName.includes(".")) { ; [, tag] = tagName.split("."); } else if (result.type === "jsx-pattern") { const patternName = panda.patterns.find(fnName); const patternConfig = panda.patterns.getConfig(patternName); tag = patternConfig.jsxElement ?? "div"; } s.update( node.getStart(), node.getEnd(), `<${tag} className="${className2}" ${otherProps.map((n) => n.getText()).join(" ")}${Node3.isJsxSelfClosingElement(node) ? "/" : ""}>` ); if (Node3.isJsxOpeningElement(node)) { const parent = node.getParent(); if (Node3.isJsxElement(parent)) { const closing = parent.getClosingElement(); if (closing) { s.update(closing.getStart(), closing.getEnd(), `</${tag}>`); } } } return; } if (!Node3.isCallExpression(node) || !fnName) return; const identifier = node.getExpression().getText(); const isRaw = identifier.includes(".raw"); if (isRaw) { const rawIndex = identifier.indexOf(".raw"); const obj = s.slice(node.getStart() + rawIndex + 4, node.getEnd()); s.update(node.getStart(), node.getEnd(), obj); return; } if (result.type === "cva") { result.data.forEach((recipe) => { const varName = getVariableName(node); if (!varName) return; const resolve2 = createCva(recipe, mergeCss); cvaConfigs.set(varName, { config: recipe, resolve: resolve2 }); s.update(node.getStart(), node.getEnd(), transformCva(varName, recipe, css)); needInlineCvaImport = true; if (recipe.compoundVariants?.length) { needCompoundVariantsImport = true; } }); cvaUsages.forEach((data, key) => { const { variants } = data; const cva = cvaConfigs.get(key); if (!cva) return; const computed = cva.resolve(variants); const className2 = css(computed); s.update(data.node.getStart() - 1, data.node.getEnd() + 1, `"${className2}"`); }); return; } const classList = /* @__PURE__ */ new Set(); const styleObjects = /* @__PURE__ */ new Set(); const processAtomic = (data) => { styleObjects.add(data); }; if (result.type === "css") { result.data.forEach((d) => processAtomic(d)); } else if (result.type === "pattern") { result.data.forEach((data) => { const styleProps = panda.patterns.transform(fnName, data); processAtomic(styleProps); }); } else if (result.type === "recipe") { const config = panda.recipes.getConfig(fnName); if (!config) return; const transform = panda.recipes.getTransform(fnName, panda.recipes.isSlotRecipe(fnName)); const base = transform("__ignore__", "__ignore__"); classList.add(base.className); config.base && processAtomic(config.base); result.data.forEach((variants) => { const computedVariants = Object.assign({}, config.defaultVariants, variants); Object.entries(computedVariants).forEach(([key, value]) => { const transformed = transform(key, value); classList.add(transformed.className); const variantStyles = config.variants?.[key]?.[value]; variantStyles && processAtomic(variantStyles); }); }); config.compoundVariants?.forEach((compoundVariant) => { if (!compoundVariant) return; processAtomic(compoundVariant.css); }); } const merged = mergeCss(...Array.from(styleObjects)); const className = result.type === "recipe" ? Array.from(classList).join(" ") : css(merged); s.update(node.getStart(), node.getEnd(), `"${className}"`); }); if (needCompoundVariantsImport) { s.prepend(`import { addCompoundVariantCss } from 'virtual:panda-compound-variants'; `); } if (needInlineCvaImport) { s.prepend(`import { inlineCva } from 'virtual:panda-inline-cva'; `); } return { code: s.toString(), map: s.generateMap({ hires: true }) }; }; var collectCvaNames = (parserResult) => { const cvaNames = /* @__PURE__ */ new Set(); parserResult.cva.forEach((cva) => { const node = cva.box?.getNode(); if (!node) return; const varName = getVariableName(node); if (!varName) return; return cvaNames.add(varName); }); return cvaNames; }; var extractCvaUsages = (sourceFile, cvaNames) => { const cvaUsages = /* @__PURE__ */ new Map(); sourceFile.forEachDescendant((node) => { if (!Node3.isIdentifier(node)) return; const fnName = node.getText(); if (!cvaNames.has(fnName)) return; const parent = node.getParent(); if (!Node3.isCallExpression(parent)) return; const array = extractCallExpressionArguments(parent, { flags: { skipTraverseFiles: true } }); array.value.forEach((arg) => { if (box.isMap(arg)) { const unboxed = combineResult(unbox(arg)); unboxed.forEach((variants) => { cvaUsages.set(fnName, { variants, node: parent }); }); } }); }); return cvaUsages; }; var getImportedIdentifier = (node) => { if (Node3.isJsxOpeningElement(node) || Node3.isJsxSelfClosingElement(node)) { const tagName = node.getTagNameNode(); if (Node3.isIdentifier(tagName)) { return tagName.getText(); } if (Node3.isPropertyAccessExpression(tagName)) { return tagName.getExpression().getText().split(".")[0]; } } if (Node3.isCallExpression(node)) { const expr = node.getExpression(); return expr.getText().split(".")[0]; } }; // src/plugin/core.ts import path from "node:path"; // src/plugin/cva-fns.ts function addCompoundVariantCss(compoundVariants, variants, classList) { compoundVariants.forEach(({ css, ...compoundVariant }) => { if (css) { const isMatching = Object.entries(compoundVariant).every(([key, value]) => { const values = Array.isArray(value) ? value : [value]; return values.some((value2) => variants[key] === value2); }); if (isMatching) { classList.push(css); } } }); } function inlineCva(base, defaultVariants, variantStyles, variants) { const classList = [base]; const variantProps = { ...defaultVariants, ...variants }; for (const [key, value] of Object.entries(variantProps)) { if (variantStyles[key][value]) { classList.push(variantStyles[key][value]); } } return classList.join(" "); } // src/plugin/core.ts import { throttle } from "es-toolkit"; import { existsSync } from "node:fs"; var createVirtualModuleId = (id) => { const base = `virtual:panda${id}`; return { id: base, resolved: "\0" + base }; }; var ids = { css: createVirtualModuleId(".css"), inlineCva: createVirtualModuleId("-inline-cva"), compoundVariants: createVirtualModuleId("-compound-variants") }; var pandaPreamble = "/*! PANDA_CSS */"; var throttleWaitMs = 1e3; var unpluginFactory = (rawOptions) => { const options = resolveOptions(rawOptions ?? {}); const filter = createFilter(options.include, options.exclude); let outfile = options.outfile ? ensureAbsolute(options.outfile, options.cwd) : ids.css.resolved; let _ctx; let initPromise; const getCtx = async () => { await init(); if (!_ctx) throw new Error("@pandabox/unplugin context not initialized"); return _ctx; }; const init = () => { if (initPromise) return initPromise; initPromise = loadConfig({ cwd: options.cwd, file: options.configPath }).then(async (conf) => { conf.config.cwd = options.cwd; _ctx = createContext({ root: options.cwd, conf, codegen: options.codegen }); if (options.contextCreated) { await options.contextCreated({ context: _ctx.panda }); } }); return initPromise; }; let server; let lastCss; let updateCssOnTransform = true; const updateCss = async () => { const ctx = await getCtx(); const css = await ctx.toCss(ctx.panda.createSheet(), options); const isCssUpdated = lastCss !== css; lastCss = css; if (!isCssUpdated) return; if (outfile !== ids.css.resolved) { await writeFile(outfile, css); } else { if (!server) return; const mod = server.moduleGraph.getModuleById(outfile.replaceAll("\\", "/")); if (!mod) return; await server.reloadModule(mod); } }; const requestUpdateCss = throttle(updateCss, throttleWaitMs, { edges: ["leading", "trailing"] }); return { name: "unplugin-panda", enforce: "pre", resolveId(id) { if (id === ids.css.id) { return ids.css.resolved; } if (id === ids.inlineCva.id) { return ids.inlineCva.resolved; } if (id === ids.compoundVariants.id) { return ids.compoundVariants.resolved; } }, async load(id) { if (id === ids.inlineCva.resolved) { return `export ${inlineCva.toString()}`; } if (id === ids.compoundVariants.resolved) { return `export ${addCompoundVariantCss.toString()}`; } if (id !== outfile) return; if (!server) return pandaPreamble; const ctx = await getCtx(); const sheet = ctx.panda.createSheet(); const css = await ctx.toCss(sheet, options); return css; }, transformInclude(id) { return filter(id); }, async transform(code, id) { const ctx = await getCtx(); const { panda } = ctx; let transformResult = { code, map: void 0 }; if (options.transform) { const result2 = await options.transform({ filePath: id, content: code, context: ctx.panda }) || code; if (typeof result2 === "string") { transformResult.code = result2; } else if (result2) { transformResult = result2; } } const sourceFile = panda.project.addSourceFile(id, transformResult.code); if (options.onSourceFile) { await options.onSourceFile({ sourceFile, context: ctx.panda }); } const parserResult = panda.project.parseSourceFile(id); if (!parserResult) return null; if (!parserResult.isEmpty()) { ctx.files.set(id, code); if (updateCssOnTransform) requestUpdateCss(); } if (!options.optimizeJs) { return transformResult.code !== code ? transformResult : null; } const result = tranformPanda(ctx, { code: transformResult.code, id, sourceFile, parserResult, optimizeJs: options.optimizeJs }); return result; }, vite: { name: "unplugin-panda", configResolved(config) { if (!options.cwd) { options.cwd = config.configFile ? path.dirname(config.configFile) : config.root; outfile = options.outfile ? ensureAbsolute(options.outfile, options.cwd) : ids.css.resolved; } updateCssOnTransform = config.command !== "build"; }, async configureServer(_server) { server = _server; const ctx = await getCtx(); if (outfile !== ids.css.resolved) { if (!existsSync(outfile)) await writeFile(outfile, ""); let prevState = updateCssOnTransform; updateCssOnTransform = false; try { for (const file of ctx.panda.getFiles()) { if (path.basename(file) === "panda.buildinfo.json") { ctx.panda.project.parseSourceFile(file); } else { await server.transformRequest(file); } } } finally { updateCssOnTransform = prevState; } await updateCss(); } if (options.codegen) { const { msg } = await codegen2(ctx.panda); } const sources = new Set( [ctx.panda.conf.path, ...ctx.panda.conf.dependencies ?? [], ...ctx.panda.config.dependencies ?? []].map( (f) => ensureAbsolute(f, ctx.root) ) ); sources.forEach((file) => server.watcher.add(file)); server.watcher.on("change", async (file) => { const filePath = ensureAbsolute(file, ctx.root); if (!sources.has(filePath)) return; await ctx.reloadContext(); const timestamp = Date.now(); const invalidate = (file2) => { const mod = server.moduleGraph.getModuleById(file2); if (mod) { server.moduleGraph.invalidateModule(mod, /* @__PURE__ */ new Set(), timestamp, true); } }; invalidate(outfile); }); }, async generateBundle(_, bundles) { const cssBundle = Object.values(bundles).find( (bundle) => bundle.type === "asset" && bundle.name?.endsWith(".css") && typeof bundle.source === "string" && bundle.source.includes(pandaPreamble) ); if (cssBundle) { const source = cssBundle.source; const ctx = await getCtx(); const sheet = ctx.panda.createSheet(); const css = await ctx.toCss(sheet, options); cssBundle.source = source.replace(pandaPreamble, css); } } } }; }; var resolveOptions = (options) => { let optimizeJs = options.optimizeJs ?? "auto"; if (typeof optimizeJs === "object") { optimizeJs = { css: optimizeJs.css ?? "auto", cva: optimizeJs.cva ?? "auto", pattern: optimizeJs.cva ?? "auto", recipe: optimizeJs.cva ?? "auto", "jsx-factory": optimizeJs.cva ?? "auto", "jsx-pattern": optimizeJs.cva ?? "auto", ...optimizeJs }; } return { ...options, cwd: options.cwd || "", configPath: options.configPath, include: options.include || [/\.[cm]?[jt]sx?$/], exclude: options.exclude || [/node_modules/, /styled-system/], optimizeCss: options.optimizeCss ?? true, minifyCss: options.minifyCss ?? false, optimizeJs: options.optimizeJs ?? "macro", codegen: options.codegen ?? options.codeGen ?? true }; }; export { unpluginFactory };