Skip to content

Commit 53e98b1

Browse files
authored
feat(material/core): migrate to the Sass module system (#21204)
* refactor(material/core): sass module migration preparation * refactor(material/core): run sass module migration * refactor(material/core): post migration fixup
1 parent 8eb59f8 commit 53e98b1

File tree

355 files changed

+6722
-4972
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

355 files changed

+6722
-4972
lines changed

BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package(default_visibility = ["//visibility:public"])
88

99
exports_files([
1010
"LICENSE",
11-
"scss-bundle.config.json",
1211
])
1312

1413
genrule(

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"@types/node-fetch": "^2.5.5",
109109
"@types/parse5": "^6.0.0",
110110
"@types/semver": "^7.3.4",
111+
"@types/sass": "^1.16.0",
111112
"@types/send": "^0.14.5",
112113
"@types/stylelint": "^9.10.1",
113114
"@types/yaml": "^1.9.7",
@@ -156,7 +157,6 @@
156157
"rollup-plugin-node-resolve": "^5.2.0",
157158
"rollup-plugin-sourcemaps": "^0.6.3",
158159
"sass": "^1.29.0",
159-
"scss-bundle": "^3.1.2",
160160
"selenium-webdriver": "^3.6.0",
161161
"semver": "^7.3.4",
162162
"send": "^0.17.1",

scripts/migrate-sass-modules.js

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
const childProcess = require('child_process');
2+
const path = require('path');
3+
const fs = require('fs');
4+
const {sync: glob} = require('glob');
5+
6+
// Script that migrates the library source to the Sass module system while maintaining
7+
// backwards-compatibility. The script assumes that `sass-migrator` is installed
8+
// globally and that the results will be committed. Works by migrating the .scss files
9+
// based on their position in the dependency tree, starting with the files that are depended
10+
// upon the most and working downwards. Furthermore, because the `sass-migrator` isn't able to
11+
// pick up imports from the `node_modules`, there is a workaround that comments out all of the
12+
// imports from `@material/*`, runs the migration and re-adds the imports back. The script also
13+
// sorts all remaining `@import` statements lower than `@use` statements to avoid compilation
14+
// errors and auto-fixes some linting failures that are generated by the migrator.
15+
16+
const directory = path.join(__dirname, '../src');
17+
const migratedFiles = new Set();
18+
const ignorePatterns = [
19+
'**/*.import.scss',
20+
'**/test-theming-bundle.scss',
21+
'material/_theming.scss'
22+
];
23+
const materialPrefixes = [
24+
...getPrefixes('material', 'mat'),
25+
...getPrefixes('material/core', 'mat'),
26+
// Outliers that don't have a directory of their own.
27+
'mat-pseudo-checkbox-',
28+
'mat-elevation-',
29+
'mat-optgroup-',
30+
'mat-private-'
31+
];
32+
const mdcPrefixes = [
33+
...getPrefixes('material-experimental', 'mat'),
34+
...getPrefixes('material-experimental/mdc-core', 'mat'),
35+
// Outliers that don't have a directory of their own.
36+
'mat-mdc-optgroup-'
37+
].map(prefix => prefix === 'mat-' ? 'mat-mdc-' : prefix);
38+
const cdkPrefixes = getPrefixes('cdk', 'cdk');
39+
const cdkExperimentalPrefixes = getPrefixes('cdk-experimental', 'cdk');
40+
41+
// Restore the source directory to a clean state.
42+
run('git', ['clean', '-f', '-d'], false, true);
43+
run('git', ['checkout', '--', directory], false, true);
44+
45+
// --reset is a utility to easily restore the repo to its initial state.
46+
if (process.argv.indexOf('--reset') > -1) {
47+
process.exit(0);
48+
}
49+
50+
// Generate this after the repo has been reset.
51+
const importsToAdd = extractImports();
52+
53+
// Run the migrations.
54+
55+
// Clean up any existing import files, because they interfere with the migration.
56+
clearImportFiles();
57+
58+
// Migrate all the partials and forward any export symbols.
59+
migrate('cdk/**/_*.scss', cdkPrefixes, true);
60+
migrate('cdk-experimental/**/_*.scss', cdkExperimentalPrefixes, true);
61+
migrate('material/core/**/_*.scss', materialPrefixes, true, ['**/_all-*.scss', '**/_core.scss']);
62+
migrate('material/!(core)/**/_*.scss', materialPrefixes, true);
63+
migrate('material/core/**/_*.scss', materialPrefixes, true);
64+
65+
// Comment out all MDC imports since the migrator script doesn't know how to find them.
66+
commentOutMdc('material-experimental/**/*.scss');
67+
68+
// Migrate all of the MDC partials.
69+
migrate('material-experimental/mdc-helpers/**/_*.scss', mdcPrefixes, true);
70+
migrate('material-experimental/mdc-core/**/_*.scss', mdcPrefixes, true, ['**/_core.scss']);
71+
migrate('material-experimental/**/_*.scss', mdcPrefixes, true);
72+
73+
// Migrate everything else without forwarding.
74+
migrate('cdk/**/*.scss', cdkPrefixes);
75+
migrate('cdk-experimental/**/*.scss', cdkExperimentalPrefixes);
76+
migrate('material/**/*.scss', materialPrefixes);
77+
migrate('material-experimental/**/*.scss', mdcPrefixes);
78+
79+
// Migrate whatever is left in the source files, assuming that it's not a public API.
80+
migrate('**/*.scss');
81+
82+
// Restore the commented out MDC imports and sort `@use` above `@import`.
83+
restoreAndSortMdc('material-experimental/**/*.scss');
84+
85+
// Clear the files that we don't want.
86+
clearUnwantedFiles();
87+
88+
// Re-add all the imports for backwards compatibility.
89+
reAddImports(importsToAdd);
90+
91+
// Try to auto-fix some of the lint issues using Stylelint.
92+
run('yarn', ['stylelint', '--fix'], true, true);
93+
94+
// At this point most of the lint failures are going to be from long `@forward` statements inside
95+
// .import.scss files. Try to auto-resolve them and then fix everything else manually.
96+
fixSomeLongLines('**/*.import.scss', 100);
97+
98+
console.log(`Finished migrating ${migratedFiles.size} files.`);
99+
100+
function migrate(pattern, prefixes = [], forward = false, ignore = []) {
101+
const args = ['module'];
102+
forward && args.push('--forward=import-only');
103+
prefixes.length && args.push(`--remove-prefix=${prefixes.join(',')}`);
104+
105+
// Note that while the migrator allows for multiple files to be passed in, we start getting
106+
// some assertion errors along the way. Running it on a file-by-file basis works fine.
107+
const files = glob(pattern, {cwd: directory, ignore: [...ignore, ...ignorePatterns]})
108+
.filter(file => !migratedFiles.has(file));
109+
const message = `Migrating ${files.length} unmigrated files matching ${pattern}.`;
110+
console.log(ignore.length ? message + ` Ignoring ${ignore.join(', ')}.` : message);
111+
run('sass-migrator', [...args, ...files]);
112+
files.forEach(file => migratedFiles.add(file));
113+
}
114+
115+
function run(name, args, canFail = false, silent = false) {
116+
const result = childProcess.spawnSync(name, args, {shell: true, cwd: directory});
117+
const output = result.stdout.toString();
118+
!silent && output.length && console.log(output);
119+
120+
if (result.status !== 0 && !canFail) {
121+
console.error(`Script error: ${(result.stderr || result.stdout)}`);
122+
process.exit(1);
123+
}
124+
}
125+
126+
function getPrefixes(package, prefix) {
127+
return fs.readdirSync(path.join(directory, package), {withFileTypes: true})
128+
.filter(current => current.isDirectory())
129+
.map(current => current.name)
130+
.reduce((output, current) => [`${prefix}-${current}-`, ...output], [`${prefix}-`]);
131+
}
132+
133+
function commentOutMdc(pattern) {
134+
const files = glob(pattern, {cwd: directory, absolute: true});
135+
console.log(`Commenting out @material imports from ${files.length} files matching ${pattern}.`);
136+
files.forEach(file => {
137+
const content = fs.readFileSync(file, 'utf8');
138+
// Prefix the content with a marker so we know what to restore later.
139+
fs.writeFileSync(file, content.replace(/(@use|@import) '@material/g, m => '//🚀 ' + m));
140+
});
141+
}
142+
143+
function restoreAndSortMdc(pattern) {
144+
const files = glob(pattern, {cwd: directory, absolute: true});
145+
console.log(`Re-adding and sorting @material imports from ${files.length} ` +
146+
`files matching ${pattern}.`);
147+
148+
files.forEach(file => {
149+
// Remove the commented out lines with the marker from `commentOutMdc`.
150+
const content = fs.readFileSync(file, 'utf8').replace(/\/\/🚀 /g, '');
151+
const lines = content.split('\n');
152+
let headerStartIndex = -1;
153+
let headerEndIndex = -1;
154+
155+
// Find where the comments start and end.
156+
for (let i = lines.length - 1; i > -1; i--) {
157+
if (lines[i].startsWith('@use') || lines[i].startsWith('@import')) {
158+
headerStartIndex = i;
159+
160+
if (headerEndIndex === -1) {
161+
headerEndIndex = i + 1;
162+
}
163+
}
164+
}
165+
166+
// Sort the imports so that `@use` comes before `@import`. Otherwise Sass will throw an error.
167+
if (headerStartIndex > -1 && headerEndIndex > -1) {
168+
const headers = lines
169+
.splice(headerStartIndex, headerEndIndex - headerStartIndex)
170+
.sort((a, b) => a.startsWith('@use') && !b.startsWith('@use') ? -1 : 0);
171+
lines.splice(headerStartIndex, 0, ...headers);
172+
}
173+
174+
fs.writeFileSync(file, lines.join('\n'));
175+
});
176+
}
177+
178+
function clearImportFiles() {
179+
const files = glob('**/*.import.scss', {cwd: directory, absolute: true});
180+
console.log(`Clearing ${files.length} import files.`);
181+
files.forEach(file => fs.unlinkSync(file));
182+
}
183+
184+
function clearUnwantedFiles() {
185+
// The migration script generates .import files even if we don't pass in the `--forward` when
186+
// a file has top-level variables matching a prefix. Since we still want such files to be
187+
// migrated, we clear the unwanted files afterwards.
188+
const files = glob('**/*.import.scss', {cwd: directory, absolute: true, ignore: ['**/_*.scss']});
189+
console.log(`Clearing ${files.length} unwanted files.`);
190+
files.forEach(file => fs.unlinkSync(file));
191+
}
192+
193+
function extractImports() {
194+
return glob('**/*.scss', {cwd: directory, absolute: true}).reduce((result, file) => {
195+
const content = fs.readFileSync(file, 'utf8');
196+
const match = content.match(/@import '(.*)';/g);
197+
const imports = match ? match.filter(dep => !dep.includes(` '@material/`)) : [];
198+
if (imports.length) {
199+
result[file] = imports;
200+
}
201+
return result;
202+
}, {});
203+
}
204+
205+
206+
function reAddImports(mapping) {
207+
Object.keys(mapping).forEach(fileName => {
208+
const importEquivalentName = fileName.replace('.scss', '.import.scss');
209+
210+
if (fs.existsSync(importEquivalentName)) {
211+
let content = fs.readFileSync(importEquivalentName, 'utf8');
212+
mapping[fileName].forEach(importedFile => content += `\n${importedFile}`);
213+
fs.writeFileSync(importEquivalentName, content);
214+
}
215+
});
216+
}
217+
218+
219+
function fixSomeLongLines(pattern, limit) {
220+
const files = glob(pattern, {cwd: directory, absolute: true});
221+
let count = 0;
222+
223+
files.forEach(file => {
224+
const content = fs.readFileSync(file, 'utf8');
225+
let lines = content.split('\n');
226+
let fileChanged = false;
227+
228+
(function fixLines() {
229+
const newLines = [];
230+
let hasFixed = false;
231+
232+
lines.forEach(line => {
233+
if (line.length > limit) {
234+
const breakAt = line.lastIndexOf(' ', limit);
235+
if (breakAt > -1) {
236+
// Split the line in two at the limit.
237+
newLines.push(line.slice(0, breakAt), line.slice(breakAt + 1));
238+
fileChanged = hasFixed = true;
239+
} else {
240+
newLines.push(line);
241+
}
242+
} else {
243+
newLines.push(line);
244+
}
245+
});
246+
247+
lines = newLines;
248+
249+
// Keep fixing until there's nothing left. Not particularly efficient...
250+
if (hasFixed) {
251+
fixLines();
252+
}
253+
})();
254+
255+
if (fileChanged) {
256+
count++;
257+
fs.writeFileSync(file, lines.join('\n'));
258+
}
259+
});
260+
261+
console.log(`Fixed long lines in ${count} files.`);
262+
}

scss-bundle.config.json

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/cdk/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ rerootedStyles = [file for target in CDK_ENTRYPOINTS_WITH_STYLES for file in [
2222
"_%s.scss" % target,
2323
target,
2424
],
25+
[
26+
"_%s.import.scss" % target,
27+
target,
28+
],
2529
[
2630
"%s-prebuilt.css" % target,
2731
target,

src/cdk/a11y/_a11y.import.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
@forward 'a11y';
1+
@forward 'a11y' hide a11y, high-contrast;
2+
@forward 'a11y' as cdk-* hide cdk-optionally-nest-content;

src/cdk/a11y/_a11y.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@mixin cdk-a11y {
1+
@mixin a11y {
22
.cdk-visually-hidden {
33
border: 0;
44
clip: rect(0 0 0 0);
@@ -42,7 +42,7 @@
4242
/// * `on` - works for `Emulated`, `Native`, and `ShadowDom`
4343
/// * `off` - works for `None`
4444
/// * `any` - works for all encapsulation modes by emitting the CSS twice (default).
45-
@mixin cdk-high-contrast($target: active, $encapsulation: 'any') {
45+
@mixin high-contrast($target: active, $encapsulation: 'any') {
4646
@if ($target != 'active' and $target != 'black-on-white' and $target != 'white-on-black') {
4747
@error 'Unknown cdk-high-contrast value "#{$target}" provided. ' +
4848
'Allowed values are "active", "black-on-white", and "white-on-black"';

src/cdk/a11y/a11y-prebuilt.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
@import './a11y';
1+
@use './a11y';
22

3-
@include cdk-a11y();
3+
@include a11y.a11y();

src/cdk/overlay/_overlay.import.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
@forward 'overlay';
1+
@forward '../a11y/a11y' as cdk-*;
2+
@forward 'overlay' hide $dark-backdrop-background, $z-index-overlay, $z-index-overlay-backdrop,
3+
$z-index-overlay-container, overlay;
4+
@forward 'overlay' as cdk-* hide $cdk-backdrop-animation-duration,
5+
$cdk-backdrop-animation-timing-function, $cdk-dark-backdrop-background;
6+
@forward 'overlay' as cdk-overlay-* hide $cdk-overlay-backdrop-animation-duration,
7+
$cdk-overlay-backdrop-animation-timing-function, $cdk-overlay-z-index-overlay,
8+
$cdk-overlay-z-index-overlay-backdrop, $cdk-overlay-z-index-overlay-container, cdk-overlay-overlay;
9+
10+
@import '../a11y/a11y';

0 commit comments

Comments
 (0)