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 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 插件是在其上叠加规则策略和报错逻辑。
@@ -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
+ }