fix(ivy): use globally unique names for i18n constants (#25689)
Closure compiler requires that the i18n message constants of the form const MSG_XYZ = goog.getMessage('...'); have names that are unique across an entire compilation, even if the variables themselves are local to a given module. This means that in practice these names must be unique in a codebase. The best way to guarantee this requirement is met is to encode the relative file name of the file into which the constant is being written into the constant name itself. This commit implements that solution. PR Close #25689
This commit is contained in:

committed by
Misko Hevery

parent
bd0eb0d1d4
commit
cc29b9cf93
@ -122,7 +122,8 @@ export class ConstantPool {
|
||||
// */
|
||||
// const MSG_XYZ = goog.getMsg('message');
|
||||
// ```
|
||||
getTranslation(message: string, meta: {description?: string, meaning?: string}): o.Expression {
|
||||
getTranslation(message: string, meta: {description?: string, meaning?: string}, suffix: string):
|
||||
o.Expression {
|
||||
// The identity of an i18n message depends on the message and its meaning
|
||||
const key = meta.meaning ? `${message}\u0000\u0000${meta.meaning}` : message;
|
||||
|
||||
@ -138,7 +139,7 @@ export class ConstantPool {
|
||||
}
|
||||
|
||||
// Call closure to get the translation
|
||||
const variable = o.variable(this.freshTranslationName());
|
||||
const variable = o.variable(this.freshTranslationName(suffix));
|
||||
const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]);
|
||||
const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||
this.statements.push(msgStmt);
|
||||
@ -257,8 +258,8 @@ export class ConstantPool {
|
||||
|
||||
private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); }
|
||||
|
||||
private freshTranslationName(): string {
|
||||
return this.uniqueName(TRANSLATION_PREFIX).toUpperCase();
|
||||
private freshTranslationName(suffix: string): string {
|
||||
return this.uniqueName(TRANSLATION_PREFIX + suffix).toUpperCase();
|
||||
}
|
||||
|
||||
private keyOf(expression: o.Expression) {
|
||||
|
@ -127,6 +127,13 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
||||
* Selectors found in the <ng-content> tags in the template.
|
||||
*/
|
||||
ngContentSelectors: string[];
|
||||
|
||||
/**
|
||||
* Path to the .ts file in which this template's generated code will be included, relative to
|
||||
* the compilation root. This will be used to generate identifiers that need to be globally
|
||||
* unique in certain contexts (such as g3).
|
||||
*/
|
||||
relativeContextFilePath: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -209,7 +209,8 @@ export function compileComponentFromMetadata(
|
||||
const template = meta.template;
|
||||
const templateBuilder = new TemplateDefinitionBuilder(
|
||||
constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName, meta.viewQueries,
|
||||
directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML);
|
||||
directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
|
||||
meta.template.relativeContextFilePath);
|
||||
|
||||
const templateFunctionExpression = templateBuilder.buildTemplateFunction(
|
||||
template.nodes, [], template.hasNgContent, template.ngContentSelectors);
|
||||
@ -309,6 +310,7 @@ export function compileComponentFromRender2(
|
||||
nodes: render3Ast.nodes,
|
||||
hasNgContent: render3Ast.hasNgContent,
|
||||
ngContentSelectors: render3Ast.ngContentSelectors,
|
||||
relativeContextFilePath: '',
|
||||
},
|
||||
directives: typeMapToExpressionMap(directiveTypeBySel, outputCtx),
|
||||
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
|
||||
|
@ -96,18 +96,25 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// Number of binding slots
|
||||
private _bindingSlots = 0;
|
||||
|
||||
private fileBasedI18nSuffix: string;
|
||||
|
||||
constructor(
|
||||
private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0,
|
||||
private contextName: string|null, private templateName: string|null,
|
||||
private viewQueries: R3QueryMetadata[], private directiveMatcher: SelectorMatcher|null,
|
||||
private directives: Set<o.Expression>, private pipeTypeByName: Map<string, o.Expression>,
|
||||
private pipes: Set<o.Expression>, private _namespace: o.ExternalReference) {
|
||||
private pipes: Set<o.Expression>, private _namespace: o.ExternalReference,
|
||||
private relativeContextFilePath: string) {
|
||||
// view queries can take up space in data and allocation happens earlier (in the "viewQuery"
|
||||
// function)
|
||||
this._dataIndex = viewQueries.length;
|
||||
|
||||
this._bindingScope = parentBindingScope.nestedScope(level);
|
||||
|
||||
// Turn the relative context file path into an identifier by replacing non-alphanumeric
|
||||
// characters with underscores.
|
||||
this.fileBasedI18nSuffix = relativeContextFilePath.replace(/[^A-Za-z0-9]/g, '_') + '_';
|
||||
|
||||
this._valueConverter = new ValueConverter(
|
||||
constantPool, () => this.allocateDataSlot(),
|
||||
(numSlots: number) => this.allocatePureFunctionSlots(numSlots),
|
||||
@ -385,7 +392,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
attributes.push(o.literal(name));
|
||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||
const meta = parseI18nMeta(attrI18nMetas[name]);
|
||||
const variable = this.constantPool.getTranslation(value, meta);
|
||||
const variable = this.constantPool.getTranslation(value, meta, this.fileBasedI18nSuffix);
|
||||
attributes.push(variable);
|
||||
} else {
|
||||
attributes.push(o.literal(value));
|
||||
@ -698,7 +705,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// Create the template function
|
||||
const templateVisitor = new TemplateDefinitionBuilder(
|
||||
this.constantPool, this._bindingScope, this.level + 1, contextName, templateName, [],
|
||||
this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace);
|
||||
this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace,
|
||||
this.fileBasedI18nSuffix);
|
||||
|
||||
// Nested templates must not be visited until after their parent templates have completed
|
||||
// processing, so they are queued here until after the initial pass. Otherwise, we wouldn't
|
||||
@ -764,7 +772,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// ```
|
||||
visitSingleI18nTextChild(text: t.Text, i18nMeta: string) {
|
||||
const meta = parseI18nMeta(i18nMeta);
|
||||
const variable = this.constantPool.getTranslation(text.value, meta);
|
||||
const variable = this.constantPool.getTranslation(text.value, meta, this.fileBasedI18nSuffix);
|
||||
this.creationInstruction(
|
||||
text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), variable]);
|
||||
}
|
||||
@ -1336,14 +1344,25 @@ function interpolate(args: o.Expression[]): o.Expression {
|
||||
* @param templateUrl URL to use for source mapping of the parsed template
|
||||
*/
|
||||
export function parseTemplate(
|
||||
template: string, templateUrl: string, options: {preserveWhitespaces?: boolean} = {}):
|
||||
{errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} {
|
||||
template: string, templateUrl: string, options: {preserveWhitespaces?: boolean} = {},
|
||||
relativeContextFilePath: string): {
|
||||
errors?: ParseError[],
|
||||
nodes: t.Node[],
|
||||
hasNgContent: boolean,
|
||||
ngContentSelectors: string[],
|
||||
relativeContextFilePath: string
|
||||
} {
|
||||
const bindingParser = makeBindingParser();
|
||||
const htmlParser = new HtmlParser();
|
||||
const parseResult = htmlParser.parse(template, templateUrl);
|
||||
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
|
||||
return {
|
||||
errors: parseResult.errors,
|
||||
nodes: [],
|
||||
hasNgContent: false,
|
||||
ngContentSelectors: [], relativeContextFilePath
|
||||
};
|
||||
}
|
||||
|
||||
let rootNodes: html.Node[] = parseResult.rootNodes;
|
||||
@ -1354,10 +1373,15 @@ export function parseTemplate(
|
||||
const {nodes, hasNgContent, ngContentSelectors, errors} =
|
||||
htmlAstToRender3Ast(rootNodes, bindingParser);
|
||||
if (errors && errors.length > 0) {
|
||||
return {errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
|
||||
return {
|
||||
errors,
|
||||
nodes: [],
|
||||
hasNgContent: false,
|
||||
ngContentSelectors: [], relativeContextFilePath
|
||||
};
|
||||
}
|
||||
|
||||
return {nodes, hasNgContent, ngContentSelectors};
|
||||
return {nodes, hasNgContent, ngContentSelectors, relativeContextFilePath};
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user