postcss-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 +135 -0
- package/README.zh.md +135 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +408 -0
- package/package.json +58 -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,135 @@
|
|
|
1
|
+
# postcss-tailwindcss
|
|
2
|
+
|
|
3
|
+
PostCSS AST utilities for analyzing Tailwind CSS syntax.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package focuses on **analysis**, not transformation.
|
|
8
|
+
|
|
9
|
+
It parses CSS with PostCSS and extracts Tailwind-related structures such as:
|
|
10
|
+
|
|
11
|
+
- `@apply` utility candidates
|
|
12
|
+
- `theme(...)` function calls
|
|
13
|
+
- Tailwind v4 `@import "tailwindcss" ...` directives
|
|
14
|
+
- utility-like class selectors declared in authored stylesheets
|
|
15
|
+
- Tailwind runtime/config installation details from a consumer project
|
|
16
|
+
|
|
17
|
+
The output is designed for tooling such as:
|
|
18
|
+
|
|
19
|
+
- Stylelint plugins
|
|
20
|
+
- codemods
|
|
21
|
+
- custom lint rules
|
|
22
|
+
- migration scripts
|
|
23
|
+
- editor or CLI diagnostics
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add -D postcss-tailwindcss
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## When To Use This Package
|
|
32
|
+
|
|
33
|
+
Use `postcss-tailwindcss` when:
|
|
34
|
+
|
|
35
|
+
- you already work with PostCSS AST
|
|
36
|
+
- you want tolerant parsing for Tailwind syntax
|
|
37
|
+
- you need to inspect raw authored CSS rather than compile it
|
|
38
|
+
- you need project-aware Tailwind runtime resolution
|
|
39
|
+
|
|
40
|
+
If your toolchain is already built around Lightning CSS AST, use `lightningcss-tailwindcss` instead.
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
- `parseTailwindCss(input)`
|
|
45
|
+
- `detectTailwindVersion(input)`
|
|
46
|
+
- `resolveTailwindRuntime(context)`
|
|
47
|
+
- `detectInstalledTailwindVersion(context)`
|
|
48
|
+
- `collectApplyCandidates(input)`
|
|
49
|
+
- `collectThemeCalls(input)`
|
|
50
|
+
- `collectImportDirectives(input)`
|
|
51
|
+
- `collectUtilitySelectors(input)`
|
|
52
|
+
- `analyzeTailwindCss(input)`
|
|
53
|
+
|
|
54
|
+
`input` can be a CSS string or an existing PostCSS `Root`.
|
|
55
|
+
|
|
56
|
+
`context` is:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
interface TailwindResolutionContext {
|
|
60
|
+
cwd: string
|
|
61
|
+
tsconfigPath?: string
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Example
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import {
|
|
69
|
+
analyzeTailwindCss,
|
|
70
|
+
resolveTailwindRuntime,
|
|
71
|
+
} from 'postcss-tailwindcss'
|
|
72
|
+
|
|
73
|
+
const css = `
|
|
74
|
+
@import "tailwindcss" source(none) prefix(tw);
|
|
75
|
+
|
|
76
|
+
.button {
|
|
77
|
+
@apply flex hover:bg-red-500;
|
|
78
|
+
color: theme(colors.gray.900 / 75%);
|
|
79
|
+
}
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
const analysis = analyzeTailwindCss(css)
|
|
83
|
+
const runtime = resolveTailwindRuntime({
|
|
84
|
+
cwd: process.cwd(),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
console.log(analysis.version)
|
|
88
|
+
console.log(analysis.applyCandidates.map(item => item.candidate))
|
|
89
|
+
console.log(runtime.version)
|
|
90
|
+
console.log(runtime.configPath)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Return Shape
|
|
94
|
+
|
|
95
|
+
`analyzeTailwindCss()` returns:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
interface TailwindAnalysis {
|
|
99
|
+
version: 3 | 4 | 'unknown'
|
|
100
|
+
applyCandidates: UtilityCandidate[]
|
|
101
|
+
themeCalls: ThemeCall[]
|
|
102
|
+
importDirectives: TailwindImportDirective[]
|
|
103
|
+
utilitySelectors: UtilitySelector[]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Each item keeps a reference to the original PostCSS node so downstream tools can report precise diagnostics.
|
|
108
|
+
|
|
109
|
+
## Runtime Resolution
|
|
110
|
+
|
|
111
|
+
`resolveTailwindRuntime()` tries to discover:
|
|
112
|
+
|
|
113
|
+
- installed `tailwindcss/package.json`
|
|
114
|
+
- Tailwind installation directory
|
|
115
|
+
- Tailwind CSS style entry
|
|
116
|
+
- nearest `tailwind.config.*`
|
|
117
|
+
- installed Tailwind version
|
|
118
|
+
|
|
119
|
+
Resolution is project-aware:
|
|
120
|
+
|
|
121
|
+
- default Node/package resolution is used first
|
|
122
|
+
- if `tsconfigPath` is provided, `paths`/`baseUrl` are also considered via `get-tsconfig`
|
|
123
|
+
|
|
124
|
+
## Parsing Notes
|
|
125
|
+
|
|
126
|
+
- Tailwind v3 and v4 syntax are both recognized heuristically
|
|
127
|
+
- `theme(colors.gray.900 / 75%)` is normalized into `path` and `opacity`
|
|
128
|
+
- escaped selectors such as `.hover\\:bg-red-500:hover` are normalized back to `hover:bg-red-500`
|
|
129
|
+
- this package does **not** validate whether a class is truly registered by Tailwind; it only extracts and normalizes syntax
|
|
130
|
+
|
|
131
|
+
## Relationship To `stylelint-plugin-no-tailwindcss`
|
|
132
|
+
|
|
133
|
+
`stylelint-plugin-no-tailwindcss` uses this package for selector collection and Tailwind runtime resolution.
|
|
134
|
+
|
|
135
|
+
This package provides the low-level analysis layer; the Stylelint plugin provides the policy and reporting layer.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# postcss-tailwindcss
|
|
2
|
+
|
|
3
|
+
用于分析 Tailwind CSS 语法的 PostCSS AST 工具集。
|
|
4
|
+
|
|
5
|
+
## 概览
|
|
6
|
+
|
|
7
|
+
这个包专注于**分析**,不负责 transform。
|
|
8
|
+
|
|
9
|
+
它基于 PostCSS 解析 CSS,并提取下面这些 Tailwind 相关结构:
|
|
10
|
+
|
|
11
|
+
- `@apply` 中的 utility candidate
|
|
12
|
+
- `theme(...)` 函数调用
|
|
13
|
+
- Tailwind v4 的 `@import "tailwindcss" ...` 指令
|
|
14
|
+
- 手写样式里声明的 utility 风格类选择器
|
|
15
|
+
- 使用方项目中的 Tailwind 安装、配置和版本信息
|
|
16
|
+
|
|
17
|
+
适合用在:
|
|
18
|
+
|
|
19
|
+
- Stylelint 插件
|
|
20
|
+
- codemod
|
|
21
|
+
- 自定义 lint rule
|
|
22
|
+
- 迁移脚本
|
|
23
|
+
- 编辑器或 CLI 诊断工具
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add -D postcss-tailwindcss
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 适用场景
|
|
32
|
+
|
|
33
|
+
推荐在这些情况下使用 `postcss-tailwindcss`:
|
|
34
|
+
|
|
35
|
+
- 你的工具本身就是基于 PostCSS AST
|
|
36
|
+
- 你希望更宽松地处理 Tailwind 语法
|
|
37
|
+
- 你关注的是“源码分析”而不是“编译结果”
|
|
38
|
+
- 你需要按项目路径解析 Tailwind 安装和配置
|
|
39
|
+
|
|
40
|
+
如果你的工具链已经完全基于 Lightning CSS AST,优先使用 `lightningcss-tailwindcss`。
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
- `parseTailwindCss(input)`
|
|
45
|
+
- `detectTailwindVersion(input)`
|
|
46
|
+
- `resolveTailwindRuntime(context)`
|
|
47
|
+
- `detectInstalledTailwindVersion(context)`
|
|
48
|
+
- `collectApplyCandidates(input)`
|
|
49
|
+
- `collectThemeCalls(input)`
|
|
50
|
+
- `collectImportDirectives(input)`
|
|
51
|
+
- `collectUtilitySelectors(input)`
|
|
52
|
+
- `analyzeTailwindCss(input)`
|
|
53
|
+
|
|
54
|
+
`input` 可以是 CSS 字符串,也可以是已有的 PostCSS `Root`。
|
|
55
|
+
|
|
56
|
+
`context` 结构如下:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
interface TailwindResolutionContext {
|
|
60
|
+
cwd: string
|
|
61
|
+
tsconfigPath?: string
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 示例
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import {
|
|
69
|
+
analyzeTailwindCss,
|
|
70
|
+
resolveTailwindRuntime,
|
|
71
|
+
} from 'postcss-tailwindcss'
|
|
72
|
+
|
|
73
|
+
const css = `
|
|
74
|
+
@import "tailwindcss" source(none) prefix(tw);
|
|
75
|
+
|
|
76
|
+
.button {
|
|
77
|
+
@apply flex hover:bg-red-500;
|
|
78
|
+
color: theme(colors.gray.900 / 75%);
|
|
79
|
+
}
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
const analysis = analyzeTailwindCss(css)
|
|
83
|
+
const runtime = resolveTailwindRuntime({
|
|
84
|
+
cwd: process.cwd(),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
console.log(analysis.version)
|
|
88
|
+
console.log(analysis.applyCandidates.map(item => item.candidate))
|
|
89
|
+
console.log(runtime.version)
|
|
90
|
+
console.log(runtime.configPath)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 返回结果
|
|
94
|
+
|
|
95
|
+
`analyzeTailwindCss()` 返回:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
interface TailwindAnalysis {
|
|
99
|
+
version: 3 | 4 | 'unknown'
|
|
100
|
+
applyCandidates: UtilityCandidate[]
|
|
101
|
+
themeCalls: ThemeCall[]
|
|
102
|
+
importDirectives: TailwindImportDirective[]
|
|
103
|
+
utilitySelectors: UtilitySelector[]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
每一项都会保留原始 PostCSS 节点引用,方便下游工具做精确报错。
|
|
108
|
+
|
|
109
|
+
## Tailwind 运行时解析
|
|
110
|
+
|
|
111
|
+
`resolveTailwindRuntime()` 会尝试解析:
|
|
112
|
+
|
|
113
|
+
- 当前项目安装的 `tailwindcss/package.json`
|
|
114
|
+
- Tailwind 安装目录
|
|
115
|
+
- Tailwind CSS 样式入口
|
|
116
|
+
- 最近的 `tailwind.config.*`
|
|
117
|
+
- 已安装的 Tailwind 版本
|
|
118
|
+
|
|
119
|
+
解析策略是项目感知的:
|
|
120
|
+
|
|
121
|
+
- 默认先走标准 Node/package 解析
|
|
122
|
+
- 如果传入 `tsconfigPath`,还会额外考虑 `paths` / `baseUrl`
|
|
123
|
+
|
|
124
|
+
## 语法说明
|
|
125
|
+
|
|
126
|
+
- 会对 Tailwind v3 / v4 语法做启发式识别
|
|
127
|
+
- `theme(colors.gray.900 / 75%)` 会拆成 `path` 和 `opacity`
|
|
128
|
+
- `.hover\\:bg-red-500:hover` 这类转义 selector 会被还原成 `hover:bg-red-500`
|
|
129
|
+
- 这个包**不会**判断某个类名是否真的被 Tailwind 注册;它只负责提取和归一化语法
|
|
130
|
+
|
|
131
|
+
## 与 `stylelint-plugin-no-tailwindcss` 的关系
|
|
132
|
+
|
|
133
|
+
`stylelint-plugin-no-tailwindcss` 当前会复用这个包来做 selector 收集和 Tailwind 运行时解析。
|
|
134
|
+
|
|
135
|
+
这个包是底层分析层,Stylelint 插件是在其上叠加规则策略和报错逻辑。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { AtRule, Declaration, Node, Root, Rule } from "postcss";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
type TailwindVersion = 3 | 4 | 'unknown';
|
|
5
|
+
interface TailwindResolutionContext {
|
|
6
|
+
cwd: string;
|
|
7
|
+
tsconfigPath?: string;
|
|
8
|
+
}
|
|
9
|
+
interface ResolvedTailwindRuntime {
|
|
10
|
+
configPath: string | null;
|
|
11
|
+
cssEntryPath: string | null;
|
|
12
|
+
installationPath: string | null;
|
|
13
|
+
packageJsonPath: string | null;
|
|
14
|
+
version: string | null;
|
|
15
|
+
}
|
|
16
|
+
interface UtilityCandidate {
|
|
17
|
+
candidate: string;
|
|
18
|
+
node: AtRule;
|
|
19
|
+
source?: Node['source'];
|
|
20
|
+
}
|
|
21
|
+
interface ThemeCall {
|
|
22
|
+
raw: string;
|
|
23
|
+
path: string;
|
|
24
|
+
opacity: string | null;
|
|
25
|
+
node: Declaration | AtRule;
|
|
26
|
+
source?: Node['source'];
|
|
27
|
+
}
|
|
28
|
+
interface TailwindImportDirective {
|
|
29
|
+
importTarget: string;
|
|
30
|
+
functions: Array<{
|
|
31
|
+
name: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}>;
|
|
34
|
+
hasLayerKeyword: boolean;
|
|
35
|
+
mediaQuery: string | null;
|
|
36
|
+
node: AtRule;
|
|
37
|
+
source?: Node['source'];
|
|
38
|
+
}
|
|
39
|
+
interface UtilitySelector {
|
|
40
|
+
className: string;
|
|
41
|
+
rule: Rule;
|
|
42
|
+
selector: string;
|
|
43
|
+
source?: Node['source'];
|
|
44
|
+
}
|
|
45
|
+
interface TailwindAnalysis {
|
|
46
|
+
version: TailwindVersion;
|
|
47
|
+
applyCandidates: UtilityCandidate[];
|
|
48
|
+
themeCalls: ThemeCall[];
|
|
49
|
+
importDirectives: TailwindImportDirective[];
|
|
50
|
+
utilitySelectors: UtilitySelector[];
|
|
51
|
+
}
|
|
52
|
+
type TailwindInput = Root | string;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/analysis.d.ts
|
|
55
|
+
declare function detectTailwindVersion(input: TailwindInput): TailwindVersion;
|
|
56
|
+
declare function collectApplyCandidates(input: TailwindInput): UtilityCandidate[];
|
|
57
|
+
declare function collectThemeCalls(input: TailwindInput): ThemeCall[];
|
|
58
|
+
declare function collectImportDirectives(input: TailwindInput): TailwindImportDirective[];
|
|
59
|
+
declare function collectUtilitySelectors(input: TailwindInput): UtilitySelector[];
|
|
60
|
+
declare function analyzeTailwindCss(input: TailwindInput): TailwindAnalysis;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/postcss.d.ts
|
|
63
|
+
declare function parseTailwindCss(input: TailwindInput): Root;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/runtime-resolution.d.ts
|
|
66
|
+
declare function resolveTailwindRuntime(context: TailwindResolutionContext): ResolvedTailwindRuntime;
|
|
67
|
+
declare function detectInstalledTailwindVersion(context: TailwindResolutionContext): TailwindVersion;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { type ResolvedTailwindRuntime, type TailwindAnalysis, type TailwindImportDirective, type TailwindInput, type TailwindResolutionContext, type TailwindVersion, type ThemeCall, type UtilityCandidate, type UtilitySelector, analyzeTailwindCss, collectApplyCandidates, collectImportDirectives, collectThemeCalls, collectUtilitySelectors, detectInstalledTailwindVersion, detectTailwindVersion, parseTailwindCss, resolveTailwindRuntime };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import valueParser from "postcss-value-parser";
|
|
2
|
+
import postcss from "postcss";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import enhancedResolve from "enhanced-resolve";
|
|
6
|
+
import { createPathsMatcher, getTsconfig, parseTsconfig } from "get-tsconfig";
|
|
7
|
+
//#region src/constants.ts
|
|
8
|
+
const ESCAPED_BACKSLASH_RE = /\\\\/g;
|
|
9
|
+
const ESCAPED_CHAR_RE = /\\(.)/g;
|
|
10
|
+
const WHITESPACE_RE = /\s/;
|
|
11
|
+
const IMPORT_TARGET_RE = /^['"]|['"]$/g;
|
|
12
|
+
const SELECTOR_BREAK_RE = /[#.,\s>+~]/;
|
|
13
|
+
const TAILWIND_CONFIG_FILENAMES = [
|
|
14
|
+
"tailwind.config.ts",
|
|
15
|
+
"tailwind.config.mts",
|
|
16
|
+
"tailwind.config.cts",
|
|
17
|
+
"tailwind.config.js",
|
|
18
|
+
"tailwind.config.mjs",
|
|
19
|
+
"tailwind.config.cjs",
|
|
20
|
+
"tailwind.config.json"
|
|
21
|
+
];
|
|
22
|
+
const TAILWIND_V4_AT_RULES = [
|
|
23
|
+
"theme",
|
|
24
|
+
"source",
|
|
25
|
+
"utility",
|
|
26
|
+
"variant",
|
|
27
|
+
"custom-variant",
|
|
28
|
+
"plugin",
|
|
29
|
+
"reference"
|
|
30
|
+
];
|
|
31
|
+
const TAILWIND_IMPORT_FUNCTIONS = [
|
|
32
|
+
"source",
|
|
33
|
+
"prefix",
|
|
34
|
+
"layer",
|
|
35
|
+
"supports"
|
|
36
|
+
];
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/postcss.ts
|
|
39
|
+
function toRoot(input) {
|
|
40
|
+
return typeof input === "string" ? postcss.parse(input) : input;
|
|
41
|
+
}
|
|
42
|
+
function parseTailwindCss(input) {
|
|
43
|
+
return toRoot(input);
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/syntax.ts
|
|
47
|
+
function unescapeCssIdentifier(value) {
|
|
48
|
+
return value.replace(ESCAPED_BACKSLASH_RE, "\\").replace(ESCAPED_CHAR_RE, "$1");
|
|
49
|
+
}
|
|
50
|
+
function stripImportTargetQuotes(value) {
|
|
51
|
+
return value.replace(IMPORT_TARGET_RE, "");
|
|
52
|
+
}
|
|
53
|
+
function splitCandidateList(input) {
|
|
54
|
+
const candidates = [];
|
|
55
|
+
let current = "";
|
|
56
|
+
let bracketDepth = 0;
|
|
57
|
+
let parenDepth = 0;
|
|
58
|
+
let quote = null;
|
|
59
|
+
let escaped = false;
|
|
60
|
+
for (const char of input) {
|
|
61
|
+
if (escaped) {
|
|
62
|
+
current += char;
|
|
63
|
+
escaped = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (char === "\\") {
|
|
67
|
+
current += char;
|
|
68
|
+
escaped = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (quote) {
|
|
72
|
+
current += char;
|
|
73
|
+
if (char === quote) quote = null;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (char === "\"" || char === "'") {
|
|
77
|
+
current += char;
|
|
78
|
+
quote = char;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === "[") {
|
|
82
|
+
bracketDepth += 1;
|
|
83
|
+
current += char;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === "]") {
|
|
87
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
88
|
+
current += char;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (char === "(") {
|
|
92
|
+
parenDepth += 1;
|
|
93
|
+
current += char;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (char === ")") {
|
|
97
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
98
|
+
current += char;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (WHITESPACE_RE.test(char) && bracketDepth === 0 && parenDepth === 0) {
|
|
102
|
+
const value = current.trim();
|
|
103
|
+
if (value) candidates.push(value);
|
|
104
|
+
current = "";
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
current += char;
|
|
108
|
+
}
|
|
109
|
+
const last = current.trim();
|
|
110
|
+
if (last) candidates.push(last);
|
|
111
|
+
return candidates;
|
|
112
|
+
}
|
|
113
|
+
function parseThemeArgument(raw) {
|
|
114
|
+
let bracketDepth = 0;
|
|
115
|
+
let parenDepth = 0;
|
|
116
|
+
let slashIndex = -1;
|
|
117
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
118
|
+
const char = raw[i];
|
|
119
|
+
if (char === "[") bracketDepth += 1;
|
|
120
|
+
else if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
121
|
+
else if (char === "(") parenDepth += 1;
|
|
122
|
+
else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
123
|
+
else if (char === "/" && bracketDepth === 0 && parenDepth === 0) {
|
|
124
|
+
slashIndex = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (slashIndex === -1) return {
|
|
129
|
+
path: raw.trim(),
|
|
130
|
+
opacity: null
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
path: raw.slice(0, slashIndex).trim(),
|
|
134
|
+
opacity: raw.slice(slashIndex + 1).trim() || null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function collectClassNamesFromSelector(selector) {
|
|
138
|
+
const classNames = [];
|
|
139
|
+
for (let index = 0; index < selector.length; index += 1) {
|
|
140
|
+
if (selector[index] !== ".") continue;
|
|
141
|
+
if (index > 0 && selector[index - 1] === "\\") continue;
|
|
142
|
+
let cursor = index + 1;
|
|
143
|
+
let rawClassName = "";
|
|
144
|
+
while (cursor < selector.length) {
|
|
145
|
+
const char = selector[cursor];
|
|
146
|
+
if (!char) break;
|
|
147
|
+
if ((SELECTOR_BREAK_RE.test(char) || char === ":") && selector[cursor - 1] !== "\\") break;
|
|
148
|
+
rawClassName += char;
|
|
149
|
+
cursor += 1;
|
|
150
|
+
}
|
|
151
|
+
if (rawClassName) classNames.push(unescapeCssIdentifier(rawClassName));
|
|
152
|
+
index = cursor - 1;
|
|
153
|
+
}
|
|
154
|
+
return classNames;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/analysis.ts
|
|
158
|
+
function hasTailwindV4Signals(root) {
|
|
159
|
+
let found = false;
|
|
160
|
+
root.walkAtRules((atRule) => {
|
|
161
|
+
if (TAILWIND_V4_AT_RULES.includes(atRule.name)) {
|
|
162
|
+
found = true;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (atRule.name === "import" && atRule.params.includes("tailwindcss")) {
|
|
166
|
+
found = true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
if (found) return true;
|
|
171
|
+
root.walkDecls((decl) => {
|
|
172
|
+
if (decl.value.includes("--alpha(") || decl.value.includes("--spacing(")) {
|
|
173
|
+
found = true;
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
function hasTailwindV3Signals(root) {
|
|
180
|
+
let found = false;
|
|
181
|
+
root.walkAtRules((atRule) => {
|
|
182
|
+
if (["tailwind", "config"].includes(atRule.name)) {
|
|
183
|
+
found = true;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (found) return true;
|
|
188
|
+
root.walkDecls((decl) => {
|
|
189
|
+
if (decl.value.includes("theme(")) {
|
|
190
|
+
found = true;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return found;
|
|
195
|
+
}
|
|
196
|
+
function detectTailwindVersion(input) {
|
|
197
|
+
const root = toRoot(input);
|
|
198
|
+
const hasV4 = hasTailwindV4Signals(root);
|
|
199
|
+
const hasV3 = hasTailwindV3Signals(root);
|
|
200
|
+
if (hasV4) return 4;
|
|
201
|
+
if (hasV3) return 3;
|
|
202
|
+
return "unknown";
|
|
203
|
+
}
|
|
204
|
+
function collectApplyCandidates(input) {
|
|
205
|
+
const root = toRoot(input);
|
|
206
|
+
const candidates = [];
|
|
207
|
+
root.walkAtRules("apply", (atRule) => {
|
|
208
|
+
for (const candidate of splitCandidateList(atRule.params)) candidates.push({
|
|
209
|
+
candidate,
|
|
210
|
+
node: atRule,
|
|
211
|
+
source: atRule.source
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return candidates;
|
|
215
|
+
}
|
|
216
|
+
function collectThemeCallsFromNode(node, rawValue, calls) {
|
|
217
|
+
valueParser(rawValue).walk((valueNode) => {
|
|
218
|
+
if (valueNode.type !== "function" || valueNode.value !== "theme") return;
|
|
219
|
+
const raw = valueParser.stringify(valueNode.nodes).trim();
|
|
220
|
+
const { path, opacity } = parseThemeArgument(raw);
|
|
221
|
+
calls.push({
|
|
222
|
+
raw,
|
|
223
|
+
path,
|
|
224
|
+
opacity,
|
|
225
|
+
node,
|
|
226
|
+
source: node.source
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function collectThemeCalls(input) {
|
|
231
|
+
const root = toRoot(input);
|
|
232
|
+
const calls = [];
|
|
233
|
+
root.walkDecls((decl) => {
|
|
234
|
+
collectThemeCallsFromNode(decl, decl.value, calls);
|
|
235
|
+
});
|
|
236
|
+
root.walkAtRules((atRule) => {
|
|
237
|
+
if (atRule.params.includes("theme(")) collectThemeCallsFromNode(atRule, atRule.params, calls);
|
|
238
|
+
});
|
|
239
|
+
return calls;
|
|
240
|
+
}
|
|
241
|
+
function collectImportDirectives(input) {
|
|
242
|
+
const root = toRoot(input);
|
|
243
|
+
const directives = [];
|
|
244
|
+
root.walkAtRules("import", (atRule) => {
|
|
245
|
+
if (!atRule.params.includes("tailwindcss")) return;
|
|
246
|
+
const parsed = valueParser(atRule.params);
|
|
247
|
+
let importTarget = "";
|
|
248
|
+
const functions = [];
|
|
249
|
+
let hasLayerKeyword = false;
|
|
250
|
+
let mediaQuery = null;
|
|
251
|
+
let sawImportTarget = false;
|
|
252
|
+
let mediaStartIndex = -1;
|
|
253
|
+
for (const [index, node] of parsed.nodes.entries()) {
|
|
254
|
+
if (node.type === "space") continue;
|
|
255
|
+
if (!sawImportTarget && (node.type === "string" || node.type === "function" && node.value === "url")) {
|
|
256
|
+
importTarget = stripImportTargetQuotes(valueParser.stringify(node.type === "function" ? node.nodes : [node]).trim());
|
|
257
|
+
sawImportTarget = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (node.type === "function" && TAILWIND_IMPORT_FUNCTIONS.includes(node.value)) {
|
|
261
|
+
functions.push({
|
|
262
|
+
name: node.value,
|
|
263
|
+
value: valueParser.stringify(node.nodes).trim()
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (node.type === "word" && node.value === "layer") {
|
|
268
|
+
hasLayerKeyword = true;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
mediaStartIndex = index;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
if (mediaStartIndex !== -1) mediaQuery = valueParser.stringify(parsed.nodes.slice(mediaStartIndex)).trim() || null;
|
|
275
|
+
directives.push({
|
|
276
|
+
importTarget,
|
|
277
|
+
functions,
|
|
278
|
+
hasLayerKeyword,
|
|
279
|
+
mediaQuery,
|
|
280
|
+
node: atRule,
|
|
281
|
+
source: atRule.source
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
return directives;
|
|
285
|
+
}
|
|
286
|
+
function collectUtilitySelectors(input) {
|
|
287
|
+
const root = toRoot(input);
|
|
288
|
+
const selectors = [];
|
|
289
|
+
root.walkRules((rule) => {
|
|
290
|
+
for (const className of collectClassNamesFromSelector(rule.selector)) selectors.push({
|
|
291
|
+
className,
|
|
292
|
+
rule,
|
|
293
|
+
selector: rule.selector,
|
|
294
|
+
source: rule.source
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
return selectors;
|
|
298
|
+
}
|
|
299
|
+
function analyzeTailwindCss(input) {
|
|
300
|
+
const root = toRoot(input);
|
|
301
|
+
return {
|
|
302
|
+
version: detectTailwindVersion(root),
|
|
303
|
+
applyCandidates: collectApplyCandidates(root),
|
|
304
|
+
themeCalls: collectThemeCalls(root),
|
|
305
|
+
importDirectives: collectImportDirectives(root),
|
|
306
|
+
utilitySelectors: collectUtilitySelectors(root)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/runtime-resolution.ts
|
|
311
|
+
const fileSystem = new enhancedResolve.CachedInputFileSystem(fs, 3e4);
|
|
312
|
+
const pathsMatcherCache = /* @__PURE__ */ new Map();
|
|
313
|
+
function getPathsMatcher(tsconfigPath) {
|
|
314
|
+
if (!tsconfigPath) return null;
|
|
315
|
+
const cached = pathsMatcherCache.get(tsconfigPath);
|
|
316
|
+
if (cached !== void 0) return cached;
|
|
317
|
+
let matcher = null;
|
|
318
|
+
try {
|
|
319
|
+
matcher = createPathsMatcher({
|
|
320
|
+
path: tsconfigPath,
|
|
321
|
+
config: parseTsconfig(tsconfigPath)
|
|
322
|
+
});
|
|
323
|
+
} catch {
|
|
324
|
+
const resolved = getTsconfig(path.dirname(tsconfigPath));
|
|
325
|
+
matcher = resolved ? createPathsMatcher(resolved) : null;
|
|
326
|
+
}
|
|
327
|
+
pathsMatcherCache.set(tsconfigPath, matcher);
|
|
328
|
+
return matcher;
|
|
329
|
+
}
|
|
330
|
+
function createResolver(options) {
|
|
331
|
+
return enhancedResolve.ResolverFactory.createResolver({
|
|
332
|
+
conditionNames: options.conditionNames,
|
|
333
|
+
extensions: options.extensions,
|
|
334
|
+
fileSystem,
|
|
335
|
+
...options.mainFields ? { mainFields: options.mainFields } : {},
|
|
336
|
+
useSyncFileSystemCalls: true
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function resolveWithResolver(resolver, cwd, request) {
|
|
340
|
+
try {
|
|
341
|
+
const resolved = resolver.resolveSync({}, cwd, request);
|
|
342
|
+
return typeof resolved === "string" ? resolved : null;
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function resolveWithTsconfigPaths(context, resolver, cwd, request) {
|
|
348
|
+
const matcher = getPathsMatcher(context.tsconfigPath);
|
|
349
|
+
if (!matcher) return null;
|
|
350
|
+
const matches = matcher(request);
|
|
351
|
+
if (!matches?.length) return null;
|
|
352
|
+
for (const match of matches) {
|
|
353
|
+
const resolved = resolveWithResolver(resolver, cwd, path.isAbsolute(match) ? match : path.resolve(cwd, match));
|
|
354
|
+
if (resolved) return resolved;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
function findFileRecursive(cwd, filenames) {
|
|
359
|
+
let currentDir = cwd;
|
|
360
|
+
while (true) {
|
|
361
|
+
for (const filename of filenames) {
|
|
362
|
+
const candidate = path.resolve(currentDir, filename);
|
|
363
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
364
|
+
}
|
|
365
|
+
const parentDir = path.resolve(currentDir, "..");
|
|
366
|
+
if (parentDir === currentDir) return null;
|
|
367
|
+
currentDir = parentDir;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function readTailwindVersion(packageJsonPath) {
|
|
371
|
+
if (!packageJsonPath || !fs.existsSync(packageJsonPath)) return null;
|
|
372
|
+
try {
|
|
373
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).version ?? null;
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function resolveTailwindRuntime(context) {
|
|
379
|
+
const jsResolver = createResolver({
|
|
380
|
+
conditionNames: ["node", "import"],
|
|
381
|
+
extensions: [".mjs", ".js"],
|
|
382
|
+
mainFields: ["module"]
|
|
383
|
+
});
|
|
384
|
+
const cssResolver = createResolver({
|
|
385
|
+
conditionNames: ["style"],
|
|
386
|
+
extensions: [".css"],
|
|
387
|
+
mainFields: ["style"]
|
|
388
|
+
});
|
|
389
|
+
const packageJsonPath = resolveWithResolver(jsResolver, context.cwd, "tailwindcss/package.json") ?? resolveWithTsconfigPaths(context, jsResolver, context.cwd, "tailwindcss/package.json");
|
|
390
|
+
const installationPath = packageJsonPath ? path.dirname(packageJsonPath) : null;
|
|
391
|
+
const cssEntryPath = resolveWithResolver(cssResolver, context.cwd, "tailwindcss") ?? resolveWithTsconfigPaths(context, cssResolver, context.cwd, "tailwindcss");
|
|
392
|
+
return {
|
|
393
|
+
configPath: findFileRecursive(context.cwd, TAILWIND_CONFIG_FILENAMES),
|
|
394
|
+
cssEntryPath,
|
|
395
|
+
installationPath,
|
|
396
|
+
packageJsonPath,
|
|
397
|
+
version: readTailwindVersion(packageJsonPath)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function detectInstalledTailwindVersion(context) {
|
|
401
|
+
const version = resolveTailwindRuntime(context).version;
|
|
402
|
+
if (!version) return "unknown";
|
|
403
|
+
if (version.startsWith("4.")) return 4;
|
|
404
|
+
if (version.startsWith("3.")) return 3;
|
|
405
|
+
return "unknown";
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
408
|
+
export { analyzeTailwindCss, collectApplyCandidates, collectImportDirectives, collectThemeCalls, collectUtilitySelectors, detectInstalledTailwindVersion, detectTailwindVersion, parseTailwindCss, resolveTailwindRuntime };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "postcss-tailwindcss",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "PostCSS 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/postcss-tailwindcss"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/sonofmagic/dev-configs/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dev-configs",
|
|
18
|
+
"icebreakers",
|
|
19
|
+
"postcss",
|
|
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
|
+
"enhanced-resolve": "^5.20.0",
|
|
40
|
+
"get-tsconfig": "^4.13.6",
|
|
41
|
+
"postcss": "^8.5.6",
|
|
42
|
+
"postcss-selector-parser": "^7.1.0",
|
|
43
|
+
"postcss-value-parser": "^4.2.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "tsdown --watch --sourcemap",
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:dev": "vitest",
|
|
54
|
+
"release": "pnpm publish",
|
|
55
|
+
"sync": "cnpm sync postcss-tailwindcss",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
58
|
+
}
|