@@ -56,9 +56,9 @@ export function migrateFileContent(content: string,
56
56
57
57
// Try to migrate the symbols even if there are no imports. This is used
58
58
// to cover the case where the Components symbols were used transitively.
59
+ content = migrateCdkSymbols ( content , newCdkImportPath , cdkResults ) ;
59
60
content = migrateMaterialSymbols (
60
61
content , newMaterialImportPath , materialResults , extraMaterialSymbols ) ;
61
- content = migrateCdkSymbols ( content , newCdkImportPath , cdkResults ) ;
62
62
content = replaceRemovedVariables ( content , removedMaterialVariables ) ;
63
63
64
64
// We can assume that the migration has taken care of any Components symbols that were
@@ -142,7 +142,7 @@ function migrateMaterialSymbols(content: string, importPath: string,
142
142
143
143
if ( content !== initialContent ) {
144
144
// Add an import to the new API only if any of the APIs were being used.
145
- content = insertUseStatement ( content , importPath , detectedImports . imports , namespace ) ;
145
+ content = insertUseStatement ( content , importPath , namespace ) ;
146
146
}
147
147
148
148
return content ;
@@ -165,7 +165,7 @@ function migrateCdkSymbols(content: string, importPath: string,
165
165
// Previously the CDK symbols were exposed through `material/theming`, but now we have a
166
166
// dedicated entrypoint for the CDK. Only add an import for it if any of the symbols are used.
167
167
if ( content !== initialContent ) {
168
- content = insertUseStatement ( content , importPath , detectedImports . imports , namespace ) ;
168
+ content = insertUseStatement ( content , importPath , namespace ) ;
169
169
}
170
170
171
171
return content ;
@@ -186,10 +186,8 @@ function renameSymbols(content: string,
186
186
getKeyPattern : ( namespace : string | null , key : string ) => RegExp ,
187
187
formatValue : ( key : string ) => string ) : string {
188
188
// The null at the end is so that we make one last pass to cover non-namespaced symbols.
189
- [ ...namespaces . slice ( ) . sort ( sortLengthDescending ) , null ] . forEach ( namespace => {
190
- // Migrate the longest keys first so that our regex-based replacements don't accidentally
191
- // capture keys that contain other keys. E.g. `$mat-blue` is contained within `$mat-blue-grey`.
192
- Object . keys ( mapping ) . sort ( sortLengthDescending ) . forEach ( key => {
189
+ [ ...namespaces . slice ( ) , null ] . forEach ( namespace => {
190
+ Object . keys ( mapping ) . forEach ( key => {
193
191
const pattern = getKeyPattern ( namespace , key ) ;
194
192
195
193
// Sanity check since non-global regexes will only replace the first match.
@@ -205,26 +203,24 @@ function renameSymbols(content: string,
205
203
}
206
204
207
205
/** Inserts an `@use` statement in a string. */
208
- function insertUseStatement ( content : string , importPath : string , importsToIgnore : string [ ] ,
209
- namespace : string ) : string {
206
+ function insertUseStatement ( content : string , importPath : string , namespace : string ) : string {
210
207
// If the content already has the `@use` import, we don't need to add anything.
211
- const alreadyImportedPattern = new RegExp ( `@use +['"]${ importPath } ['"]` , 'g' ) ;
212
- if ( alreadyImportedPattern . test ( content ) ) {
208
+ if ( new RegExp ( `@use +['"]${ importPath } ['"]` , 'g' ) . test ( content ) ) {
213
209
return content ;
214
210
}
215
211
216
- // We want to find the first import that isn't in the list of ignored imports or find nothing,
217
- // because the imports being replaced might be the only ones in the file and they can be further
218
- // down. An easy way to do this is to replace the imports with a random character and run
219
- // `indexOf` on the result. This isn't the most efficient way of doing it, but it's more compact
220
- // and it allows us to easily deal with things like comment nodes .
221
- const contentToSearch = importsToIgnore . reduce ( ( accumulator , current ) =>
222
- accumulator . replace ( current , '◬' . repeat ( current . length ) ) , content ) ;
223
-
224
- // Sass has a limitation that all `@use` declarations have to come before `@import` so we have
225
- // to find the first import and insert before it. Technically we can get away with always
226
- // inserting at 0, but the file may start with something like a license header.
227
- const newImportIndex = Math . max ( 0 , contentToSearch . indexOf ( '@import ' ) ) ;
212
+ // Sass will throw an error if an `@use` statement comes after another statement. The safest way
213
+ // to ensure that we conform to that requirement is by always inserting our imports at the top
214
+ // of the file. Detecting where the user's content starts is tricky, because there are many
215
+ // different kinds of syntax we'd have to account for. One approach is to find the first `@import`
216
+ // and insert before it, but the problem is that Sass allows `@import` to be placed anywhere .
217
+ let newImportIndex = 0 ;
218
+
219
+ // One special case is if the file starts with a license header which we want to preserve on top.
220
+ if ( content . trim ( ) . startsWith ( '/*' ) ) {
221
+ const commentEndIndex = content . indexOf ( '*/' , content . indexOf ( '/*' ) ) ;
222
+ newImportIndex = content . indexOf ( '\n' , commentEndIndex ) + 1 ;
223
+ }
228
224
229
225
return content . slice ( 0 , newImportIndex ) + `@use '${ importPath } ' as ${ namespace } ;\n` +
230
226
content . slice ( newImportIndex ) ;
@@ -247,7 +243,8 @@ function getMixinValueFormatter(namespace: string): (name: string) => string {
247
243
248
244
/** Formats a migration key as a Sass function invocation. */
249
245
function functionKeyFormatter ( namespace : string | null , name : string ) : RegExp {
250
- return new RegExp ( escapeRegExp ( `${ namespace ? namespace + '.' : '' } ${ name } (` ) , 'g' ) ;
246
+ const functionName = escapeRegExp ( `${ namespace ? namespace + '.' : '' } ${ name } (` ) ;
247
+ return new RegExp ( `(?<![-_a-zA-Z0-9])${ functionName } ` , 'g' ) ;
251
248
}
252
249
253
250
/** Returns a function that can be used to format a Sass function replacement. */
@@ -257,7 +254,8 @@ function getFunctionValueFormatter(namespace: string): (name: string) => string
257
254
258
255
/** Formats a migration key as a Sass variable. */
259
256
function variableKeyFormatter ( namespace : string | null , name : string ) : RegExp {
260
- return new RegExp ( escapeRegExp ( `${ namespace ? namespace + '.' : '' } $${ name } ` ) , 'g' ) ;
257
+ const variableName = escapeRegExp ( `${ namespace ? namespace + '.' : '' } $${ name } ` ) ;
258
+ return new RegExp ( `${ variableName } (?![-_a-zA-Z0-9])` , 'g' ) ;
261
259
}
262
260
263
261
/** Returns a function that can be used to format a Sass variable replacement. */
@@ -270,11 +268,6 @@ function escapeRegExp(str: string): string {
270
268
return str . replace ( / ( [ . * + ? ^ = ! : $ { } ( ) | [ \] \/ \\ ] ) / g, '\\$1' ) ;
271
269
}
272
270
273
- /** Used with `Array.prototype.sort` to order strings in descending length. */
274
- function sortLengthDescending ( a : string , b : string ) {
275
- return b . length - a . length ;
276
- }
277
-
278
271
/** Removes all strings from another string. */
279
272
function removeStrings ( content : string , toRemove : string [ ] ) : string {
280
273
return toRemove
@@ -325,7 +318,7 @@ function extractNamespaceFromUseStatement(fullImport: string): string {
325
318
* @param variables Mapping between variable names and their values.
326
319
*/
327
320
function replaceRemovedVariables ( content : string , variables : Record < string , string > ) : string {
328
- Object . keys ( variables ) . sort ( sortLengthDescending ) . forEach ( variableName => {
321
+ Object . keys ( variables ) . forEach ( variableName => {
329
322
// Note that the pattern uses a negative lookahead to exclude
330
323
// variable assignments, because they can't be migrated.
331
324
const regex = new RegExp ( `\\$${ escapeRegExp ( variableName ) } (?!\\s+:|:)` , 'g' ) ;
0 commit comments