weapp-tailwindcss
Version:
把 tailwindcss 原子化样式思想,带给小程序开发者们! bring tailwindcss to miniprogram developers!
697 lines (685 loc) • 22.2 kB
JavaScript
import {
WEAPP_TW_REQUIRED_NODE_VERSION,
clearTailwindcssPatcherCache,
createPatchTargetRecorder,
logTailwindcssTarget
} from "./chunk-4Z6MHSEO.mjs";
import {
findWorkspaceRoot,
logger
} from "./chunk-SZOXLSNK.mjs";
import "./chunk-TFOTUR4L.mjs";
import "./chunk-RR5HCKVQ.mjs";
import "./chunk-SM5V25IN.mjs";
// src/cli.ts
import process5 from "process";
import semver from "semver";
import { createTailwindcssPatchCli } from "tailwindcss-patch";
// src/cli/context.ts
import path from "path";
import process from "process";
function formatOutputPath(target, baseDir) {
const root = baseDir ?? process.cwd();
const relative = path.relative(root, target);
if (!relative) {
return ".";
}
if (relative.startsWith("..")) {
return path.normalize(target);
}
return relative.startsWith(".") ? relative : `.${path.sep}${relative}`;
}
// src/cli/helpers.ts
import { mkdir } from "fs/promises";
import path2 from "path";
import process2 from "process";
// src/tailwindcss/index.ts
import { getPackageInfoSync } from "local-pkg";
function getTailwindcssPackageInfo(options) {
return getPackageInfoSync("tailwindcss", options);
}
// src/cli/helpers.ts
function readStringOption(flag, value) {
if (value == null) {
return void 0;
}
if (typeof value !== "string") {
throw new TypeError(`Option "--${flag}" expects a string value.`);
}
const trimmed = value.trim();
if (trimmed.length === 0) {
throw new TypeError(`Option "--${flag}" expects a non-empty value.`);
}
return trimmed;
}
function readStringArrayOption(flag, value) {
if (value == null) {
return void 0;
}
if (Array.isArray(value)) {
const normalized2 = value.filter((entry) => entry != null).map((entry) => {
if (typeof entry !== "string") {
throw new TypeError(`Option "--${flag}" expects string values.`);
}
const trimmed = entry.trim();
if (!trimmed) {
throw new TypeError(`Option "--${flag}" expects non-empty values.`);
}
return trimmed;
});
return normalized2.length > 0 ? normalized2 : void 0;
}
const normalized = readStringOption(flag, value);
return normalized ? [normalized] : void 0;
}
function toBoolean(value, fallback) {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
}
if (value == null) {
return fallback;
}
return Boolean(value);
}
function resolveCliCwd(value) {
const raw = readStringOption("cwd", value);
if (!raw) {
return void 0;
}
return path2.isAbsolute(raw) ? path2.normalize(raw) : path2.resolve(process2.cwd(), raw);
}
function normalizeCandidatePath(baseDir, candidate) {
if (!candidate) {
return void 0;
}
return path2.isAbsolute(candidate) ? path2.normalize(candidate) : path2.resolve(baseDir, candidate);
}
function detectTailwindWorkspace(paths) {
for (const candidate of paths) {
try {
const info = getTailwindcssPackageInfo({ paths: [candidate] });
if (info?.rootPath) {
return candidate;
}
} catch {
}
}
return void 0;
}
function resolvePatchDefaultCwd(currentCwd = process2.cwd()) {
const baseDir = path2.normalize(currentCwd);
const explicitCwd = normalizeCandidatePath(baseDir, process2.env.WEAPP_TW_PATCH_CWD);
if (explicitCwd) {
return explicitCwd;
}
const workspaceRoot = findWorkspaceRoot(baseDir);
const initCwd = normalizeCandidatePath(baseDir, process2.env.INIT_CWD);
const localPrefix = normalizeCandidatePath(baseDir, process2.env.npm_config_local_prefix);
const candidates = [
baseDir,
workspaceRoot,
initCwd,
localPrefix
].filter(Boolean);
const detected = detectTailwindWorkspace([...new Set(candidates)]);
if (detected) {
return detected;
}
return initCwd ?? localPrefix ?? workspaceRoot ?? baseDir;
}
async function ensureDir(dir) {
await mkdir(dir, { recursive: true });
}
function handleCliError(error) {
if (error instanceof Error) {
logger.error(error.message);
if (error.stack && process2.env.WEAPP_TW_DEBUG === "1") {
logger.error(error.stack);
}
} else {
logger.error(String(error));
}
}
function commandAction(handler) {
return async (...args) => {
try {
await handler(...args);
} catch (error) {
handleCliError(error);
process2.exitCode = 1;
}
};
}
// src/cli/mount-options.ts
import process4 from "process";
// src/cli/patch-options.ts
var DEFAULT_EXTEND_LENGTH_UNITS_FEATURE = {
enabled: true,
units: ["rpx"],
overwrite: true
};
function withDefaultExtendLengthUnits(options) {
const normalized = options ?? {};
const extendLengthUnits = normalized.features?.extendLengthUnits;
if (extendLengthUnits == null) {
return {
...normalized,
features: {
...normalized.features ?? {},
extendLengthUnits: DEFAULT_EXTEND_LENGTH_UNITS_FEATURE
}
};
}
return normalized;
}
function buildExtendLengthUnitsOverride(options) {
const extendLengthUnits = options?.features?.extendLengthUnits;
if (extendLengthUnits == null) {
return {
features: {
...options?.features ?? {},
extendLengthUnits: DEFAULT_EXTEND_LENGTH_UNITS_FEATURE
}
};
}
return void 0;
}
// src/cli/workspace.ts
import { existsSync, readFileSync } from "fs";
import path3 from "path";
import process3 from "process";
import fg from "fast-glob";
import { normalizeOptions, TailwindcssPatcher } from "tailwindcss-patch";
import { parse as parseYaml } from "yaml";
function tryReadJson(file) {
try {
const content = readFileSync(file, "utf8");
return JSON.parse(content);
} catch {
return void 0;
}
}
function parseWorkspaceGlobsFromPackageJson(workspaceRoot) {
const pkgJsonPath = path3.join(workspaceRoot, "package.json");
const pkg = tryReadJson(pkgJsonPath);
if (!pkg?.workspaces) {
return [];
}
if (Array.isArray(pkg.workspaces)) {
return pkg.workspaces.filter(Boolean);
}
if (Array.isArray(pkg.workspaces.packages)) {
return pkg.workspaces.packages.filter(Boolean);
}
return [];
}
function parseWorkspaceGlobsFromWorkspaceFile(workspaceRoot) {
const workspaceFile = path3.join(workspaceRoot, "pnpm-workspace.yaml");
if (!existsSync(workspaceFile)) {
return [];
}
try {
const parsed = parseYaml(readFileSync(workspaceFile, "utf8"));
return Array.isArray(parsed?.packages) ? parsed.packages.filter(Boolean) : [];
} catch {
return [];
}
}
function parseImportersFromLock(workspaceRoot) {
const lockPath = path3.join(workspaceRoot, "pnpm-lock.yaml");
if (!existsSync(lockPath)) {
return [];
}
try {
const parsed = parseYaml(readFileSync(lockPath, "utf8"));
const importers = parsed?.importers;
if (!importers) {
return [];
}
return Object.keys(importers).map((key) => {
if (!key || key === ".") {
return workspaceRoot;
}
return path3.join(workspaceRoot, key);
});
} catch {
return [];
}
}
async function resolveWorkspacePackageDirs(workspaceRoot) {
const dirs = /* @__PURE__ */ new Set();
for (const importerDir of parseImportersFromLock(workspaceRoot)) {
dirs.add(path3.normalize(importerDir));
}
if (!dirs.size) {
let globs = parseWorkspaceGlobsFromWorkspaceFile(workspaceRoot);
if (!globs.length) {
globs = parseWorkspaceGlobsFromPackageJson(workspaceRoot);
}
if (globs.length > 0) {
const patterns = globs.map((pattern) => {
const normalized = pattern.replace(/\\/g, "/").replace(/\/+$/, "");
return normalized.endsWith("package.json") ? normalized : `${normalized}/package.json`;
});
const packageJsonFiles = await fg(patterns, {
cwd: workspaceRoot,
absolute: true,
onlyFiles: true,
unique: true,
ignore: ["**/node_modules/**", "**/.git/**"]
});
for (const file of packageJsonFiles) {
dirs.add(path3.normalize(path3.dirname(file)));
}
}
}
const rootPkg = path3.join(workspaceRoot, "package.json");
if (existsSync(rootPkg)) {
dirs.add(path3.normalize(workspaceRoot));
}
return [...dirs];
}
function createWorkspacePatcher(cwd) {
const normalized = normalizeOptions(
withDefaultExtendLengthUnits({
cwd
})
);
return new TailwindcssPatcher(normalized);
}
function formatDisplayName(workspaceRoot, dir, name) {
const relative = path3.relative(workspaceRoot, dir) || ".";
return name ? `${name} (${relative})` : relative;
}
async function patchWorkspace(options) {
const cwd = options.cwd ?? process3.cwd();
const workspaceRoot = findWorkspaceRoot(cwd) ?? cwd;
const packageDirs = await resolveWorkspacePackageDirs(workspaceRoot);
if (packageDirs.length === 0) {
logger.warn("\u672A\u5728 %s \u68C0\u6D4B\u5230 workspace \u5305\uFF0C\u5DF2\u8DF3\u8FC7\u6279\u91CF patch\u3002", workspaceRoot);
return;
}
const results = [];
for (const dir of packageDirs) {
const pkgJsonPath = path3.join(dir, "package.json");
const pkgJson = tryReadJson(pkgJsonPath);
const displayName = formatDisplayName(workspaceRoot, dir, pkgJson?.name);
const tailwindInfo = getTailwindcssPackageInfo({ paths: [dir] });
if (!tailwindInfo?.rootPath) {
results.push({
dir,
name: pkgJson?.name,
status: "skipped",
message: "tailwindcss \u672A\u5B89\u88C5\uFF0C\u5DF2\u8DF3\u8FC7\u3002"
});
logger.info("[workspace] \u8DF3\u8FC7 %s\uFF08tailwindcss \u672A\u5B89\u88C5\uFF09\u3002", displayName);
continue;
}
try {
const patcher = createWorkspacePatcher(dir);
if (options.clearCache) {
await clearTailwindcssPatcherCache(patcher, { removeDirectory: true });
}
const recorder = createPatchTargetRecorder(dir, patcher, {
source: "cli",
cwd: dir,
recordTarget: options.recordTarget !== false,
alwaysRecord: true
});
if (recorder?.message) {
logger.info("[workspace] %s %s", displayName, recorder.message);
}
logTailwindcssTarget("cli", patcher, dir);
await patcher.patch();
if (recorder?.onPatched) {
await recorder.onPatched();
}
results.push({
dir,
name: pkgJson?.name,
status: "patched",
message: "\u5DF2\u5B8C\u6210 patch\u3002"
});
logger.success("[workspace] \u5DF2\u8865\u4E01 %s", displayName);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
const suggestion = `\u8BF7\u5728 ${dir} \u8FD0\u884C "weapp-tw patch --cwd ${dir}".`;
const message = `${reason}\uFF0C${suggestion}`;
results.push({
dir,
name: pkgJson?.name,
status: "failed",
message
});
logger.error("[workspace] \u8865\u4E01\u5931\u8D25 %s\uFF1A%s", displayName, message);
}
}
const patched = results.filter((result) => result.status === "patched").length;
const skipped = results.filter((result) => result.status === "skipped").length;
const failed = results.filter((result) => result.status === "failed").length;
logger.info("[workspace] \u6C47\u603B\uFF1A\u5DF2\u8865\u4E01 %d\uFF0C\u8DF3\u8FC7 %d\uFF0C\u5931\u8D25 %d", patched, skipped, failed);
}
// src/cli/mount-options.ts
function handleCliError2(error) {
if (error instanceof Error) {
logger.error(error.message);
if (error.stack && process4.env.WEAPP_TW_DEBUG === "1") {
logger.error(error.stack);
}
} else {
logger.error(String(error));
}
}
function withCommandErrorHandling(handler) {
return (async (ctx, next) => {
try {
return await handler(ctx, next);
} catch (error) {
handleCliError2(error);
process4.exitCode = 1;
return void 0;
}
});
}
async function createPatcherWithDefaultExtendLengthUnits(ctx) {
const patchOptions = await ctx.loadPatchOptions();
const extendLengthUnitsOverride = buildExtendLengthUnitsOverride(patchOptions);
if (extendLengthUnitsOverride) {
return ctx.createPatcher(extendLengthUnitsOverride);
}
return ctx.createPatcher();
}
function formatStatusFilesHint(files) {
if (!files?.length) {
return "";
}
return ` (${files.join(", ")})`;
}
function logPatchStatusReport(report) {
const applied = report.entries.filter((entry) => entry.status === "applied");
const pending = report.entries.filter((entry) => entry.status === "not-applied");
const skipped = report.entries.filter(
(entry) => entry.status === "skipped" || entry.status === "unsupported"
);
const packageLabel = `${report.package.name ?? "tailwindcss"}@${report.package.version ?? "unknown"}`;
logger.info(`Patch status for ${packageLabel} (v${report.majorVersion})`);
if (applied.length) {
logger.success("Applied:");
applied.forEach((entry) => {
logger.success(` - ${entry.name}${formatStatusFilesHint(entry.files)}`);
});
}
if (pending.length) {
logger.warn("Needs attention:");
pending.forEach((entry) => {
const details = entry.reason ? ` - ${entry.reason}` : "";
logger.warn(` - ${entry.name}${formatStatusFilesHint(entry.files)}${details}`);
});
} else {
logger.success("All applicable patches are applied.");
}
if (skipped.length) {
logger.info("Skipped:");
skipped.forEach((entry) => {
const details = entry.reason ? ` - ${entry.reason}` : "";
logger.info(` - ${entry.name}${details}`);
});
}
}
var mountOptions = {
commandOptions: {
install: {
name: "patch",
aliases: ["install"],
appendDefaultOptions: false,
optionDefs: [
{
flags: "--cwd <dir>",
description: "Working directory",
config: { default: resolvePatchDefaultCwd() }
},
{
flags: "--record-target",
description: 'Write tailwindcss target metadata (node_modules/.cache/weapp-tailwindcss/tailwindcss-target.json). Pass "--record-target false" to skip.',
config: { default: true }
},
{
flags: "--clear-cache",
description: "Clear tailwindcss-patch cache before patch (opt-in)"
},
{
flags: "--workspace",
description: "Scan pnpm workspace packages and patch each Tailwind CSS dependency"
}
]
},
status: {
appendDefaultOptions: false,
optionDefs: [
{
flags: "--cwd <dir>",
description: "Working directory",
config: { default: resolvePatchDefaultCwd() }
},
{
flags: "--json",
description: "Print a JSON report of patch status"
}
]
}
},
commandHandlers: {
install: withCommandErrorHandling(async (ctx) => {
const shouldClearCache = toBoolean(ctx.args.clearCache, false);
const shouldRecordTarget = toBoolean(ctx.args.recordTarget, true);
const runWorkspace = toBoolean(ctx.args.workspace, false);
if (runWorkspace) {
await patchWorkspace({
cwd: ctx.cwd,
clearCache: shouldClearCache,
recordTarget: shouldRecordTarget
});
return;
}
const patcher = await createPatcherWithDefaultExtendLengthUnits(ctx);
if (shouldClearCache) {
await clearTailwindcssPatcherCache(patcher, { removeDirectory: true });
}
const recorder = createPatchTargetRecorder(ctx.cwd, patcher, {
source: "cli",
cwd: ctx.cwd,
recordTarget: shouldRecordTarget,
alwaysRecord: true
});
if (recorder?.message) {
logger.info(recorder.message);
}
logTailwindcssTarget("cli", patcher, ctx.cwd);
await patcher.patch();
if (recorder?.onPatched) {
const recordPath = await recorder.onPatched();
if (recordPath) {
logger.info(`\u8BB0\u5F55 weapp-tw patch \u76EE\u6807 -> ${formatOutputPath(recordPath, ctx.cwd)}`);
}
}
logger.success("Tailwind CSS \u8FD0\u884C\u65F6\u8865\u4E01\u5DF2\u5B8C\u6210\u3002");
}),
extract: withCommandErrorHandling(async (_ctx, next) => next()),
tokens: withCommandErrorHandling(async (_ctx, next) => next()),
init: withCommandErrorHandling(async (_ctx, next) => next()),
status: withCommandErrorHandling(async (ctx) => {
const patcher = await createPatcherWithDefaultExtendLengthUnits(ctx);
const report = await patcher.getPatchStatus();
if (ctx.args.json) {
logger.log(JSON.stringify(report, null, 2));
return report;
}
logPatchStatusReport(report);
return report;
})
}
};
// src/cli/vscode-entry.ts
import { constants } from "fs";
import { access, writeFile } from "fs/promises";
import path4 from "path";
var DEFAULT_VSCODE_ENTRY_OUTPUT = ".vscode/weapp-tailwindcss.intellisense.css";
var DEFAULT_VSCODE_SOURCES = [
'not "./dist"',
'not "./unpackage"',
"./src/**/*.{wxml,axml,swan,qml,ttml,ux,uts}",
"./src/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{vue,svelte,html,md,mdx}"
];
var SINGLE_QUOTE = "'";
var DOUBLE_QUOTE = '"';
function toPosixPath(filepath) {
return filepath.replace(/\\/g, "/");
}
async function assertFileExists(filepath) {
try {
await access(filepath, constants.F_OK);
} catch (error) {
const err = error;
if (err?.code === "ENOENT") {
throw new Error(`CSS entry file not found: ${filepath}`);
}
throw err;
}
}
async function assertCanWrite(filepath, force) {
try {
await access(filepath, constants.F_OK);
if (!force) {
throw new Error(
`VS Code helper already exists at ${filepath}. Re-run with --force to overwrite it.`
);
}
} catch (error) {
const err = error;
if (err?.code === "ENOENT") {
return;
}
throw err;
}
}
function toCssLiteral(value) {
const normalized = toPosixPath(value);
return JSON.stringify(normalized);
}
function formatSource(pattern) {
const trimmed = pattern.trim();
if (!trimmed) {
return null;
}
if (trimmed.startsWith("@source ")) {
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
}
let body = trimmed;
let keyword = "";
if (body.startsWith("not ")) {
keyword = "not ";
body = body.slice(4).trim();
} else if (body.startsWith("!")) {
keyword = "not ";
body = body.slice(1).trim();
}
if (!body) {
throw new Error("Invalid @source pattern: empty body.");
}
if (!body.startsWith(SINGLE_QUOTE) && !body.startsWith(DOUBLE_QUOTE)) {
body = toCssLiteral(body);
}
return `@source ${keyword}${body};`;
}
function resolveOutputPath(baseDir, output) {
const target = output ?? DEFAULT_VSCODE_ENTRY_OUTPUT;
return path4.isAbsolute(target) ? path4.normalize(target) : path4.resolve(baseDir, target);
}
function resolveCssEntry(baseDir, entry) {
return path4.isAbsolute(entry) ? path4.normalize(entry) : path4.resolve(baseDir, entry);
}
function toRelativeImport(fromFile, targetFile) {
const fromDir = path4.dirname(fromFile);
let relative = path4.relative(fromDir, targetFile);
if (!relative) {
relative = path4.basename(targetFile);
}
if (!relative.startsWith(".")) {
relative = `./${relative}`;
}
return toPosixPath(relative);
}
async function generateVscodeIntellisenseEntry(options) {
const baseDir = options.baseDir;
const cssEntryPath = resolveCssEntry(baseDir, options.cssEntry);
await assertFileExists(cssEntryPath);
const outputPath = resolveOutputPath(baseDir, options.output);
await ensureDir(path4.dirname(outputPath));
await assertCanWrite(outputPath, options.force);
const sources = options.sources && options.sources.length > 0 ? options.sources : DEFAULT_VSCODE_SOURCES;
const formattedSources = sources.map(formatSource).filter((statement) => Boolean(statement));
const cssImport = toRelativeImport(outputPath, cssEntryPath);
const separator = formattedSources.length > 0 ? [""] : [];
const content = [
"/*",
" * Auto-generated by weapp-tailwindcss.",
" * This file exists solely to activate Tailwind CSS IntelliSense in VS Code.",
" * Do not import it in your actual mini-program bundles.",
" */",
"@import 'tailwindcss';",
"",
...formattedSources,
...separator,
`@import '${cssImport}';`,
""
].filter((line, idx, arr) => !(line === "" && arr[idx - 1] === "")).join("\n");
await writeFile(outputPath, `${content}
`, "utf8");
return { outputPath, cssEntryPath };
}
// src/cli.ts
process5.title = "node (weapp-tailwindcss)";
if (semver.lt(process5.versions.node, WEAPP_TW_REQUIRED_NODE_VERSION)) {
logger.warn(
`You are using Node.js ${process5.versions.node}. For weapp-tailwindcss, Node.js version >= v${WEAPP_TW_REQUIRED_NODE_VERSION} is required.`
);
}
var cli = createTailwindcssPatchCli({
name: "weapp-tailwindcss",
mountOptions
});
cli.command("vscode-entry", "Generate a VS Code helper CSS for Tailwind IntelliSense").option("--cwd <dir>", "Working directory").option("--css <file>", "Path to the CSS file that imports weapp-tailwindcss (required)").option("--output <file>", `Helper output path. Defaults to ${DEFAULT_VSCODE_ENTRY_OUTPUT}`).option("--source <pattern>", "Additional @source glob (can be repeated)").option("--force", "Overwrite the helper file when it already exists").action(
commandAction(async (options) => {
const resolvedCwd = resolveCliCwd(options.cwd);
const baseDir = resolvedCwd ?? process5.cwd();
const cssEntry = readStringOption("css", options.css);
if (!cssEntry) {
throw new Error('Option "--css" is required.');
}
const output = readStringOption("output", options.output);
const sources = readStringArrayOption("source", options.source);
const force = toBoolean(options.force, false);
const result = await generateVscodeIntellisenseEntry({
baseDir,
cssEntry,
output,
sources,
force
});
logger.success(
`VS Code helper generated -> ${formatOutputPath(result.outputPath, resolvedCwd)}`
);
})
);
cli.help();
cli.version(process5.env.npm_package_version ?? "0.0.0");
cli.parse();