Skip to content

Commit cd12aa7

Browse files
crisbetommalerba
authored andcommitted
fix(material/schematics): theming migration schematic not inserting use statement at the top of the file in some cases
Our logic for determining where to insert an `@use` statement is to look for the first `@import` and insert before it. The problem is that if the theming import is the only one in the file and it's further down, we'll insert it incorrectly, because `@use` has to always be above other statements.
1 parent eca9b64 commit cd12aa7

File tree

2 files changed

+69
-17
lines changed

2 files changed

+69
-17
lines changed

src/material/schematics/ng-update/migrations/theming-api-v12/migration.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import {
1616
unprefixedRemovedVariables
1717
} from './config';
1818

19+
/** The result of a search for imports and namespaces in a file. */
20+
interface DetectImportResult {
21+
imports: string[];
22+
namespaces: string[];
23+
}
24+
1925
/**
2026
* Migrates the content of a file to the new theming API. Note that this migration is using plain
2127
* string manipulation, rather than the AST from PostCSS and the schematics string manipulation
@@ -40,8 +46,8 @@ export function migrateFileContent(content: string,
4046

4147
// Try to migrate the symbols even if there are no imports. This is used
4248
// to cover the case where the Components symbols were used transitively.
43-
content = migrateMaterialSymbols(content, newMaterialImportPath, materialResults.namespaces);
44-
content = migrateCdkSymbols(content, newCdkImportPath, cdkResults.namespaces);
49+
content = migrateMaterialSymbols(content, newMaterialImportPath, materialResults);
50+
content = migrateCdkSymbols(content, newCdkImportPath, cdkResults);
4551
content = replaceRemovedVariables(content, removedMaterialVariables);
4652

4753
// We can assume that the migration has taken care of any Components symbols that were
@@ -64,7 +70,7 @@ export function migrateFileContent(content: string,
6470
* @param content File content in which to look for imports.
6571
* @param prefix Prefix that the imports should start with.
6672
*/
67-
function detectImports(content: string, prefix: string): {imports: string[], namespaces: string[]} {
73+
function detectImports(content: string, prefix: string): DetectImportResult {
6874
if (prefix[prefix.length - 1] !== '/') {
6975
// Some of the logic further down makes assumptions about the import depth.
7076
throw Error(`Prefix "${prefix}" has to end in a slash.`);
@@ -96,47 +102,49 @@ function detectImports(content: string, prefix: string): {imports: string[], nam
96102
}
97103

98104
/** Migrates the Material symbls in a file. */
99-
function migrateMaterialSymbols(content: string, importPath: string, namespaces: string[]): string {
105+
function migrateMaterialSymbols(content: string, importPath: string,
106+
detectedImports: DetectImportResult): string {
100107
const initialContent = content;
101108
const namespace = 'mat';
102109

103110
// Migrate the mixins.
104-
content = renameSymbols(content, materialMixins, namespaces, mixinKeyFormatter,
111+
content = renameSymbols(content, materialMixins, detectedImports.namespaces, mixinKeyFormatter,
105112
getMixinValueFormatter(namespace));
106113

107114
// Migrate the functions.
108-
content = renameSymbols(content, materialFunctions, namespaces, functionKeyFormatter,
109-
getFunctionValueFormatter(namespace));
115+
content = renameSymbols(content, materialFunctions, detectedImports.namespaces,
116+
functionKeyFormatter, getFunctionValueFormatter(namespace));
110117

111118
// Migrate the variables.
112-
content = renameSymbols(content, materialVariables, namespaces, variableKeyFormatter,
113-
getVariableValueFormatter(namespace));
119+
content = renameSymbols(content, materialVariables, detectedImports.namespaces,
120+
variableKeyFormatter, getVariableValueFormatter(namespace));
114121

115122
if (content !== initialContent) {
116123
// Add an import to the new API only if any of the APIs were being used.
117-
content = insertUseStatement(content, importPath, namespace);
124+
content = insertUseStatement(content, importPath, detectedImports.imports, namespace);
118125
}
119126

120127
return content;
121128
}
122129

123130
/** Migrates the CDK symbols in a file. */
124-
function migrateCdkSymbols(content: string, importPath: string, namespaces: string[]): string {
131+
function migrateCdkSymbols(content: string, importPath: string,
132+
detectedImports: DetectImportResult): string {
125133
const initialContent = content;
126134
const namespace = 'cdk';
127135

128136
// Migrate the mixins.
129-
content = renameSymbols(content, cdkMixins, namespaces, mixinKeyFormatter,
137+
content = renameSymbols(content, cdkMixins, detectedImports.namespaces, mixinKeyFormatter,
130138
getMixinValueFormatter(namespace));
131139

132140
// Migrate the variables.
133-
content = renameSymbols(content, cdkVariables, namespaces, variableKeyFormatter,
141+
content = renameSymbols(content, cdkVariables, detectedImports.namespaces, variableKeyFormatter,
134142
getVariableValueFormatter(namespace));
135143

136144
// Previously the CDK symbols were exposed through `material/theming`, but now we have a
137145
// dedicated entrypoint for the CDK. Only add an import for it if any of the symbols are used.
138146
if (content !== initialContent) {
139-
content = insertUseStatement(content, importPath, namespace);
147+
content = insertUseStatement(content, importPath, detectedImports.imports, namespace);
140148
}
141149

142150
return content;
@@ -175,11 +183,21 @@ function renameSymbols(content: string,
175183
}
176184

177185
/** Inserts an `@use` statement in a string. */
178-
function insertUseStatement(content: string, importPath: string, namespace: string): string {
186+
function insertUseStatement(content: string, importPath: string, importsToIgnore: string[],
187+
namespace: string): string {
188+
// We want to find the first import that isn't in the list of ignored imports or find nothing,
189+
// because the imports being replaced might be the only ones in the file and they can be further
190+
// down. An easy way to do this is to replace the imports with a random character and run
191+
// `indexOf` on the result. This isn't the most efficient way of doing it, but it's more compact
192+
// and it allows us to easily deal with things like comment nodes.
193+
const contentToSearch = importsToIgnore.reduce((accumulator, current) =>
194+
accumulator.replace(current, '◬'.repeat(current.length)), content);
195+
179196
// Sass has a limitation that all `@use` declarations have to come before `@import` so we have
180197
// to find the first import and insert before it. Technically we can get away with always
181198
// inserting at 0, but the file may start with something like a license header.
182-
const newImportIndex = Math.max(0, content.indexOf('@import '));
199+
const newImportIndex = Math.max(0, contentToSearch.indexOf('@import '));
200+
183201
return content.slice(0, newImportIndex) + `@use '${importPath}' as ${namespace};\n` +
184202
content.slice(newImportIndex);
185203
}

src/material/schematics/ng-update/test-cases/v12/misc/theming-api-v12.spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,8 @@ describe('v12 theming API migration', () => {
423423
await runMigration();
424424

425425
expect(splitFile(THEME_PATH)).toEqual([
426-
`@use '~@angular/material' as mat;`,
427426
`@use '~@angular/cdk' as cdk;`,
427+
`@use '~@angular/material' as mat;`,
428428

429429
`@include cdk.overlay();`,
430430

@@ -659,4 +659,38 @@ describe('v12 theming API migration', () => {
659659
]);
660660
});
661661

662+
it('should insert the @use statement at the top of the file, if the theming import is ' +
663+
'the only import in the file and there is other content before it', async () => {
664+
writeLines(THEME_PATH, [
665+
`:host {`,
666+
`display: block;`,
667+
`width: 100%;`,
668+
`}`,
669+
``,
670+
`@import '~@angular/material/theming';`,
671+
``,
672+
`.button {`,
673+
`@include mat-elevation(4);`,
674+
`padding: 8px;`,
675+
`}`,
676+
]);
677+
678+
await runMigration();
679+
680+
expect(splitFile(THEME_PATH)).toEqual([
681+
`@use '~@angular/material' as mat;`,
682+
`:host {`,
683+
`display: block;`,
684+
`width: 100%;`,
685+
`}`,
686+
``,
687+
``,
688+
`.button {`,
689+
`@include mat.elevation(4);`,
690+
`padding: 8px;`,
691+
`}`,
692+
]);
693+
});
694+
695+
662696
});

0 commit comments

Comments
 (0)