@atlaskit/eslint-plugin-platform 2.9.1 → 2.9.3
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/CHANGELOG.md +18 -0
- package/dist/cjs/index.js +15 -3
- package/dist/cjs/rules/compiled/expand-motion-shorthand/index.js +281 -0
- package/dist/cjs/rules/compiled/no-css-prop-in-object-spread/index.js +162 -0
- package/dist/cjs/rules/compiled/use-motion-token-values/index.js +506 -0
- package/dist/cjs/rules/editor-example-type-import-required/index.js +321 -0
- package/dist/cjs/rules/no-xcss-in-cx/index.js +221 -0
- package/dist/cjs/rules/visit-example-type-import-required/index.js +23 -13
- package/dist/es2019/index.js +15 -3
- package/dist/es2019/rules/compiled/expand-motion-shorthand/index.js +239 -0
- package/dist/es2019/rules/compiled/no-css-prop-in-object-spread/index.js +136 -0
- package/dist/es2019/rules/compiled/use-motion-token-values/index.js +444 -0
- package/dist/es2019/rules/editor-example-type-import-required/index.js +286 -0
- package/dist/es2019/rules/no-xcss-in-cx/index.js +187 -0
- package/dist/es2019/rules/visit-example-type-import-required/index.js +23 -14
- package/dist/esm/index.js +15 -3
- package/dist/esm/rules/compiled/expand-motion-shorthand/index.js +275 -0
- package/dist/esm/rules/compiled/no-css-prop-in-object-spread/index.js +156 -0
- package/dist/esm/rules/compiled/use-motion-token-values/index.js +499 -0
- package/dist/esm/rules/editor-example-type-import-required/index.js +314 -0
- package/dist/esm/rules/no-xcss-in-cx/index.js +216 -0
- package/dist/esm/rules/visit-example-type-import-required/index.js +23 -13
- package/dist/types/index.d.ts +282 -243
- package/dist/types/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
- package/dist/types/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
- package/dist/types/rules/compiled/use-motion-token-values/index.d.ts +3 -0
- package/dist/types/rules/editor-example-type-import-required/index.d.ts +4 -0
- package/dist/types/rules/no-xcss-in-cx/index.d.ts +31 -0
- package/dist/types-ts4.5/index.d.ts +226 -211
- package/dist/types-ts4.5/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/compiled/no-css-prop-in-object-spread/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/compiled/use-motion-token-values/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/editor-example-type-import-required/index.d.ts +4 -0
- package/dist/types-ts4.5/rules/no-xcss-in-cx/index.d.ts +31 -0
- package/package.json +2 -1
package/dist/es2019/index.js
CHANGED
|
@@ -25,6 +25,9 @@ import useRecommendedUtils from './rules/feature-gating/use-recommended-utils';
|
|
|
25
25
|
import validGateName from './rules/feature-gating/valid-gate-name';
|
|
26
26
|
import expandBackgroundShorthand from './rules/compiled/expand-background-shorthand';
|
|
27
27
|
import expandSpacingShorthand from './rules/compiled/expand-spacing-shorthand';
|
|
28
|
+
import noCssPropInObjectSpread from './rules/compiled/no-css-prop-in-object-spread';
|
|
29
|
+
import useMotionTokenValues from './rules/compiled/use-motion-token-values';
|
|
30
|
+
import expandMotionShorthand from './rules/compiled/expand-motion-shorthand';
|
|
28
31
|
import noSparseCheckout from './rules/no-sparse-checkout';
|
|
29
32
|
import noDirectDocumentUsage from './rules/no-direct-document-usage';
|
|
30
33
|
import noSetImmediate from './rules/no-set-immediate';
|
|
@@ -36,7 +39,9 @@ import noJestMockBarrelFiles from './rules/import/no-jest-mock-barrel-files';
|
|
|
36
39
|
import noRelativeBarrelFileImports from './rules/import/no-relative-barrel-file-imports';
|
|
37
40
|
import noConversationAssistantBarrelImports from './rules/import/no-conversation-assistant-barrel-imports';
|
|
38
41
|
import visitExampleTypeImportRequired from './rules/visit-example-type-import-required';
|
|
42
|
+
import editorExampleTypeImportRequired from './rules/editor-example-type-import-required';
|
|
39
43
|
import ensureUseSyncExternalStoreServerSnapshot from './rules/ensure-use-sync-external-store-server-snapshot';
|
|
44
|
+
import noXcssInCx from './rules/no-xcss-in-cx';
|
|
40
45
|
import { join, normalize } from 'node:path';
|
|
41
46
|
import { readFileSync } from 'node:fs';
|
|
42
47
|
let jiraRoot;
|
|
@@ -67,6 +72,7 @@ const rules = {
|
|
|
67
72
|
'expand-border-shorthand': expandBorderShorthand,
|
|
68
73
|
'expand-background-shorthand': expandBackgroundShorthand,
|
|
69
74
|
'expand-spacing-shorthand': expandSpacingShorthand,
|
|
75
|
+
'no-css-prop-in-object-spread': noCssPropInObjectSpread,
|
|
70
76
|
'no-duplicate-dependencies': noDuplicateDependencies,
|
|
71
77
|
'no-invalid-feature-flag-usage': noInvalidFeatureFlagUsage,
|
|
72
78
|
'no-pre-post-install-scripts': noPreAndPostInstallScripts,
|
|
@@ -93,7 +99,11 @@ const rules = {
|
|
|
93
99
|
'no-relative-barrel-file-imports': noRelativeBarrelFileImports,
|
|
94
100
|
'no-conversation-assistant-barrel-imports': noConversationAssistantBarrelImports,
|
|
95
101
|
'visit-example-type-import-required': visitExampleTypeImportRequired,
|
|
96
|
-
'
|
|
102
|
+
'no-xcss-in-cx': noXcssInCx,
|
|
103
|
+
'editor-example-type-import-required': editorExampleTypeImportRequired,
|
|
104
|
+
'ensure-use-sync-external-store-server-snapshot': ensureUseSyncExternalStoreServerSnapshot,
|
|
105
|
+
'use-motion-token-values': useMotionTokenValues,
|
|
106
|
+
'expand-motion-shorthand': expandMotionShorthand
|
|
97
107
|
};
|
|
98
108
|
const commonConfig = {
|
|
99
109
|
'@atlaskit/platform/ensure-test-runner-arguments': 'error',
|
|
@@ -105,10 +115,14 @@ const commonConfig = {
|
|
|
105
115
|
'@atlaskit/platform/no-module-level-eval-nav4': 'error',
|
|
106
116
|
'@atlaskit/platform/no-direct-document-usage': 'warn',
|
|
107
117
|
'@atlaskit/platform/no-set-immediate': 'error',
|
|
118
|
+
'@atlaskit/platform/no-xcss-in-cx': 'error',
|
|
108
119
|
// Compiled: rules that are not included via `@compiled/recommended
|
|
109
120
|
'@atlaskit/platform/expand-border-shorthand': 'error',
|
|
110
121
|
'@atlaskit/platform/expand-background-shorthand': 'error',
|
|
111
122
|
'@atlaskit/platform/expand-spacing-shorthand': 'error',
|
|
123
|
+
'@atlaskit/platform/no-css-prop-in-object-spread': 'error',
|
|
124
|
+
'@atlaskit/platform/use-motion-token-values': 'warn',
|
|
125
|
+
'@atlaskit/platform/expand-motion-shorthand': 'warn',
|
|
112
126
|
'@compiled/jsx-pragma': ['error', {
|
|
113
127
|
importSources: ['@atlaskit/css'],
|
|
114
128
|
onlyRunIfImportingCompiled: true,
|
|
@@ -153,7 +167,6 @@ const plugin = {
|
|
|
153
167
|
get '@atlaskit/platform'() {
|
|
154
168
|
return plugin;
|
|
155
169
|
},
|
|
156
|
-
// @ts-expect-error there's an issue with the types for @compiled/eslint-plugin ('no-css-prop-without-css-function' specifically)
|
|
157
170
|
'@compiled': {
|
|
158
171
|
meta: compiledPlugin.meta,
|
|
159
172
|
rules: compiledPlugin.rules
|
|
@@ -170,7 +183,6 @@ const plugin = {
|
|
|
170
183
|
get '@atlaskit/platform'() {
|
|
171
184
|
return plugin;
|
|
172
185
|
},
|
|
173
|
-
// @ts-expect-error there's an issue with the types for @compiled/eslint-plugin ('no-css-prop-without-css-function' specifically)
|
|
174
186
|
'@compiled': {
|
|
175
187
|
meta: compiledPlugin.meta,
|
|
176
188
|
rules: compiledPlugin.rules
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const EASING_KEYWORDS = ['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear', 'step-start', 'step-end'];
|
|
2
|
+
const KEYWORD_VALUES = ['none', 'all', 'inherit', 'initial', 'unset'];
|
|
3
|
+
const isDuration = token => /^\d+(?:\.\d+)?m?s$/.test(token);
|
|
4
|
+
const isEasing = token => EASING_KEYWORDS.includes(token) || token.startsWith('cubic-bezier(') || token.startsWith('steps(');
|
|
5
|
+
/**
|
|
6
|
+
* Tokenizes a CSS shorthand value string, respecting function boundaries.
|
|
7
|
+
* e.g. 'opacity 200ms cubic-bezier(0.4, 0, 0, 1) 0ms' →
|
|
8
|
+
* ['opacity', '200ms', 'cubic-bezier(0.4, 0, 0, 1)', '0ms']
|
|
9
|
+
* Splits on whitespace only when not inside parentheses.
|
|
10
|
+
*/
|
|
11
|
+
const tokenizeShorthand = value => {
|
|
12
|
+
const tokens = [];
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let current = '';
|
|
15
|
+
for (let i = 0; i < value.length; i++) {
|
|
16
|
+
const ch = value[i];
|
|
17
|
+
if (ch === '(') {
|
|
18
|
+
depth++;
|
|
19
|
+
current += ch;
|
|
20
|
+
} else if (ch === ')') {
|
|
21
|
+
depth--;
|
|
22
|
+
current += ch;
|
|
23
|
+
} else if (/\s/.test(ch) && depth === 0) {
|
|
24
|
+
if (current.length > 0) {
|
|
25
|
+
tokens.push(current);
|
|
26
|
+
current = '';
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
current += ch;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (current.length > 0) {
|
|
33
|
+
tokens.push(current);
|
|
34
|
+
}
|
|
35
|
+
return tokens;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Splits on top-level commas (outside function parens) — preserves cubic-bezier(...) commas.
|
|
39
|
+
const splitOnTopLevelCommas = value => {
|
|
40
|
+
const parts = [];
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let current = '';
|
|
43
|
+
for (const ch of value) {
|
|
44
|
+
if (ch === '(') {
|
|
45
|
+
depth++;
|
|
46
|
+
current += ch;
|
|
47
|
+
} else if (ch === ')') {
|
|
48
|
+
depth--;
|
|
49
|
+
current += ch;
|
|
50
|
+
} else if (ch === ',' && depth === 0) {
|
|
51
|
+
parts.push(current.trim());
|
|
52
|
+
current = '';
|
|
53
|
+
} else {
|
|
54
|
+
current += ch;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (current.trim().length > 0) {
|
|
58
|
+
parts.push(current.trim());
|
|
59
|
+
}
|
|
60
|
+
return parts;
|
|
61
|
+
};
|
|
62
|
+
const parseTransition = value => {
|
|
63
|
+
const parts = tokenizeShorthand(value.trim());
|
|
64
|
+
const result = {};
|
|
65
|
+
let durationCount = 0;
|
|
66
|
+
for (const part of parts) {
|
|
67
|
+
if (isDuration(part)) {
|
|
68
|
+
if (durationCount === 0) {
|
|
69
|
+
result.transitionDuration = part;
|
|
70
|
+
} else {
|
|
71
|
+
result.transitionDelay = part;
|
|
72
|
+
}
|
|
73
|
+
durationCount++;
|
|
74
|
+
} else if (isEasing(part)) {
|
|
75
|
+
result.transitionTimingFunction = part;
|
|
76
|
+
} else {
|
|
77
|
+
result.transitionProperty = part;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
const parseAnimation = value => {
|
|
83
|
+
const parts = tokenizeShorthand(value.trim());
|
|
84
|
+
const result = {};
|
|
85
|
+
let durationCount = 0;
|
|
86
|
+
for (const part of parts) {
|
|
87
|
+
if (isDuration(part)) {
|
|
88
|
+
if (durationCount === 0) {
|
|
89
|
+
result.animationDuration = part;
|
|
90
|
+
} else {
|
|
91
|
+
result.animationDelay = part;
|
|
92
|
+
}
|
|
93
|
+
durationCount++;
|
|
94
|
+
} else if (isEasing(part)) {
|
|
95
|
+
result.animationTimingFunction = part;
|
|
96
|
+
} else if (part === 'infinite' || /^\d+(\.\d+)?$/.test(part)) {
|
|
97
|
+
result.animationIterationCount = part;
|
|
98
|
+
} else if (['normal', 'reverse', 'alternate', 'alternate-reverse'].includes(part)) {
|
|
99
|
+
result.animationDirection = part;
|
|
100
|
+
} else if (['none', 'forwards', 'backwards', 'both'].includes(part)) {
|
|
101
|
+
result.animationFillMode = part;
|
|
102
|
+
} else if (['running', 'paused'].includes(part)) {
|
|
103
|
+
result.animationPlayState = part;
|
|
104
|
+
} else {
|
|
105
|
+
result.animationName = part;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Combine sub-property values across comma-separated transitions/animations.
|
|
112
|
+
// If no segment explicitly set this sub-property, omit it entirely.
|
|
113
|
+
// Otherwise, fill missing slots with the CSS spec default to preserve positional alignment.
|
|
114
|
+
const combineSubPropertyValues = (segments, subProperty, defaultValue) => {
|
|
115
|
+
if (segments.every(s => s[subProperty] === undefined)) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return segments.map(s => {
|
|
119
|
+
var _s$subProperty;
|
|
120
|
+
return (_s$subProperty = s[subProperty]) !== null && _s$subProperty !== void 0 ? _s$subProperty : defaultValue;
|
|
121
|
+
}).join(', ');
|
|
122
|
+
};
|
|
123
|
+
const buildTransitionFix = (segments, indent) => {
|
|
124
|
+
const lines = [];
|
|
125
|
+
const property = combineSubPropertyValues(segments, 'transitionProperty', 'all');
|
|
126
|
+
const duration = combineSubPropertyValues(segments, 'transitionDuration', '0s');
|
|
127
|
+
const timing = combineSubPropertyValues(segments, 'transitionTimingFunction', 'ease');
|
|
128
|
+
const delay = combineSubPropertyValues(segments, 'transitionDelay', '0s');
|
|
129
|
+
if (property !== undefined) lines.push(`transitionProperty: '${property}'`);
|
|
130
|
+
if (duration !== undefined) lines.push(`transitionDuration: '${duration}'`);
|
|
131
|
+
if (timing !== undefined) lines.push(`transitionTimingFunction: '${timing}'`);
|
|
132
|
+
if (delay !== undefined) lines.push(`transitionDelay: '${delay}'`);
|
|
133
|
+
return lines.join(`,\n${indent}`);
|
|
134
|
+
};
|
|
135
|
+
const buildAnimationFix = (segments, indent) => {
|
|
136
|
+
const lines = [];
|
|
137
|
+
const name = combineSubPropertyValues(segments, 'animationName', 'none');
|
|
138
|
+
const duration = combineSubPropertyValues(segments, 'animationDuration', '0s');
|
|
139
|
+
const timing = combineSubPropertyValues(segments, 'animationTimingFunction', 'ease');
|
|
140
|
+
const delay = combineSubPropertyValues(segments, 'animationDelay', '0s');
|
|
141
|
+
const iter = combineSubPropertyValues(segments, 'animationIterationCount', '1');
|
|
142
|
+
const direction = combineSubPropertyValues(segments, 'animationDirection', 'normal');
|
|
143
|
+
const fill = combineSubPropertyValues(segments, 'animationFillMode', 'none');
|
|
144
|
+
const playState = combineSubPropertyValues(segments, 'animationPlayState', 'running');
|
|
145
|
+
if (name !== undefined) lines.push(`animationName: '${name}'`);
|
|
146
|
+
if (duration !== undefined) lines.push(`animationDuration: '${duration}'`);
|
|
147
|
+
if (timing !== undefined) lines.push(`animationTimingFunction: '${timing}'`);
|
|
148
|
+
if (delay !== undefined) lines.push(`animationDelay: '${delay}'`);
|
|
149
|
+
if (iter !== undefined) lines.push(`animationIterationCount: '${iter}'`);
|
|
150
|
+
if (direction !== undefined) lines.push(`animationDirection: '${direction}'`);
|
|
151
|
+
if (fill !== undefined) lines.push(`animationFillMode: '${fill}'`);
|
|
152
|
+
if (playState !== undefined) lines.push(`animationPlayState: '${playState}'`);
|
|
153
|
+
return lines.join(`,\n${indent}`);
|
|
154
|
+
};
|
|
155
|
+
const TRANSITION_SUB_PROPERTIES = ['transitionProperty', 'transitionDuration', 'transitionTimingFunction', 'transitionDelay'];
|
|
156
|
+
const ANIMATION_SUB_PROPERTIES = ['animationName', 'animationDuration', 'animationTimingFunction', 'animationDelay', 'animationIterationCount', 'animationDirection', 'animationFillMode', 'animationPlayState'];
|
|
157
|
+
const executeExpandTransitionRule = (context, node, property) => {
|
|
158
|
+
var _context$sourceCode, _node$loc;
|
|
159
|
+
if (node.value.type === 'CallExpression') {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (node.value.type === 'TemplateLiteral') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (node.value.type !== 'Literal' || typeof node.value.value !== 'string') {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const rawValue = node.value.value;
|
|
169
|
+
if (KEYWORD_VALUES.includes(rawValue)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const subProperties = property === 'transition' ? TRANSITION_SUB_PROPERTIES : ANIMATION_SUB_PROPERTIES;
|
|
173
|
+
|
|
174
|
+
// Extract leading whitespace to preserve indentation style (tabs vs spaces)
|
|
175
|
+
const sourceCode = (_context$sourceCode = context.sourceCode) !== null && _context$sourceCode !== void 0 ? _context$sourceCode : context.getSourceCode();
|
|
176
|
+
const nodeStart = (_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start;
|
|
177
|
+
let indent = '\t';
|
|
178
|
+
if (nodeStart) {
|
|
179
|
+
var _sourceCode$lines;
|
|
180
|
+
const lineText = (_sourceCode$lines = sourceCode.lines[nodeStart.line - 1]) !== null && _sourceCode$lines !== void 0 ? _sourceCode$lines : '';
|
|
181
|
+
const leadingWhitespace = lineText.match(/^(\s*)/);
|
|
182
|
+
if (leadingWhitespace) {
|
|
183
|
+
indent = leadingWhitespace[1];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const segmentStrings = splitOnTopLevelCommas(rawValue);
|
|
187
|
+
if (property === 'transition') {
|
|
188
|
+
const segments = segmentStrings.map(parseTransition);
|
|
189
|
+
const fixText = buildTransitionFix(segments, indent);
|
|
190
|
+
context.report({
|
|
191
|
+
node,
|
|
192
|
+
messageId: 'expandTransitionShorthand',
|
|
193
|
+
data: {
|
|
194
|
+
property,
|
|
195
|
+
subProperties: subProperties.join(', ')
|
|
196
|
+
},
|
|
197
|
+
fix(fixer) {
|
|
198
|
+
return fixer.replaceText(node, fixText);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
const segments = segmentStrings.map(parseAnimation);
|
|
203
|
+
const fixText = buildAnimationFix(segments, indent);
|
|
204
|
+
context.report({
|
|
205
|
+
node,
|
|
206
|
+
messageId: 'expandTransitionShorthand',
|
|
207
|
+
data: {
|
|
208
|
+
property,
|
|
209
|
+
subProperties: subProperties.join(', ')
|
|
210
|
+
},
|
|
211
|
+
fix(fixer) {
|
|
212
|
+
return fixer.replaceText(node, fixText);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
export const expandTransitionShorthand = {
|
|
218
|
+
meta: {
|
|
219
|
+
type: 'suggestion',
|
|
220
|
+
fixable: 'code',
|
|
221
|
+
docs: {
|
|
222
|
+
url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/expand-transition-shorthand/'
|
|
223
|
+
},
|
|
224
|
+
messages: {
|
|
225
|
+
expandTransitionShorthand: "Use {{ subProperties }} instead of the '{{ property }}' shorthand so that individual values can be replaced with motion tokens."
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
create(context) {
|
|
229
|
+
return {
|
|
230
|
+
'Property[key.name="transition"]': function (node) {
|
|
231
|
+
executeExpandTransitionRule(context, node, 'transition');
|
|
232
|
+
},
|
|
233
|
+
'Property[key.name="animation"]': function (node) {
|
|
234
|
+
executeExpandTransitionRule(context, node, 'animation');
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
export default expandTransitionShorthand;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getScope, getSourceCode } from '../../util/context-compat';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the `css` Property node from an ObjectExpression, or null if not found.
|
|
5
|
+
*/
|
|
6
|
+
function getCssProperty(objectExpression) {
|
|
7
|
+
for (const prop of objectExpression.properties) {
|
|
8
|
+
if (prop.type !== 'Property') {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const {
|
|
12
|
+
key
|
|
13
|
+
} = prop;
|
|
14
|
+
if (key.type === 'Identifier' && key.name === 'css' || key.type === 'Literal' && key.value === 'css') {
|
|
15
|
+
return prop;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export const noCssPropInObjectSpread = {
|
|
21
|
+
meta: {
|
|
22
|
+
docs: {
|
|
23
|
+
url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/no-css-prop-in-object-spread/',
|
|
24
|
+
description: 'Disallows `css` property inside objects spread into JSX — the Compiled JSX pragma ignores it'
|
|
25
|
+
},
|
|
26
|
+
fixable: 'code',
|
|
27
|
+
messages: {
|
|
28
|
+
noCssPropInObjectSpread: 'The `css` property inside an object spread into JSX is a no-op. The Compiled JSX pragma only processes `css` as a direct JSX attribute. Move `css` out of the spread: <El css={...} />'
|
|
29
|
+
},
|
|
30
|
+
type: 'problem'
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
JSXSpreadAttribute(node) {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
const spreadNode = node;
|
|
37
|
+
const arg = spreadNode.argument;
|
|
38
|
+
|
|
39
|
+
// Case 1: inline object literal — <div {...{ css: styles, id: 'foo' }} />
|
|
40
|
+
if (arg.type === 'ObjectExpression') {
|
|
41
|
+
const objectArg = arg;
|
|
42
|
+
const cssProp = getCssProperty(objectArg);
|
|
43
|
+
if (!cssProp) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
messageId: 'noCssPropInObjectSpread',
|
|
49
|
+
fix(fixer) {
|
|
50
|
+
const sourceCode = getSourceCode(context);
|
|
51
|
+
const cssValueText = sourceCode.getText(cssProp.value);
|
|
52
|
+
const remainingProps = objectArg.properties.filter(p => p !== cssProp);
|
|
53
|
+
const directCssProp = `css={${cssValueText}}`;
|
|
54
|
+
if (remainingProps.length === 0) {
|
|
55
|
+
return fixer.replaceText(node, directCssProp);
|
|
56
|
+
}
|
|
57
|
+
const remainingText = remainingProps.map(p => sourceCode.getText(p)).join(', ');
|
|
58
|
+
return fixer.replaceText(node, `${directCssProp} {...{ ${remainingText} }}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Case 2: variable reference — <div {...props} />
|
|
65
|
+
if (arg.type === 'Identifier') {
|
|
66
|
+
const scope = getScope(context, arg);
|
|
67
|
+
let currentScope = scope;
|
|
68
|
+
let variable = null;
|
|
69
|
+
while (currentScope) {
|
|
70
|
+
const found = currentScope.variables.find(v => v.name === arg.name);
|
|
71
|
+
if (found) {
|
|
72
|
+
variable = found;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
currentScope = currentScope.upper;
|
|
76
|
+
}
|
|
77
|
+
if (!variable || variable.defs.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const def = variable.defs[0];
|
|
81
|
+
if (def.type !== 'Variable' || !def.node.init || def.node.init.type !== 'ObjectExpression') {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const initObject = def.node.init;
|
|
85
|
+
const cssProp = getCssProperty(initObject);
|
|
86
|
+
if (!cssProp) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Only auto-fix when there is exactly one JSX spread site for this variable
|
|
91
|
+
const spreadCount = variable.references.filter(ref => {
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
const refParent = ref.identifier.parent;
|
|
94
|
+
return (refParent === null || refParent === void 0 ? void 0 : refParent.type) === 'JSXSpreadAttribute';
|
|
95
|
+
}).length;
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
messageId: 'noCssPropInObjectSpread',
|
|
99
|
+
...(spreadCount === 1 ? {
|
|
100
|
+
fix(fixer) {
|
|
101
|
+
const sourceCode = getSourceCode(context);
|
|
102
|
+
const cssValueText = sourceCode.getText(cssProp.value);
|
|
103
|
+
const fixes = [];
|
|
104
|
+
const remainingProps = initObject.properties.filter(p => p !== cssProp);
|
|
105
|
+
if (remainingProps.length === 0) {
|
|
106
|
+
fixes.push(fixer.replaceText(initObject, '{}'));
|
|
107
|
+
} else {
|
|
108
|
+
const propIndex = initObject.properties.indexOf(cssProp);
|
|
109
|
+
const isLast = propIndex === initObject.properties.length - 1;
|
|
110
|
+
const tokenBefore = sourceCode.getTokenBefore(cssProp);
|
|
111
|
+
const tokenAfter = sourceCode.getTokenAfter(cssProp);
|
|
112
|
+
if (!isLast && tokenAfter && tokenAfter.value === ',') {
|
|
113
|
+
const src = sourceCode.getText();
|
|
114
|
+
const afterEnd = tokenAfter.range[1];
|
|
115
|
+
let end = afterEnd;
|
|
116
|
+
while (end < src.length && src[end] === ' ') {
|
|
117
|
+
end++;
|
|
118
|
+
}
|
|
119
|
+
fixes.push(fixer.removeRange([cssProp.range[0], end]));
|
|
120
|
+
} else if (tokenBefore && tokenBefore.value === ',') {
|
|
121
|
+
fixes.push(fixer.removeRange([tokenBefore.range[0], cssProp.range[1]]));
|
|
122
|
+
} else {
|
|
123
|
+
fixes.push(fixer.remove(cssProp));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
fixes.push(fixer.insertTextBefore(node, `css={${cssValueText}} `));
|
|
127
|
+
return fixes;
|
|
128
|
+
}
|
|
129
|
+
} : {})
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
export default noCssPropInObjectSpread;
|