Skip to content

Commit 8fb2158

Browse files
Add migration for sass z-index fn (#7310)
WHY are these changes introduced? Part of #7211 WHAT is this pull request doing? Adding a replace-sass-z-index migration script. Running npx @shopify/polaris-migrator replace-sass-z-index "**/*.scss" will target z-index function usage and do the following: * If more than one argument exists we assume the code is invoking z-index to map-get on a custom sass map. In this case, we add a comment with a migration warning and code snippet replacing the `legacy-polaris-v8.z-index` usage with `map-get` * If there's a calculation in the same declaration but the `legacy-polaris-v8.z-index` invocation is valid and corresponds to a polaris token, we add a comment with a migration warning and a code snippet replacing that invocation with a css variable invocation pointing at the corresponding polaris-token. * If its just a valid `legacy-polaris-v8.z-index` invocation that corresponds to an existing polaris token, we replace it with a css-variable invocation pointing at the corresponding polaris-token. * Otherwise we ignore the declaration. Things our codemod doesn't presently understand but should: * interpolations. `calc(#{legacy-polaris-v8.z-index} + 1)` for example is completely ignored by our code. As post-css interprets this as an invocation of a function called `#{legacy-polaris-v8.z-index`. It doesn't seem to understand interpolations at the moment. We can probably use something like style-lint's [hasInterpolation](https://github.dev/stylelint/stylelint/blob/eee9deb35190f46c5f964769d521fef5d7d1d7a8/lib/utils/__tests__/hasInterpolation.test.js#L5) to work around this, (ty @aaronccasanova), but not something we should worry about for first pass imo. Co-authored-by: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com>
1 parent a0fa96e commit 8fb2158

File tree

8 files changed

+400
-0
lines changed

8 files changed

+400
-0
lines changed

.changeset/hot-trains-laugh.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris-migrator': minor
3+
---
4+
5+
Add sass z-index migration

polaris-migrator/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,58 @@ Replace lengths (`px`, `rem`) and legacy Sass functions (`rem()`,`border()`, `bo
168168
npx @shopify/polaris-migrator replace-border-declarations <path>
169169
```
170170

171+
### `replace-sass-z-index`
172+
173+
Replace the legacy Sass `z-index()` function with the supported CSS custom property token equivalent (ex: `var(--p-z-1)`).
174+
175+
Any invocations of `z-index()` that correspond to a z-index design-token i.e. `--p-z-1` will be replaced with a css variable declaration.
176+
This includes invocations to the `$fixed-element-stacking-order` sass map i.e. `z-index(modal, $fixed-element-stacking-order)`.
177+
178+
```diff
179+
- .decl-1 {
180+
- z-index: z-index(content);
181+
- }
182+
- .decl-2 {
183+
- z-index: z-index(modal, $fixed-element-stacking-order)
184+
- }
185+
+ decl-1 {
186+
+ z-index: var(--p-z-1);
187+
+ }
188+
+ .decl-2 {
189+
+ z-index: var(--p-z-11)
190+
+ }
191+
```
192+
193+
Invocations of `z-index` within an arithmetic expression will be appended with a comment for review and manual migration.
194+
Generally in these instances you'll want to wrap the suggested code change in a `calc` however this may defer on a case by case basis in your codebase.
195+
196+
```diff
197+
.decl-3 {
198+
+ /* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
199+
+ /* z-index: var(--p-z-1) + 1 */
200+
z-index: z-index(content) + 1
201+
}
202+
```
203+
204+
Invocations of `z-index` with a custom sass map property, will also be appended with a comment for review and manual migration.
205+
206+
```diff
207+
.decl-3 {
208+
+ /* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
209+
+ /* z-index: map.get($custom-sass-map, modal) */
210+
z-index: z-index(modal, $custom-sass-map)
211+
}
212+
```
213+
214+
In these cases you may also want to run `npx sass-migrator module <path> --migrate-deps --load-path <load-path>` to ensure that
215+
`map.get` is in scope\*\*.
216+
217+
Be aware that this may also create additional code changes in your codebase, we recommend running this only if there are large number of instances of migrations from `z-index` to `map.get`. Otherwise it may be easier to add `use 'sass:map'` to the top of your `.scss` file manually.
218+
219+
```sh
220+
npx @shopify/polaris-migrator replace-sass-spacing <path>
221+
```
222+
171223
## Creating a migration
172224

173225
### Setup
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import postcss, {Plugin} from 'postcss';
2+
import type {API, FileInfo, Options} from 'jscodeshift';
3+
import valueParser, {FunctionNode, Node} from 'postcss-value-parser';
4+
5+
import {
6+
NamespaceOptions,
7+
namespace,
8+
hasSassFunction,
9+
isSassFunction,
10+
hasNumericOperator,
11+
} from '../../utilities/sass';
12+
import {POLARIS_MIGRATOR_COMMENT} from '../../constants';
13+
14+
interface PluginOptions extends Options, NamespaceOptions {}
15+
16+
const processed = Symbol('processed');
17+
18+
const zIndexMap = {
19+
content: '--p-z-1',
20+
overlay: '--p-z-2',
21+
};
22+
23+
const fixedElementStackingOrder = {
24+
'global-ribbon': '--p-z-3',
25+
'top-bar': '--p-z-4',
26+
'context-bar': '--p-z-5',
27+
'small-screen-loading-bar': '--p-z-6',
28+
'nav-backdrop': '--p-z-7',
29+
nav: '--p-z-8',
30+
'skip-to-content': '--p-z-9',
31+
backdrop: '--p-z-10',
32+
modal: '--p-z-11',
33+
toast: '--p-z-12',
34+
};
35+
36+
function isValidElement<
37+
MapType extends typeof zIndexMap | typeof fixedElementStackingOrder,
38+
>(element: unknown, mapObj: MapType): element is keyof typeof mapObj {
39+
return Object.keys(mapObj).includes(element as string);
40+
}
41+
42+
const hasMoreThanOneArgument = (node: FunctionNode) => node.nodes.length > 1;
43+
44+
const plugin = (options: PluginOptions = {}): Plugin => {
45+
const namespacedZIndex = namespace('z-index', options);
46+
const namespacedFixedElementStackingOrder = namespace(
47+
'$fixed-element-stacking-order',
48+
options,
49+
);
50+
return {
51+
postcssPlugin: 'replace-sass-z-index',
52+
Declaration(decl) {
53+
// @ts-expect-error - Skip if processed so we don't process it again
54+
if (decl[processed]) return;
55+
56+
const parsedValue = valueParser(decl.value);
57+
58+
if (!hasSassFunction(namespacedZIndex, parsedValue)) return;
59+
60+
let containsUnknownSecondArgument = false;
61+
62+
parsedValue.walk((node: Node) => {
63+
if (!isSassFunction(namespacedZIndex, node)) return;
64+
if (hasMoreThanOneArgument(node)) {
65+
// If there's more than one argument to the zIndex fn
66+
// We assume they're passing in a custom map
67+
// In this case its unlikely this will resolve to a polaris token value
68+
// transform legacy zIndex usage to map-get and move on.
69+
70+
const [key, _, map] = node.nodes;
71+
if (
72+
map.value === namespacedFixedElementStackingOrder &&
73+
isValidElement(key.value, fixedElementStackingOrder)
74+
) {
75+
const fixedElementStackingOrderToken =
76+
fixedElementStackingOrder[key.value];
77+
node.value = 'var';
78+
node.nodes = [
79+
{
80+
type: 'word',
81+
value: fixedElementStackingOrderToken,
82+
sourceIndex: node.nodes[0]?.sourceIndex ?? 0,
83+
sourceEndIndex: fixedElementStackingOrderToken.length,
84+
},
85+
];
86+
} else {
87+
// map.get arguments are in the reverse order to z-index arguments.
88+
// map.get expects the map object first, and the key second.
89+
containsUnknownSecondArgument = true;
90+
node.value = 'map.get';
91+
node.nodes.reverse();
92+
}
93+
} else {
94+
const element = node.nodes[0]?.value ?? '';
95+
if (!isValidElement<typeof zIndexMap>(element, zIndexMap)) return;
96+
const zIndexCustomProperty = zIndexMap[element];
97+
98+
node.value = 'var';
99+
node.nodes = [
100+
{
101+
type: 'word',
102+
value: zIndexCustomProperty,
103+
sourceIndex: node.nodes[0]?.sourceIndex ?? 0,
104+
sourceEndIndex: zIndexCustomProperty.length,
105+
},
106+
];
107+
}
108+
});
109+
110+
if (hasNumericOperator(parsedValue) || containsUnknownSecondArgument) {
111+
// Insert comment if the declaration value contains calculations
112+
// or if the invocation of zIndex has more than one argument
113+
decl.before(postcss.comment({text: POLARIS_MIGRATOR_COMMENT}));
114+
decl.before(
115+
postcss.comment({text: `${decl.prop}: ${parsedValue.toString()};`}),
116+
);
117+
} else {
118+
decl.value = parsedValue.toString();
119+
}
120+
121+
// @ts-expect-error - Mark the declaration as processed
122+
decl[processed] = true;
123+
},
124+
};
125+
};
126+
127+
export default function replaceSassZIndex(
128+
fileInfo: FileInfo,
129+
_: API,
130+
options: Options,
131+
) {
132+
return postcss(plugin(options)).process(fileInfo.source, {
133+
syntax: require('postcss-scss'),
134+
}).css;
135+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
$someElement: (
2+
someKey: 1000;,
3+
);
4+
5+
.scenario-1 {
6+
z-index: z-index(content) + 1;
7+
background-color: var(--p-background);
8+
}
9+
10+
.scenario-2 {
11+
z-index: z-index(overlay) + 1;
12+
background-color: var(--p-background);
13+
}
14+
15+
.scenario-3 {
16+
z-index: z-index(content);
17+
background-color: var(--p-background);
18+
}
19+
20+
.scenario-4 {
21+
z-index: z-index(overlay);
22+
background-color: var(--p-background);
23+
}
24+
25+
.scenario-5 {
26+
z-index: z-index(someKey, $someElement);
27+
background-color: var(--p-background);
28+
}
29+
30+
.scenario-6 {
31+
z-index: calc(z-index(overlay) + z-index(content));
32+
}
33+
34+
.scenario-7 {
35+
z-index: calc(#{z-index(dev-ui, $fixed-element-stacking-order)} + 1);
36+
}
37+
38+
.scenario-8 {
39+
z-index: z-index(modal, $fixed-element-stacking-order);
40+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
$someElement: (
2+
someKey: 1000;,
3+
);
4+
5+
.scenario-1 {
6+
/* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
7+
/* z-index: var(--p-z-1) + 1; */
8+
z-index: z-index(content) + 1;
9+
background-color: var(--p-background);
10+
}
11+
12+
.scenario-2 {
13+
/* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
14+
/* z-index: var(--p-z-2) + 1; */
15+
z-index: z-index(overlay) + 1;
16+
background-color: var(--p-background);
17+
}
18+
19+
.scenario-3 {
20+
z-index: var(--p-z-1);
21+
background-color: var(--p-background);
22+
}
23+
24+
.scenario-4 {
25+
z-index: var(--p-z-2);
26+
background-color: var(--p-background);
27+
}
28+
29+
.scenario-5 {
30+
/* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
31+
/* z-index: map.get($someElement, someKey); */
32+
z-index: z-index(someKey, $someElement);
33+
background-color: var(--p-background);
34+
}
35+
36+
.scenario-6 {
37+
/* polaris-migrator: Unable to migrate the following expression. Please upgrade manually. */
38+
/* z-index: calc(var(--p-z-2) + var(--p-z-1)); */
39+
z-index: calc(z-index(overlay) + z-index(content));
40+
}
41+
42+
.scenario-7 {
43+
z-index: calc(#{z-index(dev-ui, $fixed-element-stacking-order)} + 1);
44+
}
45+
46+
.scenario-8 {
47+
z-index: var(--p-z-11);
48+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {check} from '../../../utilities/testUtils';
2+
3+
const migration = 'replace-sass-z-index';
4+
const fixtures = ['replace-sass-z-index', 'with-namespace'];
5+
6+
for (const fixture of fixtures) {
7+
check(__dirname, {
8+
fixture,
9+
migration,
10+
extension: 'scss',
11+
options: {
12+
namespace: fixture.includes('with-namespace')
13+
? 'legacy-polaris-v8'
14+
: undefined,
15+
},
16+
});
17+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
@use 'global-styles/legacy-polaris-v8';
2+
$someElement: (
3+
someKey: 1000;,
4+
);
5+
6+
.scenario-1 {
7+
z-index: legacy-polaris-v8.z-index(content) + 1;
8+
background-color: var(--p-background);
9+
}
10+
11+
.scenario-2 {
12+
z-index: legacy-polaris-v8.z-index(overlay) + 1;
13+
background-color: var(--p-background);
14+
}
15+
16+
.scenario-3 {
17+
z-index: legacy-polaris-v8.z-index(content);
18+
background-color: var(--p-background);
19+
}
20+
21+
.scenario-4 {
22+
z-index: legacy-polaris-v8.z-index(overlay);
23+
background-color: var(--p-background);
24+
}
25+
26+
.scenario-5 {
27+
z-index: legacy-polaris-v8.z-index(someKey, $someElement);
28+
background-color: var(--p-background);
29+
}
30+
31+
.scenario-6 {
32+
z-index: calc(
33+
legacy-polaris-v8.z-index(overlay) + legacy-polaris-v8.z-index(content)
34+
);
35+
}
36+
37+
.scenario-7 {
38+
z-index: calc(
39+
#{legacy-polaris-v8.z-index(dev-ui, $fixed-element-stacking-order)} + 1
40+
);
41+
}
42+
43+
.scenario-8 {
44+
z-index: legacy-polaris-v8.z-index(
45+
modal,
46+
legacy-polaris-v8.$fixed-element-stacking-order
47+
);
48+
}

0 commit comments

Comments
 (0)