UNPKG

@pandabox/panda-plugins

Version:

- `missing-css-warnings` - Logs a warning message when a CSS rule was used at runtime but couldn't be statically extracted - `strict-tokens-scope` - Enforce `strictTokens` only for a set of `TokenCategory` or style props - `strict-tokens-runtime` - Enfo

602 lines (579 loc) 20.6 kB
// src/remove-features.ts var pluginRemoveFeatures = (options) => { return { name: "remove-features", hooks: { "codegen:prepare": (args) => { return transformFeatures(args, options); } } }; }; var transformFeatures = (args, options) => { const { features: transforms } = options; const features = { "no-jsx": transforms.includes("no-jsx"), "no-styled": transforms.includes("no-styled"), "no-recipes": transforms.includes("no-recipes"), "no-patterns": transforms.includes("no-patterns"), "no-cva": transforms.includes("no-cva"), "no-sva": transforms.includes("no-sva"), "only-css-dir": transforms.includes("only-css-dir") }; const filtered = args.artifacts.filter((a) => { if (features["no-jsx"] && a.dir?.at(-1) === "jsx") return false; if (features["no-recipes"] && a.dir?.at(-1) === "recipes") return false; if (features["no-patterns"] && a.dir?.at(-1) === "patterns") return false; if (features["only-css-dir"]) { const dir = a.dir?.at(-1); if (dir && ["jsx", "recipes", "patterns"].includes(dir)) return false; } if (features["no-styled"] && a.id === "jsx-factory") return false; if (features["no-cva"] && a.id.includes("cva")) return false; if (features["no-sva"] && a.id.includes("sva")) return false; return true; }); if (features["no-styled"]) { const jsxIndex = filtered.find((a) => a.id === "jsx-patterns-index"); if (jsxIndex) { jsxIndex.files.forEach((f) => { if (!f.code) return; const lines = f.code.split("\n"); const exportFactory = lines.findIndex((l) => l.includes("export * from './factory")); if (exportFactory === -1) return; lines.splice(exportFactory, 1); f.code = lines.join("\n"); }); } } if (features["no-cva"] || features["no-sva"]) { const cssIndex = filtered.find((a) => a.id === "css-index"); if (cssIndex) { cssIndex.files.forEach((f) => { if (!f.code) return; const lines = f.code.split("\n"); if (features["no-cva"]) { const exportIndex = lines.findIndex((l) => l.includes("export * from './cva")); if (exportIndex > -1) { lines.splice(exportIndex, 1); } } if (features["no-sva"]) { const exportIndex = lines.findIndex((l) => l.includes("export * from './sva")); if (exportIndex > -1) { lines.splice(exportIndex, 1); } } f.code = lines.join("\n"); }); } } return filtered; }; // src/minimal-setup.ts var pluginMinimalSetup = (options) => { return { name: "minimal-setup", hooks: { "config:resolved": (args) => { args.config.eject = true; if (!args.config.presets) { args.config.presets = []; } }, "codegen:prepare": (args) => { return transformFeatures(args, options); } } }; }; // src/missing-css-warnings.ts var pluginMissingCssWarnings = (options) => { return { name: "missing-css-warnings", hooks: { "codegen:prepare": (args) => { return transformMissingCssWarnings(args, options); } } }; }; var transformMissingCssWarnings = (args, options) => { const { enabled = true } = options ?? {}; if (!enabled) return args.artifacts; const helpersArtifact = args.artifacts.find((art) => art.id === "helpers"); const helpersFile = helpersArtifact?.files.find((f) => f.file.includes("helpers")); if (!helpersFile?.code) { return args.artifacts; } helpersFile.code = helpersFile.code.replace( "classNames.add(className)", "classNames.add(className);\nisInCss(className)" ); helpersFile.code += ` /* eslint-disable no-control-regex */ var rcssescape = /([\\0-\\x1f\\x7f]|^-?\\d)|^-$|^-|[^\\x80-\\uFFFF\\w-]/g; var fcssescape = function (ch, asCodePoint) { if (!asCodePoint) return "\\\\" + ch; if (ch === "\\0") return "\\uFFFD"; if (ch === "-" && ch.length === 1) return "\\\\-"; return ch.slice(0, -1) + "\\\\" + ch.charCodeAt(ch.length - 1).toString(16); }; var esc = (sel) => { return (sel + "").replace(rcssescape, fcssescape); }; const isCssStyleRule = (rule) => rule instanceof CSSStyleRule const isGroupingRule = (rule) => 'cssRules' in rule function traverseCSSRule(rule, className) { const stack = [] stack.push(rule) while (stack.length > 0) { const currentRule = stack.pop() if (!currentRule) continue if (isCssStyleRule(currentRule)) { const selectorText = currentRule.selectorText if (selectorText && selectorText.includes(className)) { return currentRule } } if (isGroupingRule(currentRule) && currentRule.cssRules) { stack.push(...Array.from(currentRule.cssRules)) } } } const missingRules = new Set() const isInCss = (className) => { if (typeof window === 'undefined') return const escaped = '.' + esc(className) const styleSheets = document.styleSheets for (const styleSheet of styleSheets) { const rules = styleSheet.cssRules || styleSheet.rules if (!rules) continue for (const rule of rules) { const match = traverseCSSRule(rule, escaped) if (match) return match } } if (missingRules.has(className)) return missingRules.add(className) console.error(\`No matching CSS rule found for "\${className}"\`) }`; return args.artifacts; }; // src/remove-negative-spacing.ts var pluginRemoveNegativeSpacing = (options) => { return { name: "remove-negative-spacing", hooks: { "codegen:prepare": (args) => { return transformNegativeSpacing(args, options ?? {}); } } }; }; var transformNegativeSpacing = (args, options) => { const artifact = args.artifacts.find((a) => a.id === "design-tokens"); if (!artifact) return args.artifacts; const artifactContent = artifact.files.find((f) => f.file.includes("tokens.d")); if (!artifactContent) return args.artifacts; let fileContent = artifactContent.code; if (!fileContent) return args.artifacts; const { spacingTokenType = true, tokenType = true } = options; if (spacingTokenType) { fileContent = artifactContent.code = updateSpacingTokenType(fileContent); } if (tokenType) { artifactContent.code = updateTokenType(fileContent); } return args.artifacts; }; var spacingTokenRegex = /export type SpacingToken = (.+)\n/; var tokenRegex = /export type Token = (.+)\n/; var updateSpacingTokenType = (file) => { const match = file.match(spacingTokenRegex); if (!match) return file; const spacingTokenString = match[1]; const values = spacingTokenString.split(" | ").map((v) => v.slice(1, -1)).filter((v) => !v.startsWith("-")); return file.replace(spacingTokenRegex, `export type SpacingToken = ${values.map((v) => `"${v}"`).join(" | ")} `); }; var updateTokenType = (file) => { const match = file.match(tokenRegex); if (!match) return file; const tokenString = match[1]; const values = tokenString.split(" | ").map((v) => v.slice(1, -1)).filter((v) => !v.startsWith("spacing.-")); return file.replace(tokenRegex, `export type Token = ${values.map((v) => `"${v}"`).join(" | ")} `); }; // src/remove-unused-css.ts import { removeUnusedCssVars, removeUnusedKeyframes } from "@pandabox/postcss-plugins"; import postcss from "postcss"; var defaultOptions = { removeCssVars: true, removeKeyframes: true }; var pluginRemoveUnusedCss = (options = defaultOptions) => { return { name: "remove-unused-css", hooks: { "cssgen:done": ({ artifact, content }) => { const isEnabled = options.removeCssVars || options.removeKeyframes; if (!isEnabled) return; if (artifact !== "styles.css") return; let css = content; const plugins = []; if (options.removeCssVars) { plugins.push(removeUnusedCssVars); } else if (options.removeKeyframes) { plugins.push(removeUnusedKeyframes); } return postcss(plugins).process(css).toString(); } } }; }; // src/restrict-styled-props.ts var supportedJsxFrameworks = ["react"]; var pluginRestrictStyledProps = () => { return { name: "restrict-styled-props", hooks: { "config:resolved": (args) => { const jsxFramework = args.config.jsxFramework; if (!supportedJsxFrameworks.includes(jsxFramework)) { throw new Error( `[plugin:restrict-styled-props]: Unsupported jsxFramework: ${jsxFramework}. This Panda plugin only supports: ${supportedJsxFrameworks.join(", ")}` ); } }, "codegen:prepare": (args) => { return transformRestrictStyledProps(args); } } }; }; var transformRestrictStyledProps = (args) => { const factoryArtifact = args.artifacts.find((art) => art.id === "jsx-factory"); const factoryJs = factoryArtifact?.files.find((f) => f.file.includes(".mjs") || f.file.includes(".js")); const jsxTypes = args.artifacts.find((art) => art.id === "types-jsx")?.files.find((f) => f.file.includes("jsx")); if (!factoryJs?.code || !jsxTypes?.code) { return args.artifacts; } factoryJs.code = factoryJs.code.replace( "function styledFn", ` const filterProps = (styles, allowedProps) => { if (!allowedProps) return styles return Object.fromEntries(Object.entries(styles).filter(([key]) => allowedProps.includes(key))) } function styledFn` ).replaceAll("cssStyles)", "filterProps(cssStyles, configOrCva.props))"); factoryJs.code = factoryJs.code.replace("variantProps, styleProps", "variantProps, _styleProps").replace( "function recipeClass", ` const styleProps = filterProps(_styleProps, configOrCva.props) function recipeClass` ); const startIndex = jsxTypes.code.indexOf("export interface StyledComponent"); const endIndex = jsxTypes.code.indexOf("export type JsxElements"); jsxTypes.code = jsxTypes.code.slice(0, startIndex) + `export interface StyledComponent<T extends ElementType, P extends Dict = {}, TStyleProp> { ( props: JsxHTMLProps< ComponentProps<T>, Assign<TStyleProp extends never ? JsxStyleProps : Pick<JsxStyleProps, TStyleProp>, P> >, ): JSX.Element displayName?: string } interface RecipeFn { __type: any } interface JsxFactoryOptions<TProps extends Dict> { dataAttr?: boolean defaultProps?: TProps shouldForwardProp?(prop: string, variantKeys: string[]): boolean } export type JsxRecipeProps<T extends ElementType, P extends Dict> = JsxHTMLProps<ComponentProps<T>, P> export type JsxElement<T extends ElementType, P extends Dict, TStyleProp extends StyleProp> = T extends StyledComponent<infer A, infer B, infer TStyleProp> ? StyledComponent<A, Pretty<DistributiveUnion<P, B>>, TStyleProp> : StyledComponent<T, P, TStyleProp> type StyleProp = keyof SystemProperties | 'css' interface StyledRecipeDefinition<T extends RecipeVariantRecord, TStyleProp extends StyleProp> extends RecipeDefinition<T> { props?: TStyleProp[] } export interface JsxFactory { <T extends ElementType>(component: T): StyledComponent<T, {}> <T extends ElementType, P extends RecipeVariantRecord, TStyleProp extends StyleProp>( component: T, recipe: StyledRecipeDefinition<P, TStyleProp>, options?: JsxFactoryOptions<JsxRecipeProps<T, RecipeSelection<P>>>, ): JsxElement<T, RecipeSelection<P>, TStyleProp> <T extends ElementType, P extends RecipeFn>( component: T, recipeFn: P, options?: JsxFactoryOptions<JsxRecipeProps<T, P['__type']>>, ): JsxElement<T, P['__type']> }` + jsxTypes.code.slice(endIndex); return args.artifacts; }; // src/strict-tokens-runtime.ts var pluginStrictTokensRuntime = (options) => { let config; return { name: "strict-tokens-runtime", hooks: { "context:created": (args) => { config = { strictTokens: args.ctx.config.strictTokens, shorthands: args.ctx.config.shorthands }; if (!config.strictTokens) { args.logger.debug("plugin:strict-tokens-runtime", `strictTokens is not enabled, skipping`); } }, "codegen:prepare": (args) => { if (!config.strictTokens) return args.artifacts; return transformStrictTokensRuntime(args, options, config); } } }; }; var transformStrictTokensRuntime = (args, options, config) => { const { categories, props } = options; if (categories && !categories.length && props && !props.length) return args.artifacts; const cssFn = args.artifacts.find((art) => art.id === "css-fn"); const cssFile = cssFn?.files.find((f) => f.file.includes("css")); const tokens = args.artifacts.find((art) => art.id === "design-tokens"); const tokenJs = tokens?.files.find((f) => f.file.includes("index.js") || f.file.includes("index.mjs")); const typesStyles = args.artifacts.find((art) => art.id === "types-styles"); const propTypesDts = typesStyles?.files.find((f) => f.file.includes("prop-type")); if (!cssFile?.code || !tokenJs?.code || !propTypesDts?.code) { return args.artifacts; } const propsByCategory = mapPropsToTokenCategory(propTypesDts.code); const shorthandMap = mapShorthandsToProperties(propTypesDts.code); propsByCategory.forEach((set) => { set.forEach((key) => { const shorthand = shorthandMap[key]; if (shorthand) { set.add(shorthand); } }); }); propsByCategory.forEach((set, cat) => { if (categories && !categories.includes(cat)) { propsByCategory.delete(cat); return; } if (props?.length) { set.forEach((prop) => { if (!props.includes(prop)) { set.delete(prop); } }); } }); const tokenObj = tokenJs.code.slice( tokenJs.code.indexOf("const tokens = ") + "const tokens = ".length, tokenJs.code.indexOf("export function token") ); const tokensJSON = JSON.parse(tokenObj); const tokenValuesByCategory = /* @__PURE__ */ new Map(); Object.keys(tokensJSON).map((key) => { const [category, ...tokenName] = key.split("."); if (categories && !categories.includes(category)) { return; } const prop = tokenName.join("."); if (props?.length && !props.includes(prop)) { return; } if (!tokenValuesByCategory.has(category)) { tokenValuesByCategory.set(category, /* @__PURE__ */ new Set()); } const set = tokenValuesByCategory.get(category); set.add(prop); }); cssFile.code = cssFile.code.replace( "const context = {", ` const tokenValues = ${JSON.stringify(mapToObj(tokenValuesByCategory), null, 2)} const propsByCat = ${JSON.stringify(mapToObj(propsByCategory), null, 2)} const propList = new Set(Object.values(propsByCat).flat()) const categoryByProp = new Map() propList.forEach((prop) => { Object.entries(propsByCat).forEach(([category, list]) => { if (list.includes(prop)) { categoryByProp.set(prop, category) } }) }) const context = {` ); const withStrictChecks = ` // Only throw error if the property is in the list of props // bound to a token category and the value is not a valid token value for that category if (propList.has(prop)) { const category = categoryByProp.get(prop) if (category) { const values = tokenValues[category] if (values && !values.includes(value)) { throw new Error(\`[super-strict-tokens]: Unknown value: { \${prop}: \${value} } Valid values in \${category} are: \${values.join(', ')}\`) } } }`; if (config?.shorthands) { cssFile.code = cssFile.code.replace( "transform: (prop, value) => {", `transform: (prop, value) => { ${withStrictChecks}` ); } else { const startStr = "transform: (key, value) => "; const fnStartIndex = cssFile.code.indexOf(startStr) + startStr.length; const endStr = "withoutSpace(value)}"; const fnEndIndex = cssFile.code.indexOf(endStr) + endStr.length + 4; cssFile.code = cssFile.code.slice(0, fnStartIndex) + "{\n" + withStrictChecks + "\nreturn " + cssFile.code.slice(fnStartIndex, fnEndIndex) + "\n}" + cssFile.code.slice(fnEndIndex); } return args.artifacts; }; var propertyRegex = /(\w+):.+Tokens\["(\w+)"\]\s?;/g; var mapPropsToTokenCategory = (file) => { const byCategories = /* @__PURE__ */ new Map(); for (const match of file.matchAll(propertyRegex)) { const propertyName = match[1]; const propertyType = match[2]; if (!byCategories.has(propertyType)) { byCategories.set(propertyType, /* @__PURE__ */ new Set()); } const set = byCategories.get(propertyType); set.add(propertyName); } return byCategories; }; var shorthandRegex = /(\w+):\s+Shorthand<"(\w+)">;/g; var mapShorthandsToProperties = (file) => { const shorthandMap = {}; for (const match of file.matchAll(shorthandRegex)) { const shorthand = match[1]; const fullPropertyName = match[2]; shorthandMap[fullPropertyName] = shorthand; } return shorthandMap; }; var mapToObj = (map) => { const obj = {}; map.forEach((set, key) => { obj[key] = [...set]; }); return obj; }; // src/strict-tokens-scope.ts var pluginStrictTokensScope = (options) => { let logger; let ctx; return { name: "strict-tokens-scope", hooks: { "context:created": (context) => { logger = context.logger; ctx = context.ctx.processor.context; }, "codegen:prepare": (args) => { return transformPropTypes(args, options, ctx, logger); } } }; }; var transformPropTypes = (args, options, ctx, logger) => { const { categories = [], props = [] } = options; if (!categories.length && !props.length) return args.artifacts; const artifact = args.artifacts.find((x) => x.id === "types-styles"); const content = artifact?.files.find((x) => x.file.includes("style-props")); if (!content?.code) return args.artifacts; const shorthandsByProp = /* @__PURE__ */ new Map(); ctx.utility.shorthands.forEach((longhand, shorthand) => { shorthandsByProp.set(longhand, (shorthandsByProp.get(longhand) ?? []).concat(shorthand)); }); const types = ctx.utility.getTypes(); const categoryByProp = /* @__PURE__ */ new Map(); types.forEach((values, prop) => { const categoryType = values.find((type) => type.includes("Tokens[")); if (!categoryType) return; const tokenCategory = categoryType.replace('Tokens["', "").replace('"]', ""); if (!categories.includes(tokenCategory)) { return; } categoryByProp.set(prop, tokenCategory); const shorthands = shorthandsByProp.get(prop); if (!shorthands) return; shorthands.forEach((shorthand) => { categoryByProp.set(shorthand, tokenCategory); }); }); const strictTokenProps = props.concat(Array.from(categoryByProp.keys())); if (!strictTokenProps.length) return args.artifacts; if (logger) { logger.debug("plugin:restrict-strict-tokens", `\u{1F43C} Strict token props: ${strictTokenProps.join(", ")}`); } const regex = /(\w+)\?: ConditionalValue<WithEscapeHatch<(.+)>>/g; const transformed = content.code.replace(regex, (match, prop, value) => { if (strictTokenProps.includes(prop)) return match; const longhand = ctx.utility.shorthands.get(prop); if (value.includes("CssProperties")) return match; return `${prop}?: ConditionalValue<${value} | CssProperties["${longhand || prop}"]>`; }) + [ ` type StrictTokenProps = ${strictTokenProps.map((t) => `"${t}"`).join(" | ")}`, `type Restrict<Key, Value, Fallback> = Key extends StrictTokenProps ? Value : Value | Fallback` ].join("\n\n"); content.code = transformed; return args.artifacts; }; export { pluginMinimalSetup, pluginMissingCssWarnings, pluginRemoveFeatures, pluginRemoveNegativeSpacing, pluginRemoveUnusedCss, pluginRestrictStyledProps, pluginStrictTokensRuntime, pluginStrictTokensScope, transformFeatures, transformMissingCssWarnings, transformNegativeSpacing, transformPropTypes, transformRestrictStyledProps, transformStrictTokensRuntime };