Skip to content

Commit

Permalink
refactor(material/core): sass module migration preparation
Browse files Browse the repository at this point in the history
  • Loading branch information
crisbeto committed Feb 20, 2021
1 parent 8eb59f8 commit 84f273f
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 145 deletions.
1 change: 0 additions & 1 deletion BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package(default_visibility = ["//visibility:public"])

exports_files([
"LICENSE",
"scss-bundle.config.json",
])

genrule(
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"@types/node-fetch": "^2.5.5",
"@types/parse5": "^6.0.0",
"@types/semver": "^7.3.4",
"@types/sass": "^1.16.0",
"@types/send": "^0.14.5",
"@types/stylelint": "^9.10.1",
"@types/yaml": "^1.9.7",
Expand Down Expand Up @@ -156,7 +157,6 @@
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-sourcemaps": "^0.6.3",
"sass": "^1.29.0",
"scss-bundle": "^3.1.2",
"selenium-webdriver": "^3.6.0",
"semver": "^7.3.4",
"send": "^0.17.1",
Expand Down
262 changes: 262 additions & 0 deletions scripts/migrate-sass-modules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
const childProcess = require('child_process');
const path = require('path');
const fs = require('fs');
const {sync: glob} = require('glob');

// Script that migrates the library source to the Sass module system while maintaining
// backwards-compatibility. The script assumes that `sass-migrator` is installed
// globally and that the results will be committed. Works by migrating the .scss files
// based on their position in the dependency tree, starting with the files that are depended
// upon the most and working downwards. Furthermore, because the `sass-migrator` isn't able to
// pick up imports from the `node_modules`, there is a workaround that comments out all of the
// imports from `@material/*`, runs the migration and re-adds the imports back. The script also
// sorts all remaining `@import` statements lower than `@use` statements to avoid compilation
// errors and auto-fixes some linting failures that are generated by the migrator.

const directory = path.join(__dirname, '../src');
const migratedFiles = new Set();
const ignorePatterns = [
'**/*.import.scss',
'**/test-theming-bundle.scss',
'material/_theming.scss'
];
const materialPrefixes = [
...getPrefixes('material', 'mat'),
...getPrefixes('material/core', 'mat'),
// Outliers that don't have a directory of their own.
'mat-pseudo-checkbox-',
'mat-elevation-',
'mat-optgroup-',
'mat-private-'
];
const mdcPrefixes = [
...getPrefixes('material-experimental', 'mat'),
...getPrefixes('material-experimental/mdc-core', 'mat'),
// Outliers that don't have a directory of their own.
'mat-mdc-optgroup-'
].map(prefix => prefix === 'mat-' ? 'mat-mdc-' : prefix);
const cdkPrefixes = getPrefixes('cdk', 'cdk');
const cdkExperimentalPrefixes = getPrefixes('cdk-experimental', 'cdk');

// Restore the source directory to a clean state.
run('git', ['clean', '-f', '-d'], false, true);
run('git', ['checkout', '--', directory], false, true);

// --reset is a utility to easily restore the repo to its initial state.
if (process.argv.indexOf('--reset') > -1) {
process.exit(0);
}

// Generate this after the repo has been reset.
const importsToAdd = extractImports();

// Run the migrations.

// Clean up any existing import files, because they interfere with the migration.
clearImportFiles();

// Migrate all the partials and forward any export symbols.
migrate('cdk/**/_*.scss', cdkPrefixes, true);
migrate('cdk-experimental/**/_*.scss', cdkExperimentalPrefixes, true);
migrate('material/core/**/_*.scss', materialPrefixes, true, ['**/_all-*.scss', '**/_core.scss']);
migrate('material/!(core)/**/_*.scss', materialPrefixes, true);
migrate('material/core/**/_*.scss', materialPrefixes, true);

// Comment out all MDC imports since the migrator script doesn't know how to find them.
commentOutMdc('material-experimental/**/*.scss');

// Migrate all of the MDC partials.
migrate('material-experimental/mdc-helpers/**/_*.scss', mdcPrefixes, true);
migrate('material-experimental/mdc-core/**/_*.scss', mdcPrefixes, true, ['**/_core.scss']);
migrate('material-experimental/**/_*.scss', mdcPrefixes, true);

// Migrate everything else without forwarding.
migrate('cdk/**/*.scss', cdkPrefixes);
migrate('cdk-experimental/**/*.scss', cdkExperimentalPrefixes);
migrate('material/**/*.scss', materialPrefixes);
migrate('material-experimental/**/*.scss', mdcPrefixes);

// Migrate whatever is left in the source files, assuming that it's not a public API.
migrate('**/*.scss');

// Restore the commented out MDC imports and sort `@use` above `@import`.
restoreAndSortMdc('material-experimental/**/*.scss');

// Clear the files that we don't want.
clearUnwantedFiles();

// Re-add all the imports for backwards compatibility.
reAddImports(importsToAdd);

// Try to auto-fix some of the lint issues using Stylelint.
run('yarn', ['stylelint', '--fix'], true, true);

// At this point most of the lint failures are going to be from long `@forward` statements inside
// .import.scss files. Try to auto-resolve them and then fix everything else manually.
fixSomeLongLines('**/*.import.scss', 100);

console.log(`Finished migrating ${migratedFiles.size} files.`);

function migrate(pattern, prefixes = [], forward = false, ignore = []) {
const args = ['module'];
forward && args.push('--forward=import-only');
prefixes.length && args.push(`--remove-prefix=${prefixes.join(',')}`);

// Note that while the migrator allows for multiple files to be passed in, we start getting
// some assertion errors along the way. Running it on a file-by-file basis works fine.
const files = glob(pattern, {cwd: directory, ignore: [...ignore, ...ignorePatterns]})
.filter(file => !migratedFiles.has(file));
const message = `Migrating ${files.length} unmigrated files matching ${pattern}.`;
console.log(ignore.length ? message + ` Ignoring ${ignore.join(', ')}.` : message);
run('sass-migrator', [...args, ...files]);
files.forEach(file => migratedFiles.add(file));
}

function run(name, args, canFail = false, silent = false) {
const result = childProcess.spawnSync(name, args, {shell: true, cwd: directory});
const output = result.stdout.toString();
!silent && output.length && console.log(output);

if (result.status !== 0 && !canFail) {
console.error(`Script error: ${(result.stderr || result.stdout)}`);
process.exit(1);
}
}

function getPrefixes(package, prefix) {
return fs.readdirSync(path.join(directory, package), {withFileTypes: true})
.filter(current => current.isDirectory())
.map(current => current.name)
.reduce((output, current) => [`${prefix}-${current}-`, ...output], [`${prefix}-`]);
}

function commentOutMdc(pattern) {
const files = glob(pattern, {cwd: directory, absolute: true});
console.log(`Commenting out @material imports from ${files.length} files matching ${pattern}.`);
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
// Prefix the content with a marker so we know what to restore later.
fs.writeFileSync(file, content.replace(/(@use|@import) '@material/g, m => '//🚀 ' + m));
});
}

function restoreAndSortMdc(pattern) {
const files = glob(pattern, {cwd: directory, absolute: true});
console.log(`Re-adding and sorting @material imports from ${files.length} ` +
`files matching ${pattern}.`);

files.forEach(file => {
// Remove the commented out lines with the marker from `commentOutMdc`.
const content = fs.readFileSync(file, 'utf8').replace(/\/\/🚀 /g, '');
const lines = content.split('\n');
let headerStartIndex = -1;
let headerEndIndex = -1;

// Find where the comments start and end.
for (let i = lines.length - 1; i > -1; i--) {
if (lines[i].startsWith('@use') || lines[i].startsWith('@import')) {
headerStartIndex = i;

if (headerEndIndex === -1) {
headerEndIndex = i + 1;
}
}
}

// Sort the imports so that `@use` comes before `@import`. Otherwise Sass will throw an error.
if (headerStartIndex > -1 && headerEndIndex > -1) {
const headers = lines
.splice(headerStartIndex, headerEndIndex - headerStartIndex)
.sort((a, b) => a.startsWith('@use') && !b.startsWith('@use') ? -1 : 0);
lines.splice(headerStartIndex, 0, ...headers);
}

fs.writeFileSync(file, lines.join('\n'));
});
}

function clearImportFiles() {
const files = glob('**/*.import.scss', {cwd: directory, absolute: true});
console.log(`Clearing ${files.length} import files.`);
files.forEach(file => fs.unlinkSync(file));
}

function clearUnwantedFiles() {
// The migration script generates .import files even if we don't pass in the `--forward` when
// a file has top-level variables matching a prefix. Since we still want such files to be
// migrated, we clear the unwanted files afterwards.
const files = glob('**/*.import.scss', {cwd: directory, absolute: true, ignore: ['**/_*.scss']});
console.log(`Clearing ${files.length} unwanted files.`);
files.forEach(file => fs.unlinkSync(file));
}

function extractImports() {
return glob('**/*.scss', {cwd: directory, absolute: true}).reduce((result, file) => {
const content = fs.readFileSync(file, 'utf8');
const match = content.match(/@import '(.*)';/g);
const imports = match ? match.filter(dep => !dep.includes(` '@material/`)) : [];
if (imports.length) {
result[file] = imports;
}
return result;
}, {});
}


function reAddImports(mapping) {
Object.keys(mapping).forEach(fileName => {
const importEquivalentName = fileName.replace('.scss', '.import.scss');

if (fs.existsSync(importEquivalentName)) {
let content = fs.readFileSync(importEquivalentName, 'utf8');
mapping[fileName].forEach(importedFile => content += `\n${importedFile}`);
fs.writeFileSync(importEquivalentName, content);
}
});
}


function fixSomeLongLines(pattern, limit) {
const files = glob(pattern, {cwd: directory, absolute: true});
let count = 0;

files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
let lines = content.split('\n');
let fileChanged = false;

(function fixLines() {
const newLines = [];
let hasFixed = false;

lines.forEach(line => {
if (line.length > limit) {
const breakAt = line.lastIndexOf(' ', limit);
if (breakAt > -1) {
// Split the line in two at the limit.
newLines.push(line.slice(0, breakAt), line.slice(breakAt + 1));
fileChanged = hasFixed = true;
} else {
newLines.push(line);
}
} else {
newLines.push(line);
}
});

lines = newLines;

// Keep fixing until there's nothing left. Not particularly efficient...
if (hasFixed) {
fixLines();
}
})();

if (fileChanged) {
count++;
fs.writeFileSync(file, lines.join('\n'));
}
});

console.log(`Fixed long lines in ${count} files.`);
}
10 changes: 0 additions & 10 deletions scss-bundle.config.json

This file was deleted.

4 changes: 4 additions & 0 deletions src/cdk/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ rerootedStyles = [file for target in CDK_ENTRYPOINTS_WITH_STYLES for file in [
"_%s.scss" % target,
target,
],
[
"_%s.import.scss" % target,
target,
],
[
"%s-prebuilt.css" % target,
target,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use '@material/ripple/mixins' as mdc-ripple;
@use '@material/textfield/variables' as mdc-text-field;

@import '@material/density/functions.import';
@import '@material/theme/variables.import';
Expand All @@ -15,8 +16,8 @@
// Mixin that overwrites the default MDC text-field color styles to be based on
// the given theme palette. The MDC text-field is styled using `primary` by default.
@mixin _mat-mdc-text-field-color-styles($palette-name, $query: $mat-theme-styles-query) {
$_mdc-text-field-focused-label-color: $mdc-text-field-focused-label-color;
$mdc-text-field-focused-label-color: rgba(mdc-theme-prop-value($palette-name), 0.87) !global;
$_mdc-text-field-focused-label-color: mdc-text-field.$focused-label-color;
mdc-text-field.$focused-label-color: rgba(mdc-theme-prop-value($palette-name), 0.87);

@include mdc-text-field-caret-color($palette-name, $query);
@include mdc-text-field-line-ripple-color($palette-name, $query);
Expand All @@ -33,7 +34,7 @@
@include mdc-text-field-focused-outline-color($palette-name, $query);
}

$mdc-text-field-focused-label-color: $_mdc-text-field-focused-label-color !global;
mdc-text-field.$focused-label-color: $_mdc-text-field-focused-label-color;
}

@mixin mat-mdc-form-field-color($config-or-theme) {
Expand Down
25 changes: 5 additions & 20 deletions src/material/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
load("@npm//scss-bundle:index.bzl", "scss_bundle")
load("//src/cdk:config.bzl", "CDK_SCSS_LIBS")
load(
"//src/material:config.bzl",
"MATERIAL_ENTRYPOINTS",
"MATERIAL_SCSS_LIBS",
"MATERIAL_TARGETS",
"MATERIAL_TESTING_TARGETS",
)
load("//tools:defaults.bzl", "ng_package", "ts_library")
load("//tools:defaults.bzl", "ng_package", "sass_library", "ts_library")

package(default_visibility = ["//visibility:public"])

Expand All @@ -22,24 +20,11 @@ filegroup(
srcs = ["//src/material/%s:overview" % name for name in MATERIAL_ENTRYPOINTS],
)

scss_bundle(
# Makes the theming bundle available in the release output as `angular/material/theming`.
sass_library(
name = "theming_bundle",
outs = ["_theming.scss"],
args = [
"--entryFile=$(execpath :theming-bundle.scss)",
"--outFile=$(execpath :_theming.scss)",

# The config file has to be passed in explicitly, otherwise the
# bundler will still run, but produce massive bundle files.
"--config=scss-bundle.config.json",
],
data = CDK_SCSS_LIBS + MATERIAL_SCSS_LIBS + [
"theming-bundle.scss",
"//src/material/core:theming_scss_lib",
# Config file is required by "scss-bundle" and will be automatically
# loaded by the CLI. It expects the config to be in the execroot.
"//:scss-bundle.config.json",
],
srcs = ["_theming.scss"],
deps = ["//src/material/core:theming_scss_lib"],
)

# Creates the @angular/material package published to npm.
Expand Down
8 changes: 8 additions & 0 deletions src/material/_theming.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Forwards all public API mixins so they can be imported from a single entry point.
// Note that we have to forward the `.import` files for backwards-compatibility with
// projects that don't use Sass modules and include the `mat-`-prefixed mixins.

@forward './core/color/all-color.import';
@forward './core/density/private/all-density.import';
@forward './core/theming/all-theme.import';
@forward './core/typography/all-typography.import';
Loading

0 comments on commit 84f273f

Please sign in to comment.