UNPKG

weapp-tailwindcss

Version:

把 tailwindcss 原子化样式思想,带给小程序开发者们! bring tailwindcss to miniprogram developers!

697 lines (685 loc) 22.2 kB
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();