@lark-apaas/fullstack-presets 1.1.23-alpha.0 → 1.1.23-beta.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/lib/custom-eslint-rules/no-styled-jsx-data-uri-url-ref.d.ts +33 -13
- package/lib/custom-eslint-rules/no-styled-jsx-data-uri-url-ref.js +21 -10
- package/lib/simple/recommend/eslint/eslint-client.d.ts +0 -4
- package/lib/simple/recommend/eslint/eslint-client.js +0 -16
- package/lib/simple/recommend/eslint/index.d.ts +0 -4
- package/lib/simple/recommend/stylelint/index.d.ts +0 -1
- package/lib/simple/recommend/stylelint/index.js +1 -3
- package/package.json +1 -2
- package/lib/custom-stylelint-rules/__test__/tailwind-theme-v4-required.test.d.ts +0 -1
- package/lib/custom-stylelint-rules/__test__/tailwind-theme-v4-required.test.js +0 -140
- package/lib/custom-stylelint-rules/tailwind-theme-v4-required.d.ts +0 -41
- package/lib/custom-stylelint-rules/tailwind-theme-v4-required.js +0 -130
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ESLint rule: no-styled-jsx-data-uri-url-ref
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Catches `url("data:...")` atoms inside `<style jsx>` whose body contains
|
|
5
|
+
* raw `(` or `)` characters. The stylis CSS parser (used by styled-jsx)
|
|
6
|
+
* tracks parenthesis depth while it walks the surrounding `url(...)` token,
|
|
7
|
+
* so any unescaped paren in the data URI body throws bracket matching off
|
|
8
|
+
* and causes a "Nesting detected" build failure on a perfectly valid
|
|
9
|
+
* sibling declaration further down the stylesheet.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
* instead of letting it escape to build time.
|
|
11
|
+
* Two observed flavours:
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* 1. Inline `url(#id)` references (e.g. SVG noise textures with
|
|
14
|
+
* `filter='url(%23noise)'`). This was the original 04-06~04-12 wave
|
|
15
|
+
* (107 cases / week, see referenced docs).
|
|
16
|
+
*
|
|
17
|
+
* 2. Inline CSS functions used as SVG attribute values, e.g.
|
|
18
|
+
* `fill='hsl(40,30%25,97%25)'`, `transform='translate(10,20)'`. These
|
|
19
|
+
* contain bare `(` `)` but no nested `url(`, so they slipped past the
|
|
20
|
+
* earlier nested-url-only check. W21 (2026-05-18~24) saw ~11 such cases
|
|
21
|
+
* even though the rule was already enabled — same stylis bug, different
|
|
22
|
+
* surface.
|
|
23
|
+
*
|
|
24
|
+
* This rule surfaces both at lint time (IDE red squiggly + CI gate) instead
|
|
25
|
+
* of letting them escape to build time.
|
|
14
26
|
*
|
|
15
27
|
* @see ~/docs/styled-jsx-nesting-bug-analysis-20260330.md
|
|
28
|
+
* @see ~/docs/miaoda-precommit-ts2307-rootcause-20260525.md
|
|
16
29
|
* @see https://bytedance.larkoffice.com/wiki/TTNDwzgLzi9Q0skTUT5clFX3nNd
|
|
17
30
|
*/
|
|
18
31
|
import type { Rule } from 'eslint';
|
|
19
32
|
/**
|
|
20
33
|
* Precise detection: scan the CSS text character-by-character, looking for
|
|
21
|
-
* `url(<quote>data:...<quote>)` atoms. For each such atom,
|
|
22
|
-
* body (between the matching quotes) contains `
|
|
23
|
-
*
|
|
24
|
-
* the
|
|
25
|
-
*
|
|
34
|
+
* `url(<quote>data:...<quote>)` atoms. For each such atom, return true if
|
|
35
|
+
* the body (between the matching quotes) contains a raw `(` or `)` — those
|
|
36
|
+
* characters confuse the stylis tokenizer's paren counter and cause it to
|
|
37
|
+
* misattribute the bug to a later, unrelated declaration.
|
|
38
|
+
*
|
|
39
|
+
* A data URI body should never contain raw parens — they belong either
|
|
40
|
+
* URL-encoded (`%28`/`%29`) or, better, in a sibling `.svg` file referenced
|
|
41
|
+
* by external `url(/foo.svg)`.
|
|
26
42
|
*
|
|
27
43
|
* Safety properties (can't hang / crash lint):
|
|
28
44
|
* - Single forward scan, O(n); `i` always advances past every candidate.
|
|
@@ -32,6 +48,10 @@ import type { Rule } from 'eslint';
|
|
|
32
48
|
* - Bails safely (returns false) on unterminated strings.
|
|
33
49
|
* - Zero regex backtracking; no catastrophic-regex risk.
|
|
34
50
|
*
|
|
51
|
+
* The function name is kept for backwards compatibility (existing imports);
|
|
52
|
+
* the implementation now flags any paren-bearing data URI body, not just
|
|
53
|
+
* the historical `url(...)` nested case.
|
|
54
|
+
*
|
|
35
55
|
* @param cssText the full CSS source of a single <style jsx> block
|
|
36
56
|
*/
|
|
37
57
|
export declare function hasDataUriWithNestedUrl(cssText: string): boolean;
|
|
@@ -3,11 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.hasDataUriWithNestedUrl = hasDataUriWithNestedUrl;
|
|
4
4
|
/**
|
|
5
5
|
* Precise detection: scan the CSS text character-by-character, looking for
|
|
6
|
-
* `url(<quote>data:...<quote>)` atoms. For each such atom,
|
|
7
|
-
* body (between the matching quotes) contains `
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
6
|
+
* `url(<quote>data:...<quote>)` atoms. For each such atom, return true if
|
|
7
|
+
* the body (between the matching quotes) contains a raw `(` or `)` — those
|
|
8
|
+
* characters confuse the stylis tokenizer's paren counter and cause it to
|
|
9
|
+
* misattribute the bug to a later, unrelated declaration.
|
|
10
|
+
*
|
|
11
|
+
* A data URI body should never contain raw parens — they belong either
|
|
12
|
+
* URL-encoded (`%28`/`%29`) or, better, in a sibling `.svg` file referenced
|
|
13
|
+
* by external `url(/foo.svg)`.
|
|
11
14
|
*
|
|
12
15
|
* Safety properties (can't hang / crash lint):
|
|
13
16
|
* - Single forward scan, O(n); `i` always advances past every candidate.
|
|
@@ -17,6 +20,10 @@ exports.hasDataUriWithNestedUrl = hasDataUriWithNestedUrl;
|
|
|
17
20
|
* - Bails safely (returns false) on unterminated strings.
|
|
18
21
|
* - Zero regex backtracking; no catastrophic-regex risk.
|
|
19
22
|
*
|
|
23
|
+
* The function name is kept for backwards compatibility (existing imports);
|
|
24
|
+
* the implementation now flags any paren-bearing data URI body, not just
|
|
25
|
+
* the historical `url(...)` nested case.
|
|
26
|
+
*
|
|
20
27
|
* @param cssText the full CSS source of a single <style jsx> block
|
|
21
28
|
*/
|
|
22
29
|
function hasDataUriWithNestedUrl(cssText) {
|
|
@@ -64,8 +71,11 @@ function hasDataUriWithNestedUrl(cssText) {
|
|
|
64
71
|
if (!terminated)
|
|
65
72
|
return false; // unterminated data URI — bail safely
|
|
66
73
|
const body = cssText.slice(bodyStart, j);
|
|
67
|
-
//
|
|
68
|
-
|
|
74
|
+
// Any raw `(` or `)` in the body confuses the stylis paren counter.
|
|
75
|
+
// (Nested `url(...)` is a subset of this — the original 04-06 wave.)
|
|
76
|
+
// Inline CSS-function attribute values like `hsl(...)` or
|
|
77
|
+
// `transform='translate(...)'` are the W21 surface we missed before.
|
|
78
|
+
if (body.indexOf('(') !== -1 || body.indexOf(')') !== -1)
|
|
69
79
|
return true;
|
|
70
80
|
// Move past this data URI and keep scanning for more url() atoms.
|
|
71
81
|
i = j + 1;
|
|
@@ -82,9 +92,10 @@ const rule = {
|
|
|
82
92
|
},
|
|
83
93
|
schema: [],
|
|
84
94
|
messages: {
|
|
85
|
-
dataUriUrlRef: '`url("data:...")`
|
|
86
|
-
'
|
|
87
|
-
'
|
|
95
|
+
dataUriUrlRef: '`url("data:...")` 的内容里含有未转义的括号 `(` 或 `)`(如 `url(#noise)` / `hsl(...)` / `translate(...)`),' +
|
|
96
|
+
'会让 styled-jsx 的 CSS 解析器(stylis)误算括号深度,把后面合法的 CSS 规则报成 "Nesting detected" 构建错误。' +
|
|
97
|
+
'建议将 SVG 移到独立 .svg 文件通过 import 引用(或 `client/public/*.svg` + 外链 background-image),' +
|
|
98
|
+
'不要把含 CSS 函数的 SVG 属性值内联到 data URI 里。',
|
|
88
99
|
},
|
|
89
100
|
},
|
|
90
101
|
create(context) {
|
|
@@ -17,7 +17,6 @@ declare const _default: ({
|
|
|
17
17
|
plugins: {
|
|
18
18
|
'react-hooks': any;
|
|
19
19
|
import: any;
|
|
20
|
-
tailwindcss: any;
|
|
21
20
|
};
|
|
22
21
|
settings: {
|
|
23
22
|
'import/resolver': {
|
|
@@ -28,9 +27,6 @@ declare const _default: ({
|
|
|
28
27
|
'import/parsers': {
|
|
29
28
|
'@typescript-eslint/parser': string[];
|
|
30
29
|
};
|
|
31
|
-
tailwindcss: {
|
|
32
|
-
callees: string[];
|
|
33
|
-
};
|
|
34
30
|
};
|
|
35
31
|
rules: any;
|
|
36
32
|
} | {
|
|
@@ -5,7 +5,6 @@ const globals = require('globals');
|
|
|
5
5
|
const reactHooks = require('eslint-plugin-react-hooks');
|
|
6
6
|
const tseslint = require('typescript-eslint');
|
|
7
7
|
const importPlugin = require('eslint-plugin-import');
|
|
8
|
-
const tailwindPlugin = require('eslint-plugin-tailwindcss');
|
|
9
8
|
const fs = require('fs');
|
|
10
9
|
const path = require('path');
|
|
11
10
|
// 检查是否启用宽松 lint 模式
|
|
@@ -150,7 +149,6 @@ const baseConfig = {
|
|
|
150
149
|
plugins: {
|
|
151
150
|
'react-hooks': reactHooks,
|
|
152
151
|
import: importPlugin,
|
|
153
|
-
tailwindcss: tailwindPlugin,
|
|
154
152
|
...looseSpecificPlugins,
|
|
155
153
|
},
|
|
156
154
|
settings: {
|
|
@@ -162,12 +160,6 @@ const baseConfig = {
|
|
|
162
160
|
'import/parsers': {
|
|
163
161
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
|
164
162
|
},
|
|
165
|
-
// eslint-plugin-tailwindcss v4+ 默认会自动发现 tailwind config(v4 用 CSS @theme)
|
|
166
|
-
// callees 列出运行时会拼 className 的工具函数;plugin 会扫这些函数的字符串参数
|
|
167
|
-
tailwindcss: {
|
|
168
|
-
callees: ['classnames', 'clsx', 'cn', 'cva', 'twMerge'],
|
|
169
|
-
// 用户工程可自定义 entryPoint(默认探测 tailwind.config.* 与 @config 指令)
|
|
170
|
-
},
|
|
171
163
|
},
|
|
172
164
|
rules: {
|
|
173
165
|
// React Hooks 推荐规则
|
|
@@ -181,14 +173,6 @@ const baseConfig = {
|
|
|
181
173
|
'@lark-apaas/no-styled-jsx-data-uri-url-ref': 'error',
|
|
182
174
|
// styled-jsx + 多行模板 className → 下游 transpiler 可能产出 "Unterminated string literal"
|
|
183
175
|
'@lark-apaas/no-multiline-styled-jsx-classname': 'error',
|
|
184
|
-
// Tailwind className 校验:拦截 AI 自创、tailwind.config 没声明的 utility class
|
|
185
|
-
// W21 数据:22 条 Tailwind 失败里有 9 条是 AI 凭空起名(brutal-shadow / label-micro / divider-accent 等)
|
|
186
|
-
// 此前 lint 链路里没有任何工具检查 className 字符串,错误一直要到 vite build PostCSS 才报
|
|
187
|
-
'tailwindcss/no-custom-classname': 'error',
|
|
188
|
-
// 同类 utility 冲突(如 p-2 + p-4 同时出现)
|
|
189
|
-
'tailwindcss/no-contradicting-classname': 'error',
|
|
190
|
-
// class 排序规则噪音大且不影响功能,关闭
|
|
191
|
-
'tailwindcss/classnames-order': 'off',
|
|
192
176
|
// 自定义规则:HTTP 调用响应类型必须定义在 shared/(默认关闭,需 y 位升级后对新应用开启)
|
|
193
177
|
'@lark-apaas/require-shared-request-type': 'off',
|
|
194
178
|
// TypeScript 规则
|
|
@@ -16,7 +16,6 @@ export declare const eslintPresets: {
|
|
|
16
16
|
plugins: {
|
|
17
17
|
'react-hooks': any;
|
|
18
18
|
import: any;
|
|
19
|
-
tailwindcss: any;
|
|
20
19
|
};
|
|
21
20
|
settings: {
|
|
22
21
|
'import/resolver': {
|
|
@@ -27,9 +26,6 @@ export declare const eslintPresets: {
|
|
|
27
26
|
'import/parsers': {
|
|
28
27
|
'@typescript-eslint/parser': string[];
|
|
29
28
|
};
|
|
30
|
-
tailwindcss: {
|
|
31
|
-
callees: string[];
|
|
32
|
-
};
|
|
33
29
|
};
|
|
34
30
|
rules: any;
|
|
35
31
|
} | {
|
|
@@ -35,13 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.stylelintPresets = void 0;
|
|
37
37
|
const hsl_variable_1 = __importStar(require("../../../custom-stylelint-rules/hsl-variable"));
|
|
38
|
-
const tailwind_theme_v4_required_1 = __importStar(require("../../../custom-stylelint-rules/tailwind-theme-v4-required"));
|
|
39
38
|
const isLooseMode = process.env.FORCE_FRAMEWORK_LINT_LOOSE_MODE === 'true';
|
|
40
39
|
exports.stylelintPresets = {
|
|
41
|
-
plugins: [hsl_variable_1.default
|
|
40
|
+
plugins: [hsl_variable_1.default],
|
|
42
41
|
rules: {
|
|
43
42
|
'declaration-block-no-duplicate-custom-properties': true,
|
|
44
43
|
[hsl_variable_1.ruleName]: isLooseMode || null,
|
|
45
|
-
[tailwind_theme_v4_required_1.ruleName]: isLooseMode ? null : true,
|
|
46
44
|
},
|
|
47
45
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/fullstack-presets",
|
|
3
|
-
"version": "1.1.23-
|
|
3
|
+
"version": "1.1.23-beta.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"lib"
|
|
6
6
|
],
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"eslint-plugin-import": "^2.32.0",
|
|
32
32
|
"eslint-plugin-react": "^7.37.5",
|
|
33
33
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
34
|
-
"eslint-plugin-tailwindcss": "^4.0.0-beta.0",
|
|
35
34
|
"globals": "^16.4.0",
|
|
36
35
|
"stylelint": "^17.3.0",
|
|
37
36
|
"tailwindcss-animate": "^1.0.7"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
const vitest_1 = require("vitest");
|
|
40
|
-
const stylelint_1 = __importDefault(require("stylelint"));
|
|
41
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
-
const tailwind_theme_v4_required_1 = __importStar(require("../tailwind-theme-v4-required"));
|
|
43
|
-
async function lint(code, codeFilename = 'tailwind-theme.css') {
|
|
44
|
-
return stylelint_1.default.lint({
|
|
45
|
-
code,
|
|
46
|
-
codeFilename: node_path_1.default.resolve(process.cwd(), codeFilename),
|
|
47
|
-
config: {
|
|
48
|
-
plugins: [tailwind_theme_v4_required_1.default],
|
|
49
|
-
rules: {
|
|
50
|
-
[tailwind_theme_v4_required_1.ruleName]: true,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
(0, vitest_1.describe)('custom/tailwind-theme-v4-required', () => {
|
|
56
|
-
(0, vitest_1.it)('passes on v4 syntax (@theme inline + token map)', async () => {
|
|
57
|
-
const result = await lint(`
|
|
58
|
-
:root {
|
|
59
|
-
--background: hsl(0 0% 100%);
|
|
60
|
-
--primary: hsl(221 83% 53%);
|
|
61
|
-
--border: hsl(220 14% 89%);
|
|
62
|
-
}
|
|
63
|
-
@theme inline {
|
|
64
|
-
--color-background: var(--background);
|
|
65
|
-
--color-primary: var(--primary);
|
|
66
|
-
--color-border: var(--border);
|
|
67
|
-
}
|
|
68
|
-
`);
|
|
69
|
-
(0, vitest_1.expect)(result.results[0].warnings).toEqual([]);
|
|
70
|
-
(0, vitest_1.expect)(result.errored).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
(0, vitest_1.it)('passes on plain @theme block (no inline)', async () => {
|
|
73
|
-
const result = await lint(`
|
|
74
|
-
@theme {
|
|
75
|
-
--color-background: hsl(0 0% 100%);
|
|
76
|
-
--color-primary: hsl(221 83% 53%);
|
|
77
|
-
--color-border: hsl(220 14% 89%);
|
|
78
|
-
}
|
|
79
|
-
`);
|
|
80
|
-
(0, vitest_1.expect)(result.results[0].warnings).toEqual([]);
|
|
81
|
-
(0, vitest_1.expect)(result.errored).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
(0, vitest_1.it)('flags v3 @tailwind directives', async () => {
|
|
84
|
-
const result = await lint(`
|
|
85
|
-
@tailwind base;
|
|
86
|
-
@tailwind components;
|
|
87
|
-
@tailwind utilities;
|
|
88
|
-
:root {
|
|
89
|
-
--background: oklch(1 0 0);
|
|
90
|
-
}
|
|
91
|
-
`);
|
|
92
|
-
// 3 个 @tailwind 指令各报一次
|
|
93
|
-
(0, vitest_1.expect)(result.results[0].warnings.length).toBeGreaterThanOrEqual(3);
|
|
94
|
-
(0, vitest_1.expect)(result.results[0].warnings.some(w => w.text.includes('Tailwind v4'))).toBe(true);
|
|
95
|
-
(0, vitest_1.expect)(result.errored).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
(0, vitest_1.it)('flags shadcn tokens without @theme block', async () => {
|
|
98
|
-
const result = await lint(`
|
|
99
|
-
:root {
|
|
100
|
-
--background: oklch(1 0 0);
|
|
101
|
-
--foreground: oklch(0.141 0.005 285.823);
|
|
102
|
-
--primary: oklch(0.685 0.168 243.8);
|
|
103
|
-
--border: oklch(0.92 0.004 286.32);
|
|
104
|
-
}
|
|
105
|
-
`);
|
|
106
|
-
(0, vitest_1.expect)(result.results[0].warnings.length).toBe(1);
|
|
107
|
-
(0, vitest_1.expect)(result.results[0].warnings[0].text).toContain('missing the `@theme`');
|
|
108
|
-
(0, vitest_1.expect)(result.errored).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
(0, vitest_1.it)('skips files whose name does not contain tailwind-theme', async () => {
|
|
111
|
-
const result = await lint(`@tailwind base; :root { --background: white; }`, 'random-stylesheet.css');
|
|
112
|
-
(0, vitest_1.expect)(result.results[0].warnings).toEqual([]);
|
|
113
|
-
(0, vitest_1.expect)(result.errored).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
(0, vitest_1.it)('reproduces W21 failing case (app_4k6tdqaryjmzr style v3 regression)', async () => {
|
|
116
|
-
// 真实 W21 失败 case 的退化写法
|
|
117
|
-
const result = await lint(`
|
|
118
|
-
@tailwind base;
|
|
119
|
-
@tailwind components;
|
|
120
|
-
@tailwind utilities;
|
|
121
|
-
|
|
122
|
-
@layer base {
|
|
123
|
-
:root {
|
|
124
|
-
--background: oklch(1 0 0);
|
|
125
|
-
--foreground: oklch(0.141 0.005 285.823);
|
|
126
|
-
--primary: oklch(0.685 0.168 243.8);
|
|
127
|
-
--border: oklch(0.92 0.004 286.32);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@layer base {
|
|
132
|
-
* { border-color: var(--border); }
|
|
133
|
-
}
|
|
134
|
-
`);
|
|
135
|
-
(0, vitest_1.expect)(result.errored).toBe(true);
|
|
136
|
-
// 至少 3 个 @tailwind 指令报错
|
|
137
|
-
const v3Errors = result.results[0].warnings.filter(w => w.text.includes('Tailwind v4'));
|
|
138
|
-
(0, vitest_1.expect)(v3Errors.length).toBeGreaterThanOrEqual(3);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 自定义 Stylelint 规则:禁止 tailwind-theme.css 退化到 Tailwind v3 写法
|
|
3
|
-
*
|
|
4
|
-
* 背景:
|
|
5
|
-
* 模板(nestjs-react-fullstack-template 等)2025-09-30 起已迁移到 Tailwind v4,
|
|
6
|
-
* `client/src/tailwind-theme.css` 用 `@theme inline { --color-* }` 块把 shadcn
|
|
7
|
-
* token 映射成 v4 utility(让 `border-border` `bg-primary` 等正常工作)。
|
|
8
|
-
*
|
|
9
|
-
* W21 数据观察到 AI 在用户工程里**主动重写** tailwind-theme.css 时退化到
|
|
10
|
-
* v3 写法:使用 `@tailwind base/components/utilities` 指令 + 删除
|
|
11
|
-
* `@theme inline` 块,导致 v4 下所有依赖 token 映射的 utility(如
|
|
12
|
-
* `border-border`、`bg-primary`、`focus:border-primary`)都报
|
|
13
|
-
* "Cannot apply unknown utility class",pre-commit lint 跑不到 className
|
|
14
|
-
* 层面,构建沙箱阶段才报错。
|
|
15
|
-
*
|
|
16
|
-
* 这条规则在 stylelint 阶段直接拦住 tailwind-theme.css 的退化写法。
|
|
17
|
-
*
|
|
18
|
-
* 检测的退化标志(任一命中即报错):
|
|
19
|
-
* 1. 出现 `@tailwind base/components/utilities/screens/variants` 指令
|
|
20
|
-
* (这些是 Tailwind v3 入口指令,v4 用 `@import "tailwindcss"` + `@theme`)
|
|
21
|
-
* 2. 文件中有任何 token 定义(如 `--background: ...`、`--primary: ...`)
|
|
22
|
-
* 但全文不含 `@theme` 块(v4 必备的 token 映射入口)
|
|
23
|
-
*
|
|
24
|
-
* 匹配文件:默认只对文件名包含 `tailwind-theme` 的 .css 生效;
|
|
25
|
-
* 通过 secondary options.filePattern 可自定义匹配模式。
|
|
26
|
-
*
|
|
27
|
-
* 修复指引:把 `:root { --xxx }` token 保留,删掉 `@tailwind ...` 指令,
|
|
28
|
-
* 并新增 `@theme inline { --color-xxx: var(--xxx); ... }` 块,
|
|
29
|
-
* 完整范例参考 nestjs-react-fullstack-template/client/src/tailwind-theme.css。
|
|
30
|
-
*/
|
|
31
|
-
import stylelint from 'stylelint';
|
|
32
|
-
export declare const ruleName = "custom/tailwind-theme-v4-required";
|
|
33
|
-
export declare const messages: {
|
|
34
|
-
v3Directive: (directive: string) => string;
|
|
35
|
-
missingThemeBlock: string;
|
|
36
|
-
};
|
|
37
|
-
export declare const meta: {
|
|
38
|
-
url: string;
|
|
39
|
-
};
|
|
40
|
-
declare const _default: stylelint.Plugin;
|
|
41
|
-
export default _default;
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.meta = exports.messages = exports.ruleName = void 0;
|
|
7
|
-
// @ts-nocheck
|
|
8
|
-
/**
|
|
9
|
-
* 自定义 Stylelint 规则:禁止 tailwind-theme.css 退化到 Tailwind v3 写法
|
|
10
|
-
*
|
|
11
|
-
* 背景:
|
|
12
|
-
* 模板(nestjs-react-fullstack-template 等)2025-09-30 起已迁移到 Tailwind v4,
|
|
13
|
-
* `client/src/tailwind-theme.css` 用 `@theme inline { --color-* }` 块把 shadcn
|
|
14
|
-
* token 映射成 v4 utility(让 `border-border` `bg-primary` 等正常工作)。
|
|
15
|
-
*
|
|
16
|
-
* W21 数据观察到 AI 在用户工程里**主动重写** tailwind-theme.css 时退化到
|
|
17
|
-
* v3 写法:使用 `@tailwind base/components/utilities` 指令 + 删除
|
|
18
|
-
* `@theme inline` 块,导致 v4 下所有依赖 token 映射的 utility(如
|
|
19
|
-
* `border-border`、`bg-primary`、`focus:border-primary`)都报
|
|
20
|
-
* "Cannot apply unknown utility class",pre-commit lint 跑不到 className
|
|
21
|
-
* 层面,构建沙箱阶段才报错。
|
|
22
|
-
*
|
|
23
|
-
* 这条规则在 stylelint 阶段直接拦住 tailwind-theme.css 的退化写法。
|
|
24
|
-
*
|
|
25
|
-
* 检测的退化标志(任一命中即报错):
|
|
26
|
-
* 1. 出现 `@tailwind base/components/utilities/screens/variants` 指令
|
|
27
|
-
* (这些是 Tailwind v3 入口指令,v4 用 `@import "tailwindcss"` + `@theme`)
|
|
28
|
-
* 2. 文件中有任何 token 定义(如 `--background: ...`、`--primary: ...`)
|
|
29
|
-
* 但全文不含 `@theme` 块(v4 必备的 token 映射入口)
|
|
30
|
-
*
|
|
31
|
-
* 匹配文件:默认只对文件名包含 `tailwind-theme` 的 .css 生效;
|
|
32
|
-
* 通过 secondary options.filePattern 可自定义匹配模式。
|
|
33
|
-
*
|
|
34
|
-
* 修复指引:把 `:root { --xxx }` token 保留,删掉 `@tailwind ...` 指令,
|
|
35
|
-
* 并新增 `@theme inline { --color-xxx: var(--xxx); ... }` 块,
|
|
36
|
-
* 完整范例参考 nestjs-react-fullstack-template/client/src/tailwind-theme.css。
|
|
37
|
-
*/
|
|
38
|
-
const stylelint_1 = __importDefault(require("stylelint"));
|
|
39
|
-
exports.ruleName = 'custom/tailwind-theme-v4-required';
|
|
40
|
-
const { report, ruleMessages, validateOptions } = stylelint_1.default.utils;
|
|
41
|
-
exports.messages = ruleMessages(exports.ruleName, {
|
|
42
|
-
v3Directive: (directive) => `tailwind-theme.css must use Tailwind v4 syntax. Remove the v3 directive \`@tailwind ${directive}\` and use \`@theme\` block instead. See https://tailwindcss.com/docs/v4-beta#migrating-from-v3`,
|
|
43
|
-
missingThemeBlock: 'tailwind-theme.css defines CSS tokens but is missing the `@theme` (or `@theme inline`) block that maps them to v4 utilities. Without it, utility classes like `border-border`/`bg-primary` will not work. Add a `@theme inline { --color-*: var(--*); ... }` block.',
|
|
44
|
-
});
|
|
45
|
-
exports.meta = {
|
|
46
|
-
url: 'https://github.com/bytedance/fullstack-plugin/tree/main/packages/tools/fullstack-presets/src/custom-stylelint-rules/tailwind-theme-v4-required.ts',
|
|
47
|
-
};
|
|
48
|
-
const V3_DIRECTIVES = new Set([
|
|
49
|
-
'tailwind',
|
|
50
|
-
]);
|
|
51
|
-
// Tokens that strongly indicate this is a shadcn / tailwind theme file
|
|
52
|
-
// (rather than an unrelated stylesheet that happens to use CSS variables).
|
|
53
|
-
const SHADCN_TOKEN_HINTS = [
|
|
54
|
-
'--background',
|
|
55
|
-
'--foreground',
|
|
56
|
-
'--primary',
|
|
57
|
-
'--secondary',
|
|
58
|
-
'--accent',
|
|
59
|
-
'--destructive',
|
|
60
|
-
'--border',
|
|
61
|
-
'--ring',
|
|
62
|
-
];
|
|
63
|
-
const rule = (primary, secondary) => {
|
|
64
|
-
return (root, result) => {
|
|
65
|
-
const validOptions = validateOptions(result, exports.ruleName, {
|
|
66
|
-
actual: primary,
|
|
67
|
-
possible: [true],
|
|
68
|
-
}, {
|
|
69
|
-
actual: secondary,
|
|
70
|
-
possible: {
|
|
71
|
-
filePattern: [v => typeof v === 'string'],
|
|
72
|
-
},
|
|
73
|
-
optional: true,
|
|
74
|
-
});
|
|
75
|
-
if (!validOptions || !primary)
|
|
76
|
-
return;
|
|
77
|
-
const sourceFile = root.source?.input?.file ?? '';
|
|
78
|
-
const filePattern = secondary?.filePattern ?? 'tailwind-theme';
|
|
79
|
-
// Only apply rule to files matching the configured pattern (filename substring match).
|
|
80
|
-
if (!sourceFile.includes(filePattern))
|
|
81
|
-
return;
|
|
82
|
-
let hasV3Directive = false;
|
|
83
|
-
let hasThemeBlock = false;
|
|
84
|
-
let hasShadcnToken = false;
|
|
85
|
-
// 1) 扫所有 @tailwind / @theme at-rules
|
|
86
|
-
root.walkAtRules(atRule => {
|
|
87
|
-
const name = atRule.name.toLowerCase();
|
|
88
|
-
if (V3_DIRECTIVES.has(name)) {
|
|
89
|
-
const params = atRule.params.trim().split(/\s+/)[0]?.toLowerCase() ?? '';
|
|
90
|
-
// 仅 @tailwind base / components / utilities / screens / variants 才是 v3 入口指令;
|
|
91
|
-
// @tailwind 后跟其他内容(不太可能)也一并拦下。
|
|
92
|
-
hasV3Directive = true;
|
|
93
|
-
report({
|
|
94
|
-
message: exports.messages.v3Directive(params || atRule.params),
|
|
95
|
-
node: atRule,
|
|
96
|
-
result,
|
|
97
|
-
ruleName: exports.ruleName,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (name === 'theme') {
|
|
101
|
-
hasThemeBlock = true;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
// 2) 扫所有声明里的 token,标记是否含 shadcn 风格 token
|
|
105
|
-
if (!hasThemeBlock) {
|
|
106
|
-
root.walkDecls(decl => {
|
|
107
|
-
if (hasShadcnToken)
|
|
108
|
-
return;
|
|
109
|
-
if (!decl.prop.startsWith('--'))
|
|
110
|
-
return;
|
|
111
|
-
if (SHADCN_TOKEN_HINTS.some(hint => decl.prop === hint)) {
|
|
112
|
-
hasShadcnToken = true;
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
if (hasShadcnToken) {
|
|
116
|
-
// 文件有 shadcn token 但没 @theme 块 → v3 残留 / 缺映射
|
|
117
|
-
report({
|
|
118
|
-
message: exports.messages.missingThemeBlock,
|
|
119
|
-
node: root,
|
|
120
|
-
result,
|
|
121
|
-
ruleName: exports.ruleName,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
};
|
|
127
|
-
rule.ruleName = exports.ruleName;
|
|
128
|
-
rule.messages = exports.messages;
|
|
129
|
-
rule.meta = exports.meta;
|
|
130
|
-
exports.default = stylelint_1.default.createPlugin(exports.ruleName, rule);
|