UNPKG

tailwindcss-patch

Version:

patch tailwindcss for exposing context and extract classes

1,548 lines (1,529 loc) 79.6 kB
// src/logger.ts import { createConsola } from "consola"; var logger = createConsola(); var logger_default = logger; // src/cache/store.ts import process from "process"; import fs from "fs-extra"; function isErrnoException(error) { return error instanceof Error && typeof error.code === "string"; } function isAccessDenied(error) { return isErrnoException(error) && Boolean(error.code && ["EPERM", "EBUSY", "EACCES"].includes(error.code)); } var CacheStore = class { constructor(options) { this.options = options; this.driver = options.driver ?? "file"; } driver; memoryCache = null; async ensureDir() { await fs.ensureDir(this.options.dir); } ensureDirSync() { fs.ensureDirSync(this.options.dir); } createTempPath() { const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${this.options.path}.${uniqueSuffix}.tmp`; } async replaceCacheFile(tempPath) { try { await fs.rename(tempPath, this.options.path); return true; } catch (error) { if (isErrnoException(error) && (error.code === "EEXIST" || error.code === "EPERM")) { try { await fs.remove(this.options.path); } catch (removeError) { if (isAccessDenied(removeError)) { logger_default.debug("Tailwind class cache locked or read-only, skipping update.", removeError); return false; } if (!isErrnoException(removeError) || removeError.code !== "ENOENT") { throw removeError; } } await fs.rename(tempPath, this.options.path); return true; } throw error; } } replaceCacheFileSync(tempPath) { try { fs.renameSync(tempPath, this.options.path); return true; } catch (error) { if (isErrnoException(error) && (error.code === "EEXIST" || error.code === "EPERM")) { try { fs.removeSync(this.options.path); } catch (removeError) { if (isAccessDenied(removeError)) { logger_default.debug("Tailwind class cache locked or read-only, skipping update.", removeError); return false; } if (!isErrnoException(removeError) || removeError.code !== "ENOENT") { throw removeError; } } fs.renameSync(tempPath, this.options.path); return true; } throw error; } } async cleanupTempFile(tempPath) { try { await fs.remove(tempPath); } catch { } } cleanupTempFileSync(tempPath) { try { fs.removeSync(tempPath); } catch { } } async write(data) { if (!this.options.enabled) { return void 0; } if (this.driver === "noop") { return void 0; } if (this.driver === "memory") { this.memoryCache = new Set(data); return "memory"; } const tempPath = this.createTempPath(); try { await this.ensureDir(); await fs.writeJSON(tempPath, Array.from(data)); const replaced = await this.replaceCacheFile(tempPath); if (replaced) { return this.options.path; } await this.cleanupTempFile(tempPath); return void 0; } catch (error) { await this.cleanupTempFile(tempPath); logger_default.error("Unable to persist Tailwind class cache", error); return void 0; } } writeSync(data) { if (!this.options.enabled) { return void 0; } if (this.driver === "noop") { return void 0; } if (this.driver === "memory") { this.memoryCache = new Set(data); return "memory"; } const tempPath = this.createTempPath(); try { this.ensureDirSync(); fs.writeJSONSync(tempPath, Array.from(data)); const replaced = this.replaceCacheFileSync(tempPath); if (replaced) { return this.options.path; } this.cleanupTempFileSync(tempPath); return void 0; } catch (error) { this.cleanupTempFileSync(tempPath); logger_default.error("Unable to persist Tailwind class cache", error); return void 0; } } async read() { if (!this.options.enabled) { return /* @__PURE__ */ new Set(); } if (this.driver === "noop") { return /* @__PURE__ */ new Set(); } if (this.driver === "memory") { return new Set(this.memoryCache ?? []); } try { const exists = await fs.pathExists(this.options.path); if (!exists) { return /* @__PURE__ */ new Set(); } const data = await fs.readJSON(this.options.path); if (Array.isArray(data)) { return new Set(data.filter((item) => typeof item === "string")); } } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return /* @__PURE__ */ new Set(); } logger_default.warn("Unable to read Tailwind class cache, removing invalid file.", error); try { await fs.remove(this.options.path); } catch (cleanupError) { logger_default.error("Failed to clean up invalid cache file", cleanupError); } } return /* @__PURE__ */ new Set(); } readSync() { if (!this.options.enabled) { return /* @__PURE__ */ new Set(); } if (this.driver === "noop") { return /* @__PURE__ */ new Set(); } if (this.driver === "memory") { return new Set(this.memoryCache ?? []); } try { const exists = fs.pathExistsSync(this.options.path); if (!exists) { return /* @__PURE__ */ new Set(); } const data = fs.readJSONSync(this.options.path); if (Array.isArray(data)) { return new Set(data.filter((item) => typeof item === "string")); } } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return /* @__PURE__ */ new Set(); } logger_default.warn("Unable to read Tailwind class cache, removing invalid file.", error); try { fs.removeSync(this.options.path); } catch (cleanupError) { logger_default.error("Failed to clean up invalid cache file", cleanupError); } } return /* @__PURE__ */ new Set(); } }; // src/extraction/candidate-extractor.ts import { promises as fs2 } from "fs"; import process2 from "process"; import path from "pathe"; async function importNode() { return import("@tailwindcss/node"); } async function importOxide() { return import("@tailwindcss/oxide"); } async function loadDesignSystem(css, bases) { const uniqueBases = Array.from(new Set(bases.filter(Boolean))); if (uniqueBases.length === 0) { throw new Error("No base directories provided for Tailwind CSS design system."); } const { __unstable__loadDesignSystem } = await importNode(); let lastError; for (const base of uniqueBases) { try { return await __unstable__loadDesignSystem(css, { base }); } catch (error) { lastError = error; } } if (lastError instanceof Error) { throw lastError; } throw new Error("Failed to load Tailwind CSS design system."); } async function extractRawCandidatesWithPositions(content, extension = "html") { const { Scanner } = await importOxide(); const scanner = new Scanner({}); const result = scanner.getCandidatesWithPositions({ content, extension }); return result.map(({ candidate, position }) => ({ rawCandidate: candidate, start: position, end: position + candidate.length })); } async function extractRawCandidates(sources) { const { Scanner } = await importOxide(); const scanner = new Scanner({ sources }); return scanner.scan(); } async function extractValidCandidates(options) { const providedOptions = options ?? {}; const defaultCwd = providedOptions.cwd ?? process2.cwd(); const base = providedOptions.base ?? defaultCwd; const baseFallbacks = providedOptions.baseFallbacks ?? []; const css = providedOptions.css ?? '@import "tailwindcss";'; const sources = (providedOptions.sources ?? [ { base: defaultCwd, pattern: "**/*", negated: false } ]).map((source) => ({ base: source.base ?? defaultCwd, pattern: source.pattern, negated: source.negated })); const designSystem = await loadDesignSystem(css, [base, ...baseFallbacks]); const candidates = await extractRawCandidates(sources); const parsedCandidates = candidates.filter( (rawCandidate) => designSystem.parseCandidate(rawCandidate).length > 0 ); if (parsedCandidates.length === 0) { return parsedCandidates; } const cssByCandidate = designSystem.candidatesToCss(parsedCandidates); const validCandidates = []; for (let index = 0; index < parsedCandidates.length; index++) { const css2 = cssByCandidate[index]; if (typeof css2 === "string" && css2.trim().length > 0) { validCandidates.push(parsedCandidates[index]); } } return validCandidates; } function normalizeSources(sources, cwd) { const baseSources = sources?.length ? sources : [ { base: cwd, pattern: "**/*", negated: false } ]; return baseSources.map((source) => ({ base: source.base ?? cwd, pattern: source.pattern, negated: source.negated })); } function buildLineOffsets(content) { const offsets = [0]; for (let i = 0; i < content.length; i++) { if (content[i] === "\n") { offsets.push(i + 1); } } if (offsets[offsets.length - 1] !== content.length) { offsets.push(content.length); } return offsets; } function resolveLineMeta(content, offsets, index) { let low = 0; let high = offsets.length - 1; while (low <= high) { const mid = Math.floor((low + high) / 2); const start = offsets[mid]; const nextStart = offsets[mid + 1] ?? content.length; if (index < start) { high = mid - 1; continue; } if (index >= nextStart) { low = mid + 1; continue; } const line = mid + 1; const column = index - start + 1; const lineEnd = content.indexOf("\n", start); const lineText = content.slice(start, lineEnd === -1 ? content.length : lineEnd); return { line, column, lineText }; } const lastStart = offsets[offsets.length - 2] ?? 0; return { line: offsets.length - 1, column: index - lastStart + 1, lineText: content.slice(lastStart) }; } function toExtension(filename) { const ext = path.extname(filename).replace(/^\./, ""); return ext || "txt"; } function toRelativeFile(cwd, filename) { const relative = path.relative(cwd, filename); return relative === "" ? path.basename(filename) : relative; } async function extractProjectCandidatesWithPositions(options) { const cwd = options?.cwd ? path.resolve(options.cwd) : process2.cwd(); const normalizedSources = normalizeSources(options?.sources, cwd); const { Scanner } = await importOxide(); const scanner = new Scanner({ sources: normalizedSources }); const files = scanner.files ?? []; const entries = []; const skipped = []; for (const file of files) { let content; try { content = await fs2.readFile(file, "utf8"); } catch (error) { skipped.push({ file, reason: error instanceof Error ? error.message : "Unknown error" }); continue; } const extension = toExtension(file); const matches = scanner.getCandidatesWithPositions({ file, content, extension }); if (!matches.length) { continue; } const offsets = buildLineOffsets(content); const relativeFile = toRelativeFile(cwd, file); for (const match of matches) { const info = resolveLineMeta(content, offsets, match.position); entries.push({ rawCandidate: match.candidate, file, relativeFile, extension, start: match.position, end: match.position + match.candidate.length, length: match.candidate.length, line: info.line, column: info.column, lineText: info.lineText }); } } return { entries, filesScanned: files.length, skippedFiles: skipped, sources: normalizedSources }; } function groupTokensByFile(report, options) { const key = options?.key ?? "relative"; const stripAbsolute = options?.stripAbsolutePaths ?? key !== "absolute"; return report.entries.reduce((acc, entry) => { const bucketKey = key === "absolute" ? entry.file : entry.relativeFile; if (!acc[bucketKey]) { acc[bucketKey] = []; } const value = stripAbsolute ? { ...entry, file: entry.relativeFile } : entry; acc[bucketKey].push(value); return acc; }, {}); } // src/options/normalize.ts import process3 from "process"; import path2 from "pathe"; // src/constants.ts var pkgName = "tailwindcss-patch"; // src/options/normalize.ts function toPrettyValue(value) { if (typeof value === "number") { return value > 0 ? value : false; } if (value === true) { return 2; } return false; } function normalizeCacheDriver(driver) { if (driver === "memory" || driver === "noop") { return driver; } return "file"; } function normalizeCacheOptions(cache, projectRoot) { let enabled = false; let cwd = projectRoot; let dir = path2.resolve(cwd, "node_modules/.cache", pkgName); let file = "class-cache.json"; let strategy = "merge"; let driver = "file"; if (typeof cache === "boolean") { enabled = cache; } else if (typeof cache === "object" && cache) { enabled = cache.enabled ?? true; cwd = cache.cwd ?? cwd; dir = cache.dir ? path2.resolve(cache.dir) : path2.resolve(cwd, "node_modules/.cache", pkgName); file = cache.file ?? file; strategy = cache.strategy ?? strategy; driver = normalizeCacheDriver(cache.driver); } const filename = path2.resolve(dir, file); return { enabled, cwd, dir, file, path: filename, strategy, driver }; } function normalizeOutputOptions(output) { const enabled = output?.enabled ?? true; const file = output?.file ?? ".tw-patch/tw-class-list.json"; const format = output?.format ?? "json"; const pretty = toPrettyValue(output?.pretty ?? true); const removeUniversalSelector = output?.removeUniversalSelector ?? true; return { enabled, file, format, pretty, removeUniversalSelector }; } function normalizeExposeContextOptions(features) { if (features?.exposeContext === false) { return { enabled: false, refProperty: "contextRef" }; } if (typeof features?.exposeContext === "object" && features.exposeContext) { return { enabled: true, refProperty: features.exposeContext.refProperty ?? "contextRef" }; } return { enabled: true, refProperty: "contextRef" }; } function normalizeExtendLengthUnitsOptions(features) { const extend = features?.extendLengthUnits; if (extend === false || extend === void 0) { return null; } if (extend.enabled === false) { return null; } const base = { units: ["rpx"], overwrite: true }; return { ...base, ...extend, enabled: extend.enabled ?? true, units: extend.units ?? base.units, overwrite: extend.overwrite ?? base.overwrite }; } function normalizeTailwindV4Options(v4, fallbackBase) { const configuredBase = v4?.base ? path2.resolve(v4.base) : void 0; const base = configuredBase ?? fallbackBase; const cssEntries = Array.isArray(v4?.cssEntries) ? v4.cssEntries.filter((entry) => Boolean(entry)).map((entry) => path2.resolve(entry)) : []; const userSources = v4?.sources; const hasUserDefinedSources = Boolean(userSources?.length); const sources = hasUserDefinedSources ? userSources : [ { base: fallbackBase, pattern: "**/*", negated: false } ]; return { base, configuredBase, css: v4?.css, cssEntries, sources, hasUserDefinedSources }; } function normalizeTailwindOptions(tailwind, projectRoot) { const packageName = tailwind?.packageName ?? "tailwindcss"; const versionHint = tailwind?.version; const resolve = tailwind?.resolve; const cwd = tailwind?.cwd ?? projectRoot; const config = tailwind?.config; const postcssPlugin = tailwind?.postcssPlugin; const v4 = normalizeTailwindV4Options(tailwind?.v4, cwd); return { packageName, versionHint, resolve, cwd, config, postcssPlugin, v2: tailwind?.v2, v3: tailwind?.v3, v4 }; } function normalizeOptions(options = {}) { const projectRoot = options.cwd ? path2.resolve(options.cwd) : process3.cwd(); const overwrite = options.overwrite ?? true; const output = normalizeOutputOptions(options.output); const cache = normalizeCacheOptions(options.cache, projectRoot); const tailwind = normalizeTailwindOptions(options.tailwind, projectRoot); const exposeContext = normalizeExposeContextOptions(options.features); const extendLengthUnits = normalizeExtendLengthUnitsOptions(options.features); const filter = (className) => { if (output.removeUniversalSelector && className === "*") { return false; } if (typeof options.filter === "function") { return options.filter(className) !== false; } return true; }; return { projectRoot, overwrite, tailwind, features: { exposeContext, extendLengthUnits }, output, cache, filter }; } // src/patching/status.ts import * as t4 from "@babel/types"; import fs4 from "fs-extra"; import path4 from "pathe"; // src/babel/index.ts import _babelGenerate from "@babel/generator"; import _babelTraverse from "@babel/traverse"; import { parse, parseExpression } from "@babel/parser"; function _interopDefaultCompat(e) { return e && typeof e === "object" && "default" in e ? e.default : e; } var generate = _interopDefaultCompat(_babelGenerate); var traverse = _interopDefaultCompat(_babelTraverse); // src/patching/operations/export-context/postcss-v2.ts import * as t from "@babel/types"; var IDENTIFIER_RE = /^[A-Z_$][\w$]*$/i; function toIdentifierName(property) { if (!property) { return "contextRef"; } const sanitized = property.replace(/[^\w$]/gu, "_"); if (/^\d/.test(sanitized)) { return `_${sanitized}`; } return sanitized || "contextRef"; } function createExportsMember(property) { if (IDENTIFIER_RE.test(property)) { return t.memberExpression(t.identifier("exports"), t.identifier(property)); } return t.memberExpression(t.identifier("exports"), t.stringLiteral(property), true); } function transformProcessTailwindFeaturesReturnContextV2(content) { const ast = parse(content, { sourceType: "unambiguous" }); let hasPatched = false; traverse(ast, { FunctionDeclaration(path11) { const node = path11.node; if (node.id?.name !== "processTailwindFeatures" || node.body.body.length !== 1 || !t.isReturnStatement(node.body.body[0])) { return; } const returnStatement3 = node.body.body[0]; if (!t.isFunctionExpression(returnStatement3.argument)) { return; } const body = returnStatement3.argument.body.body; const lastStatement = body[body.length - 1]; const alreadyReturnsContext = Boolean( t.isReturnStatement(lastStatement) && t.isIdentifier(lastStatement.argument) && lastStatement.argument.name === "context" ); hasPatched = alreadyReturnsContext; if (!alreadyReturnsContext) { body.push(t.returnStatement(t.identifier("context"))); } } }); return { code: hasPatched ? content : generate(ast).code, hasPatched }; } function transformPostcssPluginV2(content, options) { const refIdentifier = t.identifier(toIdentifierName(options.refProperty)); const exportMember = createExportsMember(options.refProperty); const valueMember = t.memberExpression(refIdentifier, t.identifier("value")); const ast = parse(content); let hasPatched = false; traverse(ast, { Program(path11) { const program = path11.node; const index = program.body.findIndex((statement) => { return t.isFunctionDeclaration(statement) && statement.id?.name === "_default"; }); if (index === -1) { return; } const previous = program.body[index - 1]; const beforePrevious = program.body[index - 2]; const alreadyHasVariable = Boolean( previous && t.isVariableDeclaration(previous) && previous.declarations.length === 1 && t.isIdentifier(previous.declarations[0].id) && previous.declarations[0].id.name === refIdentifier.name ); const alreadyAssignsExports = Boolean( beforePrevious && t.isExpressionStatement(beforePrevious) && t.isAssignmentExpression(beforePrevious.expression) && t.isMemberExpression(beforePrevious.expression.left) && t.isIdentifier(beforePrevious.expression.right) && beforePrevious.expression.right.name === refIdentifier.name && generate(beforePrevious.expression.left).code === generate(exportMember).code ); hasPatched = alreadyHasVariable && alreadyAssignsExports; if (!alreadyHasVariable) { program.body.splice( index, 0, t.variableDeclaration("var", [ t.variableDeclarator( refIdentifier, t.objectExpression([ t.objectProperty(t.identifier("value"), t.arrayExpression()) ]) ) ]), t.expressionStatement( t.assignmentExpression("=", exportMember, refIdentifier) ) ); } }, FunctionDeclaration(path11) { if (hasPatched) { return; } const fn = path11.node; if (fn.id?.name !== "_default") { return; } if (fn.body.body.length !== 1 || !t.isReturnStatement(fn.body.body[0])) { return; } const returnStatement3 = fn.body.body[0]; if (!t.isCallExpression(returnStatement3.argument) || !t.isMemberExpression(returnStatement3.argument.callee) || !t.isArrayExpression(returnStatement3.argument.callee.object)) { return; } const fnExpression = returnStatement3.argument.callee.object.elements[1]; if (!fnExpression || !t.isFunctionExpression(fnExpression)) { return; } const block = fnExpression.body; const statements = block.body; if (t.isExpressionStatement(statements[0]) && t.isAssignmentExpression(statements[0].expression) && t.isNumericLiteral(statements[0].expression.right)) { hasPatched = true; return; } const lastStatement = statements[statements.length - 1]; if (lastStatement && t.isExpressionStatement(lastStatement)) { statements[statements.length - 1] = t.expressionStatement( t.callExpression( t.memberExpression(valueMember, t.identifier("push")), [lastStatement.expression] ) ); } const index = statements.findIndex((statement) => t.isIfStatement(statement)); if (index > -1) { const ifStatement = statements[index]; if (t.isBlockStatement(ifStatement.consequent) && ifStatement.consequent.body[1] && t.isForOfStatement(ifStatement.consequent.body[1])) { const forOf = ifStatement.consequent.body[1]; if (t.isBlockStatement(forOf.body) && forOf.body.body.length === 1) { const nestedIf = forOf.body.body[0]; if (nestedIf && t.isIfStatement(nestedIf) && t.isBlockStatement(nestedIf.consequent) && nestedIf.consequent.body.length === 1 && t.isExpressionStatement(nestedIf.consequent.body[0])) { nestedIf.consequent.body[0] = t.expressionStatement( t.callExpression( t.memberExpression(valueMember, t.identifier("push")), [nestedIf.consequent.body[0].expression] ) ); } } } } statements.unshift( t.expressionStatement( t.assignmentExpression( "=", t.memberExpression(valueMember, t.identifier("length")), t.numericLiteral(0) ) ) ); } }); return { code: hasPatched ? content : generate(ast).code, hasPatched }; } // src/patching/operations/export-context/postcss-v3.ts import * as t2 from "@babel/types"; var IDENTIFIER_RE2 = /^[A-Z_$][\w$]*$/i; function toIdentifierName2(property) { if (!property) { return "contextRef"; } const sanitized = property.replace(/[^\w$]/gu, "_"); if (/^\d/.test(sanitized)) { return `_${sanitized}`; } return sanitized || "contextRef"; } function createModuleExportsMember(property) { const object = t2.memberExpression(t2.identifier("module"), t2.identifier("exports")); if (IDENTIFIER_RE2.test(property)) { return t2.memberExpression(object, t2.identifier(property)); } return t2.memberExpression(object, t2.stringLiteral(property), true); } function transformProcessTailwindFeaturesReturnContext(content) { const ast = parse(content); let hasPatched = false; traverse(ast, { FunctionDeclaration(path11) { const node = path11.node; if (node.id?.name !== "processTailwindFeatures" || node.body.body.length !== 1) { return; } const [returnStatement3] = node.body.body; if (!t2.isReturnStatement(returnStatement3) || !t2.isFunctionExpression(returnStatement3.argument)) { return; } const expression = returnStatement3.argument; const body = expression.body.body; const lastStatement = body[body.length - 1]; const alreadyReturnsContext = Boolean( t2.isReturnStatement(lastStatement) && t2.isIdentifier(lastStatement.argument) && lastStatement.argument.name === "context" ); hasPatched = alreadyReturnsContext; if (!alreadyReturnsContext) { body.push(t2.returnStatement(t2.identifier("context"))); } } }); return { code: hasPatched ? content : generate(ast).code, hasPatched }; } function transformPostcssPlugin(content, { refProperty }) { const ast = parse(content); const refIdentifier = t2.identifier(toIdentifierName2(refProperty)); const moduleExportsMember = createModuleExportsMember(refProperty); const valueMember = t2.memberExpression(refIdentifier, t2.identifier("value")); let hasPatched = false; traverse(ast, { Program(path11) { const program = path11.node; const index = program.body.findIndex((statement) => { return t2.isExpressionStatement(statement) && t2.isAssignmentExpression(statement.expression) && t2.isMemberExpression(statement.expression.left) && t2.isFunctionExpression(statement.expression.right) && statement.expression.right.id?.name === "tailwindcss"; }); if (index === -1) { return; } const previousStatement = program.body[index - 1]; const lastStatement = program.body[program.body.length - 1]; const alreadyHasVariable = Boolean( previousStatement && t2.isVariableDeclaration(previousStatement) && previousStatement.declarations.length === 1 && t2.isIdentifier(previousStatement.declarations[0].id) && previousStatement.declarations[0].id.name === refIdentifier.name ); const alreadyAssignsModuleExports = Boolean( t2.isExpressionStatement(lastStatement) && t2.isAssignmentExpression(lastStatement.expression) && t2.isMemberExpression(lastStatement.expression.left) && t2.isIdentifier(lastStatement.expression.right) && lastStatement.expression.right.name === refIdentifier.name && generate(lastStatement.expression.left).code === generate(moduleExportsMember).code ); hasPatched = alreadyHasVariable && alreadyAssignsModuleExports; if (!alreadyHasVariable) { program.body.splice( index, 0, t2.variableDeclaration("const", [ t2.variableDeclarator( refIdentifier, t2.objectExpression([ t2.objectProperty(t2.identifier("value"), t2.arrayExpression()) ]) ) ]) ); } if (!alreadyAssignsModuleExports) { program.body.push( t2.expressionStatement( t2.assignmentExpression("=", moduleExportsMember, refIdentifier) ) ); } }, FunctionExpression(path11) { if (hasPatched) { return; } const fn = path11.node; if (fn.id?.name !== "tailwindcss" || fn.body.body.length !== 1) { return; } const [returnStatement3] = fn.body.body; if (!returnStatement3 || !t2.isReturnStatement(returnStatement3) || !t2.isObjectExpression(returnStatement3.argument)) { return; } const properties = returnStatement3.argument.properties; if (properties.length !== 2) { return; } const pluginsProperty = properties.find( (prop) => t2.isObjectProperty(prop) && t2.isIdentifier(prop.key) && prop.key.name === "plugins" ); if (!pluginsProperty || !t2.isObjectProperty(pluginsProperty) || !t2.isCallExpression(pluginsProperty.value) || !t2.isMemberExpression(pluginsProperty.value.callee) || !t2.isArrayExpression(pluginsProperty.value.callee.object)) { return; } const pluginsArray = pluginsProperty.value.callee.object.elements; const targetPlugin = pluginsArray[1]; if (!targetPlugin || !t2.isFunctionExpression(targetPlugin)) { return; } const block = targetPlugin.body; const statements = block.body; const last = statements[statements.length - 1]; if (last && t2.isExpressionStatement(last)) { statements[statements.length - 1] = t2.expressionStatement( t2.callExpression( t2.memberExpression(valueMember, t2.identifier("push")), [last.expression] ) ); } const index = statements.findIndex((s) => t2.isIfStatement(s)); if (index > -1) { const ifStatement = statements[index]; if (t2.isBlockStatement(ifStatement.consequent)) { const [, second] = ifStatement.consequent.body; if (second && t2.isForOfStatement(second) && t2.isBlockStatement(second.body)) { const bodyStatement = second.body.body[0]; if (bodyStatement && t2.isIfStatement(bodyStatement) && t2.isBlockStatement(bodyStatement.consequent) && bodyStatement.consequent.body.length === 1 && t2.isExpressionStatement(bodyStatement.consequent.body[0])) { bodyStatement.consequent.body[0] = t2.expressionStatement( t2.callExpression( t2.memberExpression(valueMember, t2.identifier("push")), [bodyStatement.consequent.body[0].expression] ) ); } } } } statements.unshift( t2.expressionStatement( t2.assignmentExpression( "=", t2.memberExpression(valueMember, t2.identifier("length")), t2.numericLiteral(0) ) ) ); } }); return { code: hasPatched ? content : generate(ast).code, hasPatched }; } // src/patching/operations/extend-length-units.ts import * as t3 from "@babel/types"; import fs3 from "fs-extra"; import path3 from "pathe"; // src/utils.ts function isObject(val) { return val !== null && typeof val === "object" && Array.isArray(val) === false; } function spliceChangesIntoString(str, changes) { if (!changes[0]) { return str; } changes.sort((a, b) => { return a.end - b.end || a.start - b.start; }); let result = ""; let previous = changes[0]; result += str.slice(0, previous.start); result += previous.replacement; for (let i = 1; i < changes.length; ++i) { const change = changes[i]; result += str.slice(previous.end, change.start); result += change.replacement; previous = change; } result += str.slice(previous.end); return result; } // src/patching/operations/extend-length-units.ts function updateLengthUnitsArray(content, options) { const { variableName = "lengthUnits", units } = options; const ast = parse(content); let arrayRef; let changed = false; traverse(ast, { Identifier(path11) { if (path11.node.name === variableName && t3.isVariableDeclarator(path11.parent) && t3.isArrayExpression(path11.parent.init)) { arrayRef = path11.parent.init; const existing = new Set( path11.parent.init.elements.map((element) => t3.isStringLiteral(element) ? element.value : void 0).filter(Boolean) ); for (const unit of units) { if (!existing.has(unit)) { path11.parent.init.elements = path11.parent.init.elements.map((element) => { if (t3.isStringLiteral(element)) { return t3.stringLiteral(element.value); } return element; }); path11.parent.init.elements.push(t3.stringLiteral(unit)); changed = true; } } } } }); return { arrayRef, changed }; } function applyExtendLengthUnitsPatchV3(rootDir, options) { if (!options.enabled) { return { changed: false, code: void 0 }; } const opts = { ...options, lengthUnitsFilePath: options.lengthUnitsFilePath ?? "lib/util/dataTypes.js", variableName: options.variableName ?? "lengthUnits" }; const dataTypesFilePath = path3.resolve(rootDir, opts.lengthUnitsFilePath); const exists = fs3.existsSync(dataTypesFilePath); if (!exists) { return { changed: false, code: void 0 }; } const content = fs3.readFileSync(dataTypesFilePath, "utf8"); const { arrayRef, changed } = updateLengthUnitsArray(content, opts); if (!arrayRef || !changed) { return { changed: false, code: void 0 }; } const { code } = generate(arrayRef, { jsescOption: { quotes: "single" } }); if (arrayRef.start != null && arrayRef.end != null) { const nextCode = `${content.slice(0, arrayRef.start)}${code}${content.slice(arrayRef.end)}`; if (opts.overwrite) { const target = opts.destPath ? path3.resolve(opts.destPath) : dataTypesFilePath; fs3.writeFileSync(target, nextCode, "utf8"); logger_default.success("Patched Tailwind CSS length unit list (v3)."); } return { changed: true, code: nextCode }; } return { changed: false, code: void 0 }; } function applyExtendLengthUnitsPatchV4(rootDir, options) { if (!options.enabled) { return { files: [], changed: false }; } const opts = { ...options }; const distDir = path3.resolve(rootDir, "dist"); if (!fs3.existsSync(distDir)) { return { files: [], changed: false }; } const entries = fs3.readdirSync(distDir); const chunkNames = entries.filter((entry) => entry.endsWith(".js") || entry.endsWith(".mjs")); const pattern = /\[\s*["']cm["'],\s*["']mm["'],[\w,"']+\]/; const candidates = chunkNames.map((chunkName) => { const file = path3.join(distDir, chunkName); const code = fs3.readFileSync(file, "utf8"); const match = pattern.exec(code); if (!match) { return null; } return { file, code, match, hasPatched: false }; }).filter((candidate) => candidate !== null); for (const item of candidates) { const { code, file, match } = item; const ast = parse(match[0], { sourceType: "unambiguous" }); traverse(ast, { ArrayExpression(path11) { for (const unit of opts.units) { if (path11.node.elements.some((element) => t3.isStringLiteral(element) && element.value === unit)) { item.hasPatched = true; return; } path11.node.elements.push(t3.stringLiteral(unit)); } } }); if (item.hasPatched) { continue; } const { code: replacement } = generate(ast, { minified: true }); const start = match.index ?? 0; const end = start + match[0].length; item.code = spliceChangesIntoString(code, [ { start, end, replacement: replacement.endsWith(";") ? replacement.slice(0, -1) : replacement } ]); if (opts.overwrite) { fs3.writeFileSync(file, item.code, "utf8"); } } if (candidates.some((file) => !file.hasPatched)) { logger_default.success("Patched Tailwind CSS length unit list (v4)."); } return { changed: candidates.some((file) => !file.hasPatched), files: candidates }; } // src/patching/status.ts function inspectLengthUnitsArray(content, variableName, units) { const ast = parse(content); let found = false; let missingUnits = []; traverse(ast, { Identifier(path11) { if (path11.node.name === variableName && t4.isVariableDeclarator(path11.parent) && t4.isArrayExpression(path11.parent.init)) { found = true; const existing = new Set( path11.parent.init.elements.map((element) => t4.isStringLiteral(element) ? element.value : void 0).filter(Boolean) ); missingUnits = units.filter((unit) => !existing.has(unit)); path11.stop(); } } }); return { found, missingUnits }; } function checkExposeContextPatch(context) { const { packageInfo, options, majorVersion } = context; const refProperty = options.features.exposeContext.refProperty; if (!options.features.exposeContext.enabled) { return { name: "exposeContext", status: "skipped", reason: "exposeContext feature disabled", files: [] }; } if (majorVersion === 4) { return { name: "exposeContext", status: "unsupported", reason: "Context export patch is only required for Tailwind v2/v3", files: [] }; } const checks = []; function inspectFile(relative, transform) { const filePath = path4.resolve(packageInfo.rootPath, relative); if (!fs4.existsSync(filePath)) { checks.push({ relative, exists: false, patched: false }); return; } const content = fs4.readFileSync(filePath, "utf8"); const { hasPatched } = transform(content); checks.push({ relative, exists: true, patched: hasPatched }); } if (majorVersion === 3) { inspectFile("lib/processTailwindFeatures.js", transformProcessTailwindFeaturesReturnContext); const pluginCandidates = ["lib/plugin.js", "lib/index.js"]; const pluginRelative = pluginCandidates.find((candidate) => fs4.existsSync(path4.resolve(packageInfo.rootPath, candidate))); if (pluginRelative) { inspectFile(pluginRelative, (content) => transformPostcssPlugin(content, { refProperty })); } else { checks.push({ relative: "lib/plugin.js", exists: false, patched: false }); } } else { inspectFile("lib/jit/processTailwindFeatures.js", transformProcessTailwindFeaturesReturnContextV2); inspectFile("lib/jit/index.js", (content) => transformPostcssPluginV2(content, { refProperty })); } const files = checks.filter((check) => check.exists).map((check) => check.relative); const missingFiles = checks.filter((check) => !check.exists); const unpatchedFiles = checks.filter((check) => check.exists && !check.patched); const reasons = []; if (missingFiles.length) { reasons.push(`missing files: ${missingFiles.map((item) => item.relative).join(", ")}`); } if (unpatchedFiles.length) { reasons.push(`unpatched files: ${unpatchedFiles.map((item) => item.relative).join(", ")}`); } return { name: "exposeContext", status: reasons.length ? "not-applied" : "applied", reason: reasons.length ? reasons.join("; ") : void 0, files }; } function checkExtendLengthUnitsV3(rootDir, options) { const lengthUnitsFilePath = options.lengthUnitsFilePath ?? "lib/util/dataTypes.js"; const variableName = options.variableName ?? "lengthUnits"; const target = path4.resolve(rootDir, lengthUnitsFilePath); const files = fs4.existsSync(target) ? [path4.relative(rootDir, target)] : []; if (!fs4.existsSync(target)) { return { name: "extendLengthUnits", status: "not-applied", reason: `missing ${lengthUnitsFilePath}`, files }; } const content = fs4.readFileSync(target, "utf8"); const { found, missingUnits } = inspectLengthUnitsArray(content, variableName, options.units); if (!found) { return { name: "extendLengthUnits", status: "not-applied", reason: `could not locate ${variableName} array in ${lengthUnitsFilePath}`, files }; } if (missingUnits.length) { return { name: "extendLengthUnits", status: "not-applied", reason: `missing units: ${missingUnits.join(", ")}`, files }; } return { name: "extendLengthUnits", status: "applied", files }; } function checkExtendLengthUnitsV4(rootDir, options) { const distDir = path4.resolve(rootDir, "dist"); if (!fs4.existsSync(distDir)) { return { name: "extendLengthUnits", status: "not-applied", reason: "dist directory not found for Tailwind v4 package", files: [] }; } const result = applyExtendLengthUnitsPatchV4(rootDir, { ...options, enabled: true, overwrite: false }); if (result.files.length === 0) { return { name: "extendLengthUnits", status: "not-applied", reason: "no bundle chunks matched the length unit pattern", files: [] }; } const files = result.files.map((file) => path4.relative(rootDir, file.file)); const pending = result.files.filter((file) => !file.hasPatched); if (pending.length) { return { name: "extendLengthUnits", status: "not-applied", reason: `missing units in ${pending.length} bundle${pending.length > 1 ? "s" : ""}`, files: pending.map((file) => path4.relative(rootDir, file.file)) }; } return { name: "extendLengthUnits", status: "applied", files }; } function checkExtendLengthUnitsPatch(context) { const { packageInfo, options, majorVersion } = context; if (!options.features.extendLengthUnits) { return { name: "extendLengthUnits", status: "skipped", reason: "extendLengthUnits feature disabled", files: [] }; } if (majorVersion === 2) { return { name: "extendLengthUnits", status: "unsupported", reason: "length unit extension is only applied for Tailwind v3/v4", files: [] }; } if (majorVersion === 3) { return checkExtendLengthUnitsV3(packageInfo.rootPath, options.features.extendLengthUnits); } return checkExtendLengthUnitsV4(packageInfo.rootPath, options.features.extendLengthUnits); } function getPatchStatusReport(context) { return { package: { name: context.packageInfo.name ?? context.packageInfo.packageJson?.name, version: context.packageInfo.version, root: context.packageInfo.rootPath }, majorVersion: context.majorVersion, entries: [ checkExposeContextPatch(context), checkExtendLengthUnitsPatch(context) ] }; } // src/runtime/class-collector.ts import process4 from "process"; import fs5 from "fs-extra"; import path5 from "pathe"; function collectClassesFromContexts(contexts, filter) { const set = /* @__PURE__ */ new Set(); for (const context of contexts) { if (!isObject(context) || !context.classCache) { continue; } for (const key of context.classCache.keys()) { const className = key.toString(); if (filter(className)) { set.add(className); } } } return set; } async function collectClassesFromTailwindV4(options) { const set = /* @__PURE__ */ new Set(); const v4Options = options.tailwind.v4; if (!v4Options) { return set; } const toAbsolute = (value) => { if (!value) { return void 0; } return path5.isAbsolute(value) ? value : path5.resolve(options.projectRoot, value); }; const resolvedConfiguredBase = toAbsolute(v4Options.configuredBase); const resolvedDefaultBase = toAbsolute(v4Options.base) ?? process4.cwd(); const resolveSources = (base) => { if (!v4Options.sources?.length) { return void 0; } return v4Options.sources.map((source) => ({ base: source.base ?? base, pattern: source.pattern, negated: source.negated })); }; if (v4Options.cssEntries.length > 0) { for (const entry of v4Options.cssEntries) { const filePath = path5.isAbsolute(entry) ? entry : path5.resolve(options.projectRoot, entry); if (!await fs5.pathExists(filePath)) { continue; } const css = await fs5.readFile(filePath, "utf8"); const entryDir = path5.dirname(filePath); const designSystemBases = resolvedConfiguredBase && resolvedConfiguredBase !== entryDir ? [entryDir, resolvedConfiguredBase] : [entryDir]; const sourcesBase = resolvedConfiguredBase ?? entryDir; const sources = resolveSources(sourcesBase); const candidates = await extractValidCandidates({ cwd: options.projectRoot, base: designSystemBases[0], baseFallbacks: designSystemBases.slice(1), css, sources }); for (const candidate of candidates) { if (options.filter(candidate)) { set.add(candidate); } } } } else { const baseForCss = resolvedConfiguredBase ?? resolvedDefaultBase; const sources = resolveSources(baseForCss); const candidates = await extractValidCandidates({ cwd: options.projectRoot, base: baseForCss, css: v4Options.css, sources }); for (const candidate of candidates) { if (options.filter(candidate)) { set.add(candidate); } } } return set; } // src/runtime/context-registry.ts import { createRequire } from "module"; import fs6 from "fs-extra"; import path6 from "pathe"; var require2 = createRequire(import.meta.url); function resolveRuntimeEntry(packageInfo, majorVersion) { const root = packageInfo.rootPath; if (majorVersion === 2) { const jitIndex = path6.join(root, "lib/jit/index.js"); if (fs6.existsSync(jitIndex)) { return jitIndex; } } else if (majorVersion === 3) { const plugin = path6.join(root, "lib/plugin.js"); const index = path6.join(root, "lib/index.js"); if (fs6.existsSync(plugin)) { return plugin; } if (fs6.existsSync(index)) { return index; } } return void 0; } function loadRuntimeContexts(packageInfo, majorVersion, refProperty) { if (majorVersion === 4) { return []; } const entry = resolveRuntimeEntry(packageInfo, majorVersion); if (!entry) { return []; } const moduleExports = require2(entry); if (!moduleExports) { return []; } const ref = moduleExports[refProperty]; if (!ref) { return []; } if (Array.isArray(ref)) { return ref; } if (typeof ref === "object" && Array.isArray(ref.value)) { return ref.value; } return []; } // src/runtime/process-tailwindcss.ts import { createRequire as createRequire2 } from "module"; import path7 from "pathe"; import postcss from "postcss"; import { loadConfig } from "tailwindcss-config"; var require3 = createRequire2(import.meta.url); async function resolveConfigPath(options) { if (options.config && path7.isAbsolute(options.config)) { return options.config; } const result = await loadConfig({ cwd: options.cwd }); if (!result) { throw new Error(`Unable to locate Tailwind CSS config from ${options.cwd}`); } return result.filepath; } async function runTailwindBuild(options) { const configPath = await resolveConfigPath(options); const pluginName = options.postcssPlugin ?? (options.majorVersion === 4 ? "@tailwindcss/postcss" : "tailwindcss"); if (options.majorVersion === 4) { return postcss([ require3(pluginName)({ config: configPath }) ]).process("@import 'tailwindcss';", { from: void 0 }); } return postcss([ require3(pluginName)({ config: configPath }) ]).process("@tailwind base;@tailwind components;@tailwind utilities;", { from: void 0 }); } // src/api/tailwindcss-patcher.ts import process5 from "process"; import fs8 from "fs-extra"; import { getPackageInfoSync } from "local-pkg"; import path9 from "pathe"; import { coerce } from "semver"; // src/options/legacy.ts function normalizeLegacyFeatures(patch) { const apply = patch?.applyPatches; const extend = apply?.extendLengthUnits; let extendOption = false; if (extend && typeof extend === "object") { extendOption = { ...extend, enabled: true }; } else if (extend === true) { extendOption = { enabled: true, units: ["rpx"], overwrite: patch?.overwrite }; } return { exposeContext: apply?.exportContext ?? true, extendLengthUnits: extendOption }; } function fromLegacyOptions(options) { if (!options) { return {}; } const patch = options.patch; const features = normalizeLegacyFeatures(patch); const output = patch?.output; const tailwindConfig = patch?.tailwindcss; const tailwindVersion = tailwindConfig?.version; const tailwindV2 = tailwindConfig?.v2; const tailwindV3 = tailwindConfig?.v3; const tailwindV4 = tailwindConfig?.v4; const tailwindConfigPath = tailwindV3?.config ?? tailwindV2?.config; const tailwindCwd = tailwindV3?.cwd ?? tailwindV2?.cwd ?? patch?.cwd; return { cwd: patch?.cwd, overwrite: patch?.overwrite, filter: patch?.filter, cache: typeof options.cache === "boolean" ? options.cache : options.cache ? { ...options.cache, enabled: options.cache.enabled ?? true } : void 0, output: output ? { file: output.filename, pretty: output.loose ? 2 : false, removeUniversalSelector: output.removeUniversalSelector } : void 0, tailwind: { packageName: patch?.packageName, version: tailwindVersion, resolve: patch?.resolve, config: tailwindConfigPath, cwd: tailwindCwd, v2: tailwindV2,