472 lines
18 KiB
JavaScript
472 lines
18 KiB
JavaScript
import { existsSync, promises } from "node:fs";
|
|
import process from "node:process";
|
|
import { createRecoveryConfigLoader } from "@unocss/config";
|
|
import { BetterMap, createGenerator, cssIdRE, toArray } from "@unocss/core";
|
|
import { createFilter } from "unplugin-utils";
|
|
import remapping from "@jridgewell/remapping";
|
|
import MagicString from "magic-string";
|
|
import { cyan, dim, green, yellow } from "colorette";
|
|
import consola, { consola as consola$1 } from "consola";
|
|
import { basename, dirname, normalize, relative, resolve } from "pathe";
|
|
import { debounce } from "perfect-debounce";
|
|
import { glob } from "tinyglobby";
|
|
const SKIP_START_COMMENT = "@unocss-skip-start";
|
|
const SKIP_END_COMMENT = "@unocss-skip-end";
|
|
const SKIP_COMMENT_RE = new RegExp(`(\/\/\\s*?${SKIP_START_COMMENT}\\s*?|\\/\\*\\s*?${SKIP_START_COMMENT}\\s*?\\*\\/|<!--\\s*?${SKIP_START_COMMENT}\\s*?-->)[\\s\\S]*?(\/\/\\s*?${SKIP_END_COMMENT}\\s*?|\\/\\*\\s*?${SKIP_END_COMMENT}\\s*?\\*\\/|<!--\\s*?${SKIP_END_COMMENT}\\s*?-->)`, "g");
|
|
//#endregion
|
|
//#region ../../virtual-shared/integration/src/defaults.ts
|
|
const defaultPipelineExclude = [cssIdRE];
|
|
const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|vine.ts|mdx?|astro|elm|php|phtml|marko|html)($|\?)/];
|
|
//#endregion
|
|
//#region ../../virtual-shared/integration/src/context.ts
|
|
function createContext(configOrPath, defaults = {}, extraConfigSources = [], resolveConfigResult = () => {}) {
|
|
let root = process.cwd();
|
|
let rawConfig = {};
|
|
let configFileList = [];
|
|
let uno;
|
|
const _uno = createGenerator(rawConfig, defaults).then((r) => {
|
|
uno = r;
|
|
return r;
|
|
});
|
|
let rollupFilter = createFilter(defaultPipelineInclude, defaultPipelineExclude, { resolve: typeof configOrPath === "string" ? configOrPath : root });
|
|
const invalidations = [];
|
|
const reloadListeners = [];
|
|
const modules = new BetterMap();
|
|
const tokens = /* @__PURE__ */ new Set();
|
|
const tasks = [];
|
|
const affectedModules = /* @__PURE__ */ new Set();
|
|
const loadConfig = createRecoveryConfigLoader();
|
|
let ready = reloadConfig();
|
|
async function reloadConfig() {
|
|
var _rawConfig$content, _rawConfig$content2, _rawConfig$content3;
|
|
await _uno;
|
|
const result = await loadConfig(root, configOrPath, extraConfigSources, defaults);
|
|
resolveConfigResult(result);
|
|
result.config;
|
|
rawConfig = result.config;
|
|
configFileList = result.sources;
|
|
await uno.setConfig(rawConfig);
|
|
uno.config.envMode = "dev";
|
|
rollupFilter = ((_rawConfig$content = rawConfig.content) === null || _rawConfig$content === void 0 ? void 0 : _rawConfig$content.pipeline) === false ? () => false : createFilter(((_rawConfig$content2 = rawConfig.content) === null || _rawConfig$content2 === void 0 || (_rawConfig$content2 = _rawConfig$content2.pipeline) === null || _rawConfig$content2 === void 0 ? void 0 : _rawConfig$content2.include) || defaultPipelineInclude, ((_rawConfig$content3 = rawConfig.content) === null || _rawConfig$content3 === void 0 || (_rawConfig$content3 = _rawConfig$content3.pipeline) === null || _rawConfig$content3 === void 0 ? void 0 : _rawConfig$content3.exclude) || defaultPipelineExclude, { resolve: typeof configOrPath === "string" ? configOrPath : root });
|
|
tokens.clear();
|
|
await Promise.all(modules.map((code, id) => uno.applyExtractors(code.replace(SKIP_COMMENT_RE, ""), id, tokens)));
|
|
invalidate();
|
|
dispatchReload();
|
|
return result;
|
|
}
|
|
async function updateRoot(newRoot) {
|
|
if (newRoot !== root) {
|
|
root = newRoot;
|
|
ready = reloadConfig();
|
|
}
|
|
return await ready;
|
|
}
|
|
function invalidate() {
|
|
invalidations.forEach((cb) => cb());
|
|
}
|
|
function dispatchReload() {
|
|
reloadListeners.forEach((cb) => cb());
|
|
}
|
|
async function extract(code, id) {
|
|
const uno = await _uno;
|
|
if (id) modules.set(id, code);
|
|
const len = tokens.size;
|
|
await uno.applyExtractors(code.replace(SKIP_COMMENT_RE, ""), id, tokens);
|
|
if (tokens.size > len) invalidate();
|
|
}
|
|
function filter(code, id) {
|
|
if (code.includes("@unocss-ignore")) return false;
|
|
return code.includes("@unocss-include") || code.includes("@unocss-placeholder") || rollupFilter(id.replace(/\?v=\w+$/, ""));
|
|
}
|
|
async function getConfig() {
|
|
await ready;
|
|
return rawConfig;
|
|
}
|
|
async function flushTasks() {
|
|
const _tasks = [...tasks];
|
|
await Promise.all(_tasks);
|
|
if (tasks[0] === _tasks[0]) tasks.splice(0, _tasks.length);
|
|
}
|
|
/**
|
|
* Get regexes to match virtual module ids
|
|
*/
|
|
const vmpCache = /* @__PURE__ */ new Map();
|
|
async function getVMPRegexes() {
|
|
const prefix = (await getConfig()).virtualModulePrefix || "__uno";
|
|
if (vmpCache.has(prefix)) return vmpCache.get(prefix);
|
|
const regexes = {
|
|
prefix,
|
|
RESOLVED_ID_WITH_QUERY_RE: new RegExp(`[/\\\\]${prefix}(_.*?)?\\.css(\\?.*)?$`),
|
|
RESOLVED_ID_RE: new RegExp(`[/\\\\]${prefix}(?:_(.*?))?\.css$`)
|
|
};
|
|
vmpCache.set(prefix, regexes);
|
|
return regexes;
|
|
}
|
|
return {
|
|
get ready() {
|
|
return ready;
|
|
},
|
|
tokens,
|
|
modules,
|
|
affectedModules,
|
|
tasks,
|
|
flushTasks,
|
|
invalidate,
|
|
onInvalidate(fn) {
|
|
invalidations.push(fn);
|
|
},
|
|
filter,
|
|
reloadConfig,
|
|
onReload(fn) {
|
|
reloadListeners.push(fn);
|
|
},
|
|
get uno() {
|
|
if (!uno) throw new Error("Run `await context.ready` before accessing `context.uno`");
|
|
return uno;
|
|
},
|
|
extract,
|
|
getConfig,
|
|
get root() {
|
|
return root;
|
|
},
|
|
updateRoot,
|
|
getConfigFileList: () => configFileList,
|
|
getVMPRegexes
|
|
};
|
|
}
|
|
//#endregion
|
|
//#region ../../virtual-shared/integration/src/utils.ts
|
|
function hash(str) {
|
|
let i;
|
|
let l;
|
|
let hval = 2166136261;
|
|
for (i = 0, l = str.length; i < l; i++) {
|
|
hval ^= str.charCodeAt(i);
|
|
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
|
|
}
|
|
return `00000${(hval >>> 0).toString(36)}`.slice(-6);
|
|
}
|
|
function transformSkipCode(code, map, SKIP_RULES_RE, keyFlag) {
|
|
for (const item of Array.from(code.matchAll(SKIP_RULES_RE))) if (item != null) {
|
|
const matched = item[0];
|
|
const withHashKey = `${keyFlag}${hash(matched)}`;
|
|
map.set(withHashKey, matched);
|
|
code = code.replace(matched, withHashKey);
|
|
}
|
|
return code;
|
|
}
|
|
function restoreSkipCode(code, map) {
|
|
for (const [withHashKey, matched] of map.entries()) code = code.replaceAll(withHashKey, matched);
|
|
return code;
|
|
}
|
|
//#endregion
|
|
//#region ../../virtual-shared/integration/src/transformers.ts
|
|
async function applyTransformers(ctx, original, id, enforce = "default") {
|
|
if (original.includes("@unocss-ignore")) return;
|
|
const transformers = (ctx.uno.config.transformers || []).filter((i) => (i.enforce || "default") === enforce);
|
|
if (!transformers.length) return;
|
|
const skipMap = /* @__PURE__ */ new Map();
|
|
let code = original;
|
|
let s = new MagicString(transformSkipCode(code, skipMap, SKIP_COMMENT_RE, "@unocss-skip-placeholder-"));
|
|
const maps = [];
|
|
for (const t of transformers) {
|
|
if (t.idFilter) {
|
|
if (!t.idFilter(id)) continue;
|
|
} else if (!ctx.filter(code, id)) continue;
|
|
await t.transform(s, id, ctx);
|
|
if (s.hasChanged()) {
|
|
code = restoreSkipCode(s.toString(), skipMap);
|
|
maps.push(s.generateMap({
|
|
hires: true,
|
|
source: id
|
|
}));
|
|
s = new MagicString(code);
|
|
}
|
|
}
|
|
if (code !== original) return {
|
|
code,
|
|
map: remapping(maps, (_, ctx) => {
|
|
ctx.content = code;
|
|
return null;
|
|
})
|
|
};
|
|
}
|
|
//#endregion
|
|
//#region package.json
|
|
var version = "66.6.7";
|
|
//#endregion
|
|
//#region src/debug.ts
|
|
/**
|
|
* generate and print a debug details table
|
|
*
|
|
* Example:
|
|
*
|
|
* File Generation Details:
|
|
* ---------------------+----------------------
|
|
* | Output File | Source Files (1) |
|
|
* ---------------------+----------------------
|
|
* | src/styles/uno.css | src/styles/mock.css |
|
|
* ---------------------+----------------------
|
|
*/
|
|
function debugDetailsTable(options, outFile, files) {
|
|
const table = [["Output File", `Source Files (${files.length})`]];
|
|
files.forEach((f, i) => {
|
|
table.push([i === 0 ? relative(options.cwd, outFile) : "", relative(options.cwd, f.id)]);
|
|
});
|
|
const colWidths = table[0].map((_, colIndex) => Math.max(...table.map((row) => row[colIndex].length)));
|
|
const separator = colWidths.map((width) => "-".repeat(width + 3)).join("+");
|
|
consola.log(yellow("File Generation Details:"));
|
|
consola.log(separator);
|
|
for (const row of table) {
|
|
const rowStr = row.map((cell, index) => ` ${cell.padEnd(colWidths[index])} `).join("|");
|
|
consola.log(`|${rowStr}|`);
|
|
consola.log(separator);
|
|
}
|
|
}
|
|
//#endregion
|
|
//#region src/errors.ts
|
|
var PrettyError = class extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = this.constructor.name;
|
|
if (typeof Error.captureStackTrace === "function") Error.captureStackTrace(this, this.constructor);
|
|
else this.stack = new Error(message).stack;
|
|
}
|
|
};
|
|
function handleError(error) {
|
|
if (error instanceof PrettyError) consola$1.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|
|
//#endregion
|
|
//#region src/watcher.ts
|
|
let watcher;
|
|
async function getWatcher(options) {
|
|
if (watcher && !options) return watcher;
|
|
if (!options) return { close: () => {} };
|
|
const { watch } = await import("chokidar");
|
|
const ignored = ["**/{.git,node_modules}/**"];
|
|
const cwd = (options === null || options === void 0 ? void 0 : options.cwd) || process.cwd();
|
|
const newWatcher = watch((options === null || options === void 0 ? void 0 : options.patterns).map((p) => {
|
|
const abs = resolve(cwd, p);
|
|
if (abs.endsWith("/**/*")) return abs.slice(0, -5);
|
|
return abs;
|
|
}), {
|
|
ignoreInitial: false,
|
|
ignorePermissionErrors: true,
|
|
ignored,
|
|
usePolling: true,
|
|
interval: 100
|
|
});
|
|
watcher = newWatcher;
|
|
return newWatcher;
|
|
}
|
|
//#endregion
|
|
//#region src/index.ts
|
|
async function resolveOptions(options, userConfig) {
|
|
var _options$patterns, _userConfig$cli;
|
|
const resolvedOptions = {
|
|
...options,
|
|
entries: [],
|
|
debug: options.debug || false
|
|
};
|
|
if ((_options$patterns = options.patterns) === null || _options$patterns === void 0 ? void 0 : _options$patterns.length) resolvedOptions.entries.push({
|
|
patterns: options.patterns,
|
|
outFile: options.outFile ?? resolve(options.cwd, "uno.css"),
|
|
rewrite: (options.rewrite || options.writeTransformed) ?? false,
|
|
splitCss: options.splitCss ?? true
|
|
});
|
|
const configEntries = toArray(((_userConfig$cli = userConfig.cli) === null || _userConfig$cli === void 0 ? void 0 : _userConfig$cli.entry) || []).map((entry) => ({
|
|
...entry,
|
|
rewrite: entry.rewrite !== void 0 ? entry.rewrite : (options.rewrite || options.writeTransformed) ?? false,
|
|
splitCss: entry.splitCss !== void 0 ? entry.splitCss : options.splitCss ?? true
|
|
}));
|
|
resolvedOptions.entries.push(...configEntries);
|
|
if (!resolvedOptions.entries.length) throw new PrettyError(`No glob patterns provided. Try ${cyan("unocss <path/to/**/*>")} or configure entries in ${cyan("uno.config")} file. See ${cyan("https://unocss.dev/integrations/cli#configurations")}`);
|
|
if (resolvedOptions.writeTransformed) consola$1.warn(`--write-transformed is deprecated, please use ${yellow("--rewrite")} instead.`);
|
|
return resolvedOptions;
|
|
}
|
|
async function initializeConfig(options) {
|
|
const { cwd = process.cwd(), config: configPath, preset } = options;
|
|
const ctx = createContext(configPath);
|
|
if (!(await ctx.updateRoot(cwd)).sources.map(normalize).length) {
|
|
const defaultPresets = {
|
|
wind3: import("@unocss/preset-wind3").then((m) => m.default),
|
|
wind4: import("@unocss/preset-wind4").then((m) => m.default)
|
|
};
|
|
if (preset && preset in defaultPresets) await ctx.uno.setConfig({
|
|
presets: [await defaultPresets[preset]],
|
|
transformers: [(await import("@unocss/transformer-directives").then((m) => m.default))()]
|
|
});
|
|
}
|
|
return {
|
|
ctx,
|
|
config: ctx.uno.config
|
|
};
|
|
}
|
|
async function parseEntries(options, cache) {
|
|
const newCache = /* @__PURE__ */ new Map();
|
|
for (const entry of options.entries) {
|
|
const { outFile, rewrite } = entry;
|
|
const files = await glob(entry.patterns, {
|
|
cwd: options.cwd,
|
|
absolute: true,
|
|
expandDirectories: false
|
|
});
|
|
const cssFiles = files.filter((f) => f.endsWith(".css")).filter((f) => f !== resolve(options.cwd, outFile));
|
|
const otherFiles = files.filter((f) => !f.endsWith(".css"));
|
|
const singleKey = outFile.replace(/(\.css)?$/, "-merged.css");
|
|
const addToCache = (file, code, key) => {
|
|
const existing = newCache.get(key) || [];
|
|
existing.push({
|
|
id: file,
|
|
code,
|
|
rewrite
|
|
});
|
|
newCache.set(key, existing);
|
|
};
|
|
for (const file of otherFiles) addToCache(file, await promises.readFile(file, "utf-8"), outFile);
|
|
for (const file of cssFiles) {
|
|
const code = await promises.readFile(file, "utf-8");
|
|
if (entry.splitCss === true) addToCache(file, code, outFile);
|
|
else if (entry.splitCss === "single") addToCache(file, code, singleKey);
|
|
else if (entry.splitCss === "multi") {
|
|
const fileHash = hash(file);
|
|
const currentOutFile = file.replace(/(\.css)?$/, `-${fileHash}.css`);
|
|
addToCache(file, code, files.length > 1 ? currentOutFile : outFile);
|
|
}
|
|
}
|
|
}
|
|
cache.clear();
|
|
for (const [key, value] of newCache) cache.set(key, value);
|
|
}
|
|
async function build(_options) {
|
|
_options.cwd ?? (_options.cwd = process.cwd());
|
|
/**
|
|
* Cache of files to process
|
|
*
|
|
* key: output file path
|
|
*
|
|
* value: array of source files with their code
|
|
*/
|
|
const fileCache = /* @__PURE__ */ new Map();
|
|
const configResult = await initializeConfig(_options);
|
|
const options = await resolveOptions(_options, configResult.config);
|
|
options.ctx = configResult.ctx;
|
|
if (options.stdout && options.outFile) {
|
|
consola$1.fatal(`Cannot use --stdout and --out-file at the same time`);
|
|
return;
|
|
}
|
|
consola$1.log(green(`UnoCSS v${version}`));
|
|
if (options.watch) consola$1.start(`UnoCSS in watch mode...`);
|
|
else consola$1.start(`UnoCSS for production...`);
|
|
await parseEntries(options, fileCache);
|
|
if (fileCache.size === 0) {
|
|
consola$1.warn("No files matched the provided patterns.");
|
|
return;
|
|
}
|
|
if (!options.watch) return await generate(options);
|
|
const debouncedBuild = debounce(async () => {
|
|
generate(options).catch(handleError);
|
|
}, 100);
|
|
await startWatcher().catch(handleError);
|
|
async function generate(options) {
|
|
return Promise.all(Array.from(fileCache.entries()).map(([outFile, entries]) => generateSingle(options, outFile, entries)));
|
|
}
|
|
async function startWatcher() {
|
|
if (!options.watch) return;
|
|
const { ctx } = options;
|
|
const watcher = await getWatcher(options);
|
|
const watchedFiles = [...fileCache.values().flatMap((files) => files.map((f) => f.id)), ...ctx.getConfigFileList()];
|
|
watcher.add(watchedFiles);
|
|
watcher.on("all", async (type, file) => {
|
|
const absolutePath = resolve(options.cwd, file);
|
|
if (type === "addDir" || type === "unlinkDir") return;
|
|
if (ctx.getConfigFileList().map(normalize).includes(absolutePath)) {
|
|
await ctx.reloadConfig();
|
|
const newOtions = await resolveOptions(_options, ctx.uno.config);
|
|
Object.assign(options, newOtions);
|
|
await parseEntries(options, fileCache);
|
|
const configSources = ctx.getConfigFileList().map(normalize);
|
|
if (configSources.length) watcher.add(configSources);
|
|
if (type === "change") consola$1.info(`${cyan(basename(file))} changed, setting new config`);
|
|
consola$1.info(`Watching for changes in ${[...options.entries.flatMap((i) => i.patterns), ...configSources].map(cyan).join(", ")}`);
|
|
} else if (type === "change") {
|
|
consola$1.log(`${green(type)} ${dim(file)}`);
|
|
const content = await promises.readFile(absolutePath, "utf8");
|
|
if (fileCache.keys().find((outfile) => outfile === absolutePath)) return;
|
|
for (const [, files] of fileCache.entries()) {
|
|
const fileEntry = files.find((f) => f.id === absolutePath);
|
|
if (fileEntry) fileEntry.code = content;
|
|
}
|
|
} else if (type === "unlink") {
|
|
consola$1.log(`${green(type)} ${dim(file)}`);
|
|
for (const [, files] of fileCache.entries()) {
|
|
const index = files.findIndex((f) => f.id === absolutePath);
|
|
if (index !== -1) files.splice(index, 1);
|
|
}
|
|
} else if (type === "add") {
|
|
consola$1.log(`${green(type)} ${dim(file)}`);
|
|
await parseEntries(options, fileCache);
|
|
}
|
|
debouncedBuild();
|
|
});
|
|
}
|
|
}
|
|
async function transformFiles(ctx, sources) {
|
|
const run = (sources, enforce) => Promise.all(sources.map((source) => new Promise((resolve) => {
|
|
applyTransformers(ctx, source.transformedCode ?? source.code, source.id, enforce).then((transformsRes) => {
|
|
resolve({
|
|
...source,
|
|
transformedCode: (transformsRes === null || transformsRes === void 0 ? void 0 : transformsRes.code) ?? source.transformedCode
|
|
});
|
|
});
|
|
})));
|
|
return await run(await run(await run(sources, "pre"), "default"), "post");
|
|
}
|
|
async function generateSingle(options, outFile, files) {
|
|
const start = performance.now();
|
|
const { ctx } = options;
|
|
const transformedFiles = await transformFiles(ctx, files);
|
|
const tokens = /* @__PURE__ */ new Set();
|
|
const rewriter = [];
|
|
const css = [];
|
|
let matchedLen = 0;
|
|
for (const file of transformedFiles) {
|
|
const input = (file.transformedCode || file.code).replace(SKIP_COMMENT_RE, "");
|
|
if (!input) continue;
|
|
if (file.id.endsWith(".css")) css.push(process.env.CI || process.env.VITEST ? input : `/* Source: ${file.id} */\n${input}`.trim());
|
|
else (await ctx.uno.generate(input, {
|
|
preflights: false,
|
|
minify: true,
|
|
id: file.id
|
|
})).matched.forEach((i) => tokens.add(i));
|
|
if (file.rewrite && file.transformedCode) rewriter.push(promises.writeFile(file.id, file.transformedCode, "utf-8"));
|
|
}
|
|
await Promise.all(rewriter);
|
|
if (tokens.size > 0) {
|
|
const result = await ctx.uno.generate(tokens, {
|
|
preflights: options.preflights,
|
|
minify: options.minify
|
|
});
|
|
css.push(result.css);
|
|
matchedLen = result.matched.size;
|
|
}
|
|
const finalCss = css.join("\n");
|
|
if (options.stdout) {
|
|
process.stdout.write(finalCss);
|
|
return;
|
|
}
|
|
if (options.debug) debugDetailsTable(options, outFile, files);
|
|
const outFileResolved = resolve(options.cwd, outFile);
|
|
const dir = dirname(outFileResolved);
|
|
if (!existsSync(dir)) await promises.mkdir(dir, { recursive: true });
|
|
await promises.writeFile(outFileResolved, finalCss, "utf-8");
|
|
if (!options.watch) {
|
|
const duration = (performance.now() - start).toFixed(2);
|
|
if (rewriter.length > 0) consola$1.success(`${rewriter.length} file${rewriter.length > 1 ? "s" : ""} rewritten in ${green(duration)}ms`);
|
|
if (matchedLen > 0) consola$1.success(`${matchedLen} utilities generated to ${cyan(relative(process.cwd(), outFileResolved))} in ${green(duration)}ms\n`);
|
|
}
|
|
}
|
|
//#endregion
|
|
export { handleError as n, version as r, build as t };
|