|
| 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 | +} |
0 commit comments