feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-PRESENT hannoeru
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+229
View File
@@ -0,0 +1,229 @@
# @unocss/transformer-directives
<!-- @unocss-ignore -->
UnoCSS transformer for `@apply`、`@screen` and `theme()` directive
## Install
```bash
npm i -D @unocss/transformer-directives
```
```ts
import transformerDirectives from '@unocss/transformer-directives'
// uno.config.ts
import { defineConfig } from 'unocss'
export default defineConfig({
// ...
transformers: [
transformerDirectives(),
],
})
```
## Usage
### `@apply`
```css
.custom-div {
@apply text-center my-0 font-medium;
}
```
Will be transformed to:
```css
.custom-div {
margin-top: 0rem;
margin-bottom: 0rem;
text-align: center;
font-weight: 500;
}
```
#### CSS Variable Style
To be compatible with vanilla CSS, you can use CSS custom properties to replace the `@apply` directive.
```css
.custom-div {
--at-apply: text-center my-0 font-medium;
}
```
To use rules with `:`, you will need to quote the value
```css
.custom-div {
--at-apply: "hover:text-red";
}
```
This feature is enabled by default with a few alias, you can configure or disable it via:
```js
transformerDirectives({
// the defaults
applyVariable: ['--at-apply', '--uno-apply', '--uno'],
// or disable with:
// applyVariable: false
})
```
### `@screen`
The `@screen` directive allows you to create media queries that reference your breakpoints by name comes from [`theme.breakpoints`](https://github.com/unocss/unocss/blob/main/README.md#extend-theme).
```css
.grid {
--uno: grid grid-cols-2;
}
@screen xs {
.grid {
--uno: grid-cols-1;
}
}
@screen sm {
.grid {
--uno: grid-cols-3;
}
}
/* ... */
...
```
Will be transformed to:
```css
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 320px) {
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (min-width: 640px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* ... */
```
#### Breakpoint Variant Support
`@screen` also supports `lt`、`at` variants
##### `@screen lt`
```css
.grid {
--uno: grid grid-cols-2;
}
@screen lt-xs {
.grid {
--uno: grid-cols-1;
}
}
@screen lt-sm {
.grid {
--uno: grid-cols-3;
}
}
/* ... */
```
Will be transformed to:
```css
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 319.9px) {
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (max-width: 639.9px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* ... */
```
##### `@screen at`
```css
.grid {
--uno: grid grid-cols-2;
}
@screen at-xs {
.grid {
--uno: grid-cols-1;
}
}
@screen at-xl {
.grid {
--uno: grid-cols-3;
}
}
@screen at-xxl {
.grid {
--uno: grid-cols-4;
}
}
/* ... */
```
Will be transformed to:
```css
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 320px) and (max-width: 639.9px) {
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (min-width: 1280px) and (max-width: 1535.9px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1536px) {
.grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
/* ... */
```
### `theme()`
Use the `theme()` function to access your theme config values using dot notation.
```css
.btn-blue {
background-color: theme('colors.blue.500');
}
```
Will be compiled to:
```css
.btn-blue {
background-color: #3b82f6;
}
```
## License
MIT License &copy; 2022-PRESENT [hannoeru](https://github.com/hannoeru)
@@ -0,0 +1,41 @@
import { SourceCodeTransformer, UnoGenerator } from '@unocss/core';
import MagicString from 'magic-string';
interface TransformerDirectivesOptions {
enforce?: SourceCodeTransformer['enforce'];
/**
* Throw an error if utils or themes are not found.
*
* @default true
*/
throwOnMissing?: boolean;
/**
* Treat CSS custom properties as @apply directives for CSS syntax compatibility.
*
* Pass `false` to disable.
*
* @default ['--at-apply', '--uno-apply', '--uno']
*/
applyVariable?: false | string | string[];
/**
* Treat CSS custom properties as directives for CSS syntax compatibility.
*
* Pass `false` to disable, or a string to use as a prefix.
*
* @deprecated use `applyVariable` to specify the full var name instead.
* @default '--at-'
*/
varStyle?: false | string;
}
interface TransformerDirectivesContext {
code: MagicString;
uno: UnoGenerator;
options: TransformerDirectivesOptions;
applyVariable: string[];
offset?: number;
filename?: string;
}
declare function transformerDirectives(options?: TransformerDirectivesOptions): SourceCodeTransformer;
export { type TransformerDirectivesContext, type TransformerDirectivesOptions, transformerDirectives as default };
@@ -0,0 +1,41 @@
import { SourceCodeTransformer, UnoGenerator } from '@unocss/core';
import MagicString from 'magic-string';
interface TransformerDirectivesOptions {
enforce?: SourceCodeTransformer['enforce'];
/**
* Throw an error if utils or themes are not found.
*
* @default true
*/
throwOnMissing?: boolean;
/**
* Treat CSS custom properties as @apply directives for CSS syntax compatibility.
*
* Pass `false` to disable.
*
* @default ['--at-apply', '--uno-apply', '--uno']
*/
applyVariable?: false | string | string[];
/**
* Treat CSS custom properties as directives for CSS syntax compatibility.
*
* Pass `false` to disable, or a string to use as a prefix.
*
* @deprecated use `applyVariable` to specify the full var name instead.
* @default '--at-'
*/
varStyle?: false | string;
}
interface TransformerDirectivesContext {
code: MagicString;
uno: UnoGenerator;
options: TransformerDirectivesOptions;
applyVariable: string[];
offset?: number;
filename?: string;
}
declare function transformerDirectives(options?: TransformerDirectivesOptions): SourceCodeTransformer;
export { type TransformerDirectivesContext, type TransformerDirectivesOptions, transformerDirectives as default };
@@ -0,0 +1,288 @@
import { expandVariantGroup, notNull, regexScopePlaceholder, toArray, cssIdRE } from '@unocss/core';
import { transformThemeFn, transformThemeString, hasThemeFn, hasIconFn } from '@unocss/rule-utils';
import { generate, parse, clone, List, walk } from 'css-tree';
async function handleApply(ctx, node) {
const { code, uno, options, filename } = ctx;
await Promise.all(
node.block.children.map(async (childNode) => {
if (childNode.type === "Raw")
return transformDirectives(code, uno, options, filename, childNode.value, childNode.loc.start.offset);
await parseApply(ctx, node, childNode);
}).toArray()
);
}
async function parseApply({ code, uno, applyVariable }, node, childNode) {
const original = code.original;
let body;
if (childNode.type === "Atrule" && childNode.name === "apply" && childNode.prelude && childNode.prelude.type === "Raw") {
body = removeQuotes(childNode.prelude.value.trim());
} else if (childNode.type === "Declaration" && applyVariable.includes(childNode.property) && (childNode.value.type === "Value" || childNode.value.type === "Raw")) {
let rawValue = original.slice(
childNode.value.loc.start.offset,
childNode.value.loc.end.offset
).trim();
rawValue = removeQuotes(rawValue);
const items = rawValue.split(/\s+/g).filter(Boolean).map((i) => removeQuotes(i));
body = items.join(" ");
}
if (!body)
return;
body = removeComments(body);
const classNames = expandVariantGroup(body).split(/\s+/g).map((className) => className.trim().replace(/\\/, ""));
const utils = (await Promise.all(
classNames.map((i) => uno.parseToken(i, "-"))
)).filter(notNull).flat().sort((a, b) => a[0] - b[0]).sort((a, b) => (a[3] ? uno.parentOrders.get(a[3]) ?? 0 : 0) - (b[3] ? uno.parentOrders.get(b[3]) ?? 0 : 0)).reduce((acc, item) => {
const target = acc.find((i) => i[1] === item[1] && i[3] === item[3]);
if (target)
target[2] += item[2];
else
acc.push([...item]);
return acc;
}, []);
if (!utils.length)
return;
let semicolonOffset = original[childNode.loc.end.offset] === ";" ? 1 : original[childNode.loc.end.offset] === "@" ? -1 : 0;
for (const i of utils) {
const [, _selector, body2, parent] = i;
const selectorOrGroup = _selector?.replace(regexScopePlaceholder, " ") || _selector;
if (parent || selectorOrGroup && selectorOrGroup !== ".\\-") {
let newSelector = generate(node.prelude);
const className = code.slice(node.prelude.loc.start.offset, node.prelude.loc.end.offset);
if (selectorOrGroup && selectorOrGroup !== ".\\-") {
const ruleAST = parse(`${selectorOrGroup}{}`, {
context: "rule"
});
const prelude = clone(node.prelude);
prelude.children?.forEach((child) => {
const selectorListAst = clone(ruleAST.prelude);
const classSelectors = new List();
selectorListAst.children.forEach((selectorAst) => {
classSelectors.appendList(selectorAst.children.filter((i2) => i2.type === "ClassSelector" && i2.name === "\\-"));
});
classSelectors.forEach((i2) => Object.assign(i2, clone(child)));
Object.assign(child, selectorListAst);
});
newSelector = generate(prelude);
}
let css = `${newSelector.replace(/.\\-/g, className)}{${body2}}`;
if (parent)
css = `${parent}{${css}}`;
semicolonOffset = 0;
code.appendLeft(node.loc.end.offset, css);
} else {
if (body2.includes("@"))
code.appendRight(original.length, body2);
else
code.appendRight(childNode.loc.end.offset + semicolonOffset, body2);
}
}
code.remove(
childNode.loc.start.offset,
childNode.loc.end.offset + semicolonOffset
);
}
function removeQuotes(value) {
return value.replace(/^(['"])(.*)\1$/, "$2");
}
function removeComments(value) {
return value.replace(/(\/\*(?:.|\n)*?\*\/)|(\/\/.*)/g, "");
}
async function transformIconString(uno, icon, color) {
const presetIcons = uno.config.presets?.flat()?.find((i) => i.name === "@unocss/preset-icons");
if (!presetIcons) {
console.warn("@unocss/preset-icons not found, icon() directive will be keep as-is");
return;
}
const {
scale = 1,
prefix = "i-",
collections: customCollections,
customizations = {},
autoInstall = false,
iconifyCollectionsNames,
collectionsNodeResolvePath,
unit
} = presetIcons.options;
const api = presetIcons.api;
const loaderOptions = {
addXmlNs: true,
scale,
customCollections,
autoInstall,
cwd: collectionsNodeResolvePath,
// avoid warn from @iconify/loader: we'll warn below if not found
warn: void 0,
customizations: {
...customizations,
trimCustomSvg: true,
async iconCustomizer(collection, icon2, props) {
await customizations.iconCustomizer?.(collection, icon2, props);
if (unit) {
if (!props.width)
props.width = `${scale}${unit}`;
if (!props.height)
props.height = `${scale}${unit}`;
}
}
}
};
const loader = await api.createNodeLoader?.() || (async () => void 0);
for (const p of toArray(prefix)) {
if (icon.startsWith(p)) {
icon = icon.slice(p.length);
const parsed = await api.parseIconWithLoader(
icon,
loader,
loaderOptions,
iconifyCollectionsNames
);
if (parsed)
return `url("data:image/svg+xml;utf8,${color ? api.encodeSvgForCss(parsed.svg).replace(/currentcolor/gi, color) : api.encodeSvgForCss(parsed.svg)}")`;
}
}
}
async function handleFunction({ code, uno, options }, node) {
const { throwOnMissing = true } = options;
switch (node.name) {
case "theme": {
if (node.children.size !== 1)
throw new Error("theme() expect exact one argument");
if (node.children.first.type !== "String")
throw new Error("theme() expect a string argument");
const themeStr = node.children.first.value;
const value = transformThemeString(themeStr, uno.config.theme, throwOnMissing);
if (value)
code.overwrite(node.loc.start.offset, node.loc.end.offset, value);
break;
}
case "icon": {
const params = node.children.toArray().filter((node2) => node2.type === "String").map((node2) => node2.value);
if (params.length === 0)
throw new Error("icon() expects at least one argument");
let [icon, color] = params;
if (color) {
color = encodeURIComponent(transformThemeFn(color, uno.config.theme, throwOnMissing));
}
const value = await transformIconString(uno, icon, color);
if (value)
code.overwrite(node.loc.start.offset, node.loc.end.offset, value);
break;
}
}
}
const screenRuleRE = /(@screen [^{]+)(.+)/g;
function handleScreen({ code, uno }, node) {
let breakpointName = "";
let prefix = "";
if (node.prelude?.type === "Raw")
breakpointName = node.prelude.value.trim();
if (!breakpointName)
return;
const match = breakpointName.match(/^(?:(lt|at)-)?(\w+)$/);
if (match) {
prefix = match[1];
breakpointName = match[2];
}
const resolveBreakpoints = () => {
let breakpoints;
if (uno.userConfig && uno.userConfig.theme)
breakpoints = uno.userConfig.theme.breakpoints;
if (!breakpoints)
breakpoints = uno.config.theme.breakpoints;
return breakpoints ? Object.entries(breakpoints).sort((a, b) => Number.parseInt(a[1].replace(/[a-z]+/gi, "")) - Number.parseInt(b[1].replace(/[a-z]+/gi, ""))).map(([point, size]) => ({ point, size })) : void 0;
};
const variantEntries = (resolveBreakpoints() ?? []).map(({ point, size }, idx) => [point, size, idx]);
const generateMediaQuery = (breakpointName2, prefix2) => {
const [, size, idx] = variantEntries.find((i) => i[0] === breakpointName2);
if (prefix2) {
if (prefix2 === "lt")
return `@media (max-width: ${calcMaxWidthBySize(size)})`;
else if (prefix2 === "at")
return `@media (min-width: ${size})${variantEntries[idx + 1] ? ` and (max-width: ${calcMaxWidthBySize(variantEntries[idx + 1][1])})` : ""}`;
else throw new Error(`breakpoint variant not supported: ${prefix2}`);
}
return `@media (min-width: ${size})`;
};
if (!variantEntries.find((i) => i[0] === breakpointName))
throw new Error(`breakpoint ${breakpointName} not found`);
const offset = node.loc.start.offset;
const str = code.original.slice(offset, node.loc.end.offset);
const matches = Array.from(str.matchAll(screenRuleRE));
if (!matches.length)
return;
for (const match2 of matches) {
code.overwrite(
offset + match2.index,
offset + match2.index + match2[1].length,
`${generateMediaQuery(breakpointName, prefix)}`
);
}
}
function calcMaxWidthBySize(size) {
const value = size.match(/^-?\d+\.?\d*/)?.[0] || "";
const unit = size.slice(value.length);
const maxWidth = Number.parseFloat(value) - 0.1;
return Number.isNaN(maxWidth) ? size : `${maxWidth}${unit}`;
}
async function transformDirectives(code, uno, options, filename, originalCode, offset) {
let { applyVariable } = options;
const varStyle = options.varStyle;
if (applyVariable === void 0) {
if (varStyle !== void 0)
applyVariable = varStyle ? [`${varStyle}apply`] : [];
applyVariable = ["--at-apply", "--uno-apply", "--uno"];
}
applyVariable = toArray(applyVariable || []);
const parseCode = originalCode || code.original;
const hasApply = parseCode.includes("@apply") || applyVariable.some((s) => parseCode.includes(s));
const hasScreen = parseCode.includes("@screen");
const hasFn = hasThemeFn(parseCode) || hasIconFn(parseCode);
if (!hasApply && !hasFn && !hasScreen)
return;
const ast = parse(parseCode, {
parseCustomProperty: true,
parseAtrulePrelude: false,
positions: true,
filename,
offset
});
if (ast.type !== "StyleSheet")
return;
const stack = [];
const ctx = {
options,
applyVariable,
uno,
code,
filename,
offset
};
const processNode = async (node, _item, _list) => {
if (hasScreen && node.type === "Atrule" && node.name === "screen")
handleScreen(ctx, node);
else if (node.type === "Function")
await handleFunction(ctx, node);
else if (hasApply && node.type === "Rule")
await handleApply(ctx, node);
};
walk(ast, (...args) => stack.push(processNode(...args)));
await Promise.all(stack);
}
function transformerDirectives(options = {}) {
return {
name: "@unocss/transformer-directives",
enforce: options?.enforce,
idFilter: (id) => cssIdRE.test(id),
transform: (code, id, ctx) => {
return transformDirectives(code, ctx.uno, options, id);
}
};
}
export { transformerDirectives as default };
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@unocss/transformer-directives",
"type": "module",
"version": "0.65.4",
"description": "UnoCSS transformer for `@apply` directive",
"author": "hannoeru <me@hanlee.co>",
"license": "MIT",
"homepage": "https://github.com/unocss/unocss/tree/main/packages/transformer-directives#readme",
"repository": {
"type": "git",
"url": "https://github.com/unocss/unocss",
"directory": "packages/transformer-directives"
},
"bugs": {
"url": "https://github.com/unocss/unocss/issues"
},
"keywords": [
"unocss",
"unocss-transformer"
],
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"dependencies": {
"css-tree": "^3.1.0",
"@unocss/core": "0.65.4",
"@unocss/rule-utils": "0.65.4"
},
"devDependencies": {
"magic-string": "^0.30.17"
},
"scripts": {
"build": "unbuild",
"stub": "unbuild --stub",
"test:attw": "attw --pack --config-path ../../.attw-esm-only.json"
}
}