@unocss/svelte-scoped
Version:
Use UnoCSS in a modular fashion with styles being stored only in the Svelte component they are used in: Vite plugin for apps, Svelte preprocessor for component libraries
574 lines (551 loc) • 21.8 kB
JavaScript
import process from 'node:process';
import { createRecoveryConfigLoader } from '@unocss/config';
import { expandVariantGroup, warnOnce, regexScopePlaceholder, toArray, createGenerator } from '@unocss/core';
import presetUno from '@unocss/preset-uno';
import MagicString from 'magic-string';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { generate, parse, clone, walk as walk$1 } from 'css-tree';
const notInCommentRE = /(?<!<!--\s*)/;
const stylesTagWithCapturedDirectivesRE = /<style([^>]*)>[\s\S]*?<\/style\s*>/;
const actualStylesTagWithCapturedDirectivesRE = new RegExp(notInCommentRE.source + stylesTagWithCapturedDirectivesRE.source, "g");
const captureOpeningStyleTagWithAttributesRE = /(<style[^>]*>)/;
function addGeneratedStylesIntoStyleBlock(code, styles) {
const preExistingStylesTag = code.match(actualStylesTagWithCapturedDirectivesRE);
if (preExistingStylesTag)
return code.replace(captureOpeningStyleTagWithAttributesRE, `$1${styles}`);
return `${code}
<style>${styles}</style>`;
}
const classesRE$1 = /class=(["'`])([\s\S]*?)\1/g;
const classDirectivesRE = /class:(\S+?)="?\{/g;
const classDirectivesShorthandRE = /class:([^=>\s/]+)[{>\s/]/g;
const classClsxRE = /(?<prefix>class=\{\w*)[[{(]/g;
function findClasses(code) {
const matchedClasses = [...code.matchAll(classesRE$1)];
const matchedClassDirectives = [...code.matchAll(classDirectivesRE)];
const matchedClassDirectivesShorthand = [...code.matchAll(classDirectivesShorthandRE)];
const matchedClassClsx = [...code.matchAll(classClsxRE)];
const classes = parseMatches(matchedClasses, "regular", 'class="'.length);
const classDirectives = parseMatches(matchedClassDirectives, "directive", "class:".length);
const classDirectivesShorthand = parseMatches(matchedClassDirectivesShorthand, "directiveShorthand", "class:".length);
const classClsx = parseMatchesWithAcorn(matchedClassClsx, code);
return [...classes, ...classDirectives, ...classDirectivesShorthand, ...classClsx];
}
function parseMatches(matches, type, prefixLength) {
return matches.map((match) => {
const body = match[type === "regular" ? 2 : 1];
const start = match.index + prefixLength;
return {
body: body.trim(),
start,
end: start + body.length,
type
};
}).filter(hasBody);
}
function hasBody(foundClass) {
return foundClass.body;
}
function parseMatchesWithAcorn(matches, code) {
return matches.flatMap((match) => {
const start = match.index + match.groups.prefix.length;
const ast = acorn.parseExpressionAt(code, start, {
sourceType: "module",
ecmaVersion: 16,
locations: true
});
const classes = [];
function fromProperty(body, node, property) {
return {
body,
start: node.start,
end: node.end,
type: property.shorthand ? "clsxObjectShorthand" : "clsxObject"
};
}
function fromString(body, node) {
return {
body,
start: node.start + 1,
end: node.end - 1,
type: "regular"
};
}
walk(
ast,
{ property: void 0 },
{
Property(node, { visit }) {
visit(node.key, { property: node });
},
Identifier(node, { state, next }) {
if (state.property) {
classes.push(fromProperty(node.name, node, state.property));
}
next();
},
Literal(node, { state, next }) {
if (typeof node.value === "string") {
const body = node.value;
if (state.property) {
classes.push(fromProperty(body, node, state.property));
} else {
classes.push(fromString(body, node));
}
}
next();
},
TemplateLiteral(node, { state, next }) {
if (node.expressions.length === 0 && node.quasis.length === 1) {
const body = node.quasis[0].value.raw;
if (state.property) {
classes.push(fromProperty(body, node, state.property));
} else {
classes.push(fromString(body, node));
}
}
next();
}
}
);
return classes;
}).filter(hasBody);
}
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 generateClassName(body, options, filename) {
const {
classPrefix = "uno-",
combine = true,
hashFn = hash
} = options;
if (combine) {
const classPlusFilenameHash = hashFn(body + filename);
return `${classPrefix}${classPlusFilenameHash}`;
} else {
const filenameHash = hashFn(filename);
return `_${body}_${filenameHash}`;
}
}
function isShortcut(token, shortcuts) {
return shortcuts.some((s) => s[0] === token);
}
async function needsGenerated(token, uno) {
const inSafelist = uno.config.safelist.includes(token);
if (inSafelist)
return false;
const result = await uno.parseToken(token);
return !!result;
}
async function sortClassesIntoCategories(body, options, uno, filename) {
const { combine = true } = options;
const rulesToGenerate = {};
const ignore = [];
const classes = body.trim().split(/\s+/);
const knownClassesToCombine = [];
for (const token of classes) {
const isShortcutOrUtility = isShortcut(token, uno.config.shortcuts) || await needsGenerated(token, uno);
if (!isShortcutOrUtility) {
ignore.push(token);
continue;
}
if (combine) {
knownClassesToCombine.push(token);
} else {
const generatedClassName = generateClassName(token, options, filename);
rulesToGenerate[generatedClassName] = [token];
}
}
if (knownClassesToCombine.length) {
const generatedClassName = generateClassName(knownClassesToCombine.join(" "), options, filename);
rulesToGenerate[generatedClassName] = knownClassesToCombine;
}
return { rulesToGenerate, ignore };
}
const expressionsRE = /\S*\{[^{}]+\}\S*/g;
const classesRE = /(?<=\?\s*|:\s*)(["'`])([\s\S]*?)\1/g;
async function processExpressions(body, options, uno, filename) {
const rulesToGenerate = {};
const updatedExpressions = [];
let restOfBody = body;
const expressions = [...body.matchAll(expressionsRE)];
for (let [expression] of expressions) {
restOfBody = restOfBody.replace(expression, "").trim();
const classes = [...expression.matchAll(classesRE)];
for (const [withQuotes, quoteMark, withoutQuotes] of classes) {
const { rulesToGenerate: rulesFromExpression, ignore } = await sortClassesIntoCategories(withoutQuotes, options, uno, filename);
Object.assign(rulesToGenerate, rulesFromExpression);
const updatedClasses = Object.keys(rulesFromExpression).concat(ignore).join(" ");
expression = expression.replace(
withQuotes,
quoteMark + updatedClasses + quoteMark
);
}
updatedExpressions.push(expression);
}
return { rulesToGenerate, updatedExpressions, restOfBody };
}
async function processClassBody({ body, start, end }, options, uno, filename) {
const expandedBody = expandVariantGroup(body);
const { rulesToGenerate: rulesFromExpressions, restOfBody, updatedExpressions } = await processExpressions(expandedBody, options, uno, filename);
const { rulesToGenerate: rulesFromRegularClasses, ignore } = await sortClassesIntoCategories(restOfBody, options, uno, filename);
const rulesToGenerate = { ...rulesFromExpressions, ...rulesFromRegularClasses };
if (!Object.keys(rulesToGenerate).length)
return {};
const content = Object.keys(rulesFromRegularClasses).concat(ignore).concat(updatedExpressions).join(" ");
const codeUpdate = {
content,
start,
end
};
return { rulesToGenerate, codeUpdate };
}
async function processDirective({ body: token, start, end, type }, options, uno, filename) {
const isShortcutOrUtility = isShortcut(token, uno.config.shortcuts) || await needsGenerated(token, uno);
if (!isShortcutOrUtility)
return;
const generatedClassName = generateClassName(token, options, filename);
const content = type === "directiveShorthand" ? `${generatedClassName}={${token}}` : generatedClassName;
return {
rulesToGenerate: { [generatedClassName]: [token] },
codeUpdate: { content, start, end }
};
}
async function processClsx(cls, options, uno, filename) {
if (cls.type === "clsxObject") {
const { rulesToGenerate, codeUpdate } = await processClassBody({ ...cls}, options, uno, filename);
if (rulesToGenerate && codeUpdate) {
codeUpdate.content = `"${codeUpdate.content}"`;
return { rulesToGenerate, codeUpdate };
}
} else if (cls.type === "clsxObjectShorthand") {
const { rulesToGenerate, codeUpdate } = await processDirective({ ...cls, type: "directive" }, options, uno, filename) ?? {};
if (rulesToGenerate && codeUpdate) {
codeUpdate.content = `"${codeUpdate.content}": ${cls.body}`;
return { rulesToGenerate, codeUpdate };
}
}
}
async function processClasses(classes, options, uno, filename) {
const result = {
rulesToGenerate: {},
codeUpdates: []
};
for (const foundClass of classes) {
const { rulesToGenerate, codeUpdate } = await processClass(foundClass, options, uno, filename);
if (rulesToGenerate)
Object.assign(result.rulesToGenerate, rulesToGenerate);
if (codeUpdate)
result.codeUpdates.push(codeUpdate);
}
return result;
}
async function processClass(foundClass, options, uno, filename) {
if (foundClass.type === "regular") {
return await processClassBody(foundClass, options, uno, filename);
}
if (foundClass.type === "clsxObject" || foundClass.type === "clsxObjectShorthand") {
return await processClsx(foundClass, options, uno, filename) ?? {};
}
return await processDirective(foundClass, options, uno, filename) ?? {};
}
const NOT_PRECEDED_BY_DIGIT_OR_OPEN_PARENTHESIS_RE = /(?<![\d(])/;
const SELECTOR_STARTING_WITH_BRACKET_OR_PERIOD_RE = /([[.][\s\S]+?)/;
const STYLES_RE = /(\{[\s\S]+?\})/;
const EXTRACT_SELECTOR_RE = new RegExp(NOT_PRECEDED_BY_DIGIT_OR_OPEN_PARENTHESIS_RE.source + SELECTOR_STARTING_WITH_BRACKET_OR_PERIOD_RE.source + STYLES_RE.source, "g");
function wrapSelectorsWithGlobal(css) {
return css.replace(EXTRACT_SELECTOR_RE, ":global($1)$2");
}
async function transformClasses({ content, filename, uno, options }) {
const classesToProcess = findClasses(content);
if (!classesToProcess.length)
return;
const { rulesToGenerate, codeUpdates } = await processClasses(classesToProcess, options, uno, filename);
if (!Object.keys(rulesToGenerate).length)
return;
const { map, code } = updateTemplateCodeIfNeeded(codeUpdates, content, filename);
const generatedStyles = await generateStyles(rulesToGenerate, uno);
const codeWithGeneratedStyles = addGeneratedStylesIntoStyleBlock(code, generatedStyles);
return {
code: codeWithGeneratedStyles,
map
};
}
function updateTemplateCodeIfNeeded(codeUpdates, source, filename) {
if (!codeUpdates.length)
return { code: source, map: void 0 };
const s = new MagicString(source);
for (const { start, end, content } of codeUpdates)
s.overwrite(start, end, content);
return {
code: s.toString(),
map: s.generateMap({ hires: true, source: filename })
};
}
const REMOVE_COMMENTS_TO_MAKE_GLOBAL_WRAPPING_EASY = true;
async function generateStyles(rulesToGenerate, uno) {
const shortcutsForThisComponent = Object.entries(rulesToGenerate);
uno.config.shortcuts.push(...shortcutsForThisComponent);
const selectorsToGenerate = Object.keys(rulesToGenerate);
const { css } = await uno.generate(selectorsToGenerate, {
preflights: false,
safelist: false,
minify: REMOVE_COMMENTS_TO_MAKE_GLOBAL_WRAPPING_EASY
});
const cssPreparedForSvelteCompiler = wrapSelectorsWithGlobal(css);
return cssPreparedForSvelteCompiler;
}
async function getUtils(body, uno) {
const classNames = expandVariantGroup(body).split(/\s+/g).map((className) => className.trim().replace(/\\/, ""));
const utils = await parseUtils(classNames, uno);
const sortedByRankIndex = utils.sort(([aIndex], [bIndex]) => aIndex - bIndex);
const sortedByParentOrders = sortedByRankIndex.sort(([, , , aParent], [, , , bParent]) => (aParent ? uno.parentOrders.get(aParent) ?? 0 : 0) - (bParent ? uno.parentOrders.get(bParent) ?? 0 : 0));
return sortedByParentOrders.reduce((acc, item) => {
const [, selector, body2, parent] = item;
const sibling = acc.find(([, targetSelector, , targetParent]) => targetSelector === selector && targetParent === parent);
if (sibling)
sibling[2] += body2;
else
acc.push([...item]);
return acc;
}, []);
}
async function parseUtils(classNames, uno) {
const foundUtils = [];
for (const token of classNames) {
const util = await uno.parseToken(token, "-");
if (util)
foundUtils.push(util);
else
warnOnce(`'${token}' not found. You have a typo or need to add a preset.`);
}
return foundUtils.flat();
}
function removeOuterQuotes(input) {
if (!input)
return "";
return /^(['"]).*\1$/.test(input) ? input.slice(1, -1) : input;
}
function writeUtilStyles([, selector, body, parent], s, node, childNode) {
if (!selector)
return;
const selectorChanged = selector !== ".\\-";
if (!parent && !selectorChanged)
return s.appendRight(childNode.loc.end.offset, body);
const originalSelector = generate(node.prelude);
if (parent && !selectorChanged) {
const css2 = `${parent}{${originalSelector}{${body}}}`;
return s.appendLeft(node.loc.end.offset, css2);
}
const utilSelector = selector.replace(regexScopePlaceholder, " ");
const updatedSelector = generateUpdatedSelector(utilSelector, node.prelude);
const svelteCompilerReadySelector = surroundAllButOriginalSelectorWithGlobal(originalSelector, updatedSelector);
const rule = `${svelteCompilerReadySelector}{${body}}`;
const css = parent ? `${parent}{${rule}}` : rule;
s.appendLeft(node.loc.end.offset, css);
}
function generateUpdatedSelector(selector, _prelude) {
const selectorAST = parse(selector, {
context: "selector"
});
const prelude = clone(_prelude);
prelude.children.forEach((child) => {
const parentSelectorAst = clone(selectorAST);
parentSelectorAst.children.forEach((i) => {
if (i.type === "ClassSelector" && i.name === "\\-")
Object.assign(i, clone(child));
});
Object.assign(child, parentSelectorAst);
});
return generate(prelude);
}
function surroundAllButOriginalSelectorWithGlobal(originalSelector, updatedSelector) {
const wrapWithGlobal = (str) => `:global(${str})`;
const originalSelectors = originalSelector.split(",").map((s) => s.trim());
const updatedSelectors = updatedSelector.split(",").map((s) => s.trim());
const resultSelectors = originalSelectors.map((original, index) => {
const updated = updatedSelectors[index];
const [prefix, suffix] = updated.split(original).map((s) => s.trim());
const wrappedPrefix = prefix ? wrapWithGlobal(prefix) : "";
if (!suffix)
return `${wrappedPrefix} ${original}`.trim();
const indexOfFirstCombinator = findFirstCombinatorIndex(suffix);
if (indexOfFirstCombinator === -1)
return `${wrappedPrefix} ${original}${suffix}`.trim();
const pseudo = suffix.substring(0, indexOfFirstCombinator).trim();
const siblingsOrDescendants = suffix.substring(indexOfFirstCombinator).trim();
return `${wrappedPrefix} ${original}${pseudo} ${wrapWithGlobal(siblingsOrDescendants)}`.trim();
});
return resultSelectors.join(", ");
}
function findFirstCombinatorIndex(input) {
const combinators = [" ", ">", "~", "+"];
for (const c of combinators) {
const indexOfFirstCombinator = input.indexOf(c);
if (indexOfFirstCombinator !== -1)
return indexOfFirstCombinator;
}
return -1;
}
async function transformApply(ctx) {
const ast = parse(ctx.s.original, {
parseAtrulePrelude: false,
positions: true
});
if (ast.type !== "StyleSheet")
return ctx.s;
const stack = [];
walk$1(ast, (node) => {
if (node.type === "Rule")
stack.push(handleApply(ctx, node));
});
await Promise.all(stack);
return ctx.s;
}
async function handleApply(ctx, node) {
const parsePromises = node.block.children.map(async (childNode) => {
await parseApply(ctx, node, childNode);
});
await Promise.all(parsePromises);
}
async function parseApply({ s, uno, applyVariables }, node, childNode) {
const body = getChildNodeValue(childNode, applyVariables);
if (!body)
return;
const utils = await getUtils(body, uno);
if (!utils.length)
return;
for (const util of utils)
writeUtilStyles(util, s, node, childNode);
s.remove(childNode.loc.start.offset, childNode.loc.end.offset);
}
function getChildNodeValue(childNode, applyVariables) {
if (childNode.type === "Atrule" && childNode.name === "apply" && childNode.prelude && childNode.prelude.type === "Raw")
return childNode.prelude.value.trim();
if (childNode.type === "Declaration" && applyVariables.includes(childNode.property) && childNode.value.type === "Raw")
return removeOuterQuotes(childNode.value.value.trim());
}
const themeRE = /theme\((.+?)\)/g;
function transformTheme(s, theme) {
return s.replace(themeRE, (_, match) => {
const argumentsWithoutQuotes = match.slice(1, -1);
return getThemeValue(argumentsWithoutQuotes, theme);
});
}
function getThemeValue(rawArguments, theme) {
const keys = rawArguments.split(".");
let current = theme;
for (const key of keys) {
if (current[key] === void 0)
throw new Error(`"${rawArguments}" is not found in your theme`);
else
current = current[key];
}
return current;
}
const DEFAULT_APPLY_VARIABLES = ["--at-apply"];
function checkForApply(content, _applyVariables) {
if (_applyVariables === false)
return { hasApply: false, applyVariables: [] };
const applyVariables = toArray(_applyVariables || DEFAULT_APPLY_VARIABLES);
return {
hasApply: content.includes("@apply") || applyVariables.some((v) => content.includes(v)),
applyVariables
};
}
async function transformStyle({
content,
uno,
prepend,
filename,
applyVariables,
transformThemeFn
}) {
const s = new MagicString(content);
if (applyVariables?.length)
await transformApply({ s, uno, applyVariables });
if (transformThemeFn)
transformTheme(s, uno.config.theme);
if (!s.hasChanged())
return;
if (prepend)
s.prepend(prepend);
return {
code: s.toString(),
map: s.generateMap({ hires: true, source: filename || "" })
};
}
function UnocssSveltePreprocess(options = {}, unoContextFromVite, isViteBuild) {
if (!options.classPrefix)
options.classPrefix = "usp-";
let uno;
const loadConfig = createRecoveryConfigLoader();
return {
markup: async ({ content, filename }) => {
if (!uno)
uno = await getGenerator((await loadConfig(process.cwd(), options.configOrPath)).config, unoContextFromVite);
if (isViteBuild && options.combine === void 0)
options.combine = isViteBuild();
return await transformClasses({ content, filename: filename || "", uno, options });
},
style: async ({ content, attributes, filename }) => {
const svelte3AddPreflights = attributes["uno:preflights"];
const svelte3AddSafelist = attributes["uno:safelist"];
const svelte4DeprecatedAddPreflights = attributes.uno && attributes.preflights;
const svelte4DeprecatedAddSafelist = attributes.uno && attributes.safelist;
let addPreflights = attributes["uno-preflights"] || svelte3AddPreflights || svelte4DeprecatedAddPreflights;
let addSafelist = attributes["uno-safelist"] || svelte3AddSafelist || svelte4DeprecatedAddSafelist;
if (unoContextFromVite && (addPreflights || addSafelist)) {
addPreflights = false;
addSafelist = false;
warnOnce("Notice for those transitioning to @unocss/svelte-scoped/vite: uno-preflights and uno-safelist are only for use in component libraries. Please see the documentation for how to add preflights and safelist into your head tag. If you are consuming a component library built by @unocss/svelte-scoped/preprocess, you can ignore this upgrade notice.");
}
const { hasApply, applyVariables } = checkForApply(content, options.applyVariables);
const hasThemeFn = options.transformThemeDirective === false ? false : !!content.match(themeRE);
const changeNeeded = addPreflights || addSafelist || hasApply || hasThemeFn;
if (!changeNeeded)
return;
if (!uno)
uno = await getGenerator((await loadConfig()).config);
let preflightsSafelistCss = "";
if (addPreflights || addSafelist) {
const { css } = await uno.generate([], { preflights: !!addPreflights, safelist: !!addSafelist, minify: true });
preflightsSafelistCss = wrapSelectorsWithGlobal(css);
}
if (hasApply || hasThemeFn) {
return await transformStyle({
content,
uno,
filename,
prepend: preflightsSafelistCss,
applyVariables,
transformThemeFn: hasThemeFn
});
}
if (preflightsSafelistCss)
return { code: preflightsSafelistCss + content };
}
};
}
async function getGenerator(config, unoContextFromVite) {
if (unoContextFromVite) {
await unoContextFromVite.ready;
return unoContextFromVite.uno;
}
const defaults = {
presets: [
presetUno()
]
};
return createGenerator(config, defaults);
}
export { UnocssSveltePreprocess as U };