@@ -56,9 +56,9 @@ export function migrateFileContent(content: string,
5656
5757 // Try to migrate the symbols even if there are no imports. This is used
5858 // to cover the case where the Components symbols were used transitively.
59+ content = migrateCdkSymbols ( content , newCdkImportPath , cdkResults ) ;
5960 content = migrateMaterialSymbols (
6061 content , newMaterialImportPath , materialResults , extraMaterialSymbols ) ;
61- content = migrateCdkSymbols ( content , newCdkImportPath , cdkResults ) ;
6262 content = replaceRemovedVariables ( content , removedMaterialVariables ) ;
6363
6464 // 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,
142142
143143 if ( content !== initialContent ) {
144144 // 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 ) ;
146146 }
147147
148148 return content ;
@@ -165,7 +165,7 @@ function migrateCdkSymbols(content: string, importPath: string,
165165 // Previously the CDK symbols were exposed through `material/theming`, but now we have a
166166 // dedicated entrypoint for the CDK. Only add an import for it if any of the symbols are used.
167167 if ( content !== initialContent ) {
168- content = insertUseStatement ( content , importPath , detectedImports . imports , namespace ) ;
168+ content = insertUseStatement ( content , importPath , namespace ) ;
169169 }
170170
171171 return content ;
@@ -186,10 +186,8 @@ function renameSymbols(content: string,
186186 getKeyPattern : ( namespace : string | null , key : string ) => RegExp ,
187187 formatValue : ( key : string ) => string ) : string {
188188 // 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 => {
193191 const pattern = getKeyPattern ( namespace , key ) ;
194192
195193 // Sanity check since non-global regexes will only replace the first match.
@@ -205,26 +203,24 @@ function renameSymbols(content: string,
205203}
206204
207205/** 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 {
210207 // 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 ) ) {
213209 return content ;
214210 }
215211
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+ }
228224
229225 return content . slice ( 0 , newImportIndex ) + `@use '${ importPath } ' as ${ namespace } ;\n` +
230226 content . slice ( newImportIndex ) ;
@@ -247,7 +243,8 @@ function getMixinValueFormatter(namespace: string): (name: string) => string {
247243
248244/** Formats a migration key as a Sass function invocation. */
249245function 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' ) ;
251248}
252249
253250/** Returns a function that can be used to format a Sass function replacement. */
@@ -257,7 +254,8 @@ function getFunctionValueFormatter(namespace: string): (name: string) => string
257254
258255/** Formats a migration key as a Sass variable. */
259256function 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' ) ;
261259}
262260
263261/** Returns a function that can be used to format a Sass variable replacement. */
@@ -270,11 +268,6 @@ function escapeRegExp(str: string): string {
270268 return str . replace ( / ( [ . * + ? ^ = ! : $ { } ( ) | [ \] \/ \\ ] ) / g, '\\$1' ) ;
271269}
272270
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-
278271/** Removes all strings from another string. */
279272function removeStrings ( content : string , toRemove : string [ ] ) : string {
280273 return toRemove
@@ -325,7 +318,7 @@ function extractNamespaceFromUseStatement(fullImport: string): string {
325318 * @param variables Mapping between variable names and their values.
326319 */
327320function replaceRemovedVariables ( content : string , variables : Record < string , string > ) : string {
328- Object . keys ( variables ) . sort ( sortLengthDescending ) . forEach ( variableName => {
321+ Object . keys ( variables ) . forEach ( variableName => {
329322 // Note that the pattern uses a negative lookahead to exclude
330323 // variable assignments, because they can't be migrated.
331324 const regex = new RegExp ( `\\$${ escapeRegExp ( variableName ) } (?!\\s+:|:)` , 'g' ) ;
0 commit comments