@angular/core
Version:
Angular - the core framework
1,201 lines (1,189 loc) • 901 kB
JavaScript
'use strict';
/**
* @license Angular v20.0.4
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var checker = require('./checker-Bu1Wu4f7.cjs');
var ts = require('typescript');
var p = require('path');
require('os');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var p__namespace = /*#__PURE__*/_interopNamespaceDefault(p);
class XmlTagDefinition {
closedByParent = false;
implicitNamespacePrefix = null;
isVoid = false;
ignoreFirstLf = false;
canSelfClose = true;
preventNamespaceInheritance = false;
requireExtraParent(currentParent) {
return false;
}
isClosedByChild(name) {
return false;
}
getContentType() {
return checker.TagContentType.PARSABLE_DATA;
}
}
const _TAG_DEFINITION = new XmlTagDefinition();
function getXmlTagDefinition(tagName) {
return _TAG_DEFINITION;
}
class XmlParser extends checker.Parser {
constructor() {
super(getXmlTagDefinition);
}
parse(source, url, options = {}) {
// Blocks and let declarations aren't supported in an XML context.
return super.parse(source, url, {
...options,
tokenizeBlocks: false,
tokenizeLet: false,
selectorlessEnabled: false,
});
}
}
const _VERSION$1 = '1.2';
const _XMLNS$1 = 'urn:oasis:names:tc:xliff:document:1.2';
// TODO(vicb): make this a param (s/_/-/)
const _DEFAULT_SOURCE_LANG$1 = 'en';
const _PLACEHOLDER_TAG$1 = 'x';
const _MARKER_TAG$1 = 'mrk';
const _FILE_TAG = 'file';
const _SOURCE_TAG$1 = 'source';
const _SEGMENT_SOURCE_TAG = 'seg-source';
const _ALT_TRANS_TAG = 'alt-trans';
const _TARGET_TAG$1 = 'target';
const _UNIT_TAG$1 = 'trans-unit';
const _CONTEXT_GROUP_TAG = 'context-group';
const _CONTEXT_TAG = 'context';
// https://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
// https://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
class Xliff extends checker.Serializer {
write(messages, locale) {
const visitor = new _WriteVisitor$1();
const transUnits = [];
messages.forEach((message) => {
let contextTags = [];
message.sources.forEach((source) => {
let contextGroupTag = new checker.Tag(_CONTEXT_GROUP_TAG, { purpose: 'location' });
contextGroupTag.children.push(new checker.CR(10), new checker.Tag(_CONTEXT_TAG, { 'context-type': 'sourcefile' }, [
new checker.Text$1(source.filePath),
]), new checker.CR(10), new checker.Tag(_CONTEXT_TAG, { 'context-type': 'linenumber' }, [
new checker.Text$1(`${source.startLine}`),
]), new checker.CR(8));
contextTags.push(new checker.CR(8), contextGroupTag);
});
const transUnit = new checker.Tag(_UNIT_TAG$1, { id: message.id, datatype: 'html' });
transUnit.children.push(new checker.CR(8), new checker.Tag(_SOURCE_TAG$1, {}, visitor.serialize(message.nodes)), ...contextTags);
if (message.description) {
transUnit.children.push(new checker.CR(8), new checker.Tag('note', { priority: '1', from: 'description' }, [
new checker.Text$1(message.description),
]));
}
if (message.meaning) {
transUnit.children.push(new checker.CR(8), new checker.Tag('note', { priority: '1', from: 'meaning' }, [new checker.Text$1(message.meaning)]));
}
transUnit.children.push(new checker.CR(6));
transUnits.push(new checker.CR(6), transUnit);
});
const body = new checker.Tag('body', {}, [...transUnits, new checker.CR(4)]);
const file = new checker.Tag('file', {
'source-language': locale || _DEFAULT_SOURCE_LANG$1,
datatype: 'plaintext',
original: 'ng2.template',
}, [new checker.CR(4), body, new checker.CR(2)]);
const xliff = new checker.Tag('xliff', { version: _VERSION$1, xmlns: _XMLNS$1 }, [
new checker.CR(2),
file,
new checker.CR(),
]);
return checker.serialize([
new checker.Declaration({ version: '1.0', encoding: 'UTF-8' }),
new checker.CR(),
xliff,
new checker.CR(),
]);
}
load(content, url) {
// xliff to xml nodes
const xliffParser = new XliffParser();
const { locale, msgIdToHtml, errors } = xliffParser.parse(content, url);
// xml nodes to i18n nodes
const i18nNodesByMsgId = {};
const converter = new XmlToI18n$1();
Object.keys(msgIdToHtml).forEach((msgId) => {
const { i18nNodes, errors: e } = converter.convert(msgIdToHtml[msgId], url);
errors.push(...e);
i18nNodesByMsgId[msgId] = i18nNodes;
});
if (errors.length) {
throw new Error(`xliff parse errors:\n${errors.join('\n')}`);
}
return { locale: locale, i18nNodesByMsgId };
}
digest(message) {
return checker.digest(message);
}
}
let _WriteVisitor$1 = class _WriteVisitor {
visitText(text, context) {
return [new checker.Text$1(text.value)];
}
visitContainer(container, context) {
const nodes = [];
container.children.forEach((node) => nodes.push(...node.visit(this)));
return nodes;
}
visitIcu(icu, context) {
const nodes = [new checker.Text$1(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
Object.keys(icu.cases).forEach((c) => {
nodes.push(new checker.Text$1(`${c} {`), ...icu.cases[c].visit(this), new checker.Text$1(`} `));
});
nodes.push(new checker.Text$1(`}`));
return nodes;
}
visitTagPlaceholder(ph, context) {
const ctype = getCtypeForTag(ph.tag);
if (ph.isVoid) {
// void tags have no children nor closing tags
return [
new checker.Tag(_PLACEHOLDER_TAG$1, { id: ph.startName, ctype, 'equiv-text': `<${ph.tag}/>` }),
];
}
const startTagPh = new checker.Tag(_PLACEHOLDER_TAG$1, {
id: ph.startName,
ctype,
'equiv-text': `<${ph.tag}>`,
});
const closeTagPh = new checker.Tag(_PLACEHOLDER_TAG$1, {
id: ph.closeName,
ctype,
'equiv-text': `</${ph.tag}>`,
});
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
}
visitPlaceholder(ph, context) {
return [new checker.Tag(_PLACEHOLDER_TAG$1, { id: ph.name, 'equiv-text': `{{${ph.value}}}` })];
}
visitBlockPlaceholder(ph, context) {
const ctype = `x-${ph.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
const startTagPh = new checker.Tag(_PLACEHOLDER_TAG$1, {
id: ph.startName,
ctype,
'equiv-text': `@${ph.name}`,
});
const closeTagPh = new checker.Tag(_PLACEHOLDER_TAG$1, { id: ph.closeName, ctype, 'equiv-text': `}` });
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
}
visitIcuPlaceholder(ph, context) {
const equivText = `{${ph.value.expression}, ${ph.value.type}, ${Object.keys(ph.value.cases)
.map((value) => value + ' {...}')
.join(' ')}}`;
return [new checker.Tag(_PLACEHOLDER_TAG$1, { id: ph.name, 'equiv-text': equivText })];
}
serialize(nodes) {
return [].concat(...nodes.map((node) => node.visit(this)));
}
};
// TODO(vicb): add error management (structure)
// Extract messages as xml nodes from the xliff file
class XliffParser {
// using non-null assertions because they're re(set) by parse()
_unitMlString;
_errors;
_msgIdToHtml;
_locale = null;
parse(xliff, url) {
this._unitMlString = null;
this._msgIdToHtml = {};
const xml = new XmlParser().parse(xliff, url);
this._errors = xml.errors;
checker.visitAll$1(this, xml.rootNodes, null);
return {
msgIdToHtml: this._msgIdToHtml,
errors: this._errors,
locale: this._locale,
};
}
visitElement(element, context) {
switch (element.name) {
case _UNIT_TAG$1:
this._unitMlString = null;
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_UNIT_TAG$1}> misses the "id" attribute`);
}
else {
const id = idAttr.value;
if (this._msgIdToHtml.hasOwnProperty(id)) {
this._addError(element, `Duplicated translations for msg ${id}`);
}
else {
checker.visitAll$1(this, element.children, null);
if (typeof this._unitMlString === 'string') {
this._msgIdToHtml[id] = this._unitMlString;
}
else {
this._addError(element, `Message ${id} misses a translation`);
}
}
}
break;
// ignore those tags
case _SOURCE_TAG$1:
case _SEGMENT_SOURCE_TAG:
case _ALT_TRANS_TAG:
break;
case _TARGET_TAG$1:
const innerTextStart = element.startSourceSpan.end.offset;
const innerTextEnd = element.endSourceSpan.start.offset;
const content = element.startSourceSpan.start.file.content;
const innerText = content.slice(innerTextStart, innerTextEnd);
this._unitMlString = innerText;
break;
case _FILE_TAG:
const localeAttr = element.attrs.find((attr) => attr.name === 'target-language');
if (localeAttr) {
this._locale = localeAttr.value;
}
checker.visitAll$1(this, element.children, null);
break;
default:
// TODO(vicb): assert file structure, xliff version
// For now only recurse on unhandled nodes
checker.visitAll$1(this, element.children, null);
}
}
visitAttribute(attribute, context) { }
visitText(text, context) { }
visitComment(comment, context) { }
visitExpansion(expansion, context) { }
visitExpansionCase(expansionCase, context) { }
visitBlock(block, context) { }
visitBlockParameter(parameter, context) { }
visitLetDeclaration(decl, context) { }
visitComponent(component, context) { }
visitDirective(directive, context) { }
_addError(node, message) {
this._errors.push(new checker.I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xliff syntax) to i18n nodes
let XmlToI18n$1 = class XmlToI18n {
// using non-null assertion because it's re(set) by convert()
_errors;
convert(message, url) {
const xmlIcu = new XmlParser().parse(message, url, { tokenizeExpansionForms: true });
this._errors = xmlIcu.errors;
const i18nNodes = this._errors.length > 0 || xmlIcu.rootNodes.length == 0
? []
: [].concat(...checker.visitAll$1(this, xmlIcu.rootNodes));
return {
i18nNodes: i18nNodes,
errors: this._errors,
};
}
visitText(text, context) {
return new checker.Text$2(text.value, text.sourceSpan);
}
visitElement(el, context) {
if (el.name === _PLACEHOLDER_TAG$1) {
const nameAttr = el.attrs.find((attr) => attr.name === 'id');
if (nameAttr) {
return new checker.Placeholder('', nameAttr.value, el.sourceSpan);
}
this._addError(el, `<${_PLACEHOLDER_TAG$1}> misses the "id" attribute`);
return null;
}
if (el.name === _MARKER_TAG$1) {
return [].concat(...checker.visitAll$1(this, el.children));
}
this._addError(el, `Unexpected tag`);
return null;
}
visitExpansion(icu, context) {
const caseMap = {};
checker.visitAll$1(this, icu.cases).forEach((c) => {
caseMap[c.value] = new checker.Container(c.nodes, icu.sourceSpan);
});
return new checker.Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
}
visitExpansionCase(icuCase, context) {
return {
value: icuCase.value,
nodes: checker.visitAll$1(this, icuCase.expression),
};
}
visitComment(comment, context) { }
visitAttribute(attribute, context) { }
visitBlock(block, context) { }
visitBlockParameter(parameter, context) { }
visitLetDeclaration(decl, context) { }
visitComponent(component, context) {
this._addError(component, 'Unexpected node');
}
visitDirective(directive, context) {
this._addError(directive, 'Unexpected node');
}
_addError(node, message) {
this._errors.push(new checker.I18nError(node.sourceSpan, message));
}
};
function getCtypeForTag(tag) {
switch (tag.toLowerCase()) {
case 'br':
return 'lb';
case 'img':
return 'image';
default:
return `x-${tag}`;
}
}
const _VERSION = '2.0';
const _XMLNS = 'urn:oasis:names:tc:xliff:document:2.0';
// TODO(vicb): make this a param (s/_/-/)
const _DEFAULT_SOURCE_LANG = 'en';
const _PLACEHOLDER_TAG = 'ph';
const _PLACEHOLDER_SPANNING_TAG = 'pc';
const _MARKER_TAG = 'mrk';
const _XLIFF_TAG = 'xliff';
const _SOURCE_TAG = 'source';
const _TARGET_TAG = 'target';
const _UNIT_TAG = 'unit';
// https://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
class Xliff2 extends checker.Serializer {
write(messages, locale) {
const visitor = new _WriteVisitor();
const units = [];
messages.forEach((message) => {
const unit = new checker.Tag(_UNIT_TAG, { id: message.id });
const notes = new checker.Tag('notes');
if (message.description || message.meaning) {
if (message.description) {
notes.children.push(new checker.CR(8), new checker.Tag('note', { category: 'description' }, [new checker.Text$1(message.description)]));
}
if (message.meaning) {
notes.children.push(new checker.CR(8), new checker.Tag('note', { category: 'meaning' }, [new checker.Text$1(message.meaning)]));
}
}
message.sources.forEach((source) => {
notes.children.push(new checker.CR(8), new checker.Tag('note', { category: 'location' }, [
new checker.Text$1(`${source.filePath}:${source.startLine}${source.endLine !== source.startLine ? ',' + source.endLine : ''}`),
]));
});
notes.children.push(new checker.CR(6));
unit.children.push(new checker.CR(6), notes);
const segment = new checker.Tag('segment');
segment.children.push(new checker.CR(8), new checker.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)), new checker.CR(6));
unit.children.push(new checker.CR(6), segment, new checker.CR(4));
units.push(new checker.CR(4), unit);
});
const file = new checker.Tag('file', { 'original': 'ng.template', id: 'ngi18n' }, [
...units,
new checker.CR(2),
]);
const xliff = new checker.Tag(_XLIFF_TAG, { version: _VERSION, xmlns: _XMLNS, srcLang: locale || _DEFAULT_SOURCE_LANG }, [new checker.CR(2), file, new checker.CR()]);
return checker.serialize([
new checker.Declaration({ version: '1.0', encoding: 'UTF-8' }),
new checker.CR(),
xliff,
new checker.CR(),
]);
}
load(content, url) {
// xliff to xml nodes
const xliff2Parser = new Xliff2Parser();
const { locale, msgIdToHtml, errors } = xliff2Parser.parse(content, url);
// xml nodes to i18n nodes
const i18nNodesByMsgId = {};
const converter = new XmlToI18n();
Object.keys(msgIdToHtml).forEach((msgId) => {
const { i18nNodes, errors: e } = converter.convert(msgIdToHtml[msgId], url);
errors.push(...e);
i18nNodesByMsgId[msgId] = i18nNodes;
});
if (errors.length) {
throw new Error(`xliff2 parse errors:\n${errors.join('\n')}`);
}
return { locale: locale, i18nNodesByMsgId };
}
digest(message) {
return checker.decimalDigest(message);
}
}
class _WriteVisitor {
_nextPlaceholderId = 0;
visitText(text, context) {
return [new checker.Text$1(text.value)];
}
visitContainer(container, context) {
const nodes = [];
container.children.forEach((node) => nodes.push(...node.visit(this)));
return nodes;
}
visitIcu(icu, context) {
const nodes = [new checker.Text$1(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
Object.keys(icu.cases).forEach((c) => {
nodes.push(new checker.Text$1(`${c} {`), ...icu.cases[c].visit(this), new checker.Text$1(`} `));
});
nodes.push(new checker.Text$1(`}`));
return nodes;
}
visitTagPlaceholder(ph, context) {
const type = getTypeForTag(ph.tag);
if (ph.isVoid) {
const tagPh = new checker.Tag(_PLACEHOLDER_TAG, {
id: (this._nextPlaceholderId++).toString(),
equiv: ph.startName,
type: type,
disp: `<${ph.tag}/>`,
});
return [tagPh];
}
const tagPc = new checker.Tag(_PLACEHOLDER_SPANNING_TAG, {
id: (this._nextPlaceholderId++).toString(),
equivStart: ph.startName,
equivEnd: ph.closeName,
type: type,
dispStart: `<${ph.tag}>`,
dispEnd: `</${ph.tag}>`,
});
const nodes = [].concat(...ph.children.map((node) => node.visit(this)));
if (nodes.length) {
nodes.forEach((node) => tagPc.children.push(node));
}
else {
tagPc.children.push(new checker.Text$1(''));
}
return [tagPc];
}
visitPlaceholder(ph, context) {
const idStr = (this._nextPlaceholderId++).toString();
return [
new checker.Tag(_PLACEHOLDER_TAG, {
id: idStr,
equiv: ph.name,
disp: `{{${ph.value}}}`,
}),
];
}
visitBlockPlaceholder(ph, context) {
const tagPc = new checker.Tag(_PLACEHOLDER_SPANNING_TAG, {
id: (this._nextPlaceholderId++).toString(),
equivStart: ph.startName,
equivEnd: ph.closeName,
type: 'other',
dispStart: `@${ph.name}`,
dispEnd: `}`,
});
const nodes = [].concat(...ph.children.map((node) => node.visit(this)));
if (nodes.length) {
nodes.forEach((node) => tagPc.children.push(node));
}
else {
tagPc.children.push(new checker.Text$1(''));
}
return [tagPc];
}
visitIcuPlaceholder(ph, context) {
const cases = Object.keys(ph.value.cases)
.map((value) => value + ' {...}')
.join(' ');
const idStr = (this._nextPlaceholderId++).toString();
return [
new checker.Tag(_PLACEHOLDER_TAG, {
id: idStr,
equiv: ph.name,
disp: `{${ph.value.expression}, ${ph.value.type}, ${cases}}`,
}),
];
}
serialize(nodes) {
this._nextPlaceholderId = 0;
return [].concat(...nodes.map((node) => node.visit(this)));
}
}
// Extract messages as xml nodes from the xliff file
class Xliff2Parser {
// using non-null assertions because they're all (re)set by parse()
_unitMlString;
_errors;
_msgIdToHtml;
_locale = null;
parse(xliff, url) {
this._unitMlString = null;
this._msgIdToHtml = {};
const xml = new XmlParser().parse(xliff, url);
this._errors = xml.errors;
checker.visitAll$1(this, xml.rootNodes, null);
return {
msgIdToHtml: this._msgIdToHtml,
errors: this._errors,
locale: this._locale,
};
}
visitElement(element, context) {
switch (element.name) {
case _UNIT_TAG:
this._unitMlString = null;
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_UNIT_TAG}> misses the "id" attribute`);
}
else {
const id = idAttr.value;
if (this._msgIdToHtml.hasOwnProperty(id)) {
this._addError(element, `Duplicated translations for msg ${id}`);
}
else {
checker.visitAll$1(this, element.children, null);
if (typeof this._unitMlString === 'string') {
this._msgIdToHtml[id] = this._unitMlString;
}
else {
this._addError(element, `Message ${id} misses a translation`);
}
}
}
break;
case _SOURCE_TAG:
// ignore source message
break;
case _TARGET_TAG:
const innerTextStart = element.startSourceSpan.end.offset;
const innerTextEnd = element.endSourceSpan.start.offset;
const content = element.startSourceSpan.start.file.content;
const innerText = content.slice(innerTextStart, innerTextEnd);
this._unitMlString = innerText;
break;
case _XLIFF_TAG:
const localeAttr = element.attrs.find((attr) => attr.name === 'trgLang');
if (localeAttr) {
this._locale = localeAttr.value;
}
const versionAttr = element.attrs.find((attr) => attr.name === 'version');
if (versionAttr) {
const version = versionAttr.value;
if (version !== '2.0') {
this._addError(element, `The XLIFF file version ${version} is not compatible with XLIFF 2.0 serializer`);
}
else {
checker.visitAll$1(this, element.children, null);
}
}
break;
default:
checker.visitAll$1(this, element.children, null);
}
}
visitAttribute(attribute, context) { }
visitText(text, context) { }
visitComment(comment, context) { }
visitExpansion(expansion, context) { }
visitExpansionCase(expansionCase, context) { }
visitBlock(block, context) { }
visitBlockParameter(parameter, context) { }
visitLetDeclaration(decl, context) { }
visitComponent(component, context) { }
visitDirective(directive, context) { }
_addError(node, message) {
this._errors.push(new checker.I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xliff syntax) to i18n nodes
class XmlToI18n {
// using non-null assertion because re(set) by convert()
_errors;
convert(message, url) {
const xmlIcu = new XmlParser().parse(message, url, { tokenizeExpansionForms: true });
this._errors = xmlIcu.errors;
const i18nNodes = this._errors.length > 0 || xmlIcu.rootNodes.length == 0
? []
: [].concat(...checker.visitAll$1(this, xmlIcu.rootNodes));
return {
i18nNodes,
errors: this._errors,
};
}
visitText(text, context) {
return new checker.Text$2(text.value, text.sourceSpan);
}
visitElement(el, context) {
switch (el.name) {
case _PLACEHOLDER_TAG:
const nameAttr = el.attrs.find((attr) => attr.name === 'equiv');
if (nameAttr) {
return [new checker.Placeholder('', nameAttr.value, el.sourceSpan)];
}
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "equiv" attribute`);
break;
case _PLACEHOLDER_SPANNING_TAG:
const startAttr = el.attrs.find((attr) => attr.name === 'equivStart');
const endAttr = el.attrs.find((attr) => attr.name === 'equivEnd');
if (!startAttr) {
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "equivStart" attribute`);
}
else if (!endAttr) {
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "equivEnd" attribute`);
}
else {
const startId = startAttr.value;
const endId = endAttr.value;
const nodes = [];
return nodes.concat(new checker.Placeholder('', startId, el.sourceSpan), ...el.children.map((node) => node.visit(this, null)), new checker.Placeholder('', endId, el.sourceSpan));
}
break;
case _MARKER_TAG:
return [].concat(...checker.visitAll$1(this, el.children));
default:
this._addError(el, `Unexpected tag`);
}
return null;
}
visitExpansion(icu, context) {
const caseMap = {};
checker.visitAll$1(this, icu.cases).forEach((c) => {
caseMap[c.value] = new checker.Container(c.nodes, icu.sourceSpan);
});
return new checker.Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
}
visitExpansionCase(icuCase, context) {
return {
value: icuCase.value,
nodes: [].concat(...checker.visitAll$1(this, icuCase.expression)),
};
}
visitComment(comment, context) { }
visitAttribute(attribute, context) { }
visitBlock(block, context) { }
visitBlockParameter(parameter, context) { }
visitLetDeclaration(decl, context) { }
visitComponent(component, context) {
this._addError(component, 'Unexpected node');
}
visitDirective(directive, context) {
this._addError(directive, 'Unexpected node');
}
_addError(node, message) {
this._errors.push(new checker.I18nError(node.sourceSpan, message));
}
}
function getTypeForTag(tag) {
switch (tag.toLowerCase()) {
case 'br':
case 'b':
case 'i':
case 'u':
return 'fmt';
case 'img':
return 'image';
case 'a':
return 'link';
default:
return 'other';
}
}
/**
* A container for message extracted from the templates.
*/
class MessageBundle {
_htmlParser;
_implicitTags;
_implicitAttrs;
_locale;
_preserveWhitespace;
_messages = [];
constructor(_htmlParser, _implicitTags, _implicitAttrs, _locale = null, _preserveWhitespace = true) {
this._htmlParser = _htmlParser;
this._implicitTags = _implicitTags;
this._implicitAttrs = _implicitAttrs;
this._locale = _locale;
this._preserveWhitespace = _preserveWhitespace;
}
updateFromTemplate(source, url, interpolationConfig) {
const htmlParserResult = this._htmlParser.parse(source, url, {
tokenizeExpansionForms: true,
interpolationConfig,
});
if (htmlParserResult.errors.length) {
return htmlParserResult.errors;
}
// Trim unnecessary whitespace from extracted messages if requested. This
// makes the messages more durable to trivial whitespace changes without
// affected message IDs.
const rootNodes = this._preserveWhitespace
? htmlParserResult.rootNodes
: checker.visitAllWithSiblings(new checker.WhitespaceVisitor(/* preserveSignificantWhitespace */ false), htmlParserResult.rootNodes);
const i18nParserResult = checker.extractMessages(rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs,
/* preserveSignificantWhitespace */ this._preserveWhitespace);
if (i18nParserResult.errors.length) {
return i18nParserResult.errors;
}
this._messages.push(...i18nParserResult.messages);
return [];
}
// Return the message in the internal format
// The public (serialized) format might be different, see the `write` method.
getMessages() {
return this._messages;
}
write(serializer, filterSources) {
const messages = {};
const mapperVisitor = new MapPlaceholderNames();
// Deduplicate messages based on their ID
this._messages.forEach((message) => {
const id = serializer.digest(message);
if (!messages.hasOwnProperty(id)) {
messages[id] = message;
}
else {
messages[id].sources.push(...message.sources);
}
});
// Transform placeholder names using the serializer mapping
const msgList = Object.keys(messages).map((id) => {
const mapper = serializer.createNameMapper(messages[id]);
const src = messages[id];
const nodes = mapper ? mapperVisitor.convert(src.nodes, mapper) : src.nodes;
let transformedMessage = new checker.Message(nodes, {}, {}, src.meaning, src.description, id);
transformedMessage.sources = src.sources;
if (filterSources) {
transformedMessage.sources.forEach((source) => (source.filePath = filterSources(source.filePath)));
}
return transformedMessage;
});
return serializer.write(msgList, this._locale);
}
}
// Transform an i18n AST by renaming the placeholder nodes with the given mapper
class MapPlaceholderNames extends checker.CloneVisitor {
convert(nodes, mapper) {
return mapper ? nodes.map((n) => n.visit(this, mapper)) : nodes;
}
visitTagPlaceholder(ph, mapper) {
const startName = mapper.toPublicName(ph.startName);
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName) : ph.closeName;
const children = ph.children.map((n) => n.visit(this, mapper));
return new checker.TagPlaceholder(ph.tag, ph.attrs, startName, closeName, children, ph.isVoid, ph.sourceSpan, ph.startSourceSpan, ph.endSourceSpan);
}
visitBlockPlaceholder(ph, mapper) {
const startName = mapper.toPublicName(ph.startName);
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName) : ph.closeName;
const children = ph.children.map((n) => n.visit(this, mapper));
return new checker.BlockPlaceholder(ph.name, ph.parameters, startName, closeName, children, ph.sourceSpan, ph.startSourceSpan, ph.endSourceSpan);
}
visitPlaceholder(ph, mapper) {
return new checker.Placeholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
}
visitIcuPlaceholder(ph, mapper) {
return new checker.IcuPlaceholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
}
}
function compileClassMetadata(metadata) {
const fnCall = internalCompileClassMetadata(metadata);
return checker.arrowFn([], [checker.devOnlyGuardedExpression(fnCall).toStmt()]).callFn([]);
}
/** Compiles only the `setClassMetadata` call without any additional wrappers. */
function internalCompileClassMetadata(metadata) {
return checker.importExpr(checker.Identifiers.setClassMetadata)
.callFn([
metadata.type,
metadata.decorators,
metadata.ctorParameters ?? checker.literal(null),
metadata.propDecorators ?? checker.literal(null),
]);
}
/**
* Wraps the `setClassMetadata` function with extra logic that dynamically
* loads dependencies from `@defer` blocks.
*
* Generates a call like this:
* ```ts
* setClassMetadataAsync(type, () => [
* import('./cmp-a').then(m => m.CmpA);
* import('./cmp-b').then(m => m.CmpB);
* ], (CmpA, CmpB) => {
* setClassMetadata(type, decorators, ctorParameters, propParameters);
* });
* ```
*
* Similar to the `setClassMetadata` call, it's wrapped into the `ngDevMode`
* check to tree-shake away this code in production mode.
*/
function compileComponentClassMetadata(metadata, dependencies) {
if (dependencies === null || dependencies.length === 0) {
// If there are no deferrable symbols - just generate a regular `setClassMetadata` call.
return compileClassMetadata(metadata);
}
return internalCompileSetClassMetadataAsync(metadata, dependencies.map((dep) => new checker.FnParam(dep.symbolName, checker.DYNAMIC_TYPE)), compileComponentMetadataAsyncResolver(dependencies));
}
/**
* Internal logic used to compile a `setClassMetadataAsync` call.
* @param metadata Class metadata for the internal `setClassMetadata` call.
* @param wrapperParams Parameters to be set on the callback that wraps `setClassMetata`.
* @param dependencyResolverFn Function to resolve the deferred dependencies.
*/
function internalCompileSetClassMetadataAsync(metadata, wrapperParams, dependencyResolverFn) {
// Omit the wrapper since it'll be added around `setClassMetadataAsync` instead.
const setClassMetadataCall = internalCompileClassMetadata(metadata);
const setClassMetaWrapper = checker.arrowFn(wrapperParams, [setClassMetadataCall.toStmt()]);
const setClassMetaAsync = checker.importExpr(checker.Identifiers.setClassMetadataAsync)
.callFn([metadata.type, dependencyResolverFn, setClassMetaWrapper]);
return checker.arrowFn([], [checker.devOnlyGuardedExpression(setClassMetaAsync).toStmt()]).callFn([]);
}
/**
* Compiles the function that loads the dependencies for the
* entire component in `setClassMetadataAsync`.
*/
function compileComponentMetadataAsyncResolver(dependencies) {
const dynamicImports = dependencies.map(({ symbolName, importPath, isDefaultImport }) => {
// e.g. `(m) => m.CmpA`
const innerFn =
// Default imports are always accessed through the `default` property.
checker.arrowFn([new checker.FnParam('m', checker.DYNAMIC_TYPE)], checker.variable('m').prop(isDefaultImport ? 'default' : symbolName));
// e.g. `import('./cmp-a').then(...)`
return new checker.DynamicImportExpr(importPath).prop('then').callFn([innerFn]);
});
// e.g. `() => [ ... ];`
return checker.arrowFn([], checker.literalArr(dynamicImports));
}
/**
* Every time we make a breaking change to the declaration interface or partial-linker behavior, we
* must update this constant to prevent old partial-linkers from incorrectly processing the
* declaration.
*
* Do not include any prerelease in these versions as they are ignored.
*/
const MINIMUM_PARTIAL_LINKER_VERSION$5 = '12.0.0';
/**
* Minimum version at which deferred blocks are supported in the linker.
*/
const MINIMUM_PARTIAL_LINKER_DEFER_SUPPORT_VERSION = '18.0.0';
function compileDeclareClassMetadata(metadata) {
const definitionMap = new checker.DefinitionMap();
definitionMap.set('minVersion', checker.literal(MINIMUM_PARTIAL_LINKER_VERSION$5));
definitionMap.set('version', checker.literal('20.0.4'));
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
definitionMap.set('type', metadata.type);
definitionMap.set('decorators', metadata.decorators);
definitionMap.set('ctorParameters', metadata.ctorParameters);
definitionMap.set('propDecorators', metadata.propDecorators);
return checker.importExpr(checker.Identifiers.declareClassMetadata).callFn([definitionMap.toLiteralMap()]);
}
function compileComponentDeclareClassMetadata(metadata, dependencies) {
if (dependencies === null || dependencies.length === 0) {
return compileDeclareClassMetadata(metadata);
}
const definitionMap = new checker.DefinitionMap();
const callbackReturnDefinitionMap = new checker.DefinitionMap();
callbackReturnDefinitionMap.set('decorators', metadata.decorators);
callbackReturnDefinitionMap.set('ctorParameters', metadata.ctorParameters ?? checker.literal(null));
callbackReturnDefinitionMap.set('propDecorators', metadata.propDecorators ?? checker.literal(null));
definitionMap.set('minVersion', checker.literal(MINIMUM_PARTIAL_LINKER_DEFER_SUPPORT_VERSION));
definitionMap.set('version', checker.literal('20.0.4'));
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
definitionMap.set('type', metadata.type);
definitionMap.set('resolveDeferredDeps', compileComponentMetadataAsyncResolver(dependencies));
definitionMap.set('resolveMetadata', checker.arrowFn(dependencies.map((dep) => new checker.FnParam(dep.symbolName, checker.DYNAMIC_TYPE)), callbackReturnDefinitionMap.toLiteralMap()));
return checker.importExpr(checker.Identifiers.declareClassMetadataAsync).callFn([definitionMap.toLiteralMap()]);
}
/**
* Creates an array literal expression from the given array, mapping all values to an expression
* using the provided mapping function. If the array is empty or null, then null is returned.
*
* @param values The array to transfer into literal array expression.
* @param mapper The logic to use for creating an expression for the array's values.
* @returns An array literal expression representing `values`, or null if `values` is empty or
* is itself null.
*/
function toOptionalLiteralArray(values, mapper) {
if (values === null || values.length === 0) {
return null;
}
return checker.literalArr(values.map((value) => mapper(value)));
}
/**
* Creates an object literal expression from the given object, mapping all values to an expression
* using the provided mapping function. If the object has no keys, then null is returned.
*
* @param object The object to transfer into an object literal expression.
* @param mapper The logic to use for creating an expression for the object's values.
* @returns An object literal expression representing `object`, or null if `object` does not have
* any keys.
*/
function toOptionalLiteralMap(object, mapper) {
const entries = Object.keys(object).map((key) => {
const value = object[key];
return { key, value: mapper(value), quoted: true };
});
if (entries.length > 0) {
return checker.literalMap(entries);
}
else {
return null;
}
}
function compileDependencies(deps) {
if (deps === 'invalid') {
// The `deps` can be set to the string "invalid" by the `unwrapConstructorDependencies()`
// function, which tries to convert `ConstructorDeps` into `R3DependencyMetadata[]`.
return checker.literal('invalid');
}
else if (deps === null) {
return checker.literal(null);
}
else {
return checker.literalArr(deps.map(compileDependency));
}
}
function compileDependency(dep) {
const depMeta = new checker.DefinitionMap();
depMeta.set('token', dep.token);
if (dep.attributeNameType !== null) {
depMeta.set('attribute', checker.literal(true));
}
if (dep.host) {
depMeta.set('host', checker.literal(true));
}
if (dep.optional) {
depMeta.set('optional', checker.literal(true));
}
if (dep.self) {
depMeta.set('self', checker.literal(true));
}
if (dep.skipSelf) {
depMeta.set('skipSelf', checker.literal(true));
}
return depMeta.toLiteralMap();
}
/**
* Compile a directive declaration defined by the `R3DirectiveMetadata`.
*/
function compileDeclareDirectiveFromMetadata(meta) {
const definitionMap = createDirectiveDefinitionMap(meta);
const expression = checker.importExpr(checker.Identifiers.declareDirective).callFn([definitionMap.toLiteralMap()]);
const type = checker.createDirectiveType(meta);
return { expression, type, statements: [] };
}
/**
* Gathers the declaration fields for a directive into a `DefinitionMap`. This allows for reusing
* this logic for components, as they extend the directive metadata.
*/
function createDirectiveDefinitionMap(meta) {
const definitionMap = new checker.DefinitionMap();
const minVersion = getMinimumVersionForPartialOutput(meta);
definitionMap.set('minVersion', checker.literal(minVersion));
definitionMap.set('version', checker.literal('20.0.4'));
// e.g. `type: MyDirective`
definitionMap.set('type', meta.type.value);
if (meta.isStandalone !== undefined) {
definitionMap.set('isStandalone', checker.literal(meta.isStandalone));
}
if (meta.isSignal) {
definitionMap.set('isSignal', checker.literal(meta.isSignal));
}
// e.g. `selector: 'some-dir'`
if (meta.selector !== null) {
definitionMap.set('selector', checker.literal(meta.selector));
}
definitionMap.set('inputs', needsNewInputPartialOutput(meta)
? createInputsPartialMetadata(meta.inputs)
: legacyInputsPartialMetadata(meta.inputs));
definitionMap.set('outputs', checker.conditionallyCreateDirectiveBindingLiteral(meta.outputs));
definitionMap.set('host', compileHostMetadata(meta.host));
definitionMap.set('providers', meta.providers);
if (meta.queries.length > 0) {
definitionMap.set('queries', checker.literalArr(meta.queries.map(compileQuery)));
}
if (meta.viewQueries.length > 0) {
definitionMap.set('viewQueries', checker.literalArr(meta.viewQueries.map(compileQuery)));
}
if (meta.exportAs !== null) {
definitionMap.set('exportAs', checker.asLiteral(meta.exportAs));
}
if (meta.usesInheritance) {
definitionMap.set('usesInheritance', checker.literal(true));
}
if (meta.lifecycle.usesOnChanges) {
definitionMap.set('usesOnChanges', checker.literal(true));
}
if (meta.hostDirectives?.length) {
definitionMap.set('hostDirectives', createHostDirectives(meta.hostDirectives));
}
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
return definitionMap;
}
/**
* Determines the minimum linker version for the partial output
* generated for this directive.
*
* Every time we make a breaking change to the declaration interface or partial-linker
* behavior, we must update the minimum versions to prevent old partial-linkers from
* incorrectly processing the declaration.
*
* NOTE: Do not include any prerelease in these versions as they are ignored.
*/
function getMinimumVersionForPartialOutput(meta) {
// We are starting with the oldest minimum version that can work for common
// directive partial compilation output. As we discover usages of new features
// that require a newer partial output emit, we bump the `minVersion`. Our goal
// is to keep libraries as much compatible with older linker versions as possible.
let minVersion = '14.0.0';
// Note: in order to allow consuming Angular libraries that have been compiled with 16.1+ in
// Angular 16.0, we only force a minimum version of 16.1 if input transform feature as introduced
// in 16.1 is actually used.
const hasDecoratorTransformFunctions = Object.values(meta.inputs).some((input) => input.transformFunction !== null);
if (hasDecoratorTransformFunctions) {
minVersion = '16.1.0';
}
// If there are input flags and we need the new emit, use the actual minimum version,
// where this was introduced. i.e. in 17.1.0
// TODO(legacy-partial-output-inputs): Remove in v18.
if (needsNewInputPartialOutput(meta)) {
minVersion = '17.1.0';
}
// If there are signal-based queries, partial output generates an extra field
// that should be parsed by linkers. Ensure a proper minimum linker version.
if (meta.queries.some((q) => q.isSignal) || meta.viewQueries.some((q) => q.isSignal)) {
minVersion = '17.2.0';
}
return minVersion;
}
/**
* Gets whether the given directive needs the new input partial output structure
* that can hold additional metadata like `isRequired`, `isSignal` etc.
*/
function needsNewInputPartialOutput(meta) {
return Object.values(meta.inputs).some((input) => input.isSignal);
}
/**
* Compiles the metadata of a single query into its partial declaration form as declared
* by `R3DeclareQueryMetadata`.
*/
function compileQuery(query) {
const meta = new checker.DefinitionMap();
meta.set('propertyName', checker.literal(query.propertyName));
if (query.first) {
meta.set('first', checker.literal(true));
}
meta.set('predicate', Array.isArray(query.predicate)
? checker.asLiteral(query.predicate)
: checker.convertFromMaybeForwardRefExpression(query.predicate));
if (!query.emitDistinctChangesOnly) {
// `emitDistinctChangesOnly` is special because we expect it to be `true`.
// Therefore we explicitly emit the field, and explicitly place it only when it's `false`.
meta.set('emitDistinctChangesOnly', checker.literal(false));
}
if (query.descendants) {
meta.set('descendants', checker.literal(true));
}
meta.set('read', query.read);
if (query.static) {
meta.set('static', checker.literal(true));
}
if (query.isSignal) {
meta.set('isSignal', checker.literal(true));
}
return meta.toLiteralMap();
}
/**
* Compiles the host metadata into its partial declaration form as declared
* in `R3DeclareDirectiveMetadata['host']`
*/
function compileHostMetadata(meta) {
const hostMetadata = new checker.DefinitionMap();
hostMetadata.set('attributes', toOptionalLiteralMap(meta.attributes, (expression) => expression));
hostMetadata.set('listeners', toOptionalLiteralMap(meta.listeners, checker.literal));
hostMetadata.set('properties', toOptionalLiteralMap(meta.properties, checker.literal));
if (meta.specialAttributes.styleAttr) {
hostMetadata.set('styleAttribute', checker.literal(meta.specialAttributes.styleAttr));
}
if (meta.specialAttributes.classAttr) {
hostMetadata.set('classAttribute', checker.literal(meta.specialAttributes.classAttr));
}
if (hostMetadata.values.length > 0) {
return hostMetadata.toLiteralMap();
}
else {
return null;
}
}
function createHostDirectives(hostDirectives) {
const expressions = hostDirectives.map((current) => {
const keys = [
{
key: 'directive',
value: current.isForwardReference
? checker.generateForwardRef(current.directive.type)
: current.directive.type,
quoted: false,
},
];
const inputsLiteral = current.inputs ? checker.createHostDirectivesMappingArray(current.inputs) : null;
const outputsLiteral = current.outputs
? checker.createHostDirectivesMappingArray(current.outputs)
: null;
if (inputsLiteral) {
keys.push({ key: 'inputs', value: inputsLiteral, quoted: false });
}
if (outputsLiteral) {
keys.push({ key: 'outputs', value: outputsLiteral, quoted: false });
}
return checker.literalMap(keys);
});
// If there's a forward reference, we generate a `function() { return [{directive: HostDir}] }`,
// otherwise we can save some bytes by using a plain array, e.g. `[{directive: HostDir}]`.
return checker.literalArr(expressions);
}
/**
* Generates partial output metadata for inputs of a directive.
*
* The generated structure is expected to match `R3DeclareDirectiveFacade['inputs']`.
*/
function createInputsPartialMetadata(inputs) {
const keys = Object.getOwnPropertyNames(inputs);
if (keys.length === 0) {
return null;
}
return checker.literalMap(keys.map((declaredName) => {
const value = inputs[declaredName];
return {
key: declaredName,
// put quotes around keys that contain potentially unsafe characters
quoted: checker.UNSAFE_OBJECT_KEY_NAME_REGEXP.test(declaredName),
value: checker.literalMap([
{ key: 'classPropertyName', quoted: false, value: checker.asLiteral(value.classPropertyName) },
{ key: 'publicName', quoted: false, value: checker.asLiteral(value.bindingPropertyName) },
{ key: 'isSignal', quoted: false, value: checker.asLiteral(value.isSignal) },
{ key: 'isRequired', quoted: false, value: checker.asLiteral(value.required) },
{ key: 'transformFunction', quoted: false, value: value.transformFunction ?? checker.NULL_EXPR },
]),
};
}));
}
/**
* Pre v18 legacy partial output for inputs.
*
* Previously, inputs did not capture metadata like `isSignal` in the partial compilation output.
* To enable capturing such metadata, we restructured how input metadata is communicated in the
* partial output. This would make libraries incompatible with older Angular FW versions where the
* linker would not know how t