mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(migrations): preserve registerLocaleData calls in standalone bootstrap migration
Currently the standalone migration only copies symbols referenced from the NgModule metadata into the main file. Top-level `registerLocaleData` calls, and the default imports they depend on, are dropped silently when the module file is pruned, which breaks locale resolution at runtime with a "Missing locale data" error.
Copies top-level `registerLocaleData` calls from the file of the bootstrapped module into the main file, and adds support for carrying over default imports which were previously skipped silently by the reference resolution.
Fixes #50886
(cherry picked from commit bdc09e8183)
This commit is contained in:
@@ -13,7 +13,9 @@ import ts from 'typescript';
|
||||
|
||||
import {ChangeTracker, ImportRemapper} from '../../utils/change_tracker';
|
||||
import {getAngularDecorators} from '../../utils/ng_decorators';
|
||||
import {getImportSpecifier} from '../../utils/typescript/imports';
|
||||
import {closestNode} from '../../utils/typescript/nodes';
|
||||
import {isReferenceToImport} from '../../utils/typescript/symbol';
|
||||
|
||||
import {
|
||||
DeclarationImportsRemapper,
|
||||
@@ -255,6 +257,22 @@ function migrateBootstrapCall(
|
||||
);
|
||||
}
|
||||
|
||||
// Top-level `registerLocaleData` calls aren't referenced from the module's metadata so they
|
||||
// have to be carried over explicitly, otherwise the locale data won't be registered anymore
|
||||
// once the module file is pruned.
|
||||
if (moduleSourceFile !== sourceFile) {
|
||||
const localeDataCalls = findRegisterLocaleDataCalls(moduleSourceFile, typeChecker);
|
||||
|
||||
if (localeDataCalls.length > 0) {
|
||||
nodeLookup = nodeLookup || getNodeLookup(moduleSourceFile);
|
||||
|
||||
for (const statement of localeDataCalls) {
|
||||
addNodesToCopy(sourceFile, statement, nodeLookup, tracker, nodesToCopy, referenceResolver);
|
||||
nodesToCopy.add(statement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalProviders) {
|
||||
additionalProviders.forEach((moduleSpecifier, name) => {
|
||||
providersInNewCall.push(
|
||||
@@ -633,6 +651,30 @@ function getRouterModuleForRootFeatures(
|
||||
return featureExpressions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all the top-level `registerLocaleData` calls within a file.
|
||||
* @param sourceFile File in which to search for the calls.
|
||||
* @param typeChecker
|
||||
*/
|
||||
function findRegisterLocaleDataCalls(
|
||||
sourceFile: ts.SourceFile,
|
||||
typeChecker: ts.TypeChecker,
|
||||
): ts.ExpressionStatement[] {
|
||||
const importSpecifier = getImportSpecifier(sourceFile, '@angular/common', 'registerLocaleData');
|
||||
|
||||
if (importSpecifier === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sourceFile.statements.filter(
|
||||
(statement): statement is ts.ExpressionStatement =>
|
||||
ts.isExpressionStatement(statement) &&
|
||||
ts.isCallExpression(statement.expression) &&
|
||||
ts.isIdentifier(statement.expression.expression) &&
|
||||
isReferenceToImport(typeChecker, statement.expression.expression, importSpecifier),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all the nodes that are referenced inside a root node and would need to be copied into a
|
||||
* new file in order for the node to compile, and tracks them.
|
||||
@@ -676,6 +718,37 @@ function addNodesToCopy(
|
||||
continue;
|
||||
}
|
||||
|
||||
// The reference can also be a default import (e.g. locale data files like
|
||||
// `import localeFr from '@angular/common/locales/fr'`) which the import manager
|
||||
// doesn't support. Copy the import declaration over or recreate it as a named
|
||||
// import of the `default` symbol if it can't be copied verbatim.
|
||||
// Note: when the import clause consists only of a default import, the clause and its name
|
||||
// have the same offsets so the node lookup can resolve the reference to either of the two.
|
||||
const importClause = closestOrSelf(ref, ts.isImportClause);
|
||||
const defaultImportName =
|
||||
importClause !== null && (ref === importClause || ref === importClause.name)
|
||||
? (importClause.name ?? null)
|
||||
: null;
|
||||
|
||||
if (importClause && defaultImportName) {
|
||||
const declaration = closestNode(importClause, ts.isImportDeclaration);
|
||||
|
||||
if (declaration && ts.isStringLiteralLike(declaration.moduleSpecifier)) {
|
||||
if (
|
||||
importClause.namedBindings === undefined &&
|
||||
!declaration.moduleSpecifier.text.startsWith('.')
|
||||
) {
|
||||
nodesToCopy.add(declaration);
|
||||
} else {
|
||||
const moduleName = declaration.moduleSpecifier.text.startsWith('.')
|
||||
? remapRelativeImport(targetFile.fileName, declaration.moduleSpecifier)
|
||||
: declaration.moduleSpecifier.text;
|
||||
tracker.addImport(targetFile, 'default', moduleName, defaultImportName.text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const variableDeclaration = closestOrSelf(ref, ts.isVariableDeclaration);
|
||||
const variableStatement = variableDeclaration
|
||||
? closestNode(variableDeclaration, ts.isVariableStatement)
|
||||
|
||||
@@ -128,6 +128,8 @@ describe('standalone migration', () => {
|
||||
[typeof NgIf, typeof NgForOf], [typeof NgIf, typeof NgForOf]>;
|
||||
}
|
||||
|
||||
export declare function registerLocaleData(data: any, localeId?: any, extraData?: any): void;
|
||||
|
||||
export {NgForOf as NgFor};
|
||||
`,
|
||||
);
|
||||
@@ -4285,6 +4287,66 @@ describe('standalone migration', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should copy top-level registerLocaleData calls to the main file', async () => {
|
||||
writeFile(
|
||||
'/node_modules/@angular/common/locales/fr.d.ts',
|
||||
`
|
||||
declare const localeFr: unknown[];
|
||||
export default localeFr;
|
||||
`,
|
||||
);
|
||||
|
||||
writeFile(
|
||||
'main.ts',
|
||||
`
|
||||
import {AppModule} from './app/app.module';
|
||||
import {platformBrowser} from '@angular/platform-browser';
|
||||
|
||||
platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e));
|
||||
`,
|
||||
);
|
||||
|
||||
writeFile(
|
||||
'./app/app.module.ts',
|
||||
`
|
||||
import {NgModule, Component, LOCALE_ID} from '@angular/core';
|
||||
import {registerLocaleData} from '@angular/common';
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
|
||||
registerLocaleData(localeFr);
|
||||
|
||||
@Component({template: 'hello', standalone: false})
|
||||
export class AppComponent {}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
bootstrap: [AppComponent],
|
||||
providers: [{provide: LOCALE_ID, useValue: 'fr'}]
|
||||
})
|
||||
export class AppModule {}
|
||||
`,
|
||||
);
|
||||
|
||||
await runMigration('standalone-bootstrap');
|
||||
|
||||
const content = stripWhitespace(tree.readContent('main.ts'));
|
||||
|
||||
expect(content).toContain(
|
||||
stripWhitespace(`import localeFr from '@angular/common/locales/fr';`),
|
||||
);
|
||||
expect(content).toContain(
|
||||
stripWhitespace(`import {registerLocaleData} from '@angular/common';`),
|
||||
);
|
||||
expect(content).toContain(stripWhitespace(`registerLocaleData(localeFr);`));
|
||||
expect(content).toContain(
|
||||
stripWhitespace(`
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [{provide: LOCALE_ID, useValue: 'fr'}]
|
||||
}).catch(e => console.error(e));
|
||||
`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not copy over non-declaration references to the main file', async () => {
|
||||
writeFile(
|
||||
'main.ts',
|
||||
|
||||
Reference in New Issue
Block a user