@lark-apaas/fullstack-presets 1.1.22 → 1.1.23-alpha.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.
@@ -0,0 +1,140 @@
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
+ });
@@ -0,0 +1,41 @@
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;
@@ -0,0 +1,130 @@
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);
@@ -17,6 +17,7 @@ declare const _default: ({
17
17
  plugins: {
18
18
  'react-hooks': any;
19
19
  import: any;
20
+ tailwindcss: any;
20
21
  };
21
22
  settings: {
22
23
  'import/resolver': {
@@ -27,6 +28,9 @@ declare const _default: ({
27
28
  'import/parsers': {
28
29
  '@typescript-eslint/parser': string[];
29
30
  };
31
+ tailwindcss: {
32
+ callees: string[];
33
+ };
30
34
  };
31
35
  rules: any;
32
36
  } | {
@@ -5,6 +5,7 @@ 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');
8
9
  const fs = require('fs');
9
10
  const path = require('path');
10
11
  // 检查是否启用宽松 lint 模式
@@ -149,6 +150,7 @@ const baseConfig = {
149
150
  plugins: {
150
151
  'react-hooks': reactHooks,
151
152
  import: importPlugin,
153
+ tailwindcss: tailwindPlugin,
152
154
  ...looseSpecificPlugins,
153
155
  },
154
156
  settings: {
@@ -160,6 +162,12 @@ const baseConfig = {
160
162
  'import/parsers': {
161
163
  '@typescript-eslint/parser': ['.ts', '.tsx'],
162
164
  },
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
+ },
163
171
  },
164
172
  rules: {
165
173
  // React Hooks 推荐规则
@@ -173,6 +181,14 @@ const baseConfig = {
173
181
  '@lark-apaas/no-styled-jsx-data-uri-url-ref': 'error',
174
182
  // styled-jsx + 多行模板 className → 下游 transpiler 可能产出 "Unterminated string literal"
175
183
  '@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',
176
192
  // 自定义规则:HTTP 调用响应类型必须定义在 shared/(默认关闭,需 y 位升级后对新应用开启)
177
193
  '@lark-apaas/require-shared-request-type': 'off',
178
194
  // TypeScript 规则
@@ -16,6 +16,7 @@ export declare const eslintPresets: {
16
16
  plugins: {
17
17
  'react-hooks': any;
18
18
  import: any;
19
+ tailwindcss: any;
19
20
  };
20
21
  settings: {
21
22
  'import/resolver': {
@@ -26,6 +27,9 @@ export declare const eslintPresets: {
26
27
  'import/parsers': {
27
28
  '@typescript-eslint/parser': string[];
28
29
  };
30
+ tailwindcss: {
31
+ callees: string[];
32
+ };
29
33
  };
30
34
  rules: any;
31
35
  } | {
@@ -3,5 +3,6 @@ export declare const stylelintPresets: {
3
3
  rules: {
4
4
  'declaration-block-no-duplicate-custom-properties': boolean;
5
5
  "custom/hsl-valid-value": true | null;
6
+ "custom/tailwind-theme-v4-required": boolean | null;
6
7
  };
7
8
  };
@@ -35,11 +35,13 @@ 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"));
38
39
  const isLooseMode = process.env.FORCE_FRAMEWORK_LINT_LOOSE_MODE === 'true';
39
40
  exports.stylelintPresets = {
40
- plugins: [hsl_variable_1.default],
41
+ plugins: [hsl_variable_1.default, tailwind_theme_v4_required_1.default],
41
42
  rules: {
42
43
  'declaration-block-no-duplicate-custom-properties': true,
43
44
  [hsl_variable_1.ruleName]: isLooseMode || null,
45
+ [tailwind_theme_v4_required_1.ruleName]: isLooseMode ? null : true,
44
46
  },
45
47
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-presets",
3
- "version": "1.1.22",
3
+ "version": "1.1.23-alpha.0",
4
4
  "files": [
5
5
  "lib"
6
6
  ],
@@ -31,6 +31,7 @@
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",
34
35
  "globals": "^16.4.0",
35
36
  "stylelint": "^17.3.0",
36
37
  "tailwindcss-animate": "^1.0.7"