tailwindcss-patch
Version:
patch tailwindcss for exposing context and extract classes
1,548 lines (1,529 loc) • 79.6 kB
JavaScript
// 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,