lightningcss-tailwindcss 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/README.zh.md +101 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +310 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ice breaker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# lightningcss-tailwindcss
|
|
2
|
+
|
|
3
|
+
Lightning CSS AST utilities for analyzing Tailwind CSS syntax.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides a Tailwind-oriented analysis layer on top of **Lightning CSS AST**.
|
|
8
|
+
|
|
9
|
+
It extracts:
|
|
10
|
+
|
|
11
|
+
- `@apply` utility candidates
|
|
12
|
+
- `theme(...)` calls
|
|
13
|
+
- Tailwind import directives
|
|
14
|
+
- utility selectors declared in authored stylesheets
|
|
15
|
+
- basic Tailwind version hints from parsed syntax
|
|
16
|
+
- parser warnings emitted while Lightning CSS recovers invalid or custom syntax
|
|
17
|
+
|
|
18
|
+
This package is useful when your tooling already depends on Lightning CSS AST and you want to inspect Tailwind-related syntax without switching to PostCSS.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add -D lightningcss-tailwindcss
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## When To Use This Package
|
|
27
|
+
|
|
28
|
+
Use `lightningcss-tailwindcss` when:
|
|
29
|
+
|
|
30
|
+
- your pipeline already uses `lightningcss`
|
|
31
|
+
- you want a fast Rust-backed parser with JS visitor access
|
|
32
|
+
- you are comfortable with recovered/normalized AST output
|
|
33
|
+
|
|
34
|
+
Use `postcss-tailwindcss` instead when:
|
|
35
|
+
|
|
36
|
+
- you need more permissive authored-source parsing
|
|
37
|
+
- you want direct PostCSS node references
|
|
38
|
+
- you need project-aware Tailwind runtime/config resolution
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
- `parseTailwindCss(input)`
|
|
43
|
+
- `detectTailwindVersion(input)`
|
|
44
|
+
- `collectApplyCandidates(input)`
|
|
45
|
+
- `collectThemeCalls(input)`
|
|
46
|
+
- `collectImportDirectives(input)`
|
|
47
|
+
- `collectUtilitySelectors(input)`
|
|
48
|
+
- `analyzeTailwindCss(input)`
|
|
49
|
+
|
|
50
|
+
## Example
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { analyzeTailwindCss } from 'lightningcss-tailwindcss'
|
|
54
|
+
|
|
55
|
+
const css = `
|
|
56
|
+
@import "tailwindcss" source(none) prefix(tw);
|
|
57
|
+
|
|
58
|
+
.button {
|
|
59
|
+
@apply flex hover:bg-red-500;
|
|
60
|
+
color: theme(colors.gray.900 / 75%);
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
const result = analyzeTailwindCss(css)
|
|
65
|
+
|
|
66
|
+
console.log(result.version)
|
|
67
|
+
console.log(result.applyCandidates.map(item => item.candidate))
|
|
68
|
+
console.log(result.warnings)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Return Shape
|
|
72
|
+
|
|
73
|
+
`analyzeTailwindCss()` returns:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
interface TailwindAnalysis {
|
|
77
|
+
version: 3 | 4 | 'unknown'
|
|
78
|
+
applyCandidates: UtilityCandidate[]
|
|
79
|
+
themeCalls: ThemeCall[]
|
|
80
|
+
importDirectives: TailwindImportDirective[]
|
|
81
|
+
utilitySelectors: UtilitySelector[]
|
|
82
|
+
warnings: LightningWarning[]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Unlike the PostCSS version, this package returns Lightning CSS AST fragments and `loc` data rather than PostCSS nodes.
|
|
87
|
+
|
|
88
|
+
## Parsing Behavior
|
|
89
|
+
|
|
90
|
+
This package relies on Lightning CSS `transform()` with `errorRecovery: true`.
|
|
91
|
+
|
|
92
|
+
That means:
|
|
93
|
+
|
|
94
|
+
- unsupported Tailwind syntax may still be preserved in recovered form
|
|
95
|
+
- parser warnings are expected for some Tailwind-specific constructs
|
|
96
|
+
- v4 `@import "tailwindcss" source(...) prefix(...)` is analyzable even if Lightning CSS treats parts of it as recovered tokens
|
|
97
|
+
|
|
98
|
+
## Important Differences From `postcss-tailwindcss`
|
|
99
|
+
|
|
100
|
+
- No Tailwind runtime resolution helpers
|
|
101
|
+
- No PostCSS node references
|
|
102
|
+
- More normalized AST output
|
|
103
|
+
- Better fit for tools that already consume Lightning CSS visitors
|
|
104
|
+
|
|
105
|
+
If you need to inspect the consuming project's installed Tailwind runtime or config file, use `postcss-tailwindcss`.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# lightningcss-tailwindcss
|
|
2
|
+
|
|
3
|
+
用于分析 Tailwind CSS 语法的 Lightning CSS AST 工具集。
|
|
4
|
+
|
|
5
|
+
## 概览
|
|
6
|
+
|
|
7
|
+
这个包是在 **Lightning CSS AST** 之上做的一层 Tailwind 语法分析能力。
|
|
8
|
+
|
|
9
|
+
它可以提取:
|
|
10
|
+
|
|
11
|
+
- `@apply` 中的 utility candidate
|
|
12
|
+
- `theme(...)` 调用
|
|
13
|
+
- Tailwind import 指令
|
|
14
|
+
- 手写样式中声明的 utility selector
|
|
15
|
+
- 基于语法信号推断的 Tailwind 版本
|
|
16
|
+
- Lightning CSS 在容错解析过程中产生的 warning
|
|
17
|
+
|
|
18
|
+
如果你的工具链本身已经依赖 Lightning CSS AST,而你又想分析 Tailwind 相关语法,这个包会比较合适。
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add -D lightningcss-tailwindcss
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 适用场景
|
|
27
|
+
|
|
28
|
+
推荐在这些情况下使用 `lightningcss-tailwindcss`:
|
|
29
|
+
|
|
30
|
+
- 你的分析流程已经基于 `lightningcss`
|
|
31
|
+
- 你希望用 Rust 驱动的解析器 + JS visitor
|
|
32
|
+
- 你可以接受“恢复后”的 AST 输出
|
|
33
|
+
|
|
34
|
+
如果你需要更宽松的源码级解析、PostCSS 节点引用,或者需要解析使用方项目的 Tailwind 安装与配置,请使用 `postcss-tailwindcss`。
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
- `parseTailwindCss(input)`
|
|
39
|
+
- `detectTailwindVersion(input)`
|
|
40
|
+
- `collectApplyCandidates(input)`
|
|
41
|
+
- `collectThemeCalls(input)`
|
|
42
|
+
- `collectImportDirectives(input)`
|
|
43
|
+
- `collectUtilitySelectors(input)`
|
|
44
|
+
- `analyzeTailwindCss(input)`
|
|
45
|
+
|
|
46
|
+
## 示例
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { analyzeTailwindCss } from 'lightningcss-tailwindcss'
|
|
50
|
+
|
|
51
|
+
const css = `
|
|
52
|
+
@import "tailwindcss" source(none) prefix(tw);
|
|
53
|
+
|
|
54
|
+
.button {
|
|
55
|
+
@apply flex hover:bg-red-500;
|
|
56
|
+
color: theme(colors.gray.900 / 75%);
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
const result = analyzeTailwindCss(css)
|
|
61
|
+
|
|
62
|
+
console.log(result.version)
|
|
63
|
+
console.log(result.applyCandidates.map(item => item.candidate))
|
|
64
|
+
console.log(result.warnings)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 返回结果
|
|
68
|
+
|
|
69
|
+
`analyzeTailwindCss()` 返回:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
interface TailwindAnalysis {
|
|
73
|
+
version: 3 | 4 | 'unknown'
|
|
74
|
+
applyCandidates: UtilityCandidate[]
|
|
75
|
+
themeCalls: ThemeCall[]
|
|
76
|
+
importDirectives: TailwindImportDirective[]
|
|
77
|
+
utilitySelectors: UtilitySelector[]
|
|
78
|
+
warnings: LightningWarning[]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
和 PostCSS 版本不同,这里返回的是 Lightning CSS AST 片段与 `loc` 信息,而不是 PostCSS 节点对象。
|
|
83
|
+
|
|
84
|
+
## 解析特性
|
|
85
|
+
|
|
86
|
+
这个包内部依赖 Lightning CSS 的 `transform()`,并开启了 `errorRecovery: true`。
|
|
87
|
+
|
|
88
|
+
这意味着:
|
|
89
|
+
|
|
90
|
+
- 某些 Tailwind 扩展语法会以“容错恢复”的形式保留下来
|
|
91
|
+
- 对部分 Tailwind 特有写法出现 warning 是预期行为
|
|
92
|
+
- v4 的 `@import "tailwindcss" source(...) prefix(...)` 仍然可以被分析,即使 Lightning CSS 不会把它完全当成标准语法
|
|
93
|
+
|
|
94
|
+
## 与 `postcss-tailwindcss` 的区别
|
|
95
|
+
|
|
96
|
+
- 不提供 Tailwind 运行时/配置解析能力
|
|
97
|
+
- 不返回 PostCSS 节点
|
|
98
|
+
- AST 结果更偏“规范化输出”
|
|
99
|
+
- 更适合已经基于 Lightning CSS visitor 的工具
|
|
100
|
+
|
|
101
|
+
如果你还需要感知使用方项目里真实安装的 Tailwind 版本和配置文件位置,应该选 `postcss-tailwindcss`。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ImportRule, Location2, Selector, StyleSheet, UnknownAtRule } from "lightningcss";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
type TailwindVersion = 3 | 4 | 'unknown';
|
|
5
|
+
interface LightningWarning {
|
|
6
|
+
message: string;
|
|
7
|
+
type: string;
|
|
8
|
+
loc?: {
|
|
9
|
+
filename?: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface ParseTailwindCssResult {
|
|
15
|
+
stylesheet: StyleSheet;
|
|
16
|
+
warnings: LightningWarning[];
|
|
17
|
+
}
|
|
18
|
+
interface UtilityCandidate {
|
|
19
|
+
candidate: string;
|
|
20
|
+
ruleName: string;
|
|
21
|
+
loc: UnknownAtRule['loc'];
|
|
22
|
+
}
|
|
23
|
+
interface ThemeCall {
|
|
24
|
+
raw: string;
|
|
25
|
+
path: string;
|
|
26
|
+
opacity: string | null;
|
|
27
|
+
functionName: string;
|
|
28
|
+
}
|
|
29
|
+
interface TailwindImportDirective {
|
|
30
|
+
importTarget: string;
|
|
31
|
+
functions: Array<{
|
|
32
|
+
name: string;
|
|
33
|
+
value: string;
|
|
34
|
+
}>;
|
|
35
|
+
hasLayerKeyword: boolean;
|
|
36
|
+
mediaQuery: string | null;
|
|
37
|
+
loc: ImportRule['loc'];
|
|
38
|
+
}
|
|
39
|
+
interface UtilitySelector {
|
|
40
|
+
className: string;
|
|
41
|
+
selector: Selector;
|
|
42
|
+
loc: Location2;
|
|
43
|
+
}
|
|
44
|
+
interface TailwindAnalysis {
|
|
45
|
+
version: TailwindVersion;
|
|
46
|
+
applyCandidates: UtilityCandidate[];
|
|
47
|
+
themeCalls: ThemeCall[];
|
|
48
|
+
importDirectives: TailwindImportDirective[];
|
|
49
|
+
utilitySelectors: UtilitySelector[];
|
|
50
|
+
warnings: LightningWarning[];
|
|
51
|
+
}
|
|
52
|
+
declare function parseTailwindCss(input: string): ParseTailwindCssResult;
|
|
53
|
+
declare function detectTailwindVersion(input: string): TailwindVersion;
|
|
54
|
+
declare function collectApplyCandidates(input: string): UtilityCandidate[];
|
|
55
|
+
declare function collectThemeCalls(input: string): ThemeCall[];
|
|
56
|
+
declare function collectImportDirectives(input: string): TailwindImportDirective[];
|
|
57
|
+
declare function collectUtilitySelectors(input: string): UtilitySelector[];
|
|
58
|
+
declare function analyzeTailwindCss(input: string): TailwindAnalysis;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { LightningWarning, ParseTailwindCssResult, TailwindAnalysis, TailwindImportDirective, TailwindVersion, ThemeCall, UtilityCandidate, UtilitySelector, analyzeTailwindCss, collectApplyCandidates, collectImportDirectives, collectThemeCalls, collectUtilitySelectors, detectTailwindVersion, parseTailwindCss };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { transform } from "lightningcss";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
const WHITESPACE_RE = /\s/;
|
|
5
|
+
function tokenToString(token) {
|
|
6
|
+
switch (token.type) {
|
|
7
|
+
case "ident":
|
|
8
|
+
case "at-keyword":
|
|
9
|
+
case "hash":
|
|
10
|
+
case "id-hash":
|
|
11
|
+
case "white-space":
|
|
12
|
+
case "comment":
|
|
13
|
+
case "string":
|
|
14
|
+
case "unquoted-url":
|
|
15
|
+
case "delim":
|
|
16
|
+
case "bad-string":
|
|
17
|
+
case "bad-url": return token.value;
|
|
18
|
+
case "number": return String(token.value);
|
|
19
|
+
case "percentage": return `${token.value * 100}%`;
|
|
20
|
+
case "dimension": return `${token.value}${token.unit}`;
|
|
21
|
+
case "colon": return ":";
|
|
22
|
+
case "semicolon": return ";";
|
|
23
|
+
case "comma": return ",";
|
|
24
|
+
case "include-match": return "~=";
|
|
25
|
+
case "dash-match": return "|=";
|
|
26
|
+
case "prefix-match": return "^=";
|
|
27
|
+
case "suffix-match": return "$=";
|
|
28
|
+
case "substring-match": return "*=";
|
|
29
|
+
case "cdo": return "<!--";
|
|
30
|
+
case "cdc": return "-->";
|
|
31
|
+
case "function": return `${token.value}(`;
|
|
32
|
+
case "parenthesis-block": return "(";
|
|
33
|
+
case "square-bracket-block": return "[";
|
|
34
|
+
case "curly-bracket-block": return "{";
|
|
35
|
+
case "close-parenthesis": return ")";
|
|
36
|
+
case "close-square-bracket": return "]";
|
|
37
|
+
case "close-curly-bracket": return "}";
|
|
38
|
+
default: return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function tokenOrValueListToString(nodes) {
|
|
42
|
+
let output = "";
|
|
43
|
+
for (const [index, node] of nodes.entries()) {
|
|
44
|
+
const rendered = tokenOrValueToString(node);
|
|
45
|
+
if (node.type === "token" && node.value.type === "number" && index > 0) {
|
|
46
|
+
const previous = nodes[index - 1];
|
|
47
|
+
if (previous && previous.type === "token" && previous.value.type === "ident") {
|
|
48
|
+
const numericValue = node.value.value;
|
|
49
|
+
const normalized = numericValue > 0 && numericValue < 1 ? String(Math.round(numericValue * 1e3)) : rendered;
|
|
50
|
+
output += `.${normalized}`;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
output += rendered;
|
|
55
|
+
}
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
function tokenOrValueToString(node) {
|
|
59
|
+
switch (node.type) {
|
|
60
|
+
case "token": return tokenToString(node.value);
|
|
61
|
+
case "function": return `${node.value.name}(${tokenOrValueListToString(node.value.arguments)})`;
|
|
62
|
+
case "dashed-ident":
|
|
63
|
+
case "animation-name": return String(node.value);
|
|
64
|
+
case "length": return `${node.value.value}${node.value.unit}`;
|
|
65
|
+
case "angle": return `${node.value.value}${node.value.type}`;
|
|
66
|
+
case "time": return `${node.value.value}${node.value.type === "seconds" ? "s" : "ms"}`;
|
|
67
|
+
case "resolution": return `${node.value.value}${node.value.type}`;
|
|
68
|
+
case "url": return `url(${String(node.value.url)})`;
|
|
69
|
+
case "var": return `var(${String(node.value.name)})`;
|
|
70
|
+
case "env": return `env(${String(node.value.name)})`;
|
|
71
|
+
case "color":
|
|
72
|
+
case "unresolved-color": return JSON.stringify(node.value);
|
|
73
|
+
default: return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function splitCandidateList(input) {
|
|
77
|
+
const candidates = [];
|
|
78
|
+
let current = "";
|
|
79
|
+
let bracketDepth = 0;
|
|
80
|
+
let parenDepth = 0;
|
|
81
|
+
let quote = null;
|
|
82
|
+
let escaped = false;
|
|
83
|
+
for (const char of input) {
|
|
84
|
+
if (escaped) {
|
|
85
|
+
current += char;
|
|
86
|
+
escaped = false;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (char === "\\") {
|
|
90
|
+
current += char;
|
|
91
|
+
escaped = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (quote) {
|
|
95
|
+
current += char;
|
|
96
|
+
if (char === quote) quote = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === "\"" || char === "'") {
|
|
100
|
+
current += char;
|
|
101
|
+
quote = char;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === "[") {
|
|
105
|
+
bracketDepth += 1;
|
|
106
|
+
current += char;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === "]") {
|
|
110
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
111
|
+
current += char;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === "(") {
|
|
115
|
+
parenDepth += 1;
|
|
116
|
+
current += char;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (char === ")") {
|
|
120
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
121
|
+
current += char;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (WHITESPACE_RE.test(char) && bracketDepth === 0 && parenDepth === 0) {
|
|
125
|
+
const value = current.trim();
|
|
126
|
+
if (value) candidates.push(value);
|
|
127
|
+
current = "";
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
current += char;
|
|
131
|
+
}
|
|
132
|
+
const last = current.trim();
|
|
133
|
+
if (last) candidates.push(last);
|
|
134
|
+
return candidates;
|
|
135
|
+
}
|
|
136
|
+
function parseThemeArgument(raw) {
|
|
137
|
+
let bracketDepth = 0;
|
|
138
|
+
let parenDepth = 0;
|
|
139
|
+
let slashIndex = -1;
|
|
140
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
141
|
+
const char = raw[i];
|
|
142
|
+
if (char === "[") bracketDepth += 1;
|
|
143
|
+
else if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
144
|
+
else if (char === "(") parenDepth += 1;
|
|
145
|
+
else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
146
|
+
else if (char === "/" && bracketDepth === 0 && parenDepth === 0) {
|
|
147
|
+
slashIndex = i;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (slashIndex === -1) return {
|
|
152
|
+
path: raw.trim(),
|
|
153
|
+
opacity: null
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
path: raw.slice(0, slashIndex).trim(),
|
|
157
|
+
opacity: raw.slice(slashIndex + 1).trim() || null
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function parseWithLightningCss(input) {
|
|
161
|
+
let stylesheetRef;
|
|
162
|
+
const result = transform({
|
|
163
|
+
filename: "input.css",
|
|
164
|
+
code: Buffer.from(input),
|
|
165
|
+
errorRecovery: true,
|
|
166
|
+
visitor: { StyleSheet(stylesheet) {
|
|
167
|
+
stylesheetRef = stylesheet;
|
|
168
|
+
} }
|
|
169
|
+
});
|
|
170
|
+
if (!stylesheetRef) throw new Error("Failed to capture Lightning CSS stylesheet");
|
|
171
|
+
return {
|
|
172
|
+
stylesheet: stylesheetRef,
|
|
173
|
+
warnings: result.warnings.map((warning) => ({
|
|
174
|
+
message: warning.message,
|
|
175
|
+
type: warning.type,
|
|
176
|
+
loc: warning.loc
|
|
177
|
+
}))
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function parseTailwindCss(input) {
|
|
181
|
+
return parseWithLightningCss(input);
|
|
182
|
+
}
|
|
183
|
+
function walkRules(rules, visit) {
|
|
184
|
+
for (const rule of rules) {
|
|
185
|
+
visit(rule);
|
|
186
|
+
if (rule.type === "style") walkRules(rule.value.rules ?? [], visit);
|
|
187
|
+
else if (rule.type === "media" || rule.type === "supports" || rule.type === "container" || rule.type === "scope" || rule.type === "starting-style" || rule.type === "layer-block") walkRules(rule.value.rules, visit);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function detectTailwindVersion(input) {
|
|
191
|
+
const { stylesheet } = parseWithLightningCss(input);
|
|
192
|
+
let hasV3 = false;
|
|
193
|
+
let hasV4 = false;
|
|
194
|
+
walkRules(stylesheet.rules, (rule) => {
|
|
195
|
+
if (rule.type === "import") {
|
|
196
|
+
if (rule.value.url.includes("tailwindcss")) hasV4 = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (rule.type !== "unknown") return;
|
|
200
|
+
if (["tailwind", "config"].includes(rule.value.name)) hasV3 = true;
|
|
201
|
+
if ([
|
|
202
|
+
"theme",
|
|
203
|
+
"source",
|
|
204
|
+
"utility",
|
|
205
|
+
"variant",
|
|
206
|
+
"custom-variant",
|
|
207
|
+
"plugin",
|
|
208
|
+
"reference"
|
|
209
|
+
].includes(rule.value.name)) hasV4 = true;
|
|
210
|
+
});
|
|
211
|
+
if (hasV4) return 4;
|
|
212
|
+
if (hasV3) return 3;
|
|
213
|
+
return "unknown";
|
|
214
|
+
}
|
|
215
|
+
function collectApplyCandidates(input) {
|
|
216
|
+
const { stylesheet } = parseWithLightningCss(input);
|
|
217
|
+
const candidates = [];
|
|
218
|
+
walkRules(stylesheet.rules, (rule) => {
|
|
219
|
+
if (rule.type !== "unknown" || rule.value.name !== "apply") return;
|
|
220
|
+
const prelude = tokenOrValueListToString(rule.value.prelude);
|
|
221
|
+
for (const candidate of splitCandidateList(prelude)) candidates.push({
|
|
222
|
+
candidate,
|
|
223
|
+
ruleName: rule.value.name,
|
|
224
|
+
loc: rule.value.loc
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
return candidates;
|
|
228
|
+
}
|
|
229
|
+
function collectThemeCalls(input) {
|
|
230
|
+
const calls = [];
|
|
231
|
+
transform({
|
|
232
|
+
filename: "input.css",
|
|
233
|
+
code: Buffer.from(input),
|
|
234
|
+
errorRecovery: true,
|
|
235
|
+
visitor: { Function: { theme(fn) {
|
|
236
|
+
const raw = tokenOrValueListToString(fn.arguments).trim();
|
|
237
|
+
const { path, opacity } = parseThemeArgument(raw);
|
|
238
|
+
calls.push({
|
|
239
|
+
raw,
|
|
240
|
+
path,
|
|
241
|
+
opacity,
|
|
242
|
+
functionName: fn.name
|
|
243
|
+
});
|
|
244
|
+
} } }
|
|
245
|
+
});
|
|
246
|
+
return calls;
|
|
247
|
+
}
|
|
248
|
+
function collectImportDirectives(input) {
|
|
249
|
+
const { stylesheet } = parseWithLightningCss(input);
|
|
250
|
+
const directives = [];
|
|
251
|
+
walkRules(stylesheet.rules, (rule) => {
|
|
252
|
+
if (rule.type !== "import" || !rule.value.url.includes("tailwindcss")) return;
|
|
253
|
+
const condition = rule.value.media?.mediaQueries[0]?.condition;
|
|
254
|
+
const tokens = condition?.type === "unknown" ? condition.value : [];
|
|
255
|
+
const functions = tokens.filter((item) => item.type === "function").filter((item) => [
|
|
256
|
+
"source",
|
|
257
|
+
"prefix",
|
|
258
|
+
"layer",
|
|
259
|
+
"supports"
|
|
260
|
+
].includes(item.value.name)).map((item) => ({
|
|
261
|
+
name: item.value.name,
|
|
262
|
+
value: tokenOrValueListToString(item.value.arguments).trim()
|
|
263
|
+
}));
|
|
264
|
+
const mediaQuery = tokenOrValueListToString(tokens.filter((item) => {
|
|
265
|
+
return !(item.type === "function" && [
|
|
266
|
+
"source",
|
|
267
|
+
"prefix",
|
|
268
|
+
"layer",
|
|
269
|
+
"supports"
|
|
270
|
+
].includes(item.value.name));
|
|
271
|
+
})).trim() || null;
|
|
272
|
+
directives.push({
|
|
273
|
+
importTarget: rule.value.url,
|
|
274
|
+
functions,
|
|
275
|
+
hasLayerKeyword: Boolean(rule.value.layer),
|
|
276
|
+
mediaQuery,
|
|
277
|
+
loc: rule.value.loc
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
return directives;
|
|
281
|
+
}
|
|
282
|
+
function collectUtilitySelectors(input) {
|
|
283
|
+
const { stylesheet } = parseWithLightningCss(input);
|
|
284
|
+
const selectors = [];
|
|
285
|
+
walkRules(stylesheet.rules, (rule) => {
|
|
286
|
+
if (rule.type !== "style") return;
|
|
287
|
+
for (const selector of rule.value.selectors) for (const component of selector) {
|
|
288
|
+
if (component.type !== "class") continue;
|
|
289
|
+
selectors.push({
|
|
290
|
+
className: component.name,
|
|
291
|
+
selector,
|
|
292
|
+
loc: rule.value.loc
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return selectors;
|
|
297
|
+
}
|
|
298
|
+
function analyzeTailwindCss(input) {
|
|
299
|
+
const parsed = parseWithLightningCss(input);
|
|
300
|
+
return {
|
|
301
|
+
version: detectTailwindVersion(input),
|
|
302
|
+
applyCandidates: collectApplyCandidates(input),
|
|
303
|
+
themeCalls: collectThemeCalls(input),
|
|
304
|
+
importDirectives: collectImportDirectives(input),
|
|
305
|
+
utilitySelectors: collectUtilitySelectors(input),
|
|
306
|
+
warnings: parsed.warnings
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
export { analyzeTailwindCss, collectApplyCandidates, collectImportDirectives, collectThemeCalls, collectUtilitySelectors, detectTailwindVersion, parseTailwindCss };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lightningcss-tailwindcss",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "Lightning CSS AST utilities for analyzing Tailwind CSS syntax",
|
|
6
|
+
"author": "ice breaker <1324318532@qq.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/sonofmagic/dev-configs.git",
|
|
11
|
+
"directory": "packages/lightningcss-tailwindcss"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/sonofmagic/dev-configs/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dev-configs",
|
|
18
|
+
"icebreakers",
|
|
19
|
+
"lightningcss",
|
|
20
|
+
"tailwindcss"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"README.md",
|
|
35
|
+
"README.zh.md",
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"lightningcss": "^1.31.1"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"dev": "tsdown --watch --sourcemap",
|
|
47
|
+
"build": "tsdown",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:dev": "vitest",
|
|
50
|
+
"release": "pnpm publish",
|
|
51
|
+
"sync": "cnpm sync lightningcss-tailwindcss",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
}
|
|
54
|
+
}
|