
package.schematics.bundles.program-c49e652e.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core Show documentation
Show all versions of core Show documentation
Angular - the core framework
'use strict';
/**
* @license Angular v19.0.5
* (c) 2010-2024 Google LLC. https://angular.io/
* License: MIT
*/
'use strict';
var checker = require('./checker-eced36c5.js');
var ts = require('typescript');
var p = require('path');
require('os');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return 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 ts__default = /*#__PURE__*/_interopDefaultLegacy(ts);
var p__namespace = /*#__PURE__*/_interopNamespace(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 });
}
}
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);
}
}
class _WriteVisitor$1 {
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(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(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(this, element.children, null);
break;
default:
// TODO(vicb): assert file structure, xliff version
// For now only recurse on unhandled nodes
checker.visitAll(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) { }
_addError(node, message) {
this._errors.push(new checker.I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xliff syntax) to i18n nodes
class XmlToI18n$1 {
// 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(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(this, el.children));
}
this._addError(el, `Unexpected tag`);
return null;
}
visitExpansion(icu, context) {
const caseMap = {};
checker.visitAll(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(this, icuCase.expression),
};
}
visitComment(comment, context) { }
visitAttribute(attribute, context) { }
visitBlock(block, context) { }
visitBlockParameter(parameter, context) { }
visitLetDeclaration(decl, context) { }
_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(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(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(this, element.children, null);
}
}
break;
default:
checker.visitAll(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) { }
_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(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(this, el.children));
default:
this._addError(el, `Unexpected tag`);
}
return null;
}
visitExpansion(icu, context) {
const caseMap = {};
checker.visitAll(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(this, icuCase.expression)),
};
}
visitComment(comment, context) { }
visitAttribute(attribute, context) { }
visitBlock(block, context) { }
visitBlockParameter(parameter, context) { }
visitLetDeclaration(decl, context) { }
_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));
}
/**
* Generate an ngDevMode guarded call to setClassDebugInfo with the debug info about the class
* (e.g., the file name in which the class is defined)
*/
function compileClassDebugInfo(debugInfo) {
const debugInfoObject = {
className: debugInfo.className,
};
// Include file path and line number only if the file relative path is calculated successfully.
if (debugInfo.filePath) {
debugInfoObject.filePath = debugInfo.filePath;
debugInfoObject.lineNumber = debugInfo.lineNumber;
}
// Include forbidOrphanRendering only if it's set to true (to reduce generated code)
if (debugInfo.forbidOrphanRendering) {
debugInfoObject.forbidOrphanRendering = checker.literal(true);
}
const fnCall = checker.importExpr(checker.Identifiers.setClassDebugInfo)
.callFn([debugInfo.type, checker.mapLiteral(debugInfoObject)]);
const iife = checker.arrowFn([], [checker.devOnlyGuardedExpression(fnCall).toStmt()]);
return iife.callFn([]);
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Compiles the expression that initializes HMR for a class.
* @param meta HMR metadata extracted from the class.
*/
function compileHmrInitializer(meta) {
const id = encodeURIComponent(`${meta.filePath}@${meta.className}`);
const urlPartial = `/@ng/component?c=${id}&t=`;
const moduleName = 'm';
const dataName = 'd';
const timestampName = 't';
const importCallbackName = `${meta.className}_HmrLoad`;
const locals = meta.localDependencies.map((localName) => checker.variable(localName));
const namespaces = meta.namespaceDependencies.map((dep) => {
return new checker.ExternalExpr({ moduleName: dep.moduleName, name: null });
});
// m.default
const defaultRead = checker.variable(moduleName).prop('default');
// ɵɵreplaceMetadata(Comp, m.default, [...namespaces], [...locals]);
const replaceCall = checker.importExpr(checker.Identifiers.replaceMetadata)
.callFn([meta.type, defaultRead, checker.literalArr(namespaces), checker.literalArr(locals)]);
// (m) => m.default && ɵɵreplaceMetadata(...)
const replaceCallback = checker.arrowFn([new checker.FnParam(moduleName)], defaultRead.and(replaceCall));
// '' + encodeURIComponent(t)
const urlValue = checker.literal(urlPartial)
.plus(checker.variable('encodeURIComponent').callFn([checker.variable(timestampName)]));
// function Cmp_HmrLoad(t) {
// import(/* @vite-ignore */ url).then((m) => m.default && replaceMetadata(...));
// }
const importCallback = new checker.DeclareFunctionStmt(importCallbackName, [new checker.FnParam(timestampName)], [
// The vite-ignore special comment is required to prevent Vite from generating a superfluous
// warning for each usage within the development code. If Vite provides a method to
// programmatically avoid this warning in the future, this added comment can be removed here.
new checker.DynamicImportExpr(urlValue, null, '@vite-ignore')
.prop('then')
.callFn([replaceCallback])
.toStmt(),
], null, checker.StmtModifier.Final);
// (d) => d.id === && Cmp_HmrLoad(d.timestamp)
const updateCallback = checker.arrowFn([new checker.FnParam(dataName)], checker.variable(dataName)
.prop('id')
.identical(checker.literal(id))
.and(checker.variable(importCallbackName).callFn([checker.variable(dataName).prop('timestamp')])));
// Cmp_HmrLoad(Date.now());
// Initial call to kick off the loading in order to avoid edge cases with components
// coming from lazy chunks that change before the chunk has loaded.
const initialCall = checker.variable(importCallbackName)
.callFn([checker.variable('Date').prop('now').callFn([])]);
// import.meta.hot
const hotRead = checker.variable('import').prop('meta').prop('hot');
// import.meta.hot.on('angular:component-update', () => ...);
const hotListener = hotRead
.clone()
.prop('on')
.callFn([checker.literal('angular:component-update'), updateCallback]);
return checker.arrowFn([], [
// function Cmp_HmrLoad() {...}.
importCallback,
// ngDevMode && Cmp_HmrLoad(Date.now());
checker.devOnlyGuardedExpression(initialCall).toStmt(),
// ngDevMode && import.meta.hot && import.meta.hot.on(...)
checker.devOnlyGuardedExpression(hotRead.and(hotListener)).toStmt(),
])
.callFn([]);
}
/**
* Compiles the HMR update callback for a class.
* @param definitions Compiled definitions for the class (e.g. `defineComponent` calls).
* @param constantStatements Supporting constants statements that were generated alongside
* the definition.
* @param meta HMR metadata extracted from the class.
*/
function compileHmrUpdateCallback(definitions, constantStatements, meta) {
const namespaces = 'ɵɵnamespaces';
const params = [meta.className, namespaces, ...meta.localDependencies].map((name) => new checker.FnParam(name, checker.DYNAMIC_TYPE));
const body = [];
// Declare variables that read out the individual namespaces.
for (let i = 0; i < meta.namespaceDependencies.length; i++) {
body.push(new checker.DeclareVarStmt(meta.namespaceDependencies[i].assignedName, checker.variable(namespaces).key(checker.literal(i)), checker.DYNAMIC_TYPE, checker.StmtModifier.Final));
}
body.push(...constantStatements);
for (const field of definitions) {
if (field.initializer !== null) {
body.push(checker.variable(meta.className).prop(field.name).set(field.initializer).toStmt());
for (const stmt of field.statements) {
body.push(stmt);
}
}
}
return new checker.DeclareFunctionStmt(`${meta.className}_UpdateMetadata`, params, body, null, checker.StmtModifier.Final);
}
/**
* 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('19.0.5'));
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('19.0.5'));
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('19.0.5'));
// 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 to handle this new "format". For this reason, if we know this metadata
* does not need to be captured- we fall back to the old format. This is what this function
* generates.
*
* See:
* https://github.com/angular/angular/blob/d4b423690210872b5c32a322a6090beda30b05a3/packages/core/src/compiler/compiler_facade_interface.ts#L197-L199
*/
function legacyInputsPartialMetadata(inputs) {
// TODO(legacy-partial-output-inputs): Remove function in v18.
const keys = Object.getOwnPropertyNames(inputs);
if (keys.length === 0) {
return null;
}
return checker.literalMap(keys.map((declaredName) => {
const value = inputs[declaredName];
const publicName = value.bindingPropertyName;
const differentDeclaringName = publicName !== declaredName;
let result;
if (differentDeclaringName || value.transformFunction !== null) {
const values = [checker.asLiteral(publicName), checker.asLiteral(declaredName)];
if (value.transformFunction !== null) {
values.push(value.transformFunction);
}
result = checker.literalArr(values);
}
else {
result = checker.asLiteral(publicName);
}
return {
key: declaredName,
// put quotes around keys that contain potentially unsafe characters
quoted: checker.UNSAFE_OBJECT_KEY_NAME_REGEXP.test(declaredName),
value: result,
};
}));
}
/**
* Compile a component declaration defined by the `R3ComponentMetadata`.
*/
function compileDeclareComponentFromMetadata(meta, template, additionalTemplateInfo) {
const definitionMap = createComponentDefinitionMap(meta, template, additionalTemplateInfo);
const expression = checker.importExpr(checker.Identifiers.declareComponent).callFn([definitionMap.toLiteralMap()]);
const type = checker.createComponentType(meta);
return { expression, type, statements: [] };
}
/**
* Gathers the declaration fields for a component into a `DefinitionMap`.
*/
function createComponentDefinitionMap(meta, template, templateInfo) {
const definitionMap = createDirectiveDefinitionMap(meta);
const blockVisitor = new BlockPresenceVisitor();
checker.visitAll$1(blockVisitor, template.nodes);
definitionMap.set('template', getTemplateExpression(template, templateInfo));
if (templateInfo.isInline) {
definitionMap.set('isInline', checker.literal(true));
}
// Set the minVersion to 17.0.0 if the component is using at least one block in its template.
// We don't do this for templates without blocks, in order to preserve backwards compatibility.
if (blockVisitor.hasBlocks) {
definitionMap.set('minVersion', checker.literal('17.0.0'));
}
definitionMap.set('styles', toOptionalLiteralArray(meta.styles, checker.literal));
definitionMap.set('dependencies', compileUsedDependenciesMetadata(meta));
definitionMap.set('viewProviders', meta.viewProviders);
definitionMap.set('animations', meta.animations);
if (meta.changeDetection !== null) {
if (typeof meta.changeDetection === 'object') {
throw new Error('Impossible state! Change detection flag is not resolved!');
}
definitionMap.set('changeDetection', checker.importExpr(checker.Identifiers.ChangeDetectionStrategy)
.prop(checker.ChangeDetectionStrategy[meta.changeDetection]));
}
if (meta.encapsulation !== checker.ViewEncapsulation.Emulated) {
definitionMap.set('encapsulation', checker.importExpr(checker.Identifiers.ViewEncapsulation).prop(checker.ViewEncapsulation[meta.encapsulation]));
}
if (meta.interpolation !== checker.DEFAULT_INTERPOLATION_CONFIG) {
definitionMap.set('interpolation', checker.literalArr([checker.literal(meta.interpolation.start), checker.literal(meta.interpolation.end)]));
}
if (template.preserveWhitespaces === true) {
definitionMap.set('preserveWhitespaces', checker.literal(true));
}
if (meta.defer.mode === 0 /* DeferBlockDepsEmitMode.PerBlock */) {
const resolvers = [];
let hasResolvers = false;
for (const deps of meta.defer.blocks.values()) {
// Note: we need to push a `null` even if there are no dependencies, because matching of
// defer resolver functions to defer blocks happens by index and not adding an array
// entry for a block can throw off the blocks coming after it.
if (deps === null) {
resolvers.push(checker.literal(null));
}
else {
resolvers.push(deps);
hasResolvers = true;
}
}
// If *all* the resolvers are null, we can skip the field.
if (hasResolvers) {
definitionMap.set('deferBlockDependencies', checker.literalArr(resolvers));
}
}
else {
throw new Error('Unsupported defer function emit mode in partial compilation');
}
return definitionMap;
}
function getTemplateExpression(template, templateInfo) {
// If the template has been defined using a direct literal, we use that expression directly
// without any modifications. This is ensures proper source mapping from the partially
// compiled code to the source file declaring the template. Note that this does not capture
// template literals referenced indirectly through an identifier.
if (templateInfo.inlineTemplateLiteralExpression !== null) {
return templateInfo.inlineTemplateLiteralExpression;
}
// If the template is defined inline but not through a literal, the template has been resolved
// through static interpretation. We create a literal but cannot provide any source span. Note
// that we cannot use the expression defining the template because the linker expects the template
// to be defined as a literal in the declaration.
if (templateInfo.isInline) {
return checker.literal(templateInfo.content, null, null);
}
// The template is external so we must synthesize an expression node with
// the appropriate source-span.
const contents = templateInfo.content;
const file = new checker.ParseSourceFile(contents, templateInfo.sourceUrl);
const start = new checker.ParseLocation(file, 0, 0, 0);
const end = computeEndLocation(file, contents);
const span = new checker.ParseSourceSpan(start, end);
return checker.literal(contents, null, span);
}
function computeEndLocation(file, contents) {
const length = contents.length;
let lineStart = 0;
let lastLineStart = 0;
let line = 0;
do {
lineStart = contents.indexOf('\n', lastLineStart);
if (lineStart !== -1) {
lastLineStart = lineStart + 1;
line++;
}
} while (lineStart !== -1);
return new checker.ParseLocation(file, length, line, length - lastLineStart);
}
function compileUsedDependenciesMetadata(meta) {
const wrapType = meta.declarationListEmitMode !== 0 /* DeclarationListEmitMode.Direct */
? checker.generateForwardRef
: (expr) => expr;
if (meta.declarationListEmitMode === 3 /* DeclarationListEmitMode.RuntimeResolved */) {
throw new Error(`Unsupported emit mode`);
}
return toOptionalLiteralArray(meta.declarations, (decl) => {
switch (decl.kind) {
case checker.R3TemplateDependencyKind.Directive:
const dirMeta = new checker.DefinitionMap();
dirMeta.set('kind', checker.literal(decl.isComponent ? 'component' : 'directive'));
dirMeta.set('type', wrapType(decl.type));
dirMeta.set('selector', checker.literal(decl.selector));
dirMeta.set('inputs', toOptionalLiteralArray(decl.inputs, checker.literal));
dirMeta.set('outputs', toOptionalLiteralArray(decl.outputs, checker.literal));
dirMeta.set('exportAs', toOptionalLiteralArray(decl.exportAs, checker.literal));
return dirMeta.toLiteralMap();
case checker.R3TemplateDependencyKind.Pipe:
const pipeMeta = new checker.DefinitionMap();
pipeMeta.set('kind', checker.literal('pipe'));
pipeMeta.set('type', wrapType(decl.type));
pipeMeta.set('name', checker.literal(decl.name));
return pipeMeta.toLiteralMap();
case checker.R3TemplateDependencyKind.NgModule:
const ngModuleMeta = new checker.DefinitionMap();
ngModuleMeta.set('kind', checker.literal('ngmodule'));
ngModuleMeta.set('type', wrapType(decl.type));
return ngModuleMeta.toLiteralMap();
}
});
}
class BlockPresenceVisitor extends checker.RecursiveVisitor$1 {
hasBlocks = false;
visitDeferredBlock() {
this.hasBlocks = true;
}
visitDeferredBlockPlaceholder() {
this.hasBlocks = true;
}
visitDeferredBlockLoading() {
this.hasBlocks = true;
}
visitDeferredBlockError() {
this.hasBlocks = true;
}
visitIfBlock() {
this.hasBlocks = true;
}
visitIfBlockBranch() {
this.hasBlocks = true;
}
visitForLoopBlock() {
this.hasBlocks = true;
}
visitForLoopBlockEmpty() {
this.hasBlocks = true;
}
visitSwitchBlock() {
this.hasBlocks = true;
}
visitSwitchBlockCase() {
this.hasBlocks = true;
}
}
/**
* 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$4 = '12.0.0';
function compileDeclareFactoryFunction(meta) {
const definitionMap = new checker.DefinitionMap();
definitionMap.set('minVersion', checker.literal(MINIMUM_PARTIAL_LINKER_VERSION$4));
definitionMap.set('version', checker.literal('19.0.5'));
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
definitionMap.set('type', meta.type.value);
definitionMap.set('deps', compileDependencies(meta.deps));
definitionMap.set('target', checker.importExpr(checker.Identifiers.FactoryTarget).prop(checker.FactoryTarget[meta.target]));
return {
expression: checker.importExpr(checker.Identifiers.declareFactory).callFn([definitionMap.toLiteralMap()]),
statements: [],
type: checker.createFactoryType(meta),
};
}
/**
* 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$3 = '12.0.0';
/**
* Compile a Injectable declaration defined by the `R3InjectableMetadata`.
*/
function compileDeclareInjectableFromMetadata(meta) {
const definitionMap = createInjectableDefinitionMap(meta);
const expression = checker.importExpr(checker.Identifiers.declareInjectable).callFn([definitionMap.toLiteralMap()]);
const type = checker.createInjectableType(meta);
return { expression, type, statements: [] };
}
/**
* Gathers the declaration fields for a Injectable into a `DefinitionMap`.
*/
function createInjectableDefinitionMap(meta) {
const definitionMap = new checker.DefinitionMap();
definitionMap.set('minVersion', checker.literal(MINIMUM_PARTIAL_LINKER_VERSION$3));
definitionMap.set('version', checker.literal('19.0.5'));
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
definitionMap.set('type', meta.type.value);
// Only generate providedIn property if it has a non-null value
if (meta.providedIn !== undefined) {
const providedIn = checker.convertFromMaybeForwardRefExpression(meta.providedIn);
if (providedIn.value !== null) {
definitionMap.set('providedIn', providedIn);
}
}
if (meta.useClass !== undefined) {
definitionMap.set('useClass', checker.convertFromMaybeForwardRefExpression(meta.useClass));
}
if (meta.useExisting !== undefined) {
definitionMap.set('useExisting', checker.convertFromMaybeForwardRefExpression(meta.useExisting));
}
if (meta.useValue !== undefined) {
definitionMap.set('useValue', checker.convertFromMaybeForwardRefExpression(meta.useValue));
}
// Factories do not contain `ForwardRef`s since any types are already wrapped in a function call
// so the types will not be eagerly evaluated. Therefore we do not need to process this expression
// with `convertFromProviderExpression()`.
if (meta.useFactory !== undefined) {
definitionMap.set('useFactory', meta.useFactory);
}
if (meta.deps !== undefined) {
definitionMap.set('deps', checker.literalArr(meta.deps.map(compileDependency)));
}
return definitionMap;
}
/**
* 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$2 = '12.0.0';
function compileDeclareInjectorFromMetadata(meta) {
const definitionMap = createInjectorDefinitionMap(meta);
const expression = checker.importExpr(checker.Identifiers.declareInjector).callFn([definitionMap.toLiteralMap()]);
const type = checker.createInjectorType(meta);
return { expression, type, statements: [] };
}
/**
* Gathers the declaration fields for an Injector into a `DefinitionMap`.
*/
function createInjectorDefinitionMap(meta) {
const definitionMap = new checker.DefinitionMap();
definitionMap.set('minVersion', checker.literal(MINIMUM_PARTIAL_LINKER_VERSION$2));
definitionMap.set('version', checker.literal('19.0.5'));
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
definitionMap.set('type', meta.type.value);
definitionMap.set('providers', meta.providers);
if (meta.imports.length > 0) {
definitionMap.set('imports', checker.literalArr(meta.imports));
}
return definitionMap;
}
/**
* 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$1 = '14.0.0';
function compileDeclareNgModuleFromMetadata(meta) {
const definitionMap = createNgModuleDefinitionMap(meta);
const expression = checker.importExpr(checker.Identifiers.declareNgModule).callFn([definitionMap.toLiteralMap()]);
const type = checker.createNgModuleType(meta);
return { expression, type, statements: [] };
}
/**
* Gathers the declaration fields for an NgModule into a `DefinitionMap`.
*/
function createNgModuleDefinitionMap(meta) {
const definitionMap = new checker.DefinitionMap();
if (meta.kind === checker.R3NgModuleMetadataKind.Local) {
throw new Error('Invalid path! Local compilation mode should not get into the partial compilation path');
}
definitionMap.set('minVersion', checker.literal(MINIMUM_PARTIAL_LINKER_VERSION$1));
definitionMap.set('version', checker.literal('19.0.5'));
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
definitionMap.set('type', meta.type.value);
// We only generate the keys in the metadata if the arrays contain values.
// We must wrap the arrays inside a function if any of the values are a forward reference to a
// not-yet-declared class. This is to support JIT execution of the `ɵɵngDeclareNgModule()` call.
// In the linker these wrappers are stripped and then reapplied for the `ɵɵdefineNgModule()` call.
if (meta.bootstrap.length > 0) {
definitionMap.set('bootstrap', checker.refsToArray(meta.bootstrap, meta.containsForwardDecls));
}
if (meta.declarations.length > 0) {
definitionMap.set('declarations', checker.refsToArray(meta.declarations, meta.containsForwardDecls));
}
if (meta.imports.length > 0) {
definitionMap.set('imports', checker.refsToArray(meta.imports, meta.containsForwardDecls));
}
if (meta.exports.length > 0) {
definitionMap.set('exports', checker.refsToArray(meta.exports, meta.containsForwardDecls));
}
if (meta.schemas !== null && meta.schemas.length > 0) {
definitionMap.set('schemas', checker.literalArr(meta.schemas.map((ref) => ref.value)));
}
if (meta.id !== null) {
definitionMap.set('id', meta.id);
}
return definitionMap;
}
/**
* 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 = '14.0.0';
/**
* Compile a Pipe declaration defined by the `R3PipeMetadata`.
*/
function compileDeclarePipeFromMetadata(meta) {
const definitionMap = createPipeDefinitionMap(meta);
const expression = checker.importExpr(checker.Identifiers.declarePipe).callFn([definitionMap.toLiteralMap()]);
const type = checker.createPipeType(meta);
return { expression, type, statements: [] };
}
/**
* Gathers the declaration fields for a Pipe into a `DefinitionMap`.
*/
function createPipeDefinitionMap(meta) {
const definitionMap = new checker.DefinitionMap();
definitionMap.set('minVersion', checker.literal(MINIMUM_PARTIAL_LINKER_VERSION));
definitionMap.set('version', checker.literal('19.0.5'));
definitionMap.set('ngImport', checker.importExpr(checker.Identifiers.core));
// e.g. `type: MyPipe`
definitionMap.set('type', meta.type.value);
if (meta.isStandalone !== undefined) {
definitionMap.set('isStandalone', checker.literal(meta.isStandalone));
}
// e.g. `name: "myPipe"`
definitionMap.set('name', checker.literal(meta.pipeName));
if (meta.pure === false) {
// e.g. `pure: false`
definitionMap.set('pure', checker.literal(meta.pure));
}
return definitionMap;
}
/**
* Base URL for the error details page.
*
* Keep the files below in full sync:
* - packages/compiler-cli/src/ngtsc/diagnostics/src/error_details_base_url.ts
* - packages/core/src/error_details_base_url.ts
*/
const ERROR_DETAILS_PAGE_BASE_URL = 'https://angular.dev/errors';
// Escape anything that isn't alphanumeric, '/' or '_'.
const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_]/g;
/**
* An `AliasingHost` which generates and consumes alias re-exports when module names for each file
* are determined by a `UnifiedModulesHost`.
*
* When using a `UnifiedModulesHost`, aliasing prevents issues with transitive dependencies. See the
* README.md for more details.
*/
class UnifiedModulesAliasingHost {
unifiedModulesHost;
constructor(unifiedModulesHost) {
this.unifiedModulesHost = unifiedModulesHost;
}
/**
* With a `UnifiedModulesHost`, aliases are chosen automatically without the need to look through
* the exports present in a .d.ts file, so we can avoid cluttering the .d.ts files.
*/
aliasExportsInDts = false;
maybeAliasSymbolAs(ref, context, ngModuleName, isReExport) {
if (!isReExport) {
// Aliasing is used with a UnifiedModulesHost to prevent transitive dependencies. Thus,
// aliases
// only need to be created for directives/pipes which are not direct declarations of an
// NgModule which exports them.
return null;
}
return this.aliasName(ref.node, context);
}
/**
* Generates an `Expression` to import `decl` from `via`, assuming an export was added when `via`
* was compiled per `maybeAliasSymbolAs` above.
*/
getAliasIn(decl, via, isReExport) {
if (!isReExport) {
// Directly exported directives/pipes don't require an alias, per the logic in
// `maybeAliasSymbolAs`.
return null;
}
// viaModule is the module it'll actually be imported from.
const moduleName = this.unifiedModulesHost.fileNameToModuleName(via.fileName, via.fileName);
return new checker.ExternalExpr({ moduleName, name: this.aliasName(decl, via) });
}
/**
* Generates an alias name based on the full module name of the file which declares the aliased
* directive/pipe.
*/
aliasName(decl, context) {
// The declared module is used to get the name of the alias.
const declModule = this.unifiedModulesHost.fileNameToModuleName(decl.getSourceFile().fileName, context.fileName);
const replaced = declModule.replace(CHARS_TO_ESCAPE, '_').replace(/\//g, '$');
return 'ɵng$' + replaced + '$$' + decl.name.text;
}
}
/**
* An `AliasingHost` which exports directives from any file containing an NgModule in which they're
* declared/exported, under a private symbol name.
*
* These exports support cases where an NgModule is imported deeply from an absolute module path
* (that is, it's not part of an Angular Package Format entrypoint), and the compiler needs to
* import any matched directives/pipes from the same path (to the NgModule file). See README.md for
* more details.
*/
class PrivateExportAliasingHost {
host;
constructor(host) {
this.host = host;
}
/**
* Under private export aliasing, the `AbsoluteModuleStrategy` used for emitting references will
* will select aliased exports that it finds in the .d.ts file for an NgModule's file. Thus,
* emitting these exports in .d.ts is a requirement for the `PrivateExportAliasingHost` to
* function correctly.
*/
aliasExportsInDts = true;
maybeAliasSymbolAs(ref, context, ngModuleName) {
if (ref.hasOwningModuleGuess) {
// Skip nodes that already have an associated absolute module specifier, since they can be
// safely imported from that specifier.
return null;
}
// Look for a user-provided export of `decl` in `context`. If one exists, then an alias export
// is not needed.
// TODO(alxhub): maybe add a host method to check for the existence of an export without going
// through the entire list of exports.
const exports = this.host.getExportsOfModule(context);
if (exports === null) {
// Something went wrong, and no exports were available at all. Bail rather than risk creating
// re-exports when they're not needed.
throw new Error(`Could not determine the exports of: ${context.fileName}`);
}
let found = false;
exports.forEach((value) => {
if (value.node === ref.node) {
found = true;
}
});
if (found) {
// The module exports the declared class directly, no alias is necessary.
return null;
}
return `ɵngExportɵ${ngModuleName}ɵ${ref.node.name.text}`;
}
/**
* A `PrivateExportAliasingHost` only generates re-exports and does not direct the compiler to
* directly consume the aliases it creates.
*
* Instead, they're consumed indirectly: `AbsoluteModuleStrategy` `ReferenceEmitterStrategy` will
* select these alias exports automatically when looking for an export of the directive/pipe from
* the same path as the NgModule was imported.
*
* Thus, `getAliasIn` always returns `null`.
*/
getAliasIn() {
return null;
}
}
/**
* A `ReferenceEmitStrategy` which will consume the alias attached to a particular `Reference` to a
* directive or pipe, if it exists.
*/
class AliasStrategy {
emit(ref, context, importMode) {
if (importMode & checker.ImportFlags.NoAliasing || ref.alias === null) {
return null;
}
return {
kind: checker.ReferenceEmitKind.Success,
expression: ref.alias,
importedFile: 'unknown',
};
}
}
function relativePathBetween(from, to) {
const relativePath = checker.stripExtension(checker.relative(checker.dirname(checker.resolve(from)), checker.resolve(to)));
return relativePath !== '' ? checker.toRelativeImport(relativePath) : null;
}
function normalizeSeparators(path) {
// TODO: normalize path only for OS that need it.
return path.replace(/\\/g, '/');
}
/**
* Attempts to generate a project-relative path
* @param sourceFile
* @param rootDirs
* @param compilerHost
* @returns
*/
function getProjectRelativePath(sourceFile, rootDirs, compilerHost) {
// Note: we need to pass both the file name and the root directories through getCanonicalFileName,
// because the root directories might've been passed through it already while the source files
// definitely have not. This can break the relative return value, because in some platforms
// getCanonicalFileName lowercases the path.
const filePath = compilerHost.getCanonicalFileName(sourceFile.fileName);
for (const rootDir of rootDirs) {
const rel = checker.relative(compilerHost.getCanonicalFileName(rootDir), filePath);
if (!rel.startsWith('..')) {
return rel;
}
}
return null;
}
/**
* `ImportRewriter` that does no rewriting.
*/
class NoopImportRewriter {
rewriteSymbol(symbol, specifier) {
return symbol;
}
rewriteSpecifier(specifier, inContextOfFile) {
return specifier;
}
rewriteNamespaceImportIdentifier(specifier) {
return specifier;
}
}
/**
* A mapping of supported symbols that can be imported from within @angular/core, and the names by
* which they're exported from r3_symbols.
*/
const CORE_SUPPORTED_SYMBOLS = new Map([
['ɵɵdefineInjectable', 'ɵɵdefineInjectable'],
['ɵɵdefineInjector', 'ɵɵdefineInjector'],
['ɵɵdefineNgModule', 'ɵɵdefineNgModule'],
['ɵɵsetNgModuleScope', 'ɵɵsetNgModuleScope'],
['ɵɵinject', 'ɵɵinject'],
['ɵɵFactoryDeclaration', 'ɵɵFactoryDeclaration'],
['ɵsetClassMetadata', 'setClassMetadata'],
['ɵsetClassMetadataAsync', 'setClassMetadataAsync'],
['ɵɵInjectableDeclaration', 'ɵɵInjectableDeclaration'],
['ɵɵInjectorDeclaration', 'ɵɵInjectorDeclaration'],
['ɵɵNgModuleDeclaration', 'ɵɵNgModuleDeclaration'],
['ɵNgModuleFactory', 'NgModuleFactory'],
['ɵnoSideEffects', 'ɵnoSideEffects'],
]);
const CORE_MODULE = '@angular/core';
/**
* `ImportRewriter` that rewrites imports from '@angular/core' to be imported from the r3_symbols.ts
* file instead.
*/
class R3SymbolsImportRewriter {
r3SymbolsPath;
constructor(r3SymbolsPath) {
this.r3SymbolsPath = r3SymbolsPath;
}
rewriteSymbol(symbol, specifier) {
if (specifier !== CORE_MODULE) {
// This import isn't from core, so ignore it.
return symbol;
}
return validateAndRewriteCoreSymbol(symbol);
}
rewriteSpecifier(specifier, inContextOfFile) {
if (specifier !== CORE_MODULE) {
// This module isn't core, so ignore it.
return specifier;
}
const relativePathToR3Symbols = relativePathBetween(inContextOfFile, this.r3SymbolsPath);
if (relativePathToR3Symbols === null) {
throw new Error(`Failed to rewrite import inside ${CORE_MODULE}: ${inContextOfFile} -> ${this.r3SymbolsPath}`);
}
return relativePathToR3Symbols;
}
rewriteNamespaceImportIdentifier(specifier) {
return specifier;
}
}
function validateAndRewriteCoreSymbol(name) {
if (!CORE_SUPPORTED_SYMBOLS.has(name)) {
throw new Error(`Importing unexpected symbol ${name} while compiling ${CORE_MODULE}`);
}
return CORE_SUPPORTED_SYMBOLS.get(name);
}
const AssumeEager = 'AssumeEager';
/**
* Allows to register a symbol as deferrable and keep track of its usage.
*
* This information is later used to determine whether it's safe to drop
* a regular import of this symbol (actually the entire import declaration)
* in favor of using a dynamic import for cases when defer blocks are used.
*/
class DeferredSymbolTracker {
typeChecker;
onlyExplicitDeferDependencyImports;
imports = new Map();
/**
* Map of a component class -> all import declarations that bring symbols
* used within `@Component.deferredImports` field.
*/
explicitlyDeferredImports = new Map();
constructor(typeChecker, onlyExplicitDeferDependencyImports) {
this.typeChecker = typeChecker;
this.onlyExplicitDeferDependencyImports = onlyExplicitDeferDependencyImports;
}
/**
* Given an import declaration node, extract the names of all imported symbols
* and return them as a map where each symbol is a key and `AssumeEager` is a value.
*
* The logic recognizes the following import shapes:
*
* Case 1: `import {a, b as B} from 'a'`
* Case 2: `import X from 'a'`
* Case 3: `import * as x from 'a'`
*/
extractImportedSymbols(importDecl) {
const symbolMap = new Map();
// Unsupported case: `import 'a'`
if (importDecl.importClause === undefined) {
throw new Error(`Provided import declaration doesn't have any symbols.`);
}
// If the entire import is a type-only import, none of the symbols can be eager.
if (importDecl.importClause.isTypeOnly) {
return symbolMap;
}
if (importDecl.importClause.namedBindings !== undefined) {
const bindings = importDecl.importClause.namedBindings;
if (ts__default["default"].isNamedImports(bindings)) {
// Case 1: `import {a, b as B} from 'a'`
for (const element of bindings.elements) {
if (!element.isTypeOnly) {
symbolMap.set(element.name.text, AssumeEager);
}
}
}
else {
// Case 2: `import X from 'a'`
symbolMap.set(bindings.name.text, AssumeEager);
}
}
else if (importDecl.importClause.name !== undefined) {
// Case 2: `import * as x from 'a'`
symbolMap.set(importDecl.importClause.name.text, AssumeEager);
}
else {
throw new Error('Unrecognized import structure.');
}
return symbolMap;
}
/**
* Retrieves a list of import declarations that contain symbols used within
* `@Component.deferredImports` of a specific component class, but those imports
* can not be removed, since there are other symbols imported alongside deferred
* components.
*/
getNonRemovableDeferredImports(sourceFile, classDecl) {
const affectedImports = [];
const importDecls = this.explicitlyDeferredImports.get(classDecl) ?? [];
for (const importDecl of importDecls) {
if (importDecl.getSourceFile() === sourceFile && !this.canDefer(importDecl)) {
affectedImports.push(importDecl);
}
}
return affectedImports;
}
/**
* Marks a given identifier and an associated import declaration as a candidate
* for defer loading.
*/
markAsDeferrableCandidate(identifier, importDecl, componentClassDecl, isExplicitlyDeferred) {
if (this.onlyExplicitDeferDependencyImports && !isExplicitlyDeferred) {
// Ignore deferrable candidates when only explicit deferred imports mode is enabled.
// In that mode only dependencies from the `@Component.deferredImports` field are
// defer-loadable.
return;
}
if (isExplicitlyDeferred) {
if (this.explicitlyDeferredImports.has(componentClassDecl)) {
this.explicitlyDeferredImports.get(componentClassDecl).push(importDecl);
}
else {
this.explicitlyDeferredImports.set(componentClassDecl, [importDecl]);
}
}
let symbolMap = this.imports.get(importDecl);
// Do we come across this import for the first time?
if (!symbolMap) {
symbolMap = this.extractImportedSymbols(importDecl);
this.imports.set(importDecl, symbolMap);
}
if (!symbolMap.has(identifier.text)) {
throw new Error(`The '${identifier.text}' identifier doesn't belong ` +
`to the provided import declaration.`);
}
if (symbolMap.get(identifier.text) === AssumeEager) {
// We process this symbol for the first time, populate references.
symbolMap.set(identifier.text, this.lookupIdentifiersInSourceFile(identifier.text, importDecl));
}
const identifiers = symbolMap.get(identifier.text);
// Drop the current identifier, since we are trying to make it deferrable
// (it's used as a dependency in one of the defer blocks).
identifiers.delete(identifier);
}
/**
* Whether all symbols from a given import declaration have no references
* in a source file, thus it's safe to use dynamic imports.
*/
canDefer(importDecl) {
if (!this.imports.has(importDecl)) {
return false;
}
const symbolsMap = this.imports.get(importDecl);
for (const refs of symbolsMap.values()) {
if (refs === AssumeEager || refs.size > 0) {
// There may be still eager references to this symbol.
return false;
}
}
return true;
}
/**
* Returns a set of import declarations that is safe to remove
* from the current source file and generate dynamic imports instead.
*/
getDeferrableImportDecls() {
const deferrableDecls = new Set();
for (const [importDecl] of this.imports) {
if (this.canDefer(importDecl)) {
deferrableDecls.add(importDecl);
}
}
return deferrableDecls;
}
lookupIdentifiersInSourceFile(name, importDecl) {
const results = new Set();
const visit = (node) => {
// Don't record references from the declaration itself or inside
// type nodes which will be stripped from the JS output.
if (node === importDecl || ts__default["default"].isTypeNode(node)) {
return;
}
if (ts__default["default"].isIdentifier(node) && node.text === name) {
// Is `node` actually a reference to this symbol?
const sym = this.typeChecker.getSymbolAtLocation(node);
if (sym === undefined) {
return;
}
if (sym.declarations === undefined || sym.declarations.length === 0) {
return;
}
const importClause = sym.declarations[0];
// Is declaration from this import statement?
const decl = checker.getContainingImportDeclaration(importClause);
if (decl !== importDecl) {
return;
}
// `node` *is* a reference to the same import.
results.add(node);
}
ts__default["default"].forEachChild(node, visit);
};
visit(importDecl.getSourceFile());
return results;
}
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Tracks which symbols are imported in specific files and under what names. Allows for efficient
* querying for references to those symbols without having to consult the type checker early in the
* process.
*
* Note that the tracker doesn't account for variable shadowing so a final verification with the
* type checker may be necessary, depending on the context. Also does not track dynamic imports.
*/
class ImportedSymbolsTracker {
fileToNamedImports = new WeakMap();
fileToNamespaceImports = new WeakMap();
/**
* Checks if an identifier is a potential reference to a specific named import within the same
* file.
* @param node Identifier to be checked.
* @param exportedName Name of the exported symbol that is being searched for.
* @param moduleName Module from which the symbol should be imported.
*/
isPotentialReferenceToNamedImport(node, exportedName, moduleName) {
const sourceFile = node.getSourceFile();
this.scanImports(sourceFile);
const fileImports = this.fileToNamedImports.get(sourceFile);
const moduleImports = fileImports.get(moduleName);
const symbolImports = moduleImports?.get(exportedName);
return symbolImports !== undefined && symbolImports.has(node.text);
}
/**
* Checks if an identifier is a potential reference to a specific namespace import within the same
* file.
* @param node Identifier to be checked.
* @param moduleName Module from which the namespace is imported.
*/
isPotentialReferenceToNamespaceImport(node, moduleName) {
const sourceFile = node.getSourceFile();
this.scanImports(sourceFile);
const namespaces = this.fileToNamespaceImports.get(sourceFile);
return namespaces.get(moduleName)?.has(node.text) ?? false;
}
/**
* Checks if a file has a named imported of a certain symbol.
* @param sourceFile File to be checked.
* @param exportedName Name of the exported symbol that is being checked.
* @param moduleName Module that exports the symbol.
*/
hasNamedImport(sourceFile, exportedName, moduleName) {
this.scanImports(sourceFile);
const fileImports = this.fileToNamedImports.get(sourceFile);
const moduleImports = fileImports.get(moduleName);
return moduleImports !== undefined && moduleImports.has(exportedName);
}
/**
* Checks if a file has namespace imports of a certain symbol.
* @param sourceFile File to be checked.
* @param moduleName Module whose namespace import is being searched for.
*/
hasNamespaceImport(sourceFile, moduleName) {
this.scanImports(sourceFile);
const namespaces = this.fileToNamespaceImports.get(sourceFile);
return namespaces.has(moduleName);
}
/** Scans a `SourceFile` for import statements and caches them for later use. */
scanImports(sourceFile) {
if (this.fileToNamedImports.has(sourceFile) && this.fileToNamespaceImports.has(sourceFile)) {
return;
}
const namedImports = new Map();
const namespaceImports = new Map();
this.fileToNamedImports.set(sourceFile, namedImports);
this.fileToNamespaceImports.set(sourceFile, namespaceImports);
// Only check top-level imports.
for (const stmt of sourceFile.statements) {
if (!ts__default["default"].isImportDeclaration(stmt) ||
!ts__default["default"].isStringLiteralLike(stmt.moduleSpecifier) ||
stmt.importClause?.namedBindings === undefined) {
continue;
}
const moduleName = stmt.moduleSpecifier.text;
if (ts__default["default"].isNamespaceImport(stmt.importClause.namedBindings)) {
// import * as foo from 'module'
if (!namespaceImports.has(moduleName)) {
namespaceImports.set(moduleName, new Set());
}
namespaceImports.get(moduleName).add(stmt.importClause.namedBindings.name.text);
}
else {
// import {foo, bar as alias} from 'module'
for (const element of stmt.importClause.namedBindings.elements) {
const localName = element.name.text;
const exportedName = element.propertyName === undefined ? localName : element.propertyName.text;
if (!namedImports.has(moduleName)) {
namedImports.set(moduleName, new Map());
}
const localNames = namedImports.get(moduleName);
if (!localNames.has(exportedName)) {
localNames.set(exportedName, new Set());
}
localNames.get(exportedName)?.add(localName);
}
}
}
}
}
/**
* A tool to track extra imports to be added to the generated files in the local compilation mode.
*
* This is needed for g3 bundling mechanism which requires dev files (= locally compiled) to have
* imports resemble those generated for prod files (= full compilation mode). In full compilation
* mode Angular compiler generates extra imports for statically analyzed component dependencies. We
* need similar imports in local compilation as well.
*
* The tool offers API for adding local imports (to be added to a specific file) and global imports
* (to be added to all the files in the local compilation). For more details on how these extra
* imports are determined see this design doc:
* https://docs.google.com/document/d/1dOWoSDvOY9ozlMmyCnxoFLEzGgHmTFVRAOVdVU-bxlI/edit?tab=t.0#heading=h.5n3k516r57g5
*
* An instance of this class will be passed to each annotation handler so that they can register the
* extra imports that they see fit. Later on, the instance is passed to the Ivy transformer ({@link
* ivyTransformFactory}) and it is used to add the extra imports registered by the handlers to the
* import manager ({@link ImportManager}) in order to have these imports generated.
*
* The extra imports are all side effect imports, and so they are identified by a single string
* containing the module name.
*
*/
class LocalCompilationExtraImportsTracker {
typeChecker;
localImportsMap = new Map();
globalImportsSet = new Set();
/** Names of the files marked for extra import generation. */
markedFilesSet = new Set();
constructor(typeChecker) {
this.typeChecker = typeChecker;
}
/**
* Marks the source file for extra imports generation.
*
* The extra imports are generated only for the files marked through this method. In other words,
* the method {@link getImportsForFile} returns empty if the file is not marked. This allows the
* consumers of this tool to avoid generating extra imports for unrelated files (e.g., non-Angular
* files)
*/
markFileForExtraImportGeneration(sf) {
this.markedFilesSet.add(sf.fileName);
}
/**
* Adds an extra import to be added to the generated file of a specific source file.
*/
addImportForFile(sf, moduleName) {
if (!this.localImportsMap.has(sf.fileName)) {
this.localImportsMap.set(sf.fileName, new Set());
}
this.localImportsMap.get(sf.fileName).add(moduleName);
}
/**
* If the given node is an imported identifier, this method adds the module from which it is
* imported as an extra import to the generated file of each source file in the compilation unit,
* otherwise the method is noop.
*
* Adding an extra import to all files is not optimal though. There are rooms to optimize and a
* add the import to a subset of files (e.g., exclude all the non Angular files as they don't need
* any extra import). However for this first version of this feature we go by this mechanism for
* simplicity. There will be on-going work to further optimize this method to add the extra import
* to smallest possible candidate files instead of all files.
*/
addGlobalImportFromIdentifier(node) {
let identifier = null;
if (ts__default["default"].isIdentifier(node)) {
identifier = node;
}
else if (ts__default["default"].isPropertyAccessExpression(node) && ts__default["default"].isIdentifier(node.expression)) {
identifier = node.expression;
}
if (identifier === null) {
return;
}
const sym = this.typeChecker.getSymbolAtLocation(identifier);
if (!sym?.declarations?.length) {
return;
}
const importClause = sym.declarations[0];
const decl = checker.getContainingImportDeclaration(importClause);
if (decl !== null) {
this.globalImportsSet.add(removeQuotations(decl.moduleSpecifier.getText()));
}
}
/**
* Returns the list of all module names that the given file should include as its extra imports.
*/
getImportsForFile(sf) {
if (!this.markedFilesSet.has(sf.fileName)) {
return [];
}
return [...this.globalImportsSet, ...(this.localImportsMap.get(sf.fileName) ?? [])];
}
}
function removeQuotations(s) {
return s.substring(1, s.length - 1).trim();
}
/**
* Used by `RouterEntryPointManager` and `NgModuleRouteAnalyzer` (which is in turn is used by
* `NgModuleDecoratorHandler`) for resolving the module source-files references in lazy-loaded
* routes (relative to the source-file containing the `NgModule` that provides the route
* definitions).
*/
class ModuleResolver {
program;
compilerOptions;
host;
moduleResolutionCache;
constructor(program, compilerOptions, host, moduleResolutionCache) {
this.program = program;
this.compilerOptions = compilerOptions;
this.host = host;
this.moduleResolutionCache = moduleResolutionCache;
}
resolveModule(moduleName, containingFile) {
const resolved = checker.resolveModuleName(moduleName, containingFile, this.compilerOptions, this.host, this.moduleResolutionCache);
if (resolved === undefined) {
return null;
}
return checker.getSourceFileOrNull(this.program, checker.absoluteFrom(resolved.resolvedFileName));
}
}
function getConstructorDependencies(clazz, reflector, isCore) {
const deps = [];
const errors = [];
let ctorParams = reflector.getConstructorParameters(clazz);
if (ctorParams === null) {
if (reflector.hasBaseClass(clazz)) {
return null;
}
else {
ctorParams = [];
}
}
ctorParams.forEach((param, idx) => {
let token = checker.valueReferenceToExpression(param.typeValueReference);
let attributeNameType = null;
let optional = false, self = false, skipSelf = false, host = false;
(param.decorators || [])
.filter((dec) => isCore || checker.isAngularCore(dec))
.forEach((dec) => {
const name = isCore || dec.import === null ? dec.name : dec.import.name;
if (name === 'Inject') {
if (dec.args === null || dec.args.length !== 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, dec.node, `Unexpected number of arguments to @Inject().`);
}
token = new checker.WrappedNodeExpr(dec.args[0]);
}
else if (name === 'Optional') {
optional = true;
}
else if (name === 'SkipSelf') {
skipSelf = true;
}
else if (name === 'Self') {
self = true;
}
else if (name === 'Host') {
host = true;
}
else if (name === 'Attribute') {
if (dec.args === null || dec.args.length !== 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, dec.node, `Unexpected number of arguments to @Attribute().`);
}
const attributeName = dec.args[0];
token = new checker.WrappedNodeExpr(attributeName);
if (ts__default["default"].isStringLiteralLike(attributeName)) {
attributeNameType = new checker.LiteralExpr(attributeName.text);
}
else {
attributeNameType = new checker.WrappedNodeExpr(ts__default["default"].factory.createKeywordTypeNode(ts__default["default"].SyntaxKind.UnknownKeyword));
}
}
else {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_UNEXPECTED, dec.node, `Unexpected decorator ${name} on parameter.`);
}
});
if (token === null) {
if (param.typeValueReference.kind !== 2 /* TypeValueReferenceKind.UNAVAILABLE */) {
throw new Error('Illegal state: expected value reference to be unavailable if no token is present');
}
errors.push({
index: idx,
param,
reason: param.typeValueReference.reason,
});
}
else {
deps.push({ token, attributeNameType, optional, self, skipSelf, host });
}
});
if (errors.length === 0) {
return { deps };
}
else {
return { deps: null, errors };
}
}
/**
* Convert `ConstructorDeps` into the `R3DependencyMetadata` array for those deps if they're valid,
* or into an `'invalid'` signal if they're not.
*
* This is a companion function to `validateConstructorDependencies` which accepts invalid deps.
*/
function unwrapConstructorDependencies(deps) {
if (deps === null) {
return null;
}
else if (deps.deps !== null) {
// These constructor dependencies are valid.
return deps.deps;
}
else {
// These deps are invalid.
return 'invalid';
}
}
function getValidConstructorDependencies(clazz, reflector, isCore) {
return validateConstructorDependencies(clazz, getConstructorDependencies(clazz, reflector, isCore));
}
/**
* Validate that `ConstructorDeps` does not have any invalid dependencies and convert them into the
* `R3DependencyMetadata` array if so, or raise a diagnostic if some deps are invalid.
*
* This is a companion function to `unwrapConstructorDependencies` which does not accept invalid
* deps.
*/
function validateConstructorDependencies(clazz, deps) {
if (deps === null) {
return null;
}
else if (deps.deps !== null) {
return deps.deps;
}
else {
// There is at least one error.
const error = deps.errors[0];
throw createUnsuitableInjectionTokenError(clazz, error);
}
}
/**
* Creates a fatal error with diagnostic for an invalid injection token.
* @param clazz The class for which the injection token was unavailable.
* @param error The reason why no valid injection token is available.
*/
function createUnsuitableInjectionTokenError(clazz, error) {
const { param, index, reason } = error;
let chainMessage = undefined;
let hints = undefined;
switch (reason.kind) {
case 5 /* ValueUnavailableKind.UNSUPPORTED */:
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
hints = [
checker.makeRelatedInformation(reason.typeNode, 'This type is not supported as injection token.'),
];
break;
case 1 /* ValueUnavailableKind.NO_VALUE_DECLARATION */:
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
hints = [
checker.makeRelatedInformation(reason.typeNode, 'This type does not have a value, so it cannot be used as injection token.'),
];
if (reason.decl !== null) {
hints.push(checker.makeRelatedInformation(reason.decl, 'The type is declared here.'));
}
break;
case 2 /* ValueUnavailableKind.TYPE_ONLY_IMPORT */:
chainMessage =
'Consider changing the type-only import to a regular import, or use the @Inject decorator to specify an injection token.';
hints = [
checker.makeRelatedInformation(reason.typeNode, 'This type is imported using a type-only import, which prevents it from being usable as an injection token.'),
checker.makeRelatedInformation(reason.node, 'The type-only import occurs here.'),
];
break;
case 4 /* ValueUnavailableKind.NAMESPACE */:
chainMessage = 'Consider using the @Inject decorator to specify an injection token.';
hints = [
checker.makeRelatedInformation(reason.typeNode, 'This type corresponds with a namespace, which cannot be used as injection token.'),
checker.makeRelatedInformation(reason.importClause, 'The namespace import occurs here.'),
];
break;
case 3 /* ValueUnavailableKind.UNKNOWN_REFERENCE */:
chainMessage = 'The type should reference a known declaration.';
hints = [checker.makeRelatedInformation(reason.typeNode, 'This type could not be resolved.')];
break;
case 0 /* ValueUnavailableKind.MISSING_TYPE */:
chainMessage =
'Consider adding a type to the parameter or use the @Inject decorator to specify an injection token.';
break;
}
const chain = {
messageText: `No suitable injection token for parameter '${param.name || index}' of class '${clazz.name.text}'.`,
category: ts__default["default"].DiagnosticCategory.Error,
code: 0,
next: [
{
messageText: chainMessage,
category: ts__default["default"].DiagnosticCategory.Message,
code: 0,
},
],
};
return new checker.FatalDiagnosticError(checker.ErrorCode.PARAM_MISSING_TOKEN, param.nameNode, chain, hints);
}
/**
* A mapping of component property and template binding property names, for example containing the
* inputs of a particular directive or component.
*
* A single component property has exactly one input/output annotation (and therefore one binding
* property name) associated with it, but the same binding property name may be shared across many
* component property names.
*
* Allows bidirectional querying of the mapping - looking up all inputs/outputs with a given
* property name, or mapping from a specific class property to its binding property name.
*/
class ClassPropertyMapping {
/**
* Mapping from class property names to the single `InputOrOutput` for that class property.
*/
forwardMap;
/**
* Mapping from property names to one or more `InputOrOutput`s which share that name.
*/
reverseMap;
constructor(forwardMap) {
this.forwardMap = forwardMap;
this.reverseMap = reverseMapFromForwardMap(forwardMap);
}
/**
* Construct a `ClassPropertyMapping` with no entries.
*/
static empty() {
return new ClassPropertyMapping(new Map());
}
/**
* Construct a `ClassPropertyMapping` from a primitive JS object which maps class property names
* to either binding property names or an array that contains both names, which is used in on-disk
* metadata formats (e.g. in .d.ts files).
*/
static fromMappedObject(obj) {
const forwardMap = new Map();
for (const classPropertyName of Object.keys(obj)) {
const value = obj[classPropertyName];
let inputOrOutput;
if (typeof value === 'string') {
inputOrOutput = {
classPropertyName,
bindingPropertyName: value,
// Inputs/outputs not captured via an explicit `InputOrOutput` mapping
// value are always considered non-signal. This is the string shorthand.
isSignal: false,
};
}
else {
inputOrOutput = value;
}
forwardMap.set(classPropertyName, inputOrOutput);
}
return new ClassPropertyMapping(forwardMap);
}
/**
* Merge two mappings into one, with class properties from `b` taking precedence over class
* properties from `a`.
*/
static merge(a, b) {
const forwardMap = new Map(a.forwardMap.entries());
for (const [classPropertyName, inputOrOutput] of b.forwardMap) {
forwardMap.set(classPropertyName, inputOrOutput);
}
return new ClassPropertyMapping(forwardMap);
}
/**
* All class property names mapped in this mapping.
*/
get classPropertyNames() {
return Array.from(this.forwardMap.keys());
}
/**
* All binding property names mapped in this mapping.
*/
get propertyNames() {
return Array.from(this.reverseMap.keys());
}
/**
* Check whether a mapping for the given property name exists.
*/
hasBindingPropertyName(propertyName) {
return this.reverseMap.has(propertyName);
}
/**
* Lookup all `InputOrOutput`s that use this `propertyName`.
*/
getByBindingPropertyName(propertyName) {
return this.reverseMap.has(propertyName) ? this.reverseMap.get(propertyName) : null;
}
/**
* Lookup the `InputOrOutput` associated with a `classPropertyName`.
*/
getByClassPropertyName(classPropertyName) {
return this.forwardMap.has(classPropertyName) ? this.forwardMap.get(classPropertyName) : null;
}
/**
* Convert this mapping to a primitive JS object which maps each class property directly to the
* binding property name associated with it.
*/
toDirectMappedObject() {
const obj = {};
for (const [classPropertyName, inputOrOutput] of this.forwardMap) {
obj[classPropertyName] = inputOrOutput.bindingPropertyName;
}
return obj;
}
/**
* Convert this mapping to a primitive JS object which maps each class property either to itself
* (for cases where the binding property name is the same) or to an array which contains both
* names if they differ.
*
* This object format is used when mappings are serialized (for example into .d.ts files).
* @param transform Function used to transform the values of the generated map.
*/
toJointMappedObject(transform) {
const obj = {};
for (const [classPropertyName, inputOrOutput] of this.forwardMap) {
obj[classPropertyName] = transform(inputOrOutput);
}
return obj;
}
/**
* Implement the iterator protocol and return entry objects which contain the class and binding
* property names (and are useful for destructuring).
*/
*[Symbol.iterator]() {
for (const inputOrOutput of this.forwardMap.values()) {
yield inputOrOutput;
}
}
}
function reverseMapFromForwardMap(forwardMap) {
const reverseMap = new Map();
for (const [_, inputOrOutput] of forwardMap) {
if (!reverseMap.has(inputOrOutput.bindingPropertyName)) {
reverseMap.set(inputOrOutput.bindingPropertyName, []);
}
reverseMap.get(inputOrOutput.bindingPropertyName).push(inputOrOutput);
}
return reverseMap;
}
/**
* A `MetadataReader` that can read metadata from `.d.ts` files, which have static Ivy properties
* from an upstream compilation already.
*/
class DtsMetadataReader {
checker;
reflector;
constructor(checker, reflector) {
this.checker = checker;
this.reflector = reflector;
}
/**
* Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts
* file, or in a .ts file with a handwritten definition).
*
* @param ref `Reference` to the class of interest, with the context of how it was obtained.
*/
getNgModuleMetadata(ref) {
const clazz = ref.node;
// This operation is explicitly not memoized, as it depends on `ref.ownedByModuleGuess`.
// TODO(alxhub): investigate caching of .d.ts module metadata.
const ngModuleDef = this.reflector
.getMembersOfClass(clazz)
.find((member) => member.name === 'ɵmod' && member.isStatic);
if (ngModuleDef === undefined) {
return null;
}
else if (
// Validate that the shape of the ngModuleDef type is correct.
ngModuleDef.type === null ||
!ts__default["default"].isTypeReferenceNode(ngModuleDef.type) ||
ngModuleDef.type.typeArguments === undefined ||
ngModuleDef.type.typeArguments.length !== 4) {
return null;
}
// Read the ModuleData out of the type arguments.
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
const declarations = checker.extractReferencesFromType(this.checker, declarationMetadata, ref.bestGuessOwningModule);
const exports = checker.extractReferencesFromType(this.checker, exportMetadata, ref.bestGuessOwningModule);
const imports = checker.extractReferencesFromType(this.checker, importMetadata, ref.bestGuessOwningModule);
// The module is considered poisoned if it's exports couldn't be
// resolved completely. This would make the module not necessarily
// usable for scope computation relying on this module; so we propagate
// this "incompleteness" information to the caller.
const isPoisoned = exports.isIncomplete;
return {
kind: checker.MetaKind.NgModule,
ref,
declarations: declarations.result,
isPoisoned,
exports: exports.result,
imports: imports.result,
schemas: [],
rawDeclarations: null,
rawImports: null,
rawExports: null,
decorator: null,
// NgModules declared outside the current compilation are assumed to contain providers, as it
// would be a non-breaking change for a library to introduce providers at any point.
mayDeclareProviders: true,
};
}
/**
* Read directive (or component) metadata from a referenced class in a .d.ts file.
*/
getDirectiveMetadata(ref) {
const clazz = ref.node;
const def = this.reflector
.getMembersOfClass(clazz)
.find((field) => field.isStatic && (field.name === 'ɵcmp' || field.name === 'ɵdir'));
if (def === undefined) {
// No definition could be found.
return null;
}
else if (def.type === null ||
!ts__default["default"].isTypeReferenceNode(def.type) ||
def.type.typeArguments === undefined ||
def.type.typeArguments.length < 2) {
// The type metadata was the wrong shape.
return null;
}
const isComponent = def.name === 'ɵcmp';
const ctorParams = this.reflector.getConstructorParameters(clazz);
// A directive is considered to be structural if:
// 1) it's a directive, not a component, and
// 2) it injects `TemplateRef`
const isStructural = !isComponent &&
ctorParams !== null &&
ctorParams.some((param) => {
return (param.typeValueReference.kind === 1 /* TypeValueReferenceKind.IMPORTED */ &&
param.typeValueReference.moduleName === '@angular/core' &&
param.typeValueReference.importedName === 'TemplateRef');
});
const ngContentSelectors = def.type.typeArguments.length > 6 ? checker.readStringArrayType(def.type.typeArguments[6]) : null;
// Note: the default value is still `false` here, because only legacy .d.ts files written before
// we had so many arguments use this default.
const isStandalone = def.type.typeArguments.length > 7 && (checker.readBooleanType(def.type.typeArguments[7]) ?? false);
const inputs = ClassPropertyMapping.fromMappedObject(readInputsType(def.type.typeArguments[3]));
const outputs = ClassPropertyMapping.fromMappedObject(checker.readMapType(def.type.typeArguments[4], checker.readStringType));
const hostDirectives = def.type.typeArguments.length > 8
? readHostDirectivesType(this.checker, def.type.typeArguments[8], ref.bestGuessOwningModule)
: null;
const isSignal = def.type.typeArguments.length > 9 && (checker.readBooleanType(def.type.typeArguments[9]) ?? false);
// At this point in time, the `.d.ts` may not be fully extractable when
// trying to resolve host directive types to their declarations.
// If this cannot be done completely, the metadata is incomplete and "poisoned".
const isPoisoned = hostDirectives !== null && hostDirectives?.isIncomplete;
return {
kind: checker.MetaKind.Directive,
matchSource: checker.MatchSource.Selector,
ref,
name: clazz.name.text,
isComponent,
selector: checker.readStringType(def.type.typeArguments[1]),
exportAs: checker.readStringArrayType(def.type.typeArguments[2]),
inputs,
outputs,
hostDirectives: hostDirectives?.result ?? null,
queries: checker.readStringArrayType(def.type.typeArguments[5]),
...checker.extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector),
baseClass: readBaseClass(clazz, this.checker, this.reflector),
isPoisoned,
isStructural,
animationTriggerNames: null,
ngContentSelectors,
isStandalone,
isSignal,
// We do not transfer information about inputs from class metadata
// via `.d.ts` declarations. This is fine because this metadata is
// currently only used for classes defined in source files. E.g. in migrations.
inputFieldNamesFromMetadataArray: null,
// Imports are tracked in metadata only for template type-checking purposes,
// so standalone components from .d.ts files don't have any.
imports: null,
rawImports: null,
deferredImports: null,
// The same goes for schemas.
schemas: null,
decorator: null,
// Assume that standalone components from .d.ts files may export providers.
assumedToExportProviders: isComponent && isStandalone,
// `preserveWhitespaces` isn't encoded in the .d.ts and is only
// used to increase the accuracy of a diagnostic.
preserveWhitespaces: false,
isExplicitlyDeferred: false,
};
}
/**
* Read pipe metadata from a referenced class in a .d.ts file.
*/
getPipeMetadata(ref) {
const def = this.reflector
.getMembersOfClass(ref.node)
.find((field) => field.isStatic && field.name === 'ɵpipe');
if (def === undefined) {
// No definition could be found.
return null;
}
else if (def.type === null ||
!ts__default["default"].isTypeReferenceNode(def.type) ||
def.type.typeArguments === undefined ||
def.type.typeArguments.length < 2) {
// The type metadata was the wrong shape.
return null;
}
const type = def.type.typeArguments[1];
if (!ts__default["default"].isLiteralTypeNode(type) || !ts__default["default"].isStringLiteral(type.literal)) {
// The type metadata was the wrong type.
return null;
}
const name = type.literal.text;
const isStandalone = def.type.typeArguments.length > 2 && (checker.readBooleanType(def.type.typeArguments[2]) ?? false);
return {
kind: checker.MetaKind.Pipe,
ref,
name,
nameExpr: null,
isStandalone,
decorator: null,
isExplicitlyDeferred: false,
};
}
}
function readInputsType(type) {
const inputsMap = {};
if (ts__default["default"].isTypeLiteralNode(type)) {
for (const member of type.members) {
if (!ts__default["default"].isPropertySignature(member) ||
member.type === undefined ||
member.name === undefined ||
(!ts__default["default"].isStringLiteral(member.name) && !ts__default["default"].isIdentifier(member.name))) {
continue;
}
const stringValue = checker.readStringType(member.type);
const classPropertyName = member.name.text;
// Before v16 the inputs map has the type of `{[field: string]: string}`.
// After v16 it has the type of `{[field: string]: {alias: string, required: boolean}}`.
if (stringValue != null) {
inputsMap[classPropertyName] = {
bindingPropertyName: stringValue,
classPropertyName,
required: false,
// Signal inputs were not supported pre v16- so those inputs are never signal based.
isSignal: false,
// Input transform are only tracked for locally-compiled directives. Directives coming
// from the .d.ts already have them included through `ngAcceptInputType` class members,
// or via the `InputSignal` type of the member.
transform: null,
};
}
else {
const config = checker.readMapType(member.type, (innerValue) => {
return checker.readStringType(innerValue) ?? checker.readBooleanType(innerValue);
});
inputsMap[classPropertyName] = {
classPropertyName,
bindingPropertyName: config.alias,
required: config.required,
isSignal: !!config.isSignal,
// Input transform are only tracked for locally-compiled directives. Directives coming
// from the .d.ts already have them included through `ngAcceptInputType` class members,
// or via the `InputSignal` type of the member.
transform: null,
};
}
}
}
return inputsMap;
}
function readBaseClass(clazz, checker$1, reflector) {
if (!checker.isNamedClassDeclaration(clazz)) {
// Technically this is an error in a .d.ts file, but for the purposes of finding the base class
// it's ignored.
return reflector.hasBaseClass(clazz) ? 'dynamic' : null;
}
if (clazz.heritageClauses !== undefined) {
for (const clause of clazz.heritageClauses) {
if (clause.token === ts__default["default"].SyntaxKind.ExtendsKeyword) {
const baseExpr = clause.types[0].expression;
let symbol = checker$1.getSymbolAtLocation(baseExpr);
if (symbol === undefined) {
return 'dynamic';
}
else if (symbol.flags & ts__default["default"].SymbolFlags.Alias) {
symbol = checker$1.getAliasedSymbol(symbol);
}
if (symbol.valueDeclaration !== undefined &&
checker.isNamedClassDeclaration(symbol.valueDeclaration)) {
return new checker.Reference(symbol.valueDeclaration);
}
else {
return 'dynamic';
}
}
}
}
return null;
}
function readHostDirectivesType(checker$1, type, bestGuessOwningModule) {
if (!ts__default["default"].isTupleTypeNode(type) || type.elements.length === 0) {
return null;
}
const result = [];
let isIncomplete = false;
for (const hostDirectiveType of type.elements) {
const { directive, inputs, outputs } = checker.readMapType(hostDirectiveType, (type) => type);
if (directive) {
if (!ts__default["default"].isTypeQueryNode(directive)) {
throw new Error(`Expected TypeQueryNode: ${checker.nodeDebugInfo(directive)}`);
}
const ref = checker.extraReferenceFromTypeQuery(checker$1, directive, type, bestGuessOwningModule);
if (ref === null) {
isIncomplete = true;
continue;
}
result.push({
directive: ref,
isForwardReference: false,
inputs: checker.readMapType(inputs, checker.readStringType),
outputs: checker.readMapType(outputs, checker.readStringType),
});
}
}
return result.length > 0 ? { result, isIncomplete } : null;
}
/**
* Given a reference to a directive, return a flattened version of its `DirectiveMeta` metadata
* which includes metadata from its entire inheritance chain.
*
* The returned `DirectiveMeta` will either have `baseClass: null` if the inheritance chain could be
* fully resolved, or `baseClass: 'dynamic'` if the inheritance chain could not be completely
* followed.
*/
function flattenInheritedDirectiveMetadata(reader, dir) {
const topMeta = reader.getDirectiveMetadata(dir);
if (topMeta === null) {
return null;
}
if (topMeta.baseClass === null) {
return topMeta;
}
const coercedInputFields = new Set();
const undeclaredInputFields = new Set();
const restrictedInputFields = new Set();
const stringLiteralInputFields = new Set();
let hostDirectives = null;
let isDynamic = false;
let inputs = ClassPropertyMapping.empty();
let outputs = ClassPropertyMapping.empty();
let isStructural = false;
const addMetadata = (meta) => {
if (meta.baseClass === 'dynamic') {
isDynamic = true;
}
else if (meta.baseClass !== null) {
const baseMeta = reader.getDirectiveMetadata(meta.baseClass);
if (baseMeta !== null) {
addMetadata(baseMeta);
}
else {
// Missing metadata for the base class means it's effectively dynamic.
isDynamic = true;
}
}
isStructural = isStructural || meta.isStructural;
inputs = ClassPropertyMapping.merge(inputs, meta.inputs);
outputs = ClassPropertyMapping.merge(outputs, meta.outputs);
for (const coercedInputField of meta.coercedInputFields) {
coercedInputFields.add(coercedInputField);
}
for (const undeclaredInputField of meta.undeclaredInputFields) {
undeclaredInputFields.add(undeclaredInputField);
}
for (const restrictedInputField of meta.restrictedInputFields) {
restrictedInputFields.add(restrictedInputField);
}
for (const field of meta.stringLiteralInputFields) {
stringLiteralInputFields.add(field);
}
if (meta.hostDirectives !== null && meta.hostDirectives.length > 0) {
hostDirectives ??= [];
hostDirectives.push(...meta.hostDirectives);
}
};
addMetadata(topMeta);
return {
...topMeta,
inputs,
outputs,
coercedInputFields,
undeclaredInputFields,
restrictedInputFields,
stringLiteralInputFields,
baseClass: isDynamic ? 'dynamic' : null,
isStructural,
hostDirectives,
};
}
/**
* A registry of directive, pipe, and module metadata for types defined in the current compilation
* unit, which supports both reading and registering.
*/
class LocalMetadataRegistry {
directives = new Map();
ngModules = new Map();
pipes = new Map();
getDirectiveMetadata(ref) {
return this.directives.has(ref.node) ? this.directives.get(ref.node) : null;
}
getNgModuleMetadata(ref) {
return this.ngModules.has(ref.node) ? this.ngModules.get(ref.node) : null;
}
getPipeMetadata(ref) {
return this.pipes.has(ref.node) ? this.pipes.get(ref.node) : null;
}
registerDirectiveMetadata(meta) {
this.directives.set(meta.ref.node, meta);
}
registerNgModuleMetadata(meta) {
this.ngModules.set(meta.ref.node, meta);
}
registerPipeMetadata(meta) {
this.pipes.set(meta.ref.node, meta);
}
getKnown(kind) {
switch (kind) {
case checker.MetaKind.Directive:
return Array.from(this.directives.values()).map((v) => v.ref.node);
case checker.MetaKind.Pipe:
return Array.from(this.pipes.values()).map((v) => v.ref.node);
case checker.MetaKind.NgModule:
return Array.from(this.ngModules.values()).map((v) => v.ref.node);
}
}
}
/**
* A `MetadataRegistry` which registers metadata with multiple delegate `MetadataRegistry`
* instances.
*/
class CompoundMetadataRegistry {
registries;
constructor(registries) {
this.registries = registries;
}
registerDirectiveMetadata(meta) {
for (const registry of this.registries) {
registry.registerDirectiveMetadata(meta);
}
}
registerNgModuleMetadata(meta) {
for (const registry of this.registries) {
registry.registerNgModuleMetadata(meta);
}
}
registerPipeMetadata(meta) {
for (const registry of this.registries) {
registry.registerPipeMetadata(meta);
}
}
}
/**
* Tracks the mapping between external template/style files and the component(s) which use them.
*
* This information is produced during analysis of the program and is used mainly to support
* external tooling, for which such a mapping is challenging to determine without compiler
* assistance.
*/
class ResourceRegistry {
externalTemplateToComponentsMap = new Map();
componentToTemplateMap = new Map();
componentToStylesMap = new Map();
externalStyleToComponentsMap = new Map();
getComponentsWithTemplate(template) {
if (!this.externalTemplateToComponentsMap.has(template)) {
return new Set();
}
return this.externalTemplateToComponentsMap.get(template);
}
registerResources(resources, component) {
if (resources.template !== null) {
this.registerTemplate(resources.template, component);
}
for (const style of resources.styles) {
this.registerStyle(style, component);
}
}
registerTemplate(templateResource, component) {
const { path } = templateResource;
if (path !== null) {
if (!this.externalTemplateToComponentsMap.has(path)) {
this.externalTemplateToComponentsMap.set(path, new Set());
}
this.externalTemplateToComponentsMap.get(path).add(component);
}
this.componentToTemplateMap.set(component, templateResource);
}
getTemplate(component) {
if (!this.componentToTemplateMap.has(component)) {
return null;
}
return this.componentToTemplateMap.get(component);
}
registerStyle(styleResource, component) {
const { path } = styleResource;
if (!this.componentToStylesMap.has(component)) {
this.componentToStylesMap.set(component, new Set());
}
if (path !== null) {
if (!this.externalStyleToComponentsMap.has(path)) {
this.externalStyleToComponentsMap.set(path, new Set());
}
this.externalStyleToComponentsMap.get(path).add(component);
}
this.componentToStylesMap.get(component).add(styleResource);
}
getStyles(component) {
if (!this.componentToStylesMap.has(component)) {
return new Set();
}
return this.componentToStylesMap.get(component);
}
getComponentsWithStyle(styleUrl) {
if (!this.externalStyleToComponentsMap.has(styleUrl)) {
return new Set();
}
return this.externalStyleToComponentsMap.get(styleUrl);
}
}
/**
* Determines whether types may or may not export providers to NgModules, by transitively walking
* the NgModule & standalone import graph.
*/
class ExportedProviderStatusResolver {
metaReader;
/**
* `ClassDeclaration`s that we are in the process of determining the provider status for.
*
* This is used to detect cycles in the import graph and avoid getting stuck in them.
*/
calculating = new Set();
constructor(metaReader) {
this.metaReader = metaReader;
}
/**
* Determines whether `ref` may or may not export providers to NgModules which import it.
*
* NgModules export providers if any are declared, and standalone components export providers from
* their `imports` array (if any).
*
* If `true`, then `ref` should be assumed to export providers. In practice, this could mean
* either that `ref` is a local type that we _know_ exports providers, or it's imported from a
* .d.ts library and is declared in a way where the compiler cannot prove that it doesn't.
*
* If `false`, then `ref` is guaranteed not to export providers.
*
* @param `ref` the class for which the provider status should be determined
* @param `dependencyCallback` a callback that, if provided, will be called for every type
* which is used in the determination of provider status for `ref`
* @returns `true` if `ref` should be assumed to export providers, or `false` if the compiler can
* prove that it does not
*/
mayExportProviders(ref, dependencyCallback) {
if (this.calculating.has(ref.node)) {
// For cycles, we treat the cyclic edge as not having providers.
return false;
}
this.calculating.add(ref.node);
if (dependencyCallback !== undefined) {
dependencyCallback(ref);
}
try {
const dirMeta = this.metaReader.getDirectiveMetadata(ref);
if (dirMeta !== null) {
if (!dirMeta.isComponent || !dirMeta.isStandalone) {
return false;
}
if (dirMeta.assumedToExportProviders) {
return true;
}
// If one of the imports contains providers, then so does this component.
return (dirMeta.imports ?? []).some((importRef) => this.mayExportProviders(importRef, dependencyCallback));
}
const pipeMeta = this.metaReader.getPipeMetadata(ref);
if (pipeMeta !== null) {
return false;
}
const ngModuleMeta = this.metaReader.getNgModuleMetadata(ref);
if (ngModuleMeta !== null) {
if (ngModuleMeta.mayDeclareProviders) {
return true;
}
// If one of the NgModule's imports may contain providers, then so does this NgModule.
return ngModuleMeta.imports.some((importRef) => this.mayExportProviders(importRef, dependencyCallback));
}
return false;
}
finally {
this.calculating.delete(ref.node);
}
}
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
const EMPTY_ARRAY$1 = [];
/** Resolves the host directives of a directive to a flat array of matches. */
class HostDirectivesResolver {
metaReader;
cache = new Map();
constructor(metaReader) {
this.metaReader = metaReader;
}
/** Resolves all of the host directives that apply to a directive. */
resolve(metadata) {
if (this.cache.has(metadata.ref.node)) {
return this.cache.get(metadata.ref.node);
}
const results = metadata.hostDirectives && metadata.hostDirectives.length > 0
? this.walkHostDirectives(metadata.hostDirectives, [])
: EMPTY_ARRAY$1;
this.cache.set(metadata.ref.node, results);
return results;
}
/**
* Traverses all of the host directive chains and produces a flat array of
* directive metadata representing the host directives that apply to the host.
*/
walkHostDirectives(directives, results) {
for (const current of directives) {
if (!checker.isHostDirectiveMetaForGlobalMode(current)) {
throw new Error('Impossible state: resolving code path in local compilation mode');
}
const hostMeta = flattenInheritedDirectiveMetadata(this.metaReader, current.directive);
// This case has been checked for already and produces a diagnostic
if (hostMeta === null) {
continue;
}
if (hostMeta.hostDirectives) {
this.walkHostDirectives(hostMeta.hostDirectives, results);
}
results.push({
...hostMeta,
matchSource: checker.MatchSource.HostDirective,
inputs: ClassPropertyMapping.fromMappedObject(this.filterMappings(hostMeta.inputs, current.inputs, resolveInput)),
outputs: ClassPropertyMapping.fromMappedObject(this.filterMappings(hostMeta.outputs, current.outputs, resolveOutput)),
});
}
return results;
}
/**
* Filters the class property mappings so that only the allowed ones are present.
* @param source Property mappings that should be filtered.
* @param allowedProperties Property mappings that are allowed in the final results.
* @param valueResolver Function used to resolve the value that is assigned to the final mapping.
*/
filterMappings(source, allowedProperties, valueResolver) {
const result = {};
if (allowedProperties !== null) {
for (const publicName in allowedProperties) {
if (allowedProperties.hasOwnProperty(publicName)) {
const bindings = source.getByBindingPropertyName(publicName);
if (bindings !== null) {
for (const binding of bindings) {
result[binding.classPropertyName] = valueResolver(allowedProperties[publicName], binding);
}
}
}
}
}
return result;
}
}
function resolveInput(bindingName, binding) {
return {
bindingPropertyName: bindingName,
classPropertyName: binding.classPropertyName,
required: binding.required,
transform: binding.transform,
isSignal: binding.isSignal,
};
}
function resolveOutput(bindingName) {
return bindingName;
}
/**
* Derives a type representation from a resolved value to be reported in a diagnostic.
*
* @param value The resolved value for which a type representation should be derived.
* @param maxDepth The maximum nesting depth of objects and arrays, defaults to 1 level.
*/
function describeResolvedType(value, maxDepth = 1) {
if (value === null) {
return 'null';
}
else if (value === undefined) {
return 'undefined';
}
else if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') {
return typeof value;
}
else if (value instanceof Map) {
if (maxDepth === 0) {
return 'object';
}
const entries = Array.from(value.entries()).map(([key, v]) => {
return `${quoteKey(key)}: ${describeResolvedType(v, maxDepth - 1)}`;
});
return entries.length > 0 ? `{ ${entries.join('; ')} }` : '{}';
}
else if (value instanceof checker.ResolvedModule) {
return '(module)';
}
else if (value instanceof checker.EnumValue) {
return value.enumRef.debugName ?? '(anonymous)';
}
else if (value instanceof checker.Reference) {
return value.debugName ?? '(anonymous)';
}
else if (Array.isArray(value)) {
if (maxDepth === 0) {
return 'Array';
}
return `[${value.map((v) => describeResolvedType(v, maxDepth - 1)).join(', ')}]`;
}
else if (value instanceof checker.DynamicValue) {
return '(not statically analyzable)';
}
else if (value instanceof checker.KnownFn) {
return 'Function';
}
else {
return 'unknown';
}
}
function quoteKey(key) {
if (/^[a-z0-9_]+$/i.test(key)) {
return key;
}
else {
return `'${key.replace(/'/g, "\\'")}'`;
}
}
/**
* Creates an array of related information diagnostics for a `DynamicValue` that describe the trace
* of why an expression was evaluated as dynamic.
*
* @param node The node for which a `ts.Diagnostic` is to be created with the trace.
* @param value The dynamic value for which a trace should be created.
*/
function traceDynamicValue(node, value) {
return value.accept(new TraceDynamicValueVisitor(node));
}
class TraceDynamicValueVisitor {
node;
currentContainerNode = null;
constructor(node) {
this.node = node;
}
visitDynamicInput(value) {
const trace = value.reason.accept(this);
if (this.shouldTrace(value.node)) {
const info = checker.makeRelatedInformation(value.node, 'Unable to evaluate this expression statically.');
trace.unshift(info);
}
return trace;
}
visitSyntheticInput(value) {
return [checker.makeRelatedInformation(value.node, 'Unable to evaluate this expression further.')];
}
visitDynamicString(value) {
return [
checker.makeRelatedInformation(value.node, 'A string value could not be determined statically.'),
];
}
visitExternalReference(value) {
const name = value.reason.debugName;
const description = name !== null ? `'${name}'` : 'an anonymous declaration';
return [
checker.makeRelatedInformation(value.node, `A value for ${description} cannot be determined statically, as it is an external declaration.`),
];
}
visitComplexFunctionCall(value) {
return [
checker.makeRelatedInformation(value.node, 'Unable to evaluate function call of complex function. A function must have exactly one return statement.'),
checker.makeRelatedInformation(value.reason.node, 'Function is declared here.'),
];
}
visitInvalidExpressionType(value) {
return [checker.makeRelatedInformation(value.node, 'Unable to evaluate an invalid expression.')];
}
visitUnknown(value) {
return [checker.makeRelatedInformation(value.node, 'Unable to evaluate statically.')];
}
visitUnknownIdentifier(value) {
return [checker.makeRelatedInformation(value.node, 'Unknown reference.')];
}
visitDynamicType(value) {
return [checker.makeRelatedInformation(value.node, 'Dynamic type.')];
}
visitUnsupportedSyntax(value) {
return [checker.makeRelatedInformation(value.node, 'This syntax is not supported.')];
}
/**
* Determines whether the dynamic value reported for the node should be traced, i.e. if it is not
* part of the container for which the most recent trace was created.
*/
shouldTrace(node) {
if (node === this.node) {
// Do not include a dynamic value for the origin node, as the main diagnostic is already
// reported on that node.
return false;
}
const container = getContainerNode(node);
if (container === this.currentContainerNode) {
// The node is part of the same container as the previous trace entry, so this dynamic value
// should not become part of the trace.
return false;
}
this.currentContainerNode = container;
return true;
}
}
/**
* Determines the closest parent node that is to be considered as container, which is used to reduce
* the granularity of tracing the dynamic values to a single entry per container. Currently, full
* statements and destructuring patterns are considered as container.
*/
function getContainerNode(node) {
let currentNode = node;
while (currentNode !== undefined) {
switch (currentNode.kind) {
case ts__default["default"].SyntaxKind.ExpressionStatement:
case ts__default["default"].SyntaxKind.VariableStatement:
case ts__default["default"].SyntaxKind.ReturnStatement:
case ts__default["default"].SyntaxKind.IfStatement:
case ts__default["default"].SyntaxKind.SwitchStatement:
case ts__default["default"].SyntaxKind.DoStatement:
case ts__default["default"].SyntaxKind.WhileStatement:
case ts__default["default"].SyntaxKind.ForStatement:
case ts__default["default"].SyntaxKind.ForInStatement:
case ts__default["default"].SyntaxKind.ForOfStatement:
case ts__default["default"].SyntaxKind.ContinueStatement:
case ts__default["default"].SyntaxKind.BreakStatement:
case ts__default["default"].SyntaxKind.ThrowStatement:
case ts__default["default"].SyntaxKind.ObjectBindingPattern:
case ts__default["default"].SyntaxKind.ArrayBindingPattern:
return currentNode;
}
currentNode = currentNode.parent;
}
return node.getSourceFile();
}
class PartialEvaluator {
host;
checker;
dependencyTracker;
constructor(host, checker, dependencyTracker) {
this.host = host;
this.checker = checker;
this.dependencyTracker = dependencyTracker;
}
evaluate(expr, foreignFunctionResolver) {
const interpreter = new checker.StaticInterpreter(this.host, this.checker, this.dependencyTracker);
const sourceFile = expr.getSourceFile();
return interpreter.visit(expr, {
originatingFile: sourceFile,
absoluteModuleName: null,
resolutionContext: sourceFile.fileName,
scope: new Map(),
foreignFunctionResolver,
});
}
}
function aliasTransformFactory(exportStatements) {
return () => {
return (file) => {
if (ts__default["default"].isBundle(file) || !exportStatements.has(file.fileName)) {
return file;
}
const statements = [...file.statements];
exportStatements.get(file.fileName).forEach(([moduleName, symbolName], aliasName) => {
const stmt = ts__default["default"].factory.createExportDeclaration(
/* modifiers */ undefined,
/* isTypeOnly */ false,
/* exportClause */ ts__default["default"].factory.createNamedExports([
ts__default["default"].factory.createExportSpecifier(false, symbolName, aliasName),
]),
/* moduleSpecifier */ ts__default["default"].factory.createStringLiteral(moduleName));
statements.push(stmt);
});
return ts__default["default"].factory.updateSourceFile(file, statements);
};
};
}
///
function mark() {
return process.hrtime();
}
function timeSinceInMicros(mark) {
const delta = process.hrtime(mark);
return delta[0] * 1000000 + Math.floor(delta[1] / 1000);
}
///
/**
* A `PerfRecorder` that actively tracks performance statistics.
*/
class ActivePerfRecorder {
zeroTime;
counters;
phaseTime;
bytes;
currentPhase = checker.PerfPhase.Unaccounted;
currentPhaseEntered;
/**
* Creates an `ActivePerfRecorder` with its zero point set to the current time.
*/
static zeroedToNow() {
return new ActivePerfRecorder(mark());
}
constructor(zeroTime) {
this.zeroTime = zeroTime;
this.currentPhaseEntered = this.zeroTime;
this.counters = Array(checker.PerfEvent.LAST).fill(0);
this.phaseTime = Array(checker.PerfPhase.LAST).fill(0);
this.bytes = Array(checker.PerfCheckpoint.LAST).fill(0);
// Take an initial memory snapshot before any other compilation work begins.
this.memory(checker.PerfCheckpoint.Initial);
}
reset() {
this.counters = Array(checker.PerfEvent.LAST).fill(0);
this.phaseTime = Array(checker.PerfPhase.LAST).fill(0);
this.bytes = Array(checker.PerfCheckpoint.LAST).fill(0);
this.zeroTime = mark();
this.currentPhase = checker.PerfPhase.Unaccounted;
this.currentPhaseEntered = this.zeroTime;
}
memory(after) {
this.bytes[after] = process.memoryUsage().heapUsed;
}
phase(phase) {
const previous = this.currentPhase;
this.phaseTime[this.currentPhase] += timeSinceInMicros(this.currentPhaseEntered);
this.currentPhase = phase;
this.currentPhaseEntered = mark();
return previous;
}
inPhase(phase, fn) {
const previousPhase = this.phase(phase);
try {
return fn();
}
finally {
this.phase(previousPhase);
}
}
eventCount(counter, incrementBy = 1) {
this.counters[counter] += incrementBy;
}
/**
* Return the current performance metrics as a serializable object.
*/
finalize() {
// Track the last segment of time spent in `this.currentPhase` in the time array.
this.phase(checker.PerfPhase.Unaccounted);
const results = {
events: {},
phases: {},
memory: {},
};
for (let i = 0; i < this.phaseTime.length; i++) {
if (this.phaseTime[i] > 0) {
results.phases[checker.PerfPhase[i]] = this.phaseTime[i];
}
}
for (let i = 0; i < this.phaseTime.length; i++) {
if (this.counters[i] > 0) {
results.events[checker.PerfEvent[i]] = this.counters[i];
}
}
for (let i = 0; i < this.bytes.length; i++) {
if (this.bytes[i] > 0) {
results.memory[checker.PerfCheckpoint[i]] = this.bytes[i];
}
}
return results;
}
}
/**
* A `PerfRecorder` that delegates to a target `PerfRecorder` which can be updated later.
*
* `DelegatingPerfRecorder` is useful when a compiler class that needs a `PerfRecorder` can outlive
* the current compilation. This is true for most compiler classes as resource-only changes reuse
* the same `NgCompiler` for a new compilation.
*/
class DelegatingPerfRecorder {
target;
constructor(target) {
this.target = target;
}
eventCount(counter, incrementBy) {
this.target.eventCount(counter, incrementBy);
}
phase(phase) {
return this.target.phase(phase);
}
inPhase(phase, fn) {
// Note: this doesn't delegate to `this.target.inPhase` but instead is implemented manually here
// to avoid adding an additional frame of noise to the stack when debugging.
const previousPhase = this.target.phase(phase);
try {
return fn();
}
finally {
this.target.phase(previousPhase);
}
}
memory(after) {
this.target.memory(after);
}
reset() {
this.target.reset();
}
}
/**
* The heart of Angular compilation.
*
* The `TraitCompiler` is responsible for processing all classes in the program. Any time a
* `DecoratorHandler` matches a class, a "trait" is created to represent that Angular aspect of the
* class (such as the class having a component definition).
*
* The `TraitCompiler` transitions each trait through the various phases of compilation, culminating
* in the production of `CompileResult`s instructing the compiler to apply various mutations to the
* class (like adding fields or type declarations).
*/
class TraitCompiler {
handlers;
reflector;
perf;
incrementalBuild;
compileNonExportedClasses;
compilationMode;
dtsTransforms;
semanticDepGraphUpdater;
sourceFileTypeIdentifier;
/**
* Maps class declarations to their `ClassRecord`, which tracks the Ivy traits being applied to
* those classes.
*/
classes = new Map();
/**
* Maps source files to any class declaration(s) within them which have been discovered to contain
* Ivy traits.
*/
fileToClasses = new Map();
/**
* Tracks which source files have been analyzed but did not contain any traits. This set allows
* the compiler to skip analyzing these files in an incremental rebuild.
*/
filesWithoutTraits = new Set();
reexportMap = new Map();
handlersByName = new Map();
constructor(handlers, reflector, perf, incrementalBuild, compileNonExportedClasses, compilationMode, dtsTransforms, semanticDepGraphUpdater, sourceFileTypeIdentifier) {
this.handlers = handlers;
this.reflector = reflector;
this.perf = perf;
this.incrementalBuild = incrementalBuild;
this.compileNonExportedClasses = compileNonExportedClasses;
this.compilationMode = compilationMode;
this.dtsTransforms = dtsTransforms;
this.semanticDepGraphUpdater = semanticDepGraphUpdater;
this.sourceFileTypeIdentifier = sourceFileTypeIdentifier;
for (const handler of handlers) {
this.handlersByName.set(handler.name, handler);
}
}
analyzeSync(sf) {
this.analyze(sf, false);
}
analyzeAsync(sf) {
return this.analyze(sf, true);
}
analyze(sf, preanalyze) {
// We shouldn't analyze declaration, shim, or resource files.
if (sf.isDeclarationFile ||
this.sourceFileTypeIdentifier.isShim(sf) ||
this.sourceFileTypeIdentifier.isResource(sf)) {
return undefined;
}
// analyze() really wants to return `Promise|void`, but TypeScript cannot narrow a return
// type of 'void', so `undefined` is used instead.
const promises = [];
// Local compilation does not support incremental build.
const priorWork = this.compilationMode !== checker.CompilationMode.LOCAL
? this.incrementalBuild.priorAnalysisFor(sf)
: null;
if (priorWork !== null) {
this.perf.eventCount(checker.PerfEvent.SourceFileReuseAnalysis);
if (priorWork.length > 0) {
for (const priorRecord of priorWork) {
this.adopt(priorRecord);
}
this.perf.eventCount(checker.PerfEvent.TraitReuseAnalysis, priorWork.length);
}
else {
this.filesWithoutTraits.add(sf);
}
// Skip the rest of analysis, as this file's prior traits are being reused.
return;
}
const visit = (node) => {
if (this.reflector.isClass(node)) {
this.analyzeClass(node, preanalyze ? promises : null);
}
ts__default["default"].forEachChild(node, visit);
};
visit(sf);
if (!this.fileToClasses.has(sf)) {
// If no traits were detected in the source file we record the source file itself to not have
// any traits, such that analysis of the source file can be skipped during incremental
// rebuilds.
this.filesWithoutTraits.add(sf);
}
if (preanalyze && promises.length > 0) {
return Promise.all(promises).then(() => undefined);
}
else {
return undefined;
}
}
recordFor(clazz) {
if (this.classes.has(clazz)) {
return this.classes.get(clazz);
}
else {
return null;
}
}
getAnalyzedRecords() {
const result = new Map();
for (const [sf, classes] of this.fileToClasses) {
const records = [];
for (const clazz of classes) {
records.push(this.classes.get(clazz));
}
result.set(sf, records);
}
for (const sf of this.filesWithoutTraits) {
result.set(sf, []);
}
return result;
}
/**
* Import a `ClassRecord` from a previous compilation (only to be used in global compilation
* modes)
*
* Traits from the `ClassRecord` have accurate metadata, but the `handler` is from the old program
* and needs to be updated (matching is done by name). A new pending trait is created and then
* transitioned to analyzed using the previous analysis. If the trait is in the errored state,
* instead the errors are copied over.
*/
adopt(priorRecord) {
const record = {
hasPrimaryHandler: priorRecord.hasPrimaryHandler,
hasWeakHandlers: priorRecord.hasWeakHandlers,
metaDiagnostics: priorRecord.metaDiagnostics,
node: priorRecord.node,
traits: [],
};
for (const priorTrait of priorRecord.traits) {
const handler = this.handlersByName.get(priorTrait.handler.name);
let trait = checker.Trait.pending(handler, priorTrait.detected);
if (priorTrait.state === checker.TraitState.Analyzed || priorTrait.state === checker.TraitState.Resolved) {
const symbol = this.makeSymbolForTrait(handler, record.node, priorTrait.analysis);
trait = trait.toAnalyzed(priorTrait.analysis, priorTrait.analysisDiagnostics, symbol);
if (trait.analysis !== null && trait.handler.register !== undefined) {
trait.handler.register(record.node, trait.analysis);
}
}
else if (priorTrait.state === checker.TraitState.Skipped) {
trait = trait.toSkipped();
}
record.traits.push(trait);
}
this.classes.set(record.node, record);
const sf = record.node.getSourceFile();
if (!this.fileToClasses.has(sf)) {
this.fileToClasses.set(sf, new Set());
}
this.fileToClasses.get(sf).add(record.node);
}
scanClassForTraits(clazz) {
if (!this.compileNonExportedClasses && !this.reflector.isStaticallyExported(clazz)) {
return null;
}
const decorators = this.reflector.getDecoratorsOfDeclaration(clazz);
return this.detectTraits(clazz, decorators);
}
detectTraits(clazz, decorators) {
let record = this.recordFor(clazz);
let foundTraits = [];
// A set to track the non-Angular decorators in local compilation mode. An error will be issued
// if non-Angular decorators is found in local compilation mode.
const nonNgDecoratorsInLocalMode = this.compilationMode === checker.CompilationMode.LOCAL ? new Set(decorators) : null;
for (const handler of this.handlers) {
const result = handler.detect(clazz, decorators);
if (result === undefined) {
continue;
}
if (nonNgDecoratorsInLocalMode !== null && result.decorator !== null) {
nonNgDecoratorsInLocalMode.delete(result.decorator);
}
const isPrimaryHandler = handler.precedence === checker.HandlerPrecedence.PRIMARY;
const isWeakHandler = handler.precedence === checker.HandlerPrecedence.WEAK;
const trait = checker.Trait.pending(handler, result);
foundTraits.push(trait);
if (record === null) {
// This is the first handler to match this class. This path is a fast path through which
// most classes will flow.
record = {
node: clazz,
traits: [trait],
metaDiagnostics: null,
hasPrimaryHandler: isPrimaryHandler,
hasWeakHandlers: isWeakHandler,
};
this.classes.set(clazz, record);
const sf = clazz.getSourceFile();
if (!this.fileToClasses.has(sf)) {
this.fileToClasses.set(sf, new Set());
}
this.fileToClasses.get(sf).add(clazz);
}
else {
// This is at least the second handler to match this class. This is a slower path that some
// classes will go through, which validates that the set of decorators applied to the class
// is valid.
// Validate according to rules as follows:
//
// * WEAK handlers are removed if a non-WEAK handler matches.
// * Only one PRIMARY handler can match at a time. Any other PRIMARY handler matching a
// class with an existing PRIMARY handler is an error.
if (!isWeakHandler && record.hasWeakHandlers) {
// The current handler is not a WEAK handler, but the class has other WEAK handlers.
// Remove them.
record.traits = record.traits.filter((field) => field.handler.precedence !== checker.HandlerPrecedence.WEAK);
record.hasWeakHandlers = false;
}
else if (isWeakHandler && !record.hasWeakHandlers) {
// The current handler is a WEAK handler, but the class has non-WEAK handlers already.
// Drop the current one.
continue;
}
if (isPrimaryHandler && record.hasPrimaryHandler) {
// The class already has a PRIMARY handler, and another one just matched.
record.metaDiagnostics = [
{
category: ts__default["default"].DiagnosticCategory.Error,
code: Number('-99' + checker.ErrorCode.DECORATOR_COLLISION),
file: checker.getSourceFile(clazz),
start: clazz.getStart(undefined, false),
length: clazz.getWidth(),
messageText: 'Two incompatible decorators on class',
},
];
record.traits = foundTraits = [];
break;
}
// Otherwise, it's safe to accept the multiple decorators here. Update some of the metadata
// regarding this class.
record.traits.push(trait);
record.hasPrimaryHandler = record.hasPrimaryHandler || isPrimaryHandler;
}
}
if (nonNgDecoratorsInLocalMode !== null &&
nonNgDecoratorsInLocalMode.size > 0 &&
record !== null &&
record.metaDiagnostics === null) {
// Custom decorators found in local compilation mode! In this mode we don't support custom
// decorators yet. But will eventually do (b/320536434). For now a temporary error is thrown.
record.metaDiagnostics = [...nonNgDecoratorsInLocalMode].map((decorator) => ({
category: ts__default["default"].DiagnosticCategory.Error,
code: Number('-99' + checker.ErrorCode.DECORATOR_UNEXPECTED),
file: checker.getSourceFile(clazz),
start: decorator.node.getStart(),
length: decorator.node.getWidth(),
messageText: 'In local compilation mode, Angular does not support custom decorators. Ensure all class decorators are from Angular.',
}));
record.traits = foundTraits = [];
}
return foundTraits.length > 0 ? foundTraits : null;
}
makeSymbolForTrait(handler, decl, analysis) {
if (analysis === null) {
return null;
}
const symbol = handler.symbol(decl, analysis);
if (symbol !== null && this.semanticDepGraphUpdater !== null) {
const isPrimary = handler.precedence === checker.HandlerPrecedence.PRIMARY;
if (!isPrimary) {
throw new Error(`AssertionError: ${handler.name} returned a symbol but is not a primary handler.`);
}
this.semanticDepGraphUpdater.registerSymbol(symbol);
}
return symbol;
}
analyzeClass(clazz, preanalyzeQueue) {
const traits = this.scanClassForTraits(clazz);
if (traits === null) {
// There are no Ivy traits on the class, so it can safely be skipped.
return;
}
for (const trait of traits) {
const analyze = () => this.analyzeTrait(clazz, trait);
let preanalysis = null;
if (preanalyzeQueue !== null && trait.handler.preanalyze !== undefined) {
// Attempt to run preanalysis. This could fail with a `FatalDiagnosticError`; catch it if it
// does.
try {
preanalysis = trait.handler.preanalyze(clazz, trait.detected.metadata) || null;
}
catch (err) {
if (err instanceof checker.FatalDiagnosticError) {
trait.toAnalyzed(null, [err.toDiagnostic()], null);
return;
}
else {
throw err;
}
}
}
if (preanalysis !== null) {
preanalyzeQueue.push(preanalysis.then(analyze));
}
else {
analyze();
}
}
}
analyzeTrait(clazz, trait) {
if (trait.state !== checker.TraitState.Pending) {
throw new Error(`Attempt to analyze trait of ${clazz.name.text} in state ${checker.TraitState[trait.state]} (expected DETECTED)`);
}
this.perf.eventCount(checker.PerfEvent.TraitAnalyze);
// Attempt analysis. This could fail with a `FatalDiagnosticError`; catch it if it does.
let result;
try {
result = trait.handler.analyze(clazz, trait.detected.metadata);
}
catch (err) {
if (err instanceof checker.FatalDiagnosticError) {
trait.toAnalyzed(null, [err.toDiagnostic()], null);
return;
}
else {
throw err;
}
}
const symbol = this.makeSymbolForTrait(trait.handler, clazz, result.analysis ?? null);
if (result.analysis !== undefined && trait.handler.register !== undefined) {
trait.handler.register(clazz, result.analysis);
}
trait = trait.toAnalyzed(result.analysis ?? null, result.diagnostics ?? null, symbol);
}
resolve() {
const classes = this.classes.keys();
for (const clazz of classes) {
const record = this.classes.get(clazz);
for (let trait of record.traits) {
const handler = trait.handler;
switch (trait.state) {
case checker.TraitState.Skipped:
continue;
case checker.TraitState.Pending:
throw new Error(`Resolving a trait that hasn't been analyzed: ${clazz.name.text} / ${trait.handler.name}`);
case checker.TraitState.Resolved:
throw new Error(`Resolving an already resolved trait`);
}
if (trait.analysis === null) {
// No analysis results, cannot further process this trait.
continue;
}
if (handler.resolve === undefined) {
// No resolution of this trait needed - it's considered successful by default.
trait = trait.toResolved(null, null);
continue;
}
let result;
try {
result = handler.resolve(clazz, trait.analysis, trait.symbol);
}
catch (err) {
if (err instanceof checker.FatalDiagnosticError) {
trait = trait.toResolved(null, [err.toDiagnostic()]);
continue;
}
else {
throw err;
}
}
trait = trait.toResolved(result.data ?? null, result.diagnostics ?? null);
if (result.reexports !== undefined) {
const fileName = clazz.getSourceFile().fileName;
if (!this.reexportMap.has(fileName)) {
this.reexportMap.set(fileName, new Map());
}
const fileReexports = this.reexportMap.get(fileName);
for (const reexport of result.reexports) {
fileReexports.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]);
}
}
}
}
}
/**
* Generate type-checking code into the `TypeCheckContext` for any components within the given
* `ts.SourceFile`.
*/
typeCheck(sf, ctx) {
if (!this.fileToClasses.has(sf) || this.compilationMode === checker.CompilationMode.LOCAL) {
return;
}
for (const clazz of this.fileToClasses.get(sf)) {
const record = this.classes.get(clazz);
for (const trait of record.traits) {
if (trait.state !== checker.TraitState.Resolved) {
continue;
}
else if (trait.handler.typeCheck === undefined) {
continue;
}
if (trait.resolution !== null) {
trait.handler.typeCheck(ctx, clazz, trait.analysis, trait.resolution);
}
}
}
}
runAdditionalChecks(sf, check) {
if (this.compilationMode === checker.CompilationMode.LOCAL) {
return [];
}
const classes = this.fileToClasses.get(sf);
if (classes === undefined) {
return [];
}
const diagnostics = [];
for (const clazz of classes) {
if (!checker.isNamedClassDeclaration(clazz)) {
continue;
}
const record = this.classes.get(clazz);
for (const trait of record.traits) {
const result = check(clazz, trait.handler);
if (result !== null) {
diagnostics.push(...result);
}
}
}
return diagnostics;
}
index(ctx) {
for (const clazz of this.classes.keys()) {
const record = this.classes.get(clazz);
for (const trait of record.traits) {
if (trait.state !== checker.TraitState.Resolved) {
// Skip traits that haven't been resolved successfully.
continue;
}
else if (trait.handler.index === undefined) {
// Skip traits that don't affect indexing.
continue;
}
if (trait.resolution !== null) {
trait.handler.index(ctx, clazz, trait.analysis, trait.resolution);
}
}
}
}
xi18n(bundle) {
for (const clazz of this.classes.keys()) {
const record = this.classes.get(clazz);
for (const trait of record.traits) {
if (trait.state !== checker.TraitState.Analyzed && trait.state !== checker.TraitState.Resolved) {
// Skip traits that haven't been analyzed successfully.
continue;
}
else if (trait.handler.xi18n === undefined) {
// Skip traits that don't support xi18n.
continue;
}
if (trait.analysis !== null) {
trait.handler.xi18n(bundle, clazz, trait.analysis);
}
}
}
}
updateResources(clazz) {
// Local compilation does not support incremental
if (this.compilationMode === checker.CompilationMode.LOCAL ||
!this.reflector.isClass(clazz) ||
!this.classes.has(clazz)) {
return;
}
const record = this.classes.get(clazz);
for (const trait of record.traits) {
if (trait.state !== checker.TraitState.Resolved || trait.handler.updateResources === undefined) {
continue;
}
trait.handler.updateResources(clazz, trait.analysis, trait.resolution);
}
}
compile(clazz, constantPool) {
const original = ts__default["default"].getOriginalNode(clazz);
if (!this.reflector.isClass(clazz) ||
!this.reflector.isClass(original) ||
!this.classes.has(original)) {
return null;
}
const record = this.classes.get(original);
let res = [];
for (const trait of record.traits) {
let compileRes;
if (trait.state !== checker.TraitState.Resolved ||
containsErrors(trait.analysisDiagnostics) ||
containsErrors(trait.resolveDiagnostics)) {
// Cannot compile a trait that is not resolved, or had any errors in its declaration.
continue;
}
if (this.compilationMode === checker.CompilationMode.LOCAL) {
// `trait.analysis` is non-null asserted here because TypeScript does not recognize that
// `Readonly` is nullable (as `unknown` itself is nullable) due to the way that
// `Readonly` works.
compileRes = trait.handler.compileLocal(clazz, trait.analysis, trait.resolution, constantPool);
}
else {
// `trait.resolution` is non-null asserted below because TypeScript does not recognize that
// `Readonly` is nullable (as `unknown` itself is nullable) due to the way that
// `Readonly` works.
if (this.compilationMode === checker.CompilationMode.PARTIAL &&
trait.handler.compilePartial !== undefined) {
compileRes = trait.handler.compilePartial(clazz, trait.analysis, trait.resolution);
}
else {
compileRes = trait.handler.compileFull(clazz, trait.analysis, trait.resolution, constantPool);
}
}
const compileMatchRes = compileRes;
if (Array.isArray(compileMatchRes)) {
for (const result of compileMatchRes) {
if (!res.some((r) => r.name === result.name)) {
res.push(result);
}
}
}
else if (!res.some((result) => result.name === compileMatchRes.name)) {
res.push(compileMatchRes);
}
}
// Look up the .d.ts transformer for the input file and record that at least one field was
// generated, which will allow the .d.ts to be transformed later.
this.dtsTransforms
.getIvyDeclarationTransform(original.getSourceFile())
.addFields(original, res);
// Return the instruction to the transformer so the fields will be added.
return res.length > 0 ? res : null;
}
compileHmrUpdateCallback(clazz) {
const original = ts__default["default"].getOriginalNode(clazz);
if (!this.reflector.isClass(clazz) ||
!this.reflector.isClass(original) ||
!this.classes.has(original)) {
return null;
}
const record = this.classes.get(original);
for (const trait of record.traits) {
// Cannot compile a trait that is not resolved, or had any errors in its declaration.
if (trait.state === checker.TraitState.Resolved &&
trait.handler.compileHmrUpdateDeclaration !== undefined &&
!containsErrors(trait.analysisDiagnostics) &&
!containsErrors(trait.resolveDiagnostics)) {
return trait.handler.compileHmrUpdateDeclaration(clazz, trait.analysis, trait.resolution);
}
}
return null;
}
decoratorsFor(node) {
const original = ts__default["default"].getOriginalNode(node);
if (!this.reflector.isClass(original) || !this.classes.has(original)) {
return [];
}
const record = this.classes.get(original);
const decorators = [];
for (const trait of record.traits) {
// In global compilation mode skip the non-resolved traits.
if (this.compilationMode !== checker.CompilationMode.LOCAL && trait.state !== checker.TraitState.Resolved) {
continue;
}
if (trait.detected.trigger !== null && ts__default["default"].isDecorator(trait.detected.trigger)) {
decorators.push(trait.detected.trigger);
}
}
return decorators;
}
get diagnostics() {
const diagnostics = [];
for (const clazz of this.classes.keys()) {
const record = this.classes.get(clazz);
if (record.metaDiagnostics !== null) {
diagnostics.push(...record.metaDiagnostics);
}
for (const trait of record.traits) {
if ((trait.state === checker.TraitState.Analyzed || trait.state === checker.TraitState.Resolved) &&
trait.analysisDiagnostics !== null) {
diagnostics.push(...trait.analysisDiagnostics);
}
if (trait.state === checker.TraitState.Resolved) {
diagnostics.push(...(trait.resolveDiagnostics ?? []));
}
}
}
return diagnostics;
}
get exportStatements() {
return this.reexportMap;
}
}
function containsErrors(diagnostics) {
return (diagnostics !== null &&
diagnostics.some((diag) => diag.category === ts__default["default"].DiagnosticCategory.Error));
}
/**
* Keeps track of `DtsTransform`s per source file, so that it is known which source files need to
* have their declaration file transformed.
*/
class DtsTransformRegistry {
ivyDeclarationTransforms = new Map();
getIvyDeclarationTransform(sf) {
if (!this.ivyDeclarationTransforms.has(sf)) {
this.ivyDeclarationTransforms.set(sf, new IvyDeclarationDtsTransform());
}
return this.ivyDeclarationTransforms.get(sf);
}
/**
* Gets the dts transforms to be applied for the given source file, or `null` if no transform is
* necessary.
*/
getAllTransforms(sf) {
// No need to transform if it's not a declarations file, or if no changes have been requested
// to the input file. Due to the way TypeScript afterDeclarations transformers work, the
// `ts.SourceFile` path is the same as the original .ts. The only way we know it's actually a
// declaration file is via the `isDeclarationFile` property.
if (!sf.isDeclarationFile) {
return null;
}
const originalSf = ts__default["default"].getOriginalNode(sf);
let transforms = null;
if (this.ivyDeclarationTransforms.has(originalSf)) {
transforms = [];
transforms.push(this.ivyDeclarationTransforms.get(originalSf));
}
return transforms;
}
}
function declarationTransformFactory(transformRegistry, reflector, refEmitter, importRewriter) {
return (context) => {
const transformer = new DtsTransformer(context, reflector, refEmitter, importRewriter);
return (fileOrBundle) => {
if (ts__default["default"].isBundle(fileOrBundle)) {
// Only attempt to transform source files.
return fileOrBundle;
}
const transforms = transformRegistry.getAllTransforms(fileOrBundle);
if (transforms === null) {
return fileOrBundle;
}
return transformer.transform(fileOrBundle, transforms);
};
};
}
/**
* Processes .d.ts file text and adds static field declarations, with types.
*/
class DtsTransformer {
ctx;
reflector;
refEmitter;
importRewriter;
constructor(ctx, reflector, refEmitter, importRewriter) {
this.ctx = ctx;
this.reflector = reflector;
this.refEmitter = refEmitter;
this.importRewriter = importRewriter;
}
/**
* Transform the declaration file and add any declarations which were recorded.
*/
transform(sf, transforms) {
const imports = new checker.ImportManager({
...checker.presetImportManagerForceNamespaceImports,
rewriter: this.importRewriter,
});
const visitor = (node) => {
if (ts__default["default"].isClassDeclaration(node)) {
return this.transformClassDeclaration(node, transforms, imports);
}
else if (ts__default["default"].isFunctionDeclaration(node)) {
return this.transformFunctionDeclaration(node, transforms, imports);
}
else {
// Otherwise return node as is.
return ts__default["default"].visitEachChild(node, visitor, this.ctx);
}
};
// Recursively scan through the AST and process all nodes as desired.
sf = ts__default["default"].visitNode(sf, visitor, ts__default["default"].isSourceFile) || sf;
// Update/insert needed imports.
return imports.transformTsFile(this.ctx, sf);
}
transformClassDeclaration(clazz, transforms, imports) {
let elements = clazz.members;
let elementsChanged = false;
for (const transform of transforms) {
if (transform.transformClassElement !== undefined) {
for (let i = 0; i < elements.length; i++) {
const res = transform.transformClassElement(elements[i], imports);
if (res !== elements[i]) {
if (!elementsChanged) {
elements = [...elements];
elementsChanged = true;
}
elements[i] = res;
}
}
}
}
let newClazz = clazz;
for (const transform of transforms) {
if (transform.transformClass !== undefined) {
// If no DtsTransform has changed the class yet, then the (possibly mutated) elements have
// not yet been incorporated. Otherwise, `newClazz.members` holds the latest class members.
const inputMembers = clazz === newClazz ? elements : newClazz.members;
newClazz = transform.transformClass(newClazz, inputMembers, this.reflector, this.refEmitter, imports);
}
}
// If some elements have been transformed but the class itself has not been transformed, create
// an updated class declaration with the updated elements.
if (elementsChanged && clazz === newClazz) {
newClazz = ts__default["default"].factory.updateClassDeclaration(
/* node */ clazz,
/* modifiers */ clazz.modifiers,
/* name */ clazz.name,
/* typeParameters */ clazz.typeParameters,
/* heritageClauses */ clazz.heritageClauses,
/* members */ elements);
}
return newClazz;
}
transformFunctionDeclaration(declaration, transforms, imports) {
let newDecl = declaration;
for (const transform of transforms) {
if (transform.transformFunctionDeclaration !== undefined) {
newDecl = transform.transformFunctionDeclaration(newDecl, imports);
}
}
return newDecl;
}
}
class IvyDeclarationDtsTransform {
declarationFields = new Map();
addFields(decl, fields) {
this.declarationFields.set(decl, fields);
}
transformClass(clazz, members, reflector, refEmitter, imports) {
const original = ts__default["default"].getOriginalNode(clazz);
if (!this.declarationFields.has(original)) {
return clazz;
}
const fields = this.declarationFields.get(original);
const newMembers = fields.map((decl) => {
const modifiers = [ts__default["default"].factory.createModifier(ts__default["default"].SyntaxKind.StaticKeyword)];
const typeRef = checker.translateType(decl.type, original.getSourceFile(), reflector, refEmitter, imports);
markForEmitAsSingleLine(typeRef);
return ts__default["default"].factory.createPropertyDeclaration(
/* modifiers */ modifiers,
/* name */ decl.name,
/* questionOrExclamationToken */ undefined,
/* type */ typeRef,
/* initializer */ undefined);
});
return ts__default["default"].factory.updateClassDeclaration(
/* node */ clazz,
/* modifiers */ clazz.modifiers,
/* name */ clazz.name,
/* typeParameters */ clazz.typeParameters,
/* heritageClauses */ clazz.heritageClauses,
/* members */ [...members, ...newMembers]);
}
}
function markForEmitAsSingleLine(node) {
ts__default["default"].setEmitFlags(node, ts__default["default"].EmitFlags.SingleLine);
ts__default["default"].forEachChild(node, markForEmitAsSingleLine);
}
/**
* Visit a node with the given visitor and return a transformed copy.
*/
function visit(node, visitor, context) {
return visitor._visit(node, context);
}
/**
* Abstract base class for visitors, which processes certain nodes specially to allow insertion
* of other nodes before them.
*/
class Visitor {
/**
* Maps statements to an array of statements that should be inserted before them.
*/
_before = new Map();
/**
* Maps statements to an array of statements that should be inserted after them.
*/
_after = new Map();
_visitListEntryNode(node, visitor) {
const result = visitor(node);
if (result.before !== undefined) {
// Record that some nodes should be inserted before the given declaration. The declaration's
// parent's _visit call is responsible for performing this insertion.
this._before.set(result.node, result.before);
}
if (result.after !== undefined) {
// Same with nodes that should be inserted after.
this._after.set(result.node, result.after);
}
return result.node;
}
/**
* Visit types of nodes which don't have their own explicit visitor.
*/
visitOtherNode(node) {
return node;
}
/**
* @internal
*/
_visit(node, context) {
// First, visit the node. visitedNode starts off as `null` but should be set after visiting
// is completed.
let visitedNode = null;
node = ts__default["default"].visitEachChild(node, (child) => child && this._visit(child, context), context);
if (ts__default["default"].isClassDeclaration(node)) {
visitedNode = this._visitListEntryNode(node, (node) => this.visitClassDeclaration(node));
}
else {
visitedNode = this.visitOtherNode(node);
}
// If the visited node has a `statements` array then process them, maybe replacing the visited
// node and adding additional statements.
if (visitedNode && (ts__default["default"].isBlock(visitedNode) || ts__default["default"].isSourceFile(visitedNode))) {
visitedNode = this._maybeProcessStatements(visitedNode);
}
return visitedNode;
}
_maybeProcessStatements(node) {
// Shortcut - if every statement doesn't require nodes to be prepended or appended,
// this is a no-op.
if (node.statements.every((stmt) => !this._before.has(stmt) && !this._after.has(stmt))) {
return node;
}
// Build a new list of statements and patch it onto the clone.
const newStatements = [];
node.statements.forEach((stmt) => {
if (this._before.has(stmt)) {
newStatements.push(...this._before.get(stmt));
this._before.delete(stmt);
}
newStatements.push(stmt);
if (this._after.has(stmt)) {
newStatements.push(...this._after.get(stmt));
this._after.delete(stmt);
}
});
const statementsArray = ts__default["default"].factory.createNodeArray(newStatements, node.statements.hasTrailingComma);
if (ts__default["default"].isBlock(node)) {
return ts__default["default"].factory.updateBlock(node, statementsArray);
}
else {
return ts__default["default"].factory.updateSourceFile(node, statementsArray, node.isDeclarationFile, node.referencedFiles, node.typeReferenceDirectives, node.hasNoDefaultLib, node.libReferenceDirectives);
}
}
}
const NO_DECORATORS = new Set();
const CLOSURE_FILE_OVERVIEW_REGEXP = /\s+@fileoverview\s+/i;
function ivyTransformFactory(compilation, reflector, importRewriter, defaultImportTracker, localCompilationExtraImportsTracker, perf, isCore, isClosureCompilerEnabled) {
const recordWrappedNode = createRecorderFn(defaultImportTracker);
return (context) => {
return (file) => {
return perf.inPhase(checker.PerfPhase.Compile, () => transformIvySourceFile(compilation, context, reflector, importRewriter, localCompilationExtraImportsTracker, file, isCore, isClosureCompilerEnabled, recordWrappedNode));
};
};
}
/**
* Visits all classes, performs Ivy compilation where Angular decorators are present and collects
* result in a Map that associates a ts.ClassDeclaration with Ivy compilation results. This visitor
* does NOT perform any TS transformations.
*/
class IvyCompilationVisitor extends Visitor {
compilation;
constantPool;
classCompilationMap = new Map();
deferrableImports = new Set();
constructor(compilation, constantPool) {
super();
this.compilation = compilation;
this.constantPool = constantPool;
}
visitClassDeclaration(node) {
// Determine if this class has an Ivy field that needs to be added, and compile the field
// to an expression if so.
const result = this.compilation.compile(node, this.constantPool);
if (result !== null) {
this.classCompilationMap.set(node, result);
// Collect all deferrable imports declarations into a single set,
// so that we can pass it to the transform visitor that will drop
// corresponding regular import declarations.
for (const classResult of result) {
if (classResult.deferrableImports !== null && classResult.deferrableImports.size > 0) {
classResult.deferrableImports.forEach((importDecl) => this.deferrableImports.add(importDecl));
}
}
}
return { node };
}
}
/**
* Visits all classes and performs transformation of corresponding TS nodes based on the Ivy
* compilation results (provided as an argument).
*/
class IvyTransformationVisitor extends Visitor {
compilation;
classCompilationMap;
reflector;
importManager;
recordWrappedNodeExpr;
isClosureCompilerEnabled;
isCore;
deferrableImports;
constructor(compilation, classCompilationMap, reflector, importManager, recordWrappedNodeExpr, isClosureCompilerEnabled, isCore, deferrableImports) {
super();
this.compilation = compilation;
this.classCompilationMap = classCompilationMap;
this.reflector = reflector;
this.importManager = importManager;
this.recordWrappedNodeExpr = recordWrappedNodeExpr;
this.isClosureCompilerEnabled = isClosureCompilerEnabled;
this.isCore = isCore;
this.deferrableImports = deferrableImports;
}
visitClassDeclaration(node) {
// If this class is not registered in the map, it means that it doesn't have Angular decorators,
// thus no further processing is required.
if (!this.classCompilationMap.has(node)) {
return { node };
}
const translateOptions = {
recordWrappedNode: this.recordWrappedNodeExpr,
annotateForClosureCompiler: this.isClosureCompilerEnabled,
};
// There is at least one field to add.
const statements = [];
const members = [...node.members];
// Note: Class may be already transformed by e.g. Tsickle and
// not have a direct reference to the source file.
const sourceFile = ts__default["default"].getOriginalNode(node).getSourceFile();
for (const field of this.classCompilationMap.get(node)) {
// Type-only member.
if (field.initializer === null) {
continue;
}
// Translate the initializer for the field into TS nodes.
const exprNode = checker.translateExpression(sourceFile, field.initializer, this.importManager, translateOptions);
// Create a static property declaration for the new field.
const property = ts__default["default"].factory.createPropertyDeclaration([ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.StaticKeyword)], field.name, undefined, undefined, exprNode);
if (this.isClosureCompilerEnabled) {
// Closure compiler transforms the form `Service.ɵprov = X` into `Service$ɵprov = X`. To
// prevent this transformation, such assignments need to be annotated with @nocollapse.
// Note that tsickle is typically responsible for adding such annotations, however it
// doesn't yet handle synthetic fields added during other transformations.
ts__default["default"].addSyntheticLeadingComment(property, ts__default["default"].SyntaxKind.MultiLineCommentTrivia, '* @nocollapse ',
/* hasTrailingNewLine */ false);
}
field.statements
.map((stmt) => checker.translateStatement(sourceFile, stmt, this.importManager, translateOptions))
.forEach((stmt) => statements.push(stmt));
members.push(property);
}
const filteredDecorators =
// Remove the decorator which triggered this compilation, leaving the others alone.
maybeFilterDecorator(ts__default["default"].getDecorators(node), this.compilation.decoratorsFor(node));
const nodeModifiers = ts__default["default"].getModifiers(node);
let updatedModifiers;
if (filteredDecorators?.length || nodeModifiers?.length) {
updatedModifiers = [...(filteredDecorators || []), ...(nodeModifiers || [])];
}
// Replace the class declaration with an updated version.
node = ts__default["default"].factory.updateClassDeclaration(node, updatedModifiers, node.name, node.typeParameters, node.heritageClauses || [],
// Map over the class members and remove any Angular decorators from them.
members.map((member) => this._stripAngularDecorators(member)));
return { node, after: statements };
}
visitOtherNode(node) {
if (ts__default["default"].isImportDeclaration(node) && this.deferrableImports.has(node)) {
// Return `null` as an indication that this node should not be present
// in the final AST. Symbols from this import would be imported via
// dynamic imports.
return null;
}
return node;
}
/**
* Return all decorators on a `Declaration` which are from @angular/core, or an empty set if none
* are.
*/
_angularCoreDecorators(decl) {
const decorators = this.reflector.getDecoratorsOfDeclaration(decl);
if (decorators === null) {
return NO_DECORATORS;
}
const coreDecorators = decorators
.filter((dec) => this.isCore || isFromAngularCore(dec))
.map((dec) => dec.node);
if (coreDecorators.length > 0) {
return new Set(coreDecorators);
}
else {
return NO_DECORATORS;
}
}
_nonCoreDecoratorsOnly(node) {
const decorators = ts__default["default"].getDecorators(node);
// Shortcut if the node has no decorators.
if (decorators === undefined) {
return undefined;
}
// Build a Set of the decorators on this node from @angular/core.
const coreDecorators = this._angularCoreDecorators(node);
if (coreDecorators.size === decorators.length) {
// If all decorators are to be removed, return `undefined`.
return undefined;
}
else if (coreDecorators.size === 0) {
// If no decorators need to be removed, return the original decorators array.
return nodeArrayFromDecoratorsArray(decorators);
}
// Filter out the core decorators.
const filtered = decorators.filter((dec) => !coreDecorators.has(dec));
// If no decorators survive, return `undefined`. This can only happen if a core decorator is
// repeated on the node.
if (filtered.length === 0) {
return undefined;
}
// Create a new `NodeArray` with the filtered decorators that sourcemaps back to the original.
return nodeArrayFromDecoratorsArray(filtered);
}
/**
* Remove Angular decorators from a `ts.Node` in a shallow manner.
*
* This will remove decorators from class elements (getters, setters, properties, methods) as well
* as parameters of constructors.
*/
_stripAngularDecorators(node) {
const modifiers = ts__default["default"].canHaveModifiers(node) ? ts__default["default"].getModifiers(node) : undefined;
const nonCoreDecorators = ts__default["default"].canHaveDecorators(node)
? this._nonCoreDecoratorsOnly(node)
: undefined;
const combinedModifiers = [...(nonCoreDecorators || []), ...(modifiers || [])];
if (ts__default["default"].isParameter(node)) {
// Strip decorators from parameters (probably of the constructor).
node = ts__default["default"].factory.updateParameterDeclaration(node, combinedModifiers, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer);
}
else if (ts__default["default"].isMethodDeclaration(node)) {
// Strip decorators of methods.
node = ts__default["default"].factory.updateMethodDeclaration(node, combinedModifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, node.body);
}
else if (ts__default["default"].isPropertyDeclaration(node)) {
// Strip decorators of properties.
node = ts__default["default"].factory.updatePropertyDeclaration(node, combinedModifiers, node.name, node.questionToken, node.type, node.initializer);
}
else if (ts__default["default"].isGetAccessor(node)) {
// Strip decorators of getters.
node = ts__default["default"].factory.updateGetAccessorDeclaration(node, combinedModifiers, node.name, node.parameters, node.type, node.body);
}
else if (ts__default["default"].isSetAccessor(node)) {
// Strip decorators of setters.
node = ts__default["default"].factory.updateSetAccessorDeclaration(node, combinedModifiers, node.name, node.parameters, node.body);
}
else if (ts__default["default"].isConstructorDeclaration(node)) {
// For constructors, strip decorators of the parameters.
const parameters = node.parameters.map((param) => this._stripAngularDecorators(param));
node = ts__default["default"].factory.updateConstructorDeclaration(node, modifiers, parameters, node.body);
}
return node;
}
}
/**
* A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`.
*/
function transformIvySourceFile(compilation, context, reflector, importRewriter, localCompilationExtraImportsTracker, file, isCore, isClosureCompilerEnabled, recordWrappedNode) {
const constantPool = new checker.ConstantPool(isClosureCompilerEnabled);
const importManager = new checker.ImportManager({
...checker.presetImportManagerForceNamespaceImports,
rewriter: importRewriter,
});
// The transformation process consists of 2 steps:
//
// 1. Visit all classes, perform compilation and collect the results.
// 2. Perform actual transformation of required TS nodes using compilation results from the first
// step.
//
// This is needed to have all `o.Expression`s generated before any TS transforms happen. This
// allows `ConstantPool` to properly identify expressions that can be shared across multiple
// components declared in the same file.
// Step 1. Go though all classes in AST, perform compilation and collect the results.
const compilationVisitor = new IvyCompilationVisitor(compilation, constantPool);
visit(file, compilationVisitor, context);
// Step 2. Scan through the AST again and perform transformations based on Ivy compilation
// results obtained at Step 1.
const transformationVisitor = new IvyTransformationVisitor(compilation, compilationVisitor.classCompilationMap, reflector, importManager, recordWrappedNode, isClosureCompilerEnabled, isCore, compilationVisitor.deferrableImports);
let sf = visit(file, transformationVisitor, context);
// Generate the constant statements first, as they may involve adding additional imports
// to the ImportManager.
const downlevelTranslatedCode = getLocalizeCompileTarget(context) < ts__default["default"].ScriptTarget.ES2015;
const constants = constantPool.statements.map((stmt) => checker.translateStatement(file, stmt, importManager, {
recordWrappedNode,
downlevelTaggedTemplates: downlevelTranslatedCode,
downlevelVariableDeclarations: downlevelTranslatedCode,
annotateForClosureCompiler: isClosureCompilerEnabled,
}));
// Preserve @fileoverview comments required by Closure, since the location might change as a
// result of adding extra imports and constant pool statements.
const fileOverviewMeta = isClosureCompilerEnabled ? getFileOverviewComment(sf.statements) : null;
// Add extra imports.
if (localCompilationExtraImportsTracker !== null) {
for (const moduleName of localCompilationExtraImportsTracker.getImportsForFile(sf)) {
importManager.addSideEffectImport(sf, moduleName);
}
}
// Add new imports for this file.
sf = importManager.transformTsFile(context, sf, constants);
if (fileOverviewMeta !== null) {
sf = insertFileOverviewComment(sf, fileOverviewMeta);
}
return sf;
}
/**
* Compute the correct target output for `$localize` messages generated by Angular
*
* In some versions of TypeScript, the transformation of synthetic `$localize` tagged template
* literals is broken. See https://github.com/microsoft/TypeScript/issues/38485
*
* Here we compute what the expected final output target of the compilation will
* be so that we can generate ES5 compliant `$localize` calls instead of relying upon TS to do the
* downleveling for us.
*/
function getLocalizeCompileTarget(context) {
const target = context.getCompilerOptions().target || ts__default["default"].ScriptTarget.ES2015;
return target !== ts__default["default"].ScriptTarget.JSON ? target : ts__default["default"].ScriptTarget.ES2015;
}
function getFileOverviewComment(statements) {
if (statements.length > 0) {
const host = statements[0];
let trailing = false;
let comments = ts__default["default"].getSyntheticLeadingComments(host);
// If @fileoverview tag is not found in source file, tsickle produces fake node with trailing
// comment and inject it at the very beginning of the generated file. So we need to check for
// leading as well as trailing comments.
if (!comments || comments.length === 0) {
trailing = true;
comments = ts__default["default"].getSyntheticTrailingComments(host);
}
if (comments && comments.length > 0 && CLOSURE_FILE_OVERVIEW_REGEXP.test(comments[0].text)) {
return { comments, host, trailing };
}
}
return null;
}
function insertFileOverviewComment(sf, fileoverview) {
const { comments, host, trailing } = fileoverview;
// If host statement is no longer the first one, it means that extra statements were added at the
// very beginning, so we need to relocate @fileoverview comment and cleanup the original statement
// that hosted it.
if (sf.statements.length > 0 && host !== sf.statements[0]) {
if (trailing) {
ts__default["default"].setSyntheticTrailingComments(host, undefined);
}
else {
ts__default["default"].setSyntheticLeadingComments(host, undefined);
}
// Note: Do not use the first statement as it may be elided at runtime.
// E.g. an import declaration that is type only.
const commentNode = ts__default["default"].factory.createNotEmittedStatement(sf);
ts__default["default"].setSyntheticLeadingComments(commentNode, comments);
return ts__default["default"].factory.updateSourceFile(sf, [commentNode, ...sf.statements], sf.isDeclarationFile, sf.referencedFiles, sf.typeReferenceDirectives, sf.hasNoDefaultLib, sf.libReferenceDirectives);
}
return sf;
}
function maybeFilterDecorator(decorators, toRemove) {
if (decorators === undefined) {
return undefined;
}
const filtered = decorators.filter((dec) => toRemove.find((decToRemove) => ts__default["default"].getOriginalNode(dec) === decToRemove) === undefined);
if (filtered.length === 0) {
return undefined;
}
return ts__default["default"].factory.createNodeArray(filtered);
}
function isFromAngularCore(decorator) {
return decorator.import !== null && decorator.import.from === '@angular/core';
}
function createRecorderFn(defaultImportTracker) {
return (node) => {
const importDecl = checker.getDefaultImportDeclaration(node);
if (importDecl !== null) {
defaultImportTracker.recordUsedImport(importDecl);
}
};
}
/** Creates a `NodeArray` with the correct offsets from an array of decorators. */
function nodeArrayFromDecoratorsArray(decorators) {
const array = ts__default["default"].factory.createNodeArray(decorators);
if (array.length > 0) {
array.pos = decorators[0].pos;
array.end = decorators[decorators.length - 1].end;
}
return array;
}
/**
* Create a `ts.Diagnostic` which indicates the given class is part of the declarations of two or
* more NgModules.
*
* The resulting `ts.Diagnostic` will have a context entry for each NgModule showing the point where
* the directive/pipe exists in its `declarations` (if possible).
*/
function makeDuplicateDeclarationError(node, data, kind) {
const context = [];
for (const decl of data) {
if (decl.rawDeclarations === null) {
continue;
}
// Try to find the reference to the declaration within the declarations array, to hang the
// error there. If it can't be found, fall back on using the NgModule's name.
const contextNode = decl.ref.getOriginForDiagnostics(decl.rawDeclarations, decl.ngModule.name);
context.push(checker.makeRelatedInformation(contextNode, `'${node.name.text}' is listed in the declarations of the NgModule '${decl.ngModule.name.text}'.`));
}
// Finally, produce the diagnostic.
return checker.makeDiagnostic(checker.ErrorCode.NGMODULE_DECLARATION_NOT_UNIQUE, node.name, `The ${kind} '${node.name.text}' is declared by more than one NgModule.`, context);
}
/**
* Creates a `FatalDiagnosticError` for a node that did not evaluate to the expected type. The
* diagnostic that is created will include details on why the value is incorrect, i.e. it includes
* a representation of the actual type that was unsupported, or in the case of a dynamic value the
* trace to the node where the dynamic value originated.
*
* @param node The node for which the diagnostic should be produced.
* @param value The evaluated value that has the wrong type.
* @param messageText The message text of the error.
*/
function createValueHasWrongTypeError(node, value, messageText) {
let chainedMessage;
let relatedInformation;
if (value instanceof checker.DynamicValue) {
chainedMessage = 'Value could not be determined statically.';
relatedInformation = traceDynamicValue(node, value);
}
else if (value instanceof checker.Reference) {
const target = value.debugName !== null ? `'${value.debugName}'` : 'an anonymous declaration';
chainedMessage = `Value is a reference to ${target}.`;
const referenceNode = checker.identifierOfNode(value.node) ?? value.node;
relatedInformation = [checker.makeRelatedInformation(referenceNode, 'Reference is declared here.')];
}
else {
chainedMessage = `Value is of type '${describeResolvedType(value)}'.`;
}
const chain = {
messageText,
category: ts__default["default"].DiagnosticCategory.Error,
code: 0,
next: [
{
messageText: chainedMessage,
category: ts__default["default"].DiagnosticCategory.Message,
code: 0,
},
],
};
return new checker.FatalDiagnosticError(checker.ErrorCode.VALUE_HAS_WRONG_TYPE, node, chain, relatedInformation);
}
/**
* Gets the diagnostics for a set of provider classes.
* @param providerClasses Classes that should be checked.
* @param providersDeclaration Node that declares the providers array.
* @param registry Registry that keeps track of the registered injectable classes.
*/
function getProviderDiagnostics(providerClasses, providersDeclaration, registry) {
const diagnostics = [];
for (const provider of providerClasses) {
const injectableMeta = registry.getInjectableMeta(provider.node);
if (injectableMeta !== null) {
// The provided type is recognized as injectable, so we don't report a diagnostic for this
// provider.
continue;
}
const contextNode = provider.getOriginForDiagnostics(providersDeclaration);
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.UNDECORATED_PROVIDER, contextNode, `The class '${provider.node.name.text}' cannot be created via dependency injection, as it does not have an Angular decorator. This will result in an error at runtime.
Either add the @Injectable() decorator to '${provider.node.name.text}', or configure a different provider (such as a provider with 'useFactory').
`, [checker.makeRelatedInformation(provider.node, `'${provider.node.name.text}' is declared here.`)]));
}
return diagnostics;
}
function getDirectiveDiagnostics(node, injectableRegistry, evaluator, reflector, scopeRegistry, strictInjectionParameters, kind) {
let diagnostics = [];
const addDiagnostics = (more) => {
if (more === null) {
return;
}
else if (diagnostics === null) {
diagnostics = Array.isArray(more) ? more : [more];
}
else if (Array.isArray(more)) {
diagnostics.push(...more);
}
else {
diagnostics.push(more);
}
};
const duplicateDeclarations = scopeRegistry.getDuplicateDeclarations(node);
if (duplicateDeclarations !== null) {
addDiagnostics(makeDuplicateDeclarationError(node, duplicateDeclarations, kind));
}
addDiagnostics(checkInheritanceOfInjectable(node, injectableRegistry, reflector, evaluator, strictInjectionParameters, kind));
return diagnostics;
}
function validateHostDirectives(origin, hostDirectives, metaReader) {
const diagnostics = [];
for (const current of hostDirectives) {
if (!checker.isHostDirectiveMetaForGlobalMode(current)) {
throw new Error('Impossible state: diagnostics code path for local compilation');
}
const hostMeta = flattenInheritedDirectiveMetadata(metaReader, current.directive);
if (hostMeta === null) {
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.HOST_DIRECTIVE_INVALID, current.directive.getOriginForDiagnostics(origin), `${current.directive.debugName} must be a standalone directive to be used as a host directive`));
continue;
}
if (!hostMeta.isStandalone) {
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.HOST_DIRECTIVE_NOT_STANDALONE, current.directive.getOriginForDiagnostics(origin), `Host directive ${hostMeta.name} must be standalone`));
}
if (hostMeta.isComponent) {
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.HOST_DIRECTIVE_COMPONENT, current.directive.getOriginForDiagnostics(origin), `Host directive ${hostMeta.name} cannot be a component`));
}
const requiredInputNames = Array.from(hostMeta.inputs)
.filter((input) => input.required)
.map((input) => input.classPropertyName);
validateHostDirectiveMappings('input', current, hostMeta, origin, diagnostics, requiredInputNames.length > 0 ? new Set(requiredInputNames) : null);
validateHostDirectiveMappings('output', current, hostMeta, origin, diagnostics, null);
}
return diagnostics;
}
function validateHostDirectiveMappings(bindingType, hostDirectiveMeta, meta, origin, diagnostics, requiredBindings) {
if (!checker.isHostDirectiveMetaForGlobalMode(hostDirectiveMeta)) {
throw new Error('Impossible state: diagnostics code path for local compilation');
}
const className = meta.name;
const hostDirectiveMappings = bindingType === 'input' ? hostDirectiveMeta.inputs : hostDirectiveMeta.outputs;
const existingBindings = bindingType === 'input' ? meta.inputs : meta.outputs;
const exposedRequiredBindings = new Set();
for (const publicName in hostDirectiveMappings) {
if (hostDirectiveMappings.hasOwnProperty(publicName)) {
const bindings = existingBindings.getByBindingPropertyName(publicName);
if (bindings === null) {
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.HOST_DIRECTIVE_UNDEFINED_BINDING, hostDirectiveMeta.directive.getOriginForDiagnostics(origin), `Directive ${className} does not have an ${bindingType} with a public name of ${publicName}.`));
}
else if (requiredBindings !== null) {
for (const field of bindings) {
if (requiredBindings.has(field.classPropertyName)) {
exposedRequiredBindings.add(field.classPropertyName);
}
}
}
const remappedPublicName = hostDirectiveMappings[publicName];
const bindingsForPublicName = existingBindings.getByBindingPropertyName(remappedPublicName);
if (bindingsForPublicName !== null) {
for (const binding of bindingsForPublicName) {
if (binding.bindingPropertyName !== publicName) {
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.HOST_DIRECTIVE_CONFLICTING_ALIAS, hostDirectiveMeta.directive.getOriginForDiagnostics(origin), `Cannot alias ${bindingType} ${publicName} of host directive ${className} to ${remappedPublicName}, because it already has a different ${bindingType} with the same public name.`));
}
}
}
}
}
if (requiredBindings !== null && requiredBindings.size !== exposedRequiredBindings.size) {
const missingBindings = [];
for (const publicName of requiredBindings) {
if (!exposedRequiredBindings.has(publicName)) {
const name = existingBindings.getByClassPropertyName(publicName);
if (name) {
missingBindings.push(`'${name.bindingPropertyName}'`);
}
}
}
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.HOST_DIRECTIVE_MISSING_REQUIRED_BINDING, hostDirectiveMeta.directive.getOriginForDiagnostics(origin), `Required ${bindingType}${missingBindings.length === 1 ? '' : 's'} ${missingBindings.join(', ')} from host directive ${className} must be exposed.`));
}
}
function getUndecoratedClassWithAngularFeaturesDiagnostic(node) {
return checker.makeDiagnostic(checker.ErrorCode.UNDECORATED_CLASS_USING_ANGULAR_FEATURES, node.name, `Class is using Angular features but is not decorated. Please add an explicit ` +
`Angular decorator.`);
}
function checkInheritanceOfInjectable(node, injectableRegistry, reflector, evaluator, strictInjectionParameters, kind) {
const classWithCtor = findInheritedCtor(node, injectableRegistry, reflector, evaluator);
if (classWithCtor === null || classWithCtor.isCtorValid) {
// The class does not inherit a constructor, or the inherited constructor is compatible
// with DI; no need to report a diagnostic.
return null;
}
if (!classWithCtor.isDecorated) {
// The inherited constructor exists in a class that does not have an Angular decorator.
// This is an error, as there won't be a factory definition available for DI to invoke
// the constructor.
return getInheritedUndecoratedCtorDiagnostic(node, classWithCtor.ref, kind);
}
if (checker.isFromDtsFile(classWithCtor.ref.node)) {
// The inherited class is declared in a declaration file, in which case there is not enough
// information to detect invalid constructors as `@Inject()` metadata is not present in the
// declaration file. Consequently, we have to accept such occurrences, although they might
// still fail at runtime.
return null;
}
if (!strictInjectionParameters || checker.isAbstractClassDeclaration(node)) {
// An invalid constructor is only reported as error under `strictInjectionParameters` and
// only for concrete classes; follow the same exclusions for derived types.
return null;
}
return getInheritedInvalidCtorDiagnostic(node, classWithCtor.ref, kind);
}
function findInheritedCtor(node, injectableRegistry, reflector, evaluator) {
if (!reflector.isClass(node) || reflector.getConstructorParameters(node) !== null) {
// We should skip nodes that aren't classes. If a constructor exists, then no base class
// definition is required on the runtime side - it's legal to inherit from any class.
return null;
}
// The extends clause is an expression which can be as dynamic as the user wants. Try to
// evaluate it, but fall back on ignoring the clause if it can't be understood. This is a View
// Engine compatibility hack: View Engine ignores 'extends' expressions that it cannot understand.
let baseClass = checker.readBaseClass(node, reflector, evaluator);
while (baseClass !== null) {
if (baseClass === 'dynamic') {
return null;
}
const injectableMeta = injectableRegistry.getInjectableMeta(baseClass.node);
if (injectableMeta !== null) {
if (injectableMeta.ctorDeps !== null) {
// The class has an Angular decorator with a constructor.
return {
ref: baseClass,
isCtorValid: injectableMeta.ctorDeps !== 'invalid',
isDecorated: true,
};
}
}
else {
const baseClassConstructorParams = reflector.getConstructorParameters(baseClass.node);
if (baseClassConstructorParams !== null) {
// The class is not decorated, but it does have constructor. An undecorated class is only
// allowed to have a constructor without parameters, otherwise it is invalid.
return {
ref: baseClass,
isCtorValid: baseClassConstructorParams.length === 0,
isDecorated: false,
};
}
}
// Go up the chain and continue
baseClass = checker.readBaseClass(baseClass.node, reflector, evaluator);
}
return null;
}
function getInheritedInvalidCtorDiagnostic(node, baseClass, kind) {
const baseClassName = baseClass.debugName;
return checker.makeDiagnostic(checker.ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR, node.name, `The ${kind.toLowerCase()} ${node.name.text} inherits its constructor from ${baseClassName}, ` +
`but the latter has a constructor parameter that is not compatible with dependency injection. ` +
`Either add an explicit constructor to ${node.name.text} or change ${baseClassName}'s constructor to ` +
`use parameters that are valid for DI.`);
}
function getInheritedUndecoratedCtorDiagnostic(node, baseClass, kind) {
const baseClassName = baseClass.debugName;
const baseNeedsDecorator = kind === 'Component' || kind === 'Directive' ? 'Directive' : 'Injectable';
return checker.makeDiagnostic(checker.ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR, node.name, `The ${kind.toLowerCase()} ${node.name.text} inherits its constructor from ${baseClassName}, ` +
`but the latter does not have an Angular decorator of its own. Dependency injection will not be able to ` +
`resolve the parameters of ${baseClassName}'s constructor. Either add a @${baseNeedsDecorator} decorator ` +
`to ${baseClassName}, or add an explicit constructor to ${node.name.text}.`);
}
/**
* Throws `FatalDiagnosticError` with error code `LOCAL_COMPILATION_UNRESOLVED_CONST`
* if the compilation mode is local and the value is not resolved due to being imported
* from external files. This is a common scenario for errors in local compilation mode,
* and so this helper can be used to quickly generate the relevant errors.
*
* @param nodeToHighlight Node to be highlighted in teh error message.
* Will default to value.node if not provided.
*/
function assertLocalCompilationUnresolvedConst(compilationMode, value, nodeToHighlight, errorMessage) {
if (compilationMode === checker.CompilationMode.LOCAL &&
value instanceof checker.DynamicValue &&
value.isFromUnknownIdentifier()) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST, nodeToHighlight ?? value.node, errorMessage);
}
}
function resolveEnumValue(evaluator, metadata, field, enumSymbolName) {
let resolved = null;
if (metadata.has(field)) {
const expr = metadata.get(field);
const value = evaluator.evaluate(expr);
if (value instanceof checker.EnumValue && checker.isAngularCoreReference(value.enumRef, enumSymbolName)) {
resolved = value.resolved;
}
else {
throw createValueHasWrongTypeError(expr, value, `${field} must be a member of ${enumSymbolName} enum from @angular/core`);
}
}
return resolved;
}
/**
* Resolves a EncapsulationEnum expression locally on best effort without having to calculate the
* reference. This suites local compilation mode where each file is compiled individually.
*
* The static analysis is still needed in local compilation mode since the value of this enum will
* be used later to decide the generated code for styles.
*/
function resolveEncapsulationEnumValueLocally(expr) {
if (!expr) {
return null;
}
const exprText = expr.getText().trim();
for (const key in checker.ViewEncapsulation) {
if (!Number.isNaN(Number(key))) {
continue;
}
const suffix = `ViewEncapsulation.${key}`;
// Check whether the enum is imported by name or used by import namespace (e.g.,
// core.ViewEncapsulation.None)
if (exprText === suffix || exprText.endsWith(`.${suffix}`)) {
const ans = Number(checker.ViewEncapsulation[key]);
return ans;
}
}
return null;
}
/** Determines if the result of an evaluation is a string array. */
function isStringArray(resolvedValue) {
return Array.isArray(resolvedValue) && resolvedValue.every((elem) => typeof elem === 'string');
}
function resolveLiteral(decorator, literalCache) {
if (literalCache.has(decorator)) {
return literalCache.get(decorator);
}
if (decorator.args === null || decorator.args.length !== 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, `Incorrect number of arguments to @${decorator.name} decorator`);
}
const meta = checker.unwrapExpression(decorator.args[0]);
if (!ts__default["default"].isObjectLiteralExpression(meta)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, `Decorator argument must be literal.`);
}
literalCache.set(decorator, meta);
return meta;
}
function compileNgFactoryDefField(metadata) {
const res = checker.compileFactoryFunction(metadata);
return {
name: 'ɵfac',
initializer: res.expression,
statements: res.statements,
type: res.type,
deferrableImports: null,
};
}
function compileDeclareFactory(metadata) {
const res = compileDeclareFactoryFunction(metadata);
return {
name: 'ɵfac',
initializer: res.expression,
statements: res.statements,
type: res.type,
deferrableImports: null,
};
}
/**
* Registry that keeps track of classes that can be constructed via dependency injection (e.g.
* injectables, directives, pipes).
*/
class InjectableClassRegistry {
host;
isCore;
classes = new Map();
constructor(host, isCore) {
this.host = host;
this.isCore = isCore;
}
registerInjectable(declaration, meta) {
this.classes.set(declaration, meta);
}
getInjectableMeta(declaration) {
// Figure out whether the class is injectable based on the registered classes, otherwise
// fall back to looking at its members since we might not have been able to register the class
// if it was compiled in another compilation unit.
if (this.classes.has(declaration)) {
return this.classes.get(declaration);
}
if (!checker.hasInjectableFields(declaration, this.host)) {
return null;
}
const ctorDeps = getConstructorDependencies(declaration, this.host, this.isCore);
const meta = {
ctorDeps: unwrapConstructorDependencies(ctorDeps),
};
this.classes.set(declaration, meta);
return meta;
}
}
/**
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
* present on the class or its member fields. An ngDevMode guard is used to allow the call to be
* tree-shaken away, as the `setClassMetadata` invocation is only needed for testing purposes.
*
* If no such metadata is present, this function returns `null`. Otherwise, the call is returned
* as a `Statement` for inclusion along with the class.
*/
function extractClassMetadata(clazz, reflection, isCore, annotateForClosureCompiler, angularDecoratorTransform = (dec) => dec) {
if (!reflection.isClass(clazz)) {
return null;
}
const id = clazz.name;
// Reflect over the class decorators. If none are present, or those that are aren't from
// Angular, then return null. Otherwise, turn them into metadata.
const classDecorators = reflection.getDecoratorsOfDeclaration(clazz);
if (classDecorators === null) {
return null;
}
const ngClassDecorators = classDecorators
.filter((dec) => isAngularDecorator$1(dec, isCore))
.map((decorator) => decoratorToMetadata(angularDecoratorTransform(decorator), annotateForClosureCompiler))
// Since the `setClassMetadata` call is intended to be emitted after the class
// declaration, we have to strip references to the existing identifiers or
// TypeScript might generate invalid code when it emits to JS. In particular
// this can break when emitting a class to ES5 which has a custom decorator
// and is referenced inside of its own metadata (see #39509 for more information).
.map((decorator) => removeIdentifierReferences(decorator, id.text));
if (ngClassDecorators.length === 0) {
return null;
}
const metaDecorators = new checker.WrappedNodeExpr(ts__default["default"].factory.createArrayLiteralExpression(ngClassDecorators));
// Convert the constructor parameters to metadata, passing null if none are present.
let metaCtorParameters = null;
const classCtorParameters = reflection.getConstructorParameters(clazz);
if (classCtorParameters !== null) {
const ctorParameters = classCtorParameters.map((param) => ctorParameterToMetadata(param, isCore));
metaCtorParameters = new checker.ArrowFunctionExpr([], new checker.LiteralArrayExpr(ctorParameters));
}
// Do the same for property decorators.
let metaPropDecorators = null;
const classMembers = reflection
.getMembersOfClass(clazz)
.filter((member) => !member.isStatic && member.decorators !== null && member.decorators.length > 0);
const duplicateDecoratedMemberNames = classMembers
.map((member) => member.name)
.filter((name, i, arr) => arr.indexOf(name) < i);
if (duplicateDecoratedMemberNames.length > 0) {
// This should theoretically never happen, because the only way to have duplicate instance
// member names is getter/setter pairs and decorators cannot appear in both a getter and the
// corresponding setter.
throw new Error(`Duplicate decorated properties found on class '${clazz.name.text}': ` +
duplicateDecoratedMemberNames.join(', '));
}
const decoratedMembers = classMembers.map((member) => classMemberToMetadata(member.nameNode ?? member.name, member.decorators, isCore));
if (decoratedMembers.length > 0) {
metaPropDecorators = new checker.WrappedNodeExpr(ts__default["default"].factory.createObjectLiteralExpression(decoratedMembers));
}
return {
type: new checker.WrappedNodeExpr(id),
decorators: metaDecorators,
ctorParameters: metaCtorParameters,
propDecorators: metaPropDecorators,
};
}
/**
* Convert a reflected constructor parameter to metadata.
*/
function ctorParameterToMetadata(param, isCore) {
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
// its type is undefined.
const type = param.typeValueReference.kind !== 2 /* TypeValueReferenceKind.UNAVAILABLE */
? checker.valueReferenceToExpression(param.typeValueReference)
: new checker.LiteralExpr(undefined);
const mapEntries = [
{ key: 'type', value: type, quoted: false },
];
// If the parameter has decorators, include the ones from Angular.
if (param.decorators !== null) {
const ngDecorators = param.decorators
.filter((dec) => isAngularDecorator$1(dec, isCore))
.map((decorator) => decoratorToMetadata(decorator));
const value = new checker.WrappedNodeExpr(ts__default["default"].factory.createArrayLiteralExpression(ngDecorators));
mapEntries.push({ key: 'decorators', value, quoted: false });
}
return checker.literalMap(mapEntries);
}
/**
* Convert a reflected class member to metadata.
*/
function classMemberToMetadata(name, decorators, isCore) {
const ngDecorators = decorators
.filter((dec) => isAngularDecorator$1(dec, isCore))
.map((decorator) => decoratorToMetadata(decorator));
const decoratorMeta = ts__default["default"].factory.createArrayLiteralExpression(ngDecorators);
return ts__default["default"].factory.createPropertyAssignment(name, decoratorMeta);
}
/**
* Convert a reflected decorator to metadata.
*/
function decoratorToMetadata(decorator, wrapFunctionsInParens) {
if (decorator.identifier === null) {
throw new Error('Illegal state: synthesized decorator cannot be emitted in class metadata.');
}
// Decorators have a type.
const properties = [
ts__default["default"].factory.createPropertyAssignment('type', decorator.identifier),
];
// Sometimes they have arguments.
if (decorator.args !== null && decorator.args.length > 0) {
const args = decorator.args.map((arg) => {
return wrapFunctionsInParens ? checker.wrapFunctionExpressionsInParens(arg) : arg;
});
properties.push(ts__default["default"].factory.createPropertyAssignment('args', ts__default["default"].factory.createArrayLiteralExpression(args)));
}
return ts__default["default"].factory.createObjectLiteralExpression(properties, true);
}
/**
* Whether a given decorator should be treated as an Angular decorator.
*
* Either it's used in @angular/core, or it's imported from there.
*/
function isAngularDecorator$1(decorator, isCore) {
return isCore || (decorator.import !== null && decorator.import.from === '@angular/core');
}
/**
* Recursively recreates all of the `Identifier` descendant nodes with a particular name inside
* of an AST node, thus removing any references to them. Useful if a particular node has to be
* taken from one place any emitted to another one exactly as it has been written.
*/
function removeIdentifierReferences(node, names) {
const result = ts__default["default"].transform(node, [
(context) => (root) => ts__default["default"].visitNode(root, function walk(current) {
return (ts__default["default"].isIdentifier(current) &&
(typeof names === 'string' ? current.text === names : names.has(current.text))
? ts__default["default"].factory.createIdentifier(current.text)
: ts__default["default"].visitEachChild(current, walk, context));
}),
]);
return result.transformed[0];
}
function extractClassDebugInfo(clazz, reflection, compilerHost, rootDirs, forbidOrphanRendering) {
if (!reflection.isClass(clazz)) {
return null;
}
const srcFile = clazz.getSourceFile();
const srcFileMaybeRelativePath = getProjectRelativePath(srcFile, rootDirs, compilerHost);
return {
type: new checker.WrappedNodeExpr(clazz.name),
className: checker.literal(clazz.name.getText()),
filePath: srcFileMaybeRelativePath ? checker.literal(srcFileMaybeRelativePath) : null,
lineNumber: checker.literal(srcFile.getLineAndCharacterOfPosition(clazz.name.pos).line + 1),
forbidOrphanRendering,
};
}
/**
* This registry does nothing.
*/
class NoopReferencesRegistry {
add(source, ...references) { }
}
function extractSchemas(rawExpr, evaluator, context) {
const schemas = [];
const result = evaluator.evaluate(rawExpr);
if (!Array.isArray(result)) {
throw createValueHasWrongTypeError(rawExpr, result, `${context}.schemas must be an array`);
}
for (const schemaRef of result) {
if (!(schemaRef instanceof checker.Reference)) {
throw createValueHasWrongTypeError(rawExpr, result, `${context}.schemas must be an array of schemas`);
}
const id = schemaRef.getIdentityIn(schemaRef.node.getSourceFile());
if (id === null || schemaRef.ownedByModuleGuess !== '@angular/core') {
throw createValueHasWrongTypeError(rawExpr, result, `${context}.schemas must be an array of schemas`);
}
// Since `id` is the `ts.Identifier` within the schema ref's declaration file, it's safe to
// use `id.text` here to figure out which schema is in use. Even if the actual reference was
// renamed when the user imported it, these names will match.
switch (id.text) {
case 'CUSTOM_ELEMENTS_SCHEMA':
schemas.push(checker.CUSTOM_ELEMENTS_SCHEMA);
break;
case 'NO_ERRORS_SCHEMA':
schemas.push(checker.NO_ERRORS_SCHEMA);
break;
default:
throw createValueHasWrongTypeError(rawExpr, schemaRef, `'${schemaRef.debugName}' is not a valid ${context} schema`);
}
}
return schemas;
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/** Generates additional fields to be added to a class that has inputs with transform functions. */
function compileInputTransformFields(inputs) {
const extraFields = [];
for (const input of inputs) {
// Note: Signal inputs capture their transform `WriteT` as part of the `InputSignal`.
// Such inputs will not have a `transform` captured and not generate coercion members.
if (input.transform) {
extraFields.push({
name: `ngAcceptInputType_${input.classPropertyName}`,
type: checker.transplantedType(input.transform.type),
statements: [],
initializer: null,
deferrableImports: null,
});
}
}
return extraFields;
}
/**
* Registry that keeps track of Angular declarations that are explicitly
* marked for JIT compilation and are skipping compilation by trait handlers.
*/
class JitDeclarationRegistry {
jitDeclarations = new Set();
}
/**
* Represents a symbol that is recognizable across incremental rebuilds, which enables the captured
* metadata to be compared to the prior compilation. This allows for semantic understanding of
* the changes that have been made in a rebuild, which potentially enables more reuse of work
* from the prior compilation.
*/
class SemanticSymbol {
decl;
/**
* The path of the file that declares this symbol.
*/
path;
/**
* The identifier of this symbol, or null if no identifier could be determined. It should
* uniquely identify the symbol relative to `file`. This is typically just the name of a
* top-level class declaration, as that uniquely identifies the class within the file.
*
* If the identifier is null, then this symbol cannot be recognized across rebuilds. In that
* case, the symbol is always assumed to have semantically changed to guarantee a proper
* rebuild.
*/
identifier;
constructor(
/**
* The declaration for this symbol.
*/
decl) {
this.decl = decl;
this.path = checker.absoluteFromSourceFile(decl.getSourceFile());
this.identifier = getSymbolIdentifier(decl);
}
}
function getSymbolIdentifier(decl) {
if (!ts__default["default"].isSourceFile(decl.parent)) {
return null;
}
// If this is a top-level class declaration, the class name is used as unique identifier.
// Other scenarios are currently not supported and causes the symbol not to be identified
// across rebuilds, unless the declaration node has not changed.
return decl.name.text;
}
/**
* Represents a declaration for which no semantic symbol has been registered. For example,
* declarations from external dependencies have not been explicitly registered and are represented
* by this symbol. This allows the unresolved symbol to still be compared to a symbol from a prior
* compilation.
*/
class OpaqueSymbol extends SemanticSymbol {
isPublicApiAffected() {
return false;
}
isTypeCheckApiAffected() {
return false;
}
}
/**
* The semantic dependency graph of a single compilation.
*/
class SemanticDepGraph {
files = new Map();
// Note: the explicit type annotation is used to work around a CI failure on Windows:
// error TS2742: The inferred type of 'symbolByDecl' cannot be named without a reference to
// '../../../../../../../external/npm/node_modules/typescript/lib/typescript'. This is likely
// not portable. A type annotation is necessary.
symbolByDecl = new Map();
/**
* Registers a symbol in the graph. The symbol is given a unique identifier if possible, such that
* its equivalent symbol can be obtained from a prior graph even if its declaration node has
* changed across rebuilds. Symbols without an identifier are only able to find themselves in a
* prior graph if their declaration node is identical.
*/
registerSymbol(symbol) {
this.symbolByDecl.set(symbol.decl, symbol);
if (symbol.identifier !== null) {
// If the symbol has a unique identifier, record it in the file that declares it. This enables
// the symbol to be requested by its unique name.
if (!this.files.has(symbol.path)) {
this.files.set(symbol.path, new Map());
}
this.files.get(symbol.path).set(symbol.identifier, symbol);
}
}
/**
* Attempts to resolve a symbol in this graph that represents the given symbol from another graph.
* If no matching symbol could be found, null is returned.
*
* @param symbol The symbol from another graph for which its equivalent in this graph should be
* found.
*/
getEquivalentSymbol(symbol) {
// First lookup the symbol by its declaration. It is typical for the declaration to not have
// changed across rebuilds, so this is likely to find the symbol. Using the declaration also
// allows to diff symbols for which no unique identifier could be determined.
let previousSymbol = this.getSymbolByDecl(symbol.decl);
if (previousSymbol === null && symbol.identifier !== null) {
// The declaration could not be resolved to a symbol in a prior compilation, which may
// happen because the file containing the declaration has changed. In that case we want to
// lookup the symbol based on its unique identifier, as that allows us to still compare the
// changed declaration to the prior compilation.
previousSymbol = this.getSymbolByName(symbol.path, symbol.identifier);
}
return previousSymbol;
}
/**
* Attempts to find the symbol by its identifier.
*/
getSymbolByName(path, identifier) {
if (!this.files.has(path)) {
return null;
}
const file = this.files.get(path);
if (!file.has(identifier)) {
return null;
}
return file.get(identifier);
}
/**
* Attempts to resolve the declaration to its semantic symbol.
*/
getSymbolByDecl(decl) {
if (!this.symbolByDecl.has(decl)) {
return null;
}
return this.symbolByDecl.get(decl);
}
}
/**
* Implements the logic to go from a previous dependency graph to a new one, along with information
* on which files have been affected.
*/
class SemanticDepGraphUpdater {
priorGraph;
newGraph = new SemanticDepGraph();
/**
* Contains opaque symbols that were created for declarations for which there was no symbol
* registered, which happens for e.g. external declarations.
*/
opaqueSymbols = new Map();
constructor(
/**
* The semantic dependency graph of the most recently succeeded compilation, or null if this
* is the initial build.
*/
priorGraph) {
this.priorGraph = priorGraph;
}
/**
* Registers the symbol in the new graph that is being created.
*/
registerSymbol(symbol) {
this.newGraph.registerSymbol(symbol);
}
/**
* Takes all facts that have been gathered to create a new semantic dependency graph. In this
* process, the semantic impact of the changes is determined which results in a set of files that
* need to be emitted and/or type-checked.
*/
finalize() {
if (this.priorGraph === null) {
// If no prior dependency graph is available then this was the initial build, in which case
// we don't need to determine the semantic impact as everything is already considered
// logically changed.
return {
needsEmit: new Set(),
needsTypeCheckEmit: new Set(),
newGraph: this.newGraph,
};
}
const needsEmit = this.determineInvalidatedFiles(this.priorGraph);
const needsTypeCheckEmit = this.determineInvalidatedTypeCheckFiles(this.priorGraph);
return {
needsEmit,
needsTypeCheckEmit,
newGraph: this.newGraph,
};
}
determineInvalidatedFiles(priorGraph) {
const isPublicApiAffected = new Set();
// The first phase is to collect all symbols which have their public API affected. Any symbols
// that cannot be matched up with a symbol from the prior graph are considered affected.
for (const symbol of this.newGraph.symbolByDecl.values()) {
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isPublicApiAffected(previousSymbol)) {
isPublicApiAffected.add(symbol);
}
}
// The second phase is to find all symbols for which the emit result is affected, either because
// their used declarations have changed or any of those used declarations has had its public API
// affected as determined in the first phase.
const needsEmit = new Set();
for (const symbol of this.newGraph.symbolByDecl.values()) {
if (symbol.isEmitAffected === undefined) {
continue;
}
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isEmitAffected(previousSymbol, isPublicApiAffected)) {
needsEmit.add(symbol.path);
}
}
return needsEmit;
}
determineInvalidatedTypeCheckFiles(priorGraph) {
const isTypeCheckApiAffected = new Set();
// The first phase is to collect all symbols which have their public API affected. Any symbols
// that cannot be matched up with a symbol from the prior graph are considered affected.
for (const symbol of this.newGraph.symbolByDecl.values()) {
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isTypeCheckApiAffected(previousSymbol)) {
isTypeCheckApiAffected.add(symbol);
}
}
// The second phase is to find all symbols for which the emit result is affected, either because
// their used declarations have changed or any of those used declarations has had its public API
// affected as determined in the first phase.
const needsTypeCheckEmit = new Set();
for (const symbol of this.newGraph.symbolByDecl.values()) {
if (symbol.isTypeCheckBlockAffected === undefined) {
continue;
}
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null ||
symbol.isTypeCheckBlockAffected(previousSymbol, isTypeCheckApiAffected)) {
needsTypeCheckEmit.add(symbol.path);
}
}
return needsTypeCheckEmit;
}
/**
* Creates a `SemanticReference` for the reference to `decl` using the expression `expr`. See
* the documentation of `SemanticReference` for details.
*/
getSemanticReference(decl, expr) {
return {
symbol: this.getSymbol(decl),
importPath: getImportPath(expr),
};
}
/**
* Gets the `SemanticSymbol` that was registered for `decl` during the current compilation, or
* returns an opaque symbol that represents `decl`.
*/
getSymbol(decl) {
const symbol = this.newGraph.getSymbolByDecl(decl);
if (symbol === null) {
// No symbol has been recorded for the provided declaration, which would be the case if the
// declaration is external. Return an opaque symbol in that case, to allow the external
// declaration to be compared to a prior compilation.
return this.getOpaqueSymbol(decl);
}
return symbol;
}
/**
* Gets or creates an `OpaqueSymbol` for the provided class declaration.
*/
getOpaqueSymbol(decl) {
if (this.opaqueSymbols.has(decl)) {
return this.opaqueSymbols.get(decl);
}
const symbol = new OpaqueSymbol(decl);
this.opaqueSymbols.set(decl, symbol);
return symbol;
}
}
function getImportPath(expr) {
if (expr instanceof checker.ExternalExpr) {
return `${expr.value.moduleName}\$${expr.value.name}`;
}
else {
return null;
}
}
/**
* Determines whether the provided symbols represent the same declaration.
*/
function isSymbolEqual(a, b) {
if (a.decl === b.decl) {
// If the declaration is identical then it must represent the same symbol.
return true;
}
if (a.identifier === null || b.identifier === null) {
// Unidentifiable symbols are assumed to be different.
return false;
}
return a.path === b.path && a.identifier === b.identifier;
}
/**
* Determines whether the provided references to a semantic symbol are still equal, i.e. represent
* the same symbol and are imported by the same path.
*/
function isReferenceEqual(a, b) {
if (!isSymbolEqual(a.symbol, b.symbol)) {
// If the reference's target symbols are different, the reference itself is different.
return false;
}
// The reference still corresponds with the same symbol, now check that the path by which it is
// imported has not changed.
return a.importPath === b.importPath;
}
function referenceEquality(a, b) {
return a === b;
}
/**
* Determines if the provided arrays are equal to each other, using the provided equality tester
* that is called for all entries in the array.
*/
function isArrayEqual(a, b, equalityTester = referenceEquality) {
if (a === null || b === null) {
return a === b;
}
if (a.length !== b.length) {
return false;
}
return !a.some((item, index) => !equalityTester(item, b[index]));
}
/**
* Determines if the provided sets are equal to each other, using the provided equality tester.
* Sets that only differ in ordering are considered equal.
*/
function isSetEqual(a, b, equalityTester = referenceEquality) {
if (a === null || b === null) {
return a === b;
}
if (a.size !== b.size) {
return false;
}
for (const itemA of a) {
let found = false;
for (const itemB of b) {
if (equalityTester(itemA, itemB)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
/**
* Converts the type parameters of the given class into their semantic representation. If the class
* does not have any type parameters, then `null` is returned.
*/
function extractSemanticTypeParameters(node) {
if (!ts__default["default"].isClassDeclaration(node) || node.typeParameters === undefined) {
return null;
}
return node.typeParameters.map((typeParam) => ({
hasGenericTypeBound: typeParam.constraint !== undefined,
}));
}
/**
* Compares the list of type parameters to determine if they can be considered equal.
*/
function areTypeParametersEqual(current, previous) {
// First compare all type parameters one-to-one; any differences mean that the list of type
// parameters has changed.
if (!isArrayEqual(current, previous, isTypeParameterEqual)) {
return false;
}
// If there is a current list of type parameters and if any of them has a generic type constraint,
// then the meaning of that type parameter may have changed without us being aware; as such we
// have to assume that the type parameters have in fact changed.
if (current !== null && current.some((typeParam) => typeParam.hasGenericTypeBound)) {
return false;
}
return true;
}
function isTypeParameterEqual(a, b) {
return a.hasGenericTypeBound === b.hasGenericTypeBound;
}
/**
* A `ComponentScopeReader` that reads from an ordered set of child readers until it obtains the
* requested scope.
*
* This is used to combine `ComponentScopeReader`s that read from different sources (e.g. from a
* registry and from the incremental state).
*/
class CompoundComponentScopeReader {
readers;
constructor(readers) {
this.readers = readers;
}
getScopeForComponent(clazz) {
for (const reader of this.readers) {
const meta = reader.getScopeForComponent(clazz);
if (meta !== null) {
return meta;
}
}
return null;
}
getRemoteScope(clazz) {
for (const reader of this.readers) {
const remoteScope = reader.getRemoteScope(clazz);
if (remoteScope !== null) {
return remoteScope;
}
}
return null;
}
}
/**
* Reads Angular metadata from classes declared in .d.ts files and computes an `ExportScope`.
*
* Given an NgModule declared in a .d.ts file, this resolver can produce a transitive `ExportScope`
* of all of the directives/pipes it exports. It does this by reading metadata off of Ivy static
* fields on directives, components, pipes, and NgModules.
*/
class MetadataDtsModuleScopeResolver {
dtsMetaReader;
aliasingHost;
/**
* Cache which holds fully resolved scopes for NgModule classes from .d.ts files.
*/
cache = new Map();
/**
* @param dtsMetaReader a `MetadataReader` which can read metadata from `.d.ts` files.
*/
constructor(dtsMetaReader, aliasingHost) {
this.dtsMetaReader = dtsMetaReader;
this.aliasingHost = aliasingHost;
}
/**
* Resolve a `Reference`'d NgModule from a .d.ts file and produce a transitive `ExportScope`
* listing the directives and pipes which that NgModule exports to others.
*
* This operation relies on a `Reference` instead of a direct TypeScript node as the `Reference`s
* produced depend on how the original NgModule was imported.
*/
resolve(ref) {
const clazz = ref.node;
const sourceFile = clazz.getSourceFile();
if (!sourceFile.isDeclarationFile) {
throw new Error(`Debug error: DtsModuleScopeResolver.read(${ref.debugName} from ${sourceFile.fileName}), but not a .d.ts file`);
}
if (this.cache.has(clazz)) {
return this.cache.get(clazz);
}
// Build up the export scope - those directives and pipes made visible by this module.
const dependencies = [];
const meta = this.dtsMetaReader.getNgModuleMetadata(ref);
if (meta === null) {
this.cache.set(clazz, null);
return null;
}
const declarations = new Set();
for (const declRef of meta.declarations) {
declarations.add(declRef.node);
}
// Only the 'exports' field of the NgModule's metadata is important. Imports and declarations
// don't affect the export scope.
for (const exportRef of meta.exports) {
// Attempt to process the export as a directive.
const directive = this.dtsMetaReader.getDirectiveMetadata(exportRef);
if (directive !== null) {
const isReExport = !declarations.has(exportRef.node);
dependencies.push(this.maybeAlias(directive, sourceFile, isReExport));
continue;
}
// Attempt to process the export as a pipe.
const pipe = this.dtsMetaReader.getPipeMetadata(exportRef);
if (pipe !== null) {
const isReExport = !declarations.has(exportRef.node);
dependencies.push(this.maybeAlias(pipe, sourceFile, isReExport));
continue;
}
// Attempt to process the export as a module.
const exportScope = this.resolve(exportRef);
if (exportScope !== null) {
// It is a module. Add exported directives and pipes to the current scope. This might
// involve rewriting the `Reference`s to those types to have an alias expression if one is
// required.
if (this.aliasingHost === null) {
// Fast path when aliases aren't required.
dependencies.push(...exportScope.exported.dependencies);
}
else {
// It's necessary to rewrite the `Reference`s to add alias expressions. This way, imports
// generated to these directives and pipes will use a shallow import to `sourceFile`
// instead of a deep import directly to the directive or pipe class.
//
// One important check here is whether the directive/pipe is declared in the same
// source file as the re-exporting NgModule. This can happen if both a directive, its
// NgModule, and the re-exporting NgModule are all in the same file. In this case,
// no import alias is needed as it would go to the same file anyway.
for (const dep of exportScope.exported.dependencies) {
dependencies.push(this.maybeAlias(dep, sourceFile, /* isReExport */ true));
}
}
}
continue;
// The export was not a directive, a pipe, or a module. This is an error.
// TODO(alxhub): produce a ts.Diagnostic
}
const exportScope = {
exported: {
dependencies,
isPoisoned: meta.isPoisoned,
},
};
this.cache.set(clazz, exportScope);
return exportScope;
}
maybeAlias(dirOrPipe, maybeAliasFrom, isReExport) {
const ref = dirOrPipe.ref;
if (this.aliasingHost === null || ref.node.getSourceFile() === maybeAliasFrom) {
return dirOrPipe;
}
const alias = this.aliasingHost.getAliasIn(ref.node, maybeAliasFrom, isReExport);
if (alias === null) {
return dirOrPipe;
}
return {
...dirOrPipe,
ref: ref.cloneWithAlias(alias),
};
}
}
function getDiagnosticNode(ref, rawExpr) {
// Show the diagnostic on the node within `rawExpr` which references the declaration
// in question. `rawExpr` represents the raw expression from which `ref` was partially evaluated,
// so use that to find the right node. Note that by the type system, `rawExpr` might be `null`, so
// fall back on the declaration identifier in that case (even though in practice this should never
// happen since local NgModules always have associated expressions).
return rawExpr !== null ? ref.getOriginForDiagnostics(rawExpr) : ref.node.name;
}
function makeNotStandaloneDiagnostic(scopeReader, ref, rawExpr, kind) {
const scope = scopeReader.getScopeForComponent(ref.node);
let message = `The ${kind} '${ref.node.name.text}' appears in 'imports', but is not standalone and cannot be imported directly.`;
let relatedInformation = undefined;
if (scope !== null && scope.kind === checker.ComponentScopeKind.NgModule) {
// The directive/pipe in question is declared in an NgModule. Check if it's also exported.
const isExported = scope.exported.dependencies.some((dep) => dep.ref.node === ref.node);
const relatedInfoMessageText = isExported
? `It can be imported using its '${scope.ngModule.name.text}' NgModule instead.`
: `It's declared in the '${scope.ngModule.name.text}' NgModule, but is not exported. ` +
'Consider exporting it and importing the NgModule instead.';
relatedInformation = [checker.makeRelatedInformation(scope.ngModule.name, relatedInfoMessageText)];
}
if (relatedInformation === undefined) {
// If no contextual pointers can be provided to suggest a specific remedy, then at least tell
// the user broadly what they need to do.
message += ' It must be imported via an NgModule.';
}
return checker.makeDiagnostic(checker.ErrorCode.COMPONENT_IMPORT_NOT_STANDALONE, getDiagnosticNode(ref, rawExpr), message, relatedInformation);
}
function makeUnknownComponentImportDiagnostic(ref, rawExpr) {
return checker.makeDiagnostic(checker.ErrorCode.COMPONENT_UNKNOWN_IMPORT, getDiagnosticNode(ref, rawExpr), `Component imports must be standalone components, directives, pipes, or must be NgModules.`);
}
function makeUnknownComponentDeferredImportDiagnostic(ref, rawExpr) {
return checker.makeDiagnostic(checker.ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT, getDiagnosticNode(ref, rawExpr), `Component deferred imports must be standalone components, directives or pipes.`);
}
/** Value used to mark a module whose scope is in the process of being resolved. */
const IN_PROGRESS_RESOLUTION = {};
/**
* A registry which collects information about NgModules, Directives, Components, and Pipes which
* are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s
* which summarize the compilation scope of a component.
*
* This class implements the logic of NgModule declarations, imports, and exports and can produce,
* for a given component, the set of directives and pipes which are "visible" in that component's
* template.
*
* The `LocalModuleScopeRegistry` has two "modes" of operation. During analysis, data for each
* individual NgModule, Directive, Component, and Pipe is added to the registry. No attempt is made
* to traverse or validate the NgModule graph (imports, exports, etc). After analysis, one of
* `getScopeOfModule` or `getScopeForComponent` can be called, which traverses the NgModule graph
* and applies the NgModule logic to generate a `LocalModuleScope`, the full scope for the given
* module or component.
*
* The `LocalModuleScopeRegistry` is also capable of producing `ts.Diagnostic` errors when Angular
* semantics are violated.
*/
class LocalModuleScopeRegistry {
localReader;
fullReader;
dependencyScopeReader;
refEmitter;
aliasingHost;
/**
* Tracks whether the registry has been asked to produce scopes for a module or component. Once
* this is true, the registry cannot accept registrations of new directives/pipes/modules as it
* would invalidate the cached scope data.
*/
sealed = false;
/**
* A map of components from the current compilation unit to the NgModule which declared them.
*
* As components and directives are not distinguished at the NgModule level, this map may also
* contain directives. This doesn't cause any problems but isn't useful as there is no concept of
* a directive's compilation scope.
*/
declarationToModule = new Map();
/**
* This maps from the directive/pipe class to a map of data for each NgModule that declares the
* directive/pipe. This data is needed to produce an error for the given class.
*/
duplicateDeclarations = new Map();
moduleToRef = new Map();
/**
* A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program.
*/
cache = new Map();
/**
* Tracks the `RemoteScope` for components requiring "remote scoping".
*
* Remote scoping is when the set of directives which apply to a given component is set in the
* NgModule's file instead of directly on the component def (which is sometimes needed to get
* around cyclic import issues). This is not used in calculation of `LocalModuleScope`s, but is
* tracked here for convenience.
*/
remoteScoping = new Map();
/**
* Tracks errors accumulated in the processing of scopes for each module declaration.
*/
scopeErrors = new Map();
/**
* Tracks which NgModules have directives/pipes that are declared in more than one module.
*/
modulesWithStructuralErrors = new Set();
constructor(localReader, fullReader, dependencyScopeReader, refEmitter, aliasingHost) {
this.localReader = localReader;
this.fullReader = fullReader;
this.dependencyScopeReader = dependencyScopeReader;
this.refEmitter = refEmitter;
this.aliasingHost = aliasingHost;
}
/**
* Add an NgModule's data to the registry.
*/
registerNgModuleMetadata(data) {
this.assertCollecting();
const ngModule = data.ref.node;
this.moduleToRef.set(data.ref.node, data.ref);
// Iterate over the module's declarations, and add them to declarationToModule. If duplicates
// are found, they're instead tracked in duplicateDeclarations.
for (const decl of data.declarations) {
this.registerDeclarationOfModule(ngModule, decl, data.rawDeclarations);
}
}
registerDirectiveMetadata(directive) { }
registerPipeMetadata(pipe) { }
getScopeForComponent(clazz) {
const scope = !this.declarationToModule.has(clazz)
? null
: this.getScopeOfModule(this.declarationToModule.get(clazz).ngModule);
return scope;
}
/**
* If `node` is declared in more than one NgModule (duplicate declaration), then get the
* `DeclarationData` for each offending declaration.
*
* Ordinarily a class is only declared in one NgModule, in which case this function returns
* `null`.
*/
getDuplicateDeclarations(node) {
if (!this.duplicateDeclarations.has(node)) {
return null;
}
return Array.from(this.duplicateDeclarations.get(node).values());
}
/**
* Collects registered data for a module and its directives/pipes and convert it into a full
* `LocalModuleScope`.
*
* This method implements the logic of NgModule imports and exports. It returns the
* `LocalModuleScope` for the given NgModule if one can be produced, `null` if no scope was ever
* defined, or the string `'error'` if the scope contained errors.
*/
getScopeOfModule(clazz) {
return this.moduleToRef.has(clazz)
? this.getScopeOfModuleReference(this.moduleToRef.get(clazz))
: null;
}
/**
* Retrieves any `ts.Diagnostic`s produced during the calculation of the `LocalModuleScope` for
* the given NgModule, or `null` if no errors were present.
*/
getDiagnosticsOfModule(clazz) {
// Required to ensure the errors are populated for the given class. If it has been processed
// before, this will be a no-op due to the scope cache.
this.getScopeOfModule(clazz);
if (this.scopeErrors.has(clazz)) {
return this.scopeErrors.get(clazz);
}
else {
return null;
}
}
registerDeclarationOfModule(ngModule, decl, rawDeclarations) {
const declData = {
ngModule,
ref: decl,
rawDeclarations,
};
// First, check for duplicate declarations of the same directive/pipe.
if (this.duplicateDeclarations.has(decl.node)) {
// This directive/pipe has already been identified as being duplicated. Add this module to the
// map of modules for which a duplicate declaration exists.
this.duplicateDeclarations.get(decl.node).set(ngModule, declData);
}
else if (this.declarationToModule.has(decl.node) &&
this.declarationToModule.get(decl.node).ngModule !== ngModule) {
// This directive/pipe is already registered as declared in another module. Mark it as a
// duplicate instead.
const duplicateDeclMap = new Map();
const firstDeclData = this.declarationToModule.get(decl.node);
// Mark both modules as having duplicate declarations.
this.modulesWithStructuralErrors.add(firstDeclData.ngModule);
this.modulesWithStructuralErrors.add(ngModule);
// Being detected as a duplicate means there are two NgModules (for now) which declare this
// directive/pipe. Add both of them to the duplicate tracking map.
duplicateDeclMap.set(firstDeclData.ngModule, firstDeclData);
duplicateDeclMap.set(ngModule, declData);
this.duplicateDeclarations.set(decl.node, duplicateDeclMap);
// Remove the directive/pipe from `declarationToModule` as it's a duplicate declaration, and
// therefore not valid.
this.declarationToModule.delete(decl.node);
}
else {
// This is the first declaration of this directive/pipe, so map it.
this.declarationToModule.set(decl.node, declData);
}
}
/**
* Implementation of `getScopeOfModule` which accepts a reference to a class.
*/
getScopeOfModuleReference(ref) {
if (this.cache.has(ref.node)) {
const cachedValue = this.cache.get(ref.node);
if (cachedValue !== IN_PROGRESS_RESOLUTION) {
return cachedValue;
}
}
this.cache.set(ref.node, IN_PROGRESS_RESOLUTION);
// Seal the registry to protect the integrity of the `LocalModuleScope` cache.
this.sealed = true;
// `ref` should be an NgModule previously added to the registry. If not, a scope for it
// cannot be produced.
const ngModule = this.localReader.getNgModuleMetadata(ref);
if (ngModule === null) {
this.cache.set(ref.node, null);
return null;
}
// Errors produced during computation of the scope are recorded here. At the end, if this array
// isn't empty then `undefined` will be cached and returned to indicate this scope is invalid.
const diagnostics = [];
// At this point, the goal is to produce two distinct transitive sets:
// - the directives and pipes which are visible to components declared in the NgModule.
// - the directives and pipes which are exported to any NgModules which import this one.
// Directives and pipes in the compilation scope.
const compilationDirectives = new Map();
const compilationPipes = new Map();
const declared = new Set();
// Directives and pipes exported to any importing NgModules.
const exportDirectives = new Map();
const exportPipes = new Map();
// The algorithm is as follows:
// 1) Add all of the directives/pipes from each NgModule imported into the current one to the
// compilation scope.
// 2) Add directives/pipes declared in the NgModule to the compilation scope. At this point, the
// compilation scope is complete.
// 3) For each entry in the NgModule's exports:
// a) Attempt to resolve it as an NgModule with its own exported directives/pipes. If it is
// one, add them to the export scope of this NgModule.
// b) Otherwise, it should be a class in the compilation scope of this NgModule. If it is,
// add it to the export scope.
// c) If it's neither an NgModule nor a directive/pipe in the compilation scope, then this
// is an error.
//
let isPoisoned = false;
if (this.modulesWithStructuralErrors.has(ngModule.ref.node)) {
// If the module contains declarations that are duplicates, then it's considered poisoned.
isPoisoned = true;
}
// 1) process imports.
for (const decl of ngModule.imports) {
const importScope = this.getExportedScope(decl, diagnostics, ref.node, 'import');
if (importScope !== null) {
if (importScope === 'invalid' ||
importScope === 'cycle' ||
importScope.exported.isPoisoned) {
// An import was an NgModule but contained errors of its own. Record this as an error too,
// because this scope is always going to be incorrect if one of its imports could not be
// read.
isPoisoned = true;
// Prevent the module from reporting a diagnostic about itself when there's a cycle.
if (importScope !== 'cycle') {
diagnostics.push(invalidTransitiveNgModuleRef(decl, ngModule.rawImports, 'import'));
}
if (importScope === 'invalid' || importScope === 'cycle') {
continue;
}
}
for (const dep of importScope.exported.dependencies) {
if (dep.kind === checker.MetaKind.Directive) {
compilationDirectives.set(dep.ref.node, dep);
}
else if (dep.kind === checker.MetaKind.Pipe) {
compilationPipes.set(dep.ref.node, dep);
}
}
// Successfully processed the import as an NgModule (even if it had errors).
continue;
}
// The import wasn't an NgModule. Maybe it's a standalone entity?
const directive = this.fullReader.getDirectiveMetadata(decl);
if (directive !== null) {
if (directive.isStandalone) {
compilationDirectives.set(directive.ref.node, directive);
}
else {
// Error: can't import a non-standalone component/directive.
diagnostics.push(makeNotStandaloneDiagnostic(this, decl, ngModule.rawImports, directive.isComponent ? 'component' : 'directive'));
isPoisoned = true;
}
continue;
}
// It wasn't a directive (standalone or otherwise). Maybe a pipe?
const pipe = this.fullReader.getPipeMetadata(decl);
if (pipe !== null) {
if (pipe.isStandalone) {
compilationPipes.set(pipe.ref.node, pipe);
}
else {
diagnostics.push(makeNotStandaloneDiagnostic(this, decl, ngModule.rawImports, 'pipe'));
isPoisoned = true;
}
continue;
}
// This reference was neither another NgModule nor a standalone entity. Report it as invalid.
diagnostics.push(invalidRef(decl, ngModule.rawImports, 'import'));
isPoisoned = true;
}
// 2) add declarations.
for (const decl of ngModule.declarations) {
const directive = this.localReader.getDirectiveMetadata(decl);
const pipe = this.localReader.getPipeMetadata(decl);
if (directive !== null) {
if (directive.isStandalone) {
const refType = directive.isComponent ? 'Component' : 'Directive';
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.NGMODULE_DECLARATION_IS_STANDALONE, decl.getOriginForDiagnostics(ngModule.rawDeclarations), `${refType} ${decl.node.name.text} is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?`));
isPoisoned = true;
continue;
}
compilationDirectives.set(decl.node, { ...directive, ref: decl });
if (directive.isPoisoned) {
isPoisoned = true;
}
}
else if (pipe !== null) {
if (pipe.isStandalone) {
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.NGMODULE_DECLARATION_IS_STANDALONE, decl.getOriginForDiagnostics(ngModule.rawDeclarations), `Pipe ${decl.node.name.text} is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?`));
isPoisoned = true;
continue;
}
compilationPipes.set(decl.node, { ...pipe, ref: decl });
}
else {
const errorNode = decl.getOriginForDiagnostics(ngModule.rawDeclarations);
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode, `The class '${decl.node.name.text}' is listed in the declarations ` +
`of the NgModule '${ngModule.ref.node.name.text}', but is not a directive, a component, or a pipe. ` +
`Either remove it from the NgModule's declarations, or add an appropriate Angular decorator.`, [checker.makeRelatedInformation(decl.node.name, `'${decl.node.name.text}' is declared here.`)]));
isPoisoned = true;
continue;
}
declared.add(decl.node);
}
// 3) process exports.
// Exports can contain modules, components, or directives. They're processed differently.
// Modules are straightforward. Directives and pipes from exported modules are added to the
// export maps. Directives/pipes are different - they might be exports of declared types or
// imported types.
for (const decl of ngModule.exports) {
// Attempt to resolve decl as an NgModule.
const exportScope = this.getExportedScope(decl, diagnostics, ref.node, 'export');
if (exportScope === 'invalid' ||
exportScope === 'cycle' ||
(exportScope !== null && exportScope.exported.isPoisoned)) {
// An export was an NgModule but contained errors of its own. Record this as an error too,
// because this scope is always going to be incorrect if one of its exports could not be
// read.
isPoisoned = true;
// Prevent the module from reporting a diagnostic about itself when there's a cycle.
if (exportScope !== 'cycle') {
diagnostics.push(invalidTransitiveNgModuleRef(decl, ngModule.rawExports, 'export'));
}
if (exportScope === 'invalid' || exportScope === 'cycle') {
continue;
}
}
else if (exportScope !== null) {
// decl is an NgModule.
for (const dep of exportScope.exported.dependencies) {
if (dep.kind == checker.MetaKind.Directive) {
exportDirectives.set(dep.ref.node, dep);
}
else if (dep.kind === checker.MetaKind.Pipe) {
exportPipes.set(dep.ref.node, dep);
}
}
}
else if (compilationDirectives.has(decl.node)) {
// decl is a directive or component in the compilation scope of this NgModule.
const directive = compilationDirectives.get(decl.node);
exportDirectives.set(decl.node, directive);
}
else if (compilationPipes.has(decl.node)) {
// decl is a pipe in the compilation scope of this NgModule.
const pipe = compilationPipes.get(decl.node);
exportPipes.set(decl.node, pipe);
}
else {
// decl is an unknown export.
const dirMeta = this.fullReader.getDirectiveMetadata(decl);
const pipeMeta = this.fullReader.getPipeMetadata(decl);
if (dirMeta !== null || pipeMeta !== null) {
const isStandalone = dirMeta !== null ? dirMeta.isStandalone : pipeMeta.isStandalone;
diagnostics.push(invalidReexport(decl, ngModule.rawExports, isStandalone));
}
else {
diagnostics.push(invalidRef(decl, ngModule.rawExports, 'export'));
}
isPoisoned = true;
continue;
}
}
const exported = {
dependencies: [...exportDirectives.values(), ...exportPipes.values()],
isPoisoned,
};
const reexports = this.getReexports(ngModule, ref, declared, exported.dependencies, diagnostics);
// Finally, produce the `LocalModuleScope` with both the compilation and export scopes.
const scope = {
kind: checker.ComponentScopeKind.NgModule,
ngModule: ngModule.ref.node,
compilation: {
dependencies: [...compilationDirectives.values(), ...compilationPipes.values()],
isPoisoned,
},
exported,
reexports,
schemas: ngModule.schemas,
};
// Check if this scope had any errors during production.
if (diagnostics.length > 0) {
// Save the errors for retrieval.
this.scopeErrors.set(ref.node, diagnostics);
// Mark this module as being tainted.
this.modulesWithStructuralErrors.add(ref.node);
}
this.cache.set(ref.node, scope);
return scope;
}
/**
* Check whether a component requires remote scoping.
*/
getRemoteScope(node) {
return this.remoteScoping.has(node) ? this.remoteScoping.get(node) : null;
}
/**
* Set a component as requiring remote scoping, with the given directives and pipes to be
* registered remotely.
*/
setComponentRemoteScope(node, directives, pipes) {
this.remoteScoping.set(node, { directives, pipes });
}
/**
* Look up the `ExportScope` of a given `Reference` to an NgModule.
*
* The NgModule in question may be declared locally in the current ts.Program, or it may be
* declared in a .d.ts file.
*
* @returns `null` if no scope could be found, or `'invalid'` if the `Reference` is not a valid
* NgModule.
*
* May also contribute diagnostics of its own by adding to the given `diagnostics`
* array parameter.
*/
getExportedScope(ref, diagnostics, ownerForErrors, type) {
if (ref.node.getSourceFile().isDeclarationFile) {
// The NgModule is declared in a .d.ts file. Resolve it with the `DependencyScopeReader`.
if (!ts__default["default"].isClassDeclaration(ref.node)) {
// The NgModule is in a .d.ts file but is not declared as a ts.ClassDeclaration. This is an
// error in the .d.ts metadata.
const code = type === 'import' ? checker.ErrorCode.NGMODULE_INVALID_IMPORT : checker.ErrorCode.NGMODULE_INVALID_EXPORT;
diagnostics.push(checker.makeDiagnostic(code, checker.identifierOfNode(ref.node) || ref.node, `Appears in the NgModule.${type}s of ${checker.nodeNameForError(ownerForErrors)}, but could not be resolved to an NgModule`));
return 'invalid';
}
return this.dependencyScopeReader.resolve(ref);
}
else {
if (this.cache.get(ref.node) === IN_PROGRESS_RESOLUTION) {
diagnostics.push(checker.makeDiagnostic(type === 'import'
? checker.ErrorCode.NGMODULE_INVALID_IMPORT
: checker.ErrorCode.NGMODULE_INVALID_EXPORT, checker.identifierOfNode(ref.node) || ref.node, `NgModule "${type}" field contains a cycle`));
return 'cycle';
}
// The NgModule is declared locally in the current program. Resolve it from the registry.
return this.getScopeOfModuleReference(ref);
}
}
getReexports(ngModule, ref, declared, exported, diagnostics) {
let reexports = null;
const sourceFile = ref.node.getSourceFile();
if (this.aliasingHost === null) {
return null;
}
reexports = [];
// Track re-exports by symbol name, to produce diagnostics if two alias re-exports would share
// the same name.
const reexportMap = new Map();
// Alias ngModuleRef added for readability below.
const ngModuleRef = ref;
const addReexport = (exportRef) => {
if (exportRef.node.getSourceFile() === sourceFile) {
return;
}
const isReExport = !declared.has(exportRef.node);
const exportName = this.aliasingHost.maybeAliasSymbolAs(exportRef, sourceFile, ngModule.ref.node.name.text, isReExport);
if (exportName === null) {
return;
}
if (!reexportMap.has(exportName)) {
if (exportRef.alias && exportRef.alias instanceof checker.ExternalExpr) {
reexports.push({
fromModule: exportRef.alias.value.moduleName,
symbolName: exportRef.alias.value.name,
asAlias: exportName,
});
}
else {
const emittedRef = this.refEmitter.emit(exportRef.cloneWithNoIdentifiers(), sourceFile);
checker.assertSuccessfulReferenceEmit(emittedRef, ngModuleRef.node.name, 'class');
const expr = emittedRef.expression;
if (!(expr instanceof checker.ExternalExpr) ||
expr.value.moduleName === null ||
expr.value.name === null) {
throw new Error('Expected ExternalExpr');
}
reexports.push({
fromModule: expr.value.moduleName,
symbolName: expr.value.name,
asAlias: exportName,
});
}
reexportMap.set(exportName, exportRef);
}
else {
// Another re-export already used this name. Produce a diagnostic.
const prevRef = reexportMap.get(exportName);
diagnostics.push(reexportCollision(ngModuleRef.node, prevRef, exportRef));
}
};
for (const { ref } of exported) {
addReexport(ref);
}
return reexports;
}
assertCollecting() {
if (this.sealed) {
throw new Error(`Assertion: LocalModuleScopeRegistry is not COLLECTING`);
}
}
}
/**
* Produce a `ts.Diagnostic` for an invalid import or export from an NgModule.
*/
function invalidRef(decl, rawExpr, type) {
const code = type === 'import' ? checker.ErrorCode.NGMODULE_INVALID_IMPORT : checker.ErrorCode.NGMODULE_INVALID_EXPORT;
const resolveTarget = type === 'import' ? 'NgModule' : 'NgModule, Component, Directive, or Pipe';
const message = `'${decl.node.name.text}' does not appear to be an ${resolveTarget} class.`;
const library = decl.ownedByModuleGuess !== null ? ` (${decl.ownedByModuleGuess})` : '';
const sf = decl.node.getSourceFile();
let relatedMessage;
// Provide extra context to the error for the user.
if (!sf.isDeclarationFile) {
// This is a file in the user's program. Highlight the class as undecorated.
const annotationType = type === 'import' ? '@NgModule' : 'Angular';
relatedMessage = `Is it missing an ${annotationType} annotation?`;
}
else if (sf.fileName.indexOf('node_modules') !== -1) {
// This file comes from a third-party library in node_modules.
relatedMessage =
`This likely means that the library${library} which declares ${decl.debugName} is not ` +
'compatible with Angular Ivy. Check if a newer version of the library is available, ' +
"and update if so. Also consider checking with the library's authors to see if the " +
'library is expected to be compatible with Ivy.';
}
else {
// This is a monorepo style local dependency. Unfortunately these are too different to really
// offer much more advice than this.
relatedMessage = `This likely means that the dependency${library} which declares ${decl.debugName} is not compatible with Angular Ivy.`;
}
return checker.makeDiagnostic(code, getDiagnosticNode(decl, rawExpr), message, [
checker.makeRelatedInformation(decl.node.name, relatedMessage),
]);
}
/**
* Produce a `ts.Diagnostic` for an import or export which itself has errors.
*/
function invalidTransitiveNgModuleRef(decl, rawExpr, type) {
const code = type === 'import' ? checker.ErrorCode.NGMODULE_INVALID_IMPORT : checker.ErrorCode.NGMODULE_INVALID_EXPORT;
return checker.makeDiagnostic(code, getDiagnosticNode(decl, rawExpr), `This ${type} contains errors, which may affect components that depend on this NgModule.`);
}
/**
* Produce a `ts.Diagnostic` for an exported directive or pipe which was not declared or imported
* by the NgModule in question.
*/
function invalidReexport(decl, rawExpr, isStandalone) {
// The root error is the same here - this export is not valid. Give a helpful error message based
// on the specific circumstance.
let message = `Can't be exported from this NgModule, as `;
if (isStandalone) {
// Standalone types need to be imported into an NgModule before they can be re-exported.
message += 'it must be imported first';
}
else if (decl.node.getSourceFile().isDeclarationFile) {
// Non-standalone types can be re-exported, but need to be imported into the NgModule first.
// This requires importing their own NgModule.
message += 'it must be imported via its NgModule first';
}
else {
// Local non-standalone types must either be declared directly by this NgModule, or imported as
// above.
message +=
'it must be either declared by this NgModule, or imported here via its NgModule first';
}
return checker.makeDiagnostic(checker.ErrorCode.NGMODULE_INVALID_REEXPORT, getDiagnosticNode(decl, rawExpr), message);
}
/**
* Produce a `ts.Diagnostic` for a collision in re-export names between two directives/pipes.
*/
function reexportCollision(module, refA, refB) {
const childMessageText = `This directive/pipe is part of the exports of '${module.name.text}' and shares the same name as another exported directive/pipe.`;
return checker.makeDiagnostic(checker.ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION, module.name, `
There was a name collision between two classes named '${refA.node.name.text}', which are both part of the exports of '${module.name.text}'.
Angular generates re-exports of an NgModule's exported directives/pipes from the module's source file in certain cases, using the declared name of the class. If two classes of the same name are exported, this automatic naming does not work.
To fix this problem please re-export one or both classes directly from this file.
`.trim(), [
checker.makeRelatedInformation(refA.node.name, childMessageText),
checker.makeRelatedInformation(refB.node.name, childMessageText),
]);
}
/**
* Computes scope information to be used in template type checking.
*/
class TypeCheckScopeRegistry {
scopeReader;
metaReader;
hostDirectivesResolver;
/**
* Cache of flattened directive metadata. Because flattened metadata is scope-invariant it's
* cached individually, such that all scopes refer to the same flattened metadata.
*/
flattenedDirectiveMetaCache = new Map();
/**
* Cache of the computed type check scope per NgModule declaration.
*/
scopeCache = new Map();
constructor(scopeReader, metaReader, hostDirectivesResolver) {
this.scopeReader = scopeReader;
this.metaReader = metaReader;
this.hostDirectivesResolver = hostDirectivesResolver;
}
/**
* Computes the type-check scope information for the component declaration. If the NgModule
* contains an error, then 'error' is returned. If the component is not declared in any NgModule,
* an empty type-check scope is returned.
*/
getTypeCheckScope(node) {
const matcher = new checker.SelectorMatcher();
const directives = [];
const pipes = new Map();
const scope = this.scopeReader.getScopeForComponent(node);
if (scope === null) {
return {
matcher,
directives,
pipes,
schemas: [],
isPoisoned: false,
};
}
const isNgModuleScope = scope.kind === checker.ComponentScopeKind.NgModule;
const cacheKey = isNgModuleScope ? scope.ngModule : scope.component;
const dependencies = isNgModuleScope ? scope.compilation.dependencies : scope.dependencies;
if (this.scopeCache.has(cacheKey)) {
return this.scopeCache.get(cacheKey);
}
let allDependencies = dependencies;
if (!isNgModuleScope &&
Array.isArray(scope.deferredDependencies) &&
scope.deferredDependencies.length > 0) {
allDependencies = [...allDependencies, ...scope.deferredDependencies];
}
for (const meta of allDependencies) {
if (meta.kind === checker.MetaKind.Directive && meta.selector !== null) {
const extMeta = this.getTypeCheckDirectiveMetadata(meta.ref);
if (extMeta === null) {
continue;
}
// Carry over the `isExplicitlyDeferred` flag from the dependency info.
const directiveMeta = this.applyExplicitlyDeferredFlag(extMeta, meta.isExplicitlyDeferred);
matcher.addSelectables(checker.CssSelector.parse(meta.selector), [
...this.hostDirectivesResolver.resolve(directiveMeta),
directiveMeta,
]);
directives.push(directiveMeta);
}
else if (meta.kind === checker.MetaKind.Pipe) {
if (!ts__default["default"].isClassDeclaration(meta.ref.node)) {
throw new Error(`Unexpected non-class declaration ${ts__default["default"].SyntaxKind[meta.ref.node.kind]} for pipe ${meta.ref.debugName}`);
}
pipes.set(meta.name, meta);
}
}
const typeCheckScope = {
matcher,
directives,
pipes,
schemas: scope.schemas,
isPoisoned: scope.kind === checker.ComponentScopeKind.NgModule
? scope.compilation.isPoisoned || scope.exported.isPoisoned
: scope.isPoisoned,
};
this.scopeCache.set(cacheKey, typeCheckScope);
return typeCheckScope;
}
getTypeCheckDirectiveMetadata(ref) {
const clazz = ref.node;
if (this.flattenedDirectiveMetaCache.has(clazz)) {
return this.flattenedDirectiveMetaCache.get(clazz);
}
const meta = flattenInheritedDirectiveMetadata(this.metaReader, ref);
if (meta === null) {
return null;
}
this.flattenedDirectiveMetaCache.set(clazz, meta);
return meta;
}
applyExplicitlyDeferredFlag(meta, isExplicitlyDeferred) {
return isExplicitlyDeferred === true ? { ...meta, isExplicitlyDeferred } : meta;
}
}
const EMPTY_OBJECT = {};
const queryDecoratorNames = [
'ViewChild',
'ViewChildren',
'ContentChild',
'ContentChildren',
];
const QUERY_TYPES = new Set(queryDecoratorNames);
/**
* Helper function to extract metadata from a `Directive` or `Component`. `Directive`s without a
* selector are allowed to be used for abstract base classes. These abstract directives should not
* appear in the declarations of an `NgModule` and additional verification is done when processing
* the module.
*/
function extractDirectiveMetadata(clazz, decorator, reflector, importTracker, evaluator, refEmitter, referencesRegistry, isCore, annotateForClosureCompiler, compilationMode, defaultSelector, strictStandalone, implicitStandaloneValue) {
let directive;
if (decorator.args === null || decorator.args.length === 0) {
directive = new Map();
}
else if (decorator.args.length !== 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, `Incorrect number of arguments to @${decorator.name} decorator`);
}
else {
const meta = checker.unwrapExpression(decorator.args[0]);
if (!ts__default["default"].isObjectLiteralExpression(meta)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, `@${decorator.name} argument must be an object literal`);
}
directive = checker.reflectObjectLiteral(meta);
}
if (directive.has('jit')) {
// The only allowed value is true, so there's no need to expand further.
return { jitForced: true };
}
const members = reflector.getMembersOfClass(clazz);
// Precompute a list of ts.ClassElements that have decorators. This includes things like @Input,
// @Output, @HostBinding, etc.
const decoratedElements = members.filter((member) => !member.isStatic && member.decorators !== null);
const coreModule = isCore ? undefined : '@angular/core';
// Construct the map of inputs both from the @Directive/@Component
// decorator, and the decorated fields.
const inputsFromMeta = parseInputsArray(clazz, directive, evaluator, reflector, refEmitter, compilationMode);
const inputsFromFields = parseInputFields(clazz, members, evaluator, reflector, importTracker, refEmitter, isCore, compilationMode, inputsFromMeta, decorator);
const inputs = ClassPropertyMapping.fromMappedObject({ ...inputsFromMeta, ...inputsFromFields });
// And outputs.
const outputsFromMeta = parseOutputsArray(directive, evaluator);
const outputsFromFields = parseOutputFields(clazz, decorator, members, isCore, reflector, importTracker, evaluator, outputsFromMeta);
const outputs = ClassPropertyMapping.fromMappedObject({ ...outputsFromMeta, ...outputsFromFields });
// Parse queries of fields.
const { viewQueries, contentQueries } = parseQueriesOfClassFields(members, reflector, importTracker, evaluator, isCore);
if (directive.has('queries')) {
const signalQueryFields = new Set([...viewQueries, ...contentQueries].filter((q) => q.isSignal).map((q) => q.propertyName));
const queriesFromDecorator = extractQueriesFromDecorator(directive.get('queries'), reflector, evaluator, isCore);
// Checks if the query is already declared/reserved via class members declaration.
// If so, we throw a fatal diagnostic error to prevent this unintentional pattern.
const checkAndUnwrapQuery = (q) => {
if (signalQueryFields.has(q.metadata.propertyName)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, q.expr, `Query is declared multiple times. "@${decorator.name}" declares a query for the same property.`);
}
return q.metadata;
};
contentQueries.push(...queriesFromDecorator.content.map((q) => checkAndUnwrapQuery(q)));
viewQueries.push(...queriesFromDecorator.view.map((q) => checkAndUnwrapQuery(q)));
}
// Parse the selector.
let selector = defaultSelector;
if (directive.has('selector')) {
const expr = directive.get('selector');
const resolved = evaluator.evaluate(expr);
assertLocalCompilationUnresolvedConst(compilationMode, resolved, null, 'Unresolved identifier found for @Component.selector field! Did you ' +
'import this identifier from a file outside of the compilation unit? ' +
'This is not allowed when Angular compiler runs in local mode. Possible ' +
'solutions: 1) Move the declarations into a file within the compilation ' +
'unit, 2) Inline the selector');
if (typeof resolved !== 'string') {
throw createValueHasWrongTypeError(expr, resolved, `selector must be a string`);
}
// use default selector in case selector is an empty string
selector = resolved === '' ? defaultSelector : resolved;
if (!selector) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DIRECTIVE_MISSING_SELECTOR, expr, `Directive ${clazz.name.text} has no selector, please add it!`);
}
}
const host = extractHostBindings(decoratedElements, evaluator, coreModule, compilationMode, directive);
const providers = directive.has('providers')
? new checker.WrappedNodeExpr(annotateForClosureCompiler
? checker.wrapFunctionExpressionsInParens(directive.get('providers'))
: directive.get('providers'))
: null;
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = members.some((member) => !member.isStatic && member.kind === checker.ClassMemberKind.Method && member.name === 'ngOnChanges');
// Parse exportAs.
let exportAs = null;
if (directive.has('exportAs')) {
const expr = directive.get('exportAs');
const resolved = evaluator.evaluate(expr);
assertLocalCompilationUnresolvedConst(compilationMode, resolved, null, 'Unresolved identifier found for exportAs field! Did you import this ' +
'identifier from a file outside of the compilation unit? This is not ' +
'allowed when Angular compiler runs in local mode. Possible solutions: ' +
'1) Move the declarations into a file within the compilation unit, ' +
'2) Inline the selector');
if (typeof resolved !== 'string') {
throw createValueHasWrongTypeError(expr, resolved, `exportAs must be a string`);
}
exportAs = resolved.split(',').map((part) => part.trim());
}
const rawCtorDeps = getConstructorDependencies(clazz, reflector, isCore);
// Non-abstract directives (those with a selector) require valid constructor dependencies, whereas
// abstract directives are allowed to have invalid dependencies, given that a subclass may call
// the constructor explicitly.
const ctorDeps = selector !== null
? validateConstructorDependencies(clazz, rawCtorDeps)
: unwrapConstructorDependencies(rawCtorDeps);
// Structural directives must have a `TemplateRef` dependency.
const isStructural = ctorDeps !== null &&
ctorDeps !== 'invalid' &&
ctorDeps.some((dep) => dep.token instanceof checker.ExternalExpr &&
dep.token.value.moduleName === '@angular/core' &&
dep.token.value.name === 'TemplateRef');
let isStandalone = implicitStandaloneValue;
if (directive.has('standalone')) {
const expr = directive.get('standalone');
const resolved = evaluator.evaluate(expr);
if (typeof resolved !== 'boolean') {
throw createValueHasWrongTypeError(expr, resolved, `standalone flag must be a boolean`);
}
isStandalone = resolved;
if (!isStandalone && strictStandalone) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.NON_STANDALONE_NOT_ALLOWED, expr, `Only standalone components/directives are allowed when 'strictStandalone' is enabled.`);
}
}
let isSignal = false;
if (directive.has('signals')) {
const expr = directive.get('signals');
const resolved = evaluator.evaluate(expr);
if (typeof resolved !== 'boolean') {
throw createValueHasWrongTypeError(expr, resolved, `signals flag must be a boolean`);
}
isSignal = resolved;
}
// Detect if the component inherits from another class
const usesInheritance = reflector.hasBaseClass(clazz);
const sourceFile = clazz.getSourceFile();
const type = checker.wrapTypeReference(reflector, clazz);
const rawHostDirectives = directive.get('hostDirectives') || null;
const hostDirectives = rawHostDirectives === null
? null
: extractHostDirectives(rawHostDirectives, evaluator, compilationMode);
if (compilationMode !== checker.CompilationMode.LOCAL && hostDirectives !== null) {
// In global compilation mode where we do type checking, the template type-checker will need to
// import host directive types, so add them as referenced by `clazz`. This will ensure that
// libraries are required to export host directives which are visible from publicly exported
// components.
referencesRegistry.add(clazz, ...hostDirectives.map((hostDir) => {
if (!checker.isHostDirectiveMetaForGlobalMode(hostDir)) {
throw new Error('Impossible state');
}
return hostDir.directive;
}));
}
const metadata = {
name: clazz.name.text,
deps: ctorDeps,
host: {
...host,
},
lifecycle: {
usesOnChanges,
},
inputs: inputs.toJointMappedObject(toR3InputMetadata),
outputs: outputs.toDirectMappedObject(),
queries: contentQueries,
viewQueries,
selector,
fullInheritance: false,
type,
typeArgumentCount: reflector.getGenericArityOfClass(clazz) || 0,
typeSourceSpan: checker.createSourceSpan(clazz.name),
usesInheritance,
exportAs,
providers,
isStandalone,
isSignal,
hostDirectives: hostDirectives?.map((hostDir) => toHostDirectiveMetadata(hostDir, sourceFile, refEmitter)) ||
null,
};
return {
jitForced: false,
decorator: directive,
metadata,
inputs,
outputs,
isStructural,
hostDirectives,
rawHostDirectives,
// Track inputs from class metadata. This is useful for migration efforts.
inputFieldNamesFromMetadataArray: new Set(Object.values(inputsFromMeta).map((i) => i.classPropertyName)),
};
}
function extractDecoratorQueryMetadata(exprNode, name, args, propertyName, reflector, evaluator) {
if (args.length === 0) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`);
}
const first = name === 'ViewChild' || name === 'ContentChild';
const forwardReferenceTarget = checker.tryUnwrapForwardRef(args[0], reflector);
const node = forwardReferenceTarget ?? args[0];
const arg = evaluator.evaluate(node);
/** Whether or not this query should collect only static results (see view/api.ts) */
let isStatic = false;
// Extract the predicate
let predicate = null;
if (arg instanceof checker.Reference || arg instanceof checker.DynamicValue) {
// References and predicates that could not be evaluated statically are emitted as is.
predicate = checker.createMayBeForwardRefExpression(new checker.WrappedNodeExpr(node), forwardReferenceTarget !== null ? 2 /* ForwardRefHandling.Unwrapped */ : 0 /* ForwardRefHandling.None */);
}
else if (typeof arg === 'string') {
predicate = [arg];
}
else if (isStringArrayOrDie(arg, `@${name} predicate`, node)) {
predicate = arg;
}
else {
throw createValueHasWrongTypeError(node, arg, `@${name} predicate cannot be interpreted`);
}
// Extract the read and descendants options.
let read = null;
// The default value for descendants is true for every decorator except @ContentChildren.
let descendants = name !== 'ContentChildren';
let emitDistinctChangesOnly = checker.emitDistinctChangesOnlyDefaultValue;
if (args.length === 2) {
const optionsExpr = checker.unwrapExpression(args[1]);
if (!ts__default["default"].isObjectLiteralExpression(optionsExpr)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARG_NOT_LITERAL, optionsExpr, `@${name} options must be an object literal`);
}
const options = checker.reflectObjectLiteral(optionsExpr);
if (options.has('read')) {
read = new checker.WrappedNodeExpr(options.get('read'));
}
if (options.has('descendants')) {
const descendantsExpr = options.get('descendants');
const descendantsValue = evaluator.evaluate(descendantsExpr);
if (typeof descendantsValue !== 'boolean') {
throw createValueHasWrongTypeError(descendantsExpr, descendantsValue, `@${name} options.descendants must be a boolean`);
}
descendants = descendantsValue;
}
if (options.has('emitDistinctChangesOnly')) {
const emitDistinctChangesOnlyExpr = options.get('emitDistinctChangesOnly');
const emitDistinctChangesOnlyValue = evaluator.evaluate(emitDistinctChangesOnlyExpr);
if (typeof emitDistinctChangesOnlyValue !== 'boolean') {
throw createValueHasWrongTypeError(emitDistinctChangesOnlyExpr, emitDistinctChangesOnlyValue, `@${name} options.emitDistinctChangesOnly must be a boolean`);
}
emitDistinctChangesOnly = emitDistinctChangesOnlyValue;
}
if (options.has('static')) {
const staticValue = evaluator.evaluate(options.get('static'));
if (typeof staticValue !== 'boolean') {
throw createValueHasWrongTypeError(node, staticValue, `@${name} options.static must be a boolean`);
}
isStatic = staticValue;
}
}
else if (args.length > 2) {
// Too many arguments.
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, node, `@${name} has too many arguments`);
}
return {
isSignal: false,
propertyName,
predicate,
first,
descendants,
read,
static: isStatic,
emitDistinctChangesOnly,
};
}
function extractHostBindings(members, evaluator, coreModule, compilationMode, metadata) {
let bindings;
if (metadata && metadata.has('host')) {
bindings = evaluateHostExpressionBindings(metadata.get('host'), evaluator);
}
else {
bindings = checker.parseHostBindings({});
}
checker.filterToMembersWithDecorator(members, 'HostBinding', coreModule).forEach(({ member, decorators }) => {
decorators.forEach((decorator) => {
let hostPropertyName = member.name;
if (decorator.args !== null && decorator.args.length > 0) {
if (decorator.args.length !== 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, `@HostBinding can have at most one argument, got ${decorator.args.length} argument(s)`);
}
const resolved = evaluator.evaluate(decorator.args[0]);
// Specific error for local compilation mode if the argument cannot be resolved
assertLocalCompilationUnresolvedConst(compilationMode, resolved, null, "Unresolved identifier found for @HostBinding's argument! Did " +
'you import this identifier from a file outside of the compilation ' +
'unit? This is not allowed when Angular compiler runs in local mode. ' +
'Possible solutions: 1) Move the declaration into a file within ' +
'the compilation unit, 2) Inline the argument');
if (typeof resolved !== 'string') {
throw createValueHasWrongTypeError(decorator.node, resolved, `@HostBinding's argument must be a string`);
}
hostPropertyName = resolved;
}
// Since this is a decorator, we know that the value is a class member. Always access it
// through `this` so that further down the line it can't be confused for a literal value
// (e.g. if there's a property called `true`). There is no size penalty, because all
// values (except literals) are converted to `ctx.propName` eventually.
bindings.properties[hostPropertyName] = checker.getSafePropertyAccessString('this', member.name);
});
});
checker.filterToMembersWithDecorator(members, 'HostListener', coreModule).forEach(({ member, decorators }) => {
decorators.forEach((decorator) => {
let eventName = member.name;
let args = [];
if (decorator.args !== null && decorator.args.length > 0) {
if (decorator.args.length > 2) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], `@HostListener can have at most two arguments`);
}
const resolved = evaluator.evaluate(decorator.args[0]);
// Specific error for local compilation mode if the event name cannot be resolved
assertLocalCompilationUnresolvedConst(compilationMode, resolved, null, "Unresolved identifier found for @HostListener's event name " +
'argument! Did you import this identifier from a file outside of ' +
'the compilation unit? This is not allowed when Angular compiler ' +
'runs in local mode. Possible solutions: 1) Move the declaration ' +
'into a file within the compilation unit, 2) Inline the argument');
if (typeof resolved !== 'string') {
throw createValueHasWrongTypeError(decorator.args[0], resolved, `@HostListener's event name argument must be a string`);
}
eventName = resolved;
if (decorator.args.length === 2) {
const expression = decorator.args[1];
const resolvedArgs = evaluator.evaluate(decorator.args[1]);
if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args', expression)) {
throw createValueHasWrongTypeError(decorator.args[1], resolvedArgs, `@HostListener's second argument must be a string array`);
}
args = resolvedArgs;
}
}
bindings.listeners[eventName] = `${member.name}(${args.join(',')})`;
});
});
return bindings;
}
function extractQueriesFromDecorator(queryData, reflector, evaluator, isCore) {
const content = [];
const view = [];
if (!ts__default["default"].isObjectLiteralExpression(queryData)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, 'Decorator queries metadata must be an object literal');
}
checker.reflectObjectLiteral(queryData).forEach((queryExpr, propertyName) => {
queryExpr = checker.unwrapExpression(queryExpr);
if (!ts__default["default"].isNewExpression(queryExpr)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, 'Decorator query metadata must be an instance of a query type');
}
const queryType = ts__default["default"].isPropertyAccessExpression(queryExpr.expression)
? queryExpr.expression.name
: queryExpr.expression;
if (!ts__default["default"].isIdentifier(queryType)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, 'Decorator query metadata must be an instance of a query type');
}
const type = reflector.getImportOfIdentifier(queryType);
if (type === null ||
(!isCore && type.from !== '@angular/core') ||
!QUERY_TYPES.has(type.name)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, 'Decorator query metadata must be an instance of a query type');
}
const query = extractDecoratorQueryMetadata(queryExpr, type.name, queryExpr.arguments || [], propertyName, reflector, evaluator);
if (type.name.startsWith('Content')) {
content.push({ expr: queryExpr, metadata: query });
}
else {
view.push({ expr: queryExpr, metadata: query });
}
});
return { content, view };
}
function parseDirectiveStyles(directive, evaluator, compilationMode) {
const expression = directive.get('styles');
if (!expression) {
return null;
}
const evaluated = evaluator.evaluate(expression);
const value = typeof evaluated === 'string' ? [evaluated] : evaluated;
// Check if the identifier used for @Component.styles cannot be resolved in local compilation
// mode. if the case, an error specific to this situation is generated.
if (compilationMode === checker.CompilationMode.LOCAL) {
let unresolvedNode = null;
if (Array.isArray(value)) {
const entry = value.find((e) => e instanceof checker.DynamicValue && e.isFromUnknownIdentifier());
unresolvedNode = entry?.node ?? null;
}
else if (value instanceof checker.DynamicValue && value.isFromUnknownIdentifier()) {
unresolvedNode = value.node;
}
if (unresolvedNode !== null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST, unresolvedNode, 'Unresolved identifier found for @Component.styles field! Did you import ' +
'this identifier from a file outside of the compilation unit? This is ' +
'not allowed when Angular compiler runs in local mode. Possible ' +
'solutions: 1) Move the declarations into a file within the compilation ' +
'unit, 2) Inline the styles, 3) Move the styles into separate files and ' +
'include it using @Component.styleUrls');
}
}
if (!isStringArrayOrDie(value, 'styles', expression)) {
throw createValueHasWrongTypeError(expression, value, `Failed to resolve @Component.styles to a string or an array of strings`);
}
return value;
}
function parseFieldStringArrayValue(directive, field, evaluator) {
if (!directive.has(field)) {
return null;
}
// Resolve the field of interest from the directive metadata to a string[].
const expression = directive.get(field);
const value = evaluator.evaluate(expression);
if (!isStringArrayOrDie(value, field, expression)) {
throw createValueHasWrongTypeError(expression, value, `Failed to resolve @Directive.${field} to a string array`);
}
return value;
}
function isStringArrayOrDie(value, name, node) {
if (!Array.isArray(value)) {
return false;
}
for (let i = 0; i < value.length; i++) {
if (typeof value[i] !== 'string') {
throw createValueHasWrongTypeError(node, value[i], `Failed to resolve ${name} at position ${i} to a string`);
}
}
return true;
}
function tryGetQueryFromFieldDecorator(member, reflector, evaluator, isCore) {
const decorators = member.decorators;
if (decorators === null) {
return null;
}
const queryDecorators = checker.getAngularDecorators(decorators, queryDecoratorNames, isCore);
if (queryDecorators.length === 0) {
return null;
}
if (queryDecorators.length !== 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_COLLISION, member.node ?? queryDecorators[0].node, 'Cannot combine multiple query decorators.');
}
const decorator = queryDecorators[0];
const node = member.node || decorator.node;
// Throw in case of `@Input() @ContentChild('foo') foo: any`, which is not supported in Ivy
if (decorators.some((v) => v.name === 'Input')) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_COLLISION, node, 'Cannot combine @Input decorators with query decorators');
}
if (!isPropertyTypeMember(member)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_UNEXPECTED, node, 'Query decorator must go on a property-type member');
}
// Either the decorator was aliased, or is referenced directly with
// the proper query name.
const name = (decorator.import?.name ?? decorator.name);
return {
name,
decorator,
metadata: extractDecoratorQueryMetadata(node, name, decorator.args || [], member.name, reflector, evaluator),
};
}
function isPropertyTypeMember(member) {
return (member.kind === checker.ClassMemberKind.Getter ||
member.kind === checker.ClassMemberKind.Setter ||
member.kind === checker.ClassMemberKind.Property);
}
function parseMappingStringArray(values) {
return values.reduce((results, value) => {
if (typeof value !== 'string') {
throw new Error('Mapping value must be a string');
}
const [bindingPropertyName, fieldName] = parseMappingString(value);
results[fieldName] = bindingPropertyName;
return results;
}, {});
}
function parseMappingString(value) {
// Either the value is 'field' or 'field: property'. In the first case, `property` will
// be undefined, in which case the field name should also be used as the property name.
const [fieldName, bindingPropertyName] = value.split(':', 2).map((str) => str.trim());
return [bindingPropertyName ?? fieldName, fieldName];
}
/** Parses the `inputs` array of a directive/component decorator. */
function parseInputsArray(clazz, decoratorMetadata, evaluator, reflector, refEmitter, compilationMode) {
const inputsField = decoratorMetadata.get('inputs');
if (inputsField === undefined) {
return {};
}
const inputs = {};
const inputsArray = evaluator.evaluate(inputsField);
if (!Array.isArray(inputsArray)) {
throw createValueHasWrongTypeError(inputsField, inputsArray, `Failed to resolve @Directive.inputs to an array`);
}
for (let i = 0; i < inputsArray.length; i++) {
const value = inputsArray[i];
if (typeof value === 'string') {
// If the value is a string, we treat it as a mapping string.
const [bindingPropertyName, classPropertyName] = parseMappingString(value);
inputs[classPropertyName] = {
bindingPropertyName,
classPropertyName,
required: false,
transform: null,
// Note: Signal inputs are not allowed with the array form.
isSignal: false,
};
}
else if (value instanceof Map) {
// If it's a map, we treat it as a config object.
const name = value.get('name');
const alias = value.get('alias');
const required = value.get('required');
let transform = null;
if (typeof name !== 'string') {
throw createValueHasWrongTypeError(inputsField, name, `Value at position ${i} of @Directive.inputs array must have a "name" property`);
}
if (value.has('transform')) {
const transformValue = value.get('transform');
if (!(transformValue instanceof checker.DynamicValue) && !(transformValue instanceof checker.Reference)) {
throw createValueHasWrongTypeError(inputsField, transformValue, `Transform of value at position ${i} of @Directive.inputs array must be a function`);
}
transform = parseDecoratorInputTransformFunction(clazz, name, transformValue, reflector, refEmitter, compilationMode);
}
inputs[name] = {
classPropertyName: name,
bindingPropertyName: typeof alias === 'string' ? alias : name,
required: required === true,
// Note: Signal inputs are not allowed with the array form.
isSignal: false,
transform,
};
}
else {
throw createValueHasWrongTypeError(inputsField, value, `@Directive.inputs array can only contain strings or object literals`);
}
}
return inputs;
}
/** Attempts to find a given Angular decorator on the class member. */
function tryGetDecoratorOnMember(member, decoratorName, isCore) {
if (member.decorators === null) {
return null;
}
for (const decorator of member.decorators) {
if (checker.isAngularDecorator(decorator, decoratorName, isCore)) {
return decorator;
}
}
return null;
}
function tryParseInputFieldMapping(clazz, member, evaluator, reflector, importTracker, isCore, refEmitter, compilationMode) {
const classPropertyName = member.name;
const decorator = tryGetDecoratorOnMember(member, 'Input', isCore);
const signalInputMapping = checker.tryParseSignalInputMapping(member, reflector, importTracker);
const modelInputMapping = checker.tryParseSignalModelMapping(member, reflector, importTracker);
if (decorator !== null && signalInputMapping !== null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decorator.node, `Using @Input with a signal input is not allowed.`);
}
if (decorator !== null && modelInputMapping !== null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decorator.node, `Using @Input with a model input is not allowed.`);
}
// Check `@Input` case.
if (decorator !== null) {
if (decorator.args !== null && decorator.args.length > 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, `@${decorator.name} can have at most one argument, got ${decorator.args.length} argument(s)`);
}
const optionsNode = decorator.args !== null && decorator.args.length === 1 ? decorator.args[0] : undefined;
const options = optionsNode !== undefined ? evaluator.evaluate(optionsNode) : null;
const required = options instanceof Map ? options.get('required') === true : false;
// To preserve old behavior: Even though TypeScript types ensure proper options are
// passed, we sanity check for unsupported values here again.
if (options !== null && typeof options !== 'string' && !(options instanceof Map)) {
throw createValueHasWrongTypeError(decorator.node, options, `@${decorator.name} decorator argument must resolve to a string or an object literal`);
}
let alias = null;
if (typeof options === 'string') {
alias = options;
}
else if (options instanceof Map && typeof options.get('alias') === 'string') {
alias = options.get('alias');
}
const publicInputName = alias ?? classPropertyName;
let transform = null;
if (options instanceof Map && options.has('transform')) {
const transformValue = options.get('transform');
if (!(transformValue instanceof checker.DynamicValue) && !(transformValue instanceof checker.Reference)) {
throw createValueHasWrongTypeError(optionsNode, transformValue, `Input transform must be a function`);
}
transform = parseDecoratorInputTransformFunction(clazz, classPropertyName, transformValue, reflector, refEmitter, compilationMode);
}
return {
isSignal: false,
classPropertyName,
bindingPropertyName: publicInputName,
transform,
required,
};
}
// Look for signal inputs. e.g. `memberName = input()`
if (signalInputMapping !== null) {
return signalInputMapping;
}
if (modelInputMapping !== null) {
return modelInputMapping.input;
}
return null;
}
/** Parses the class members that declare inputs (via decorator or initializer). */
function parseInputFields(clazz, members, evaluator, reflector, importTracker, refEmitter, isCore, compilationMode, inputsFromClassDecorator, classDecorator) {
const inputs = {};
for (const member of members) {
const classPropertyName = member.name;
const inputMapping = tryParseInputFieldMapping(clazz, member, evaluator, reflector, importTracker, isCore, refEmitter, compilationMode);
if (inputMapping === null) {
continue;
}
if (member.isStatic) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, member.node ?? clazz, `Input "${member.name}" is incorrectly declared as static member of "${clazz.name.text}".`);
}
// Validate that signal inputs are not accidentally declared in the `inputs` metadata.
if (inputMapping.isSignal && inputsFromClassDecorator.hasOwnProperty(classPropertyName)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, member.node ?? clazz, `Input "${member.name}" is also declared as non-signal in @${classDecorator.name}.`);
}
inputs[classPropertyName] = inputMapping;
}
return inputs;
}
/**
* Parses the `transform` function and its type for a decorator `@Input`.
*
* This logic verifies feasibility of extracting the transform write type
* into a different place, so that the input write type can be captured at
* a later point in a static acceptance member.
*
* Note: This is not needed for signal inputs where the transform type is
* automatically captured in the type of the `InputSignal`.
*
*/
function parseDecoratorInputTransformFunction(clazz, classPropertyName, value, reflector, refEmitter, compilationMode) {
// In local compilation mode we can skip type checking the function args. This is because usually
// the type check is done in a separate build which runs in full compilation mode. So here we skip
// all the diagnostics.
if (compilationMode === checker.CompilationMode.LOCAL) {
const node = value instanceof checker.Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node;
// This should never be null since we know the reference originates
// from the same file, but we null check it just in case.
if (node === null) {
throw createValueHasWrongTypeError(value.node, value, 'Input transform function could not be referenced');
}
return {
node,
type: new checker.Reference(ts__default["default"].factory.createKeywordTypeNode(ts__default["default"].SyntaxKind.UnknownKeyword)),
};
}
const definition = reflector.getDefinitionOfFunction(value.node);
if (definition === null) {
throw createValueHasWrongTypeError(value.node, value, 'Input transform must be a function');
}
if (definition.typeParameters !== null && definition.typeParameters.length > 0) {
throw createValueHasWrongTypeError(value.node, value, 'Input transform function cannot be generic');
}
if (definition.signatureCount > 1) {
throw createValueHasWrongTypeError(value.node, value, 'Input transform function cannot have multiple signatures');
}
const members = reflector.getMembersOfClass(clazz);
for (const member of members) {
const conflictingName = `ngAcceptInputType_${classPropertyName}`;
if (member.name === conflictingName && member.isStatic) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.CONFLICTING_INPUT_TRANSFORM, value.node, `Class cannot have both a transform function on Input ${classPropertyName} and a static member called ${conflictingName}`);
}
}
const node = value instanceof checker.Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node;
// This should never be null since we know the reference originates
// from the same file, but we null check it just in case.
if (node === null) {
throw createValueHasWrongTypeError(value.node, value, 'Input transform function could not be referenced');
}
// Skip over `this` parameters since they're typing the context, not the actual parameter.
// `this` parameters are guaranteed to be first if they exist, and the only to distinguish them
// is using the name, TS doesn't have a special AST for them.
const firstParam = definition.parameters[0]?.name === 'this' ? definition.parameters[1] : definition.parameters[0];
// Treat functions with no arguments as `unknown` since returning
// the same value from the transform function is valid.
if (!firstParam) {
return {
node,
type: new checker.Reference(ts__default["default"].factory.createKeywordTypeNode(ts__default["default"].SyntaxKind.UnknownKeyword)),
};
}
// This should be caught by `noImplicitAny` already, but null check it just in case.
if (!firstParam.type) {
throw createValueHasWrongTypeError(value.node, value, 'Input transform function first parameter must have a type');
}
if (firstParam.node.dotDotDotToken) {
throw createValueHasWrongTypeError(value.node, value, 'Input transform function first parameter cannot be a spread parameter');
}
assertEmittableInputType(firstParam.type, clazz.getSourceFile(), reflector, refEmitter);
const viaModule = value instanceof checker.Reference ? value.bestGuessOwningModule : null;
return { node, type: new checker.Reference(firstParam.type, viaModule) };
}
/**
* Verifies that a type and all types contained within
* it can be referenced in a specific context file.
*/
function assertEmittableInputType(type, contextFile, reflector, refEmitter) {
(function walk(node) {
if (ts__default["default"].isTypeReferenceNode(node) && ts__default["default"].isIdentifier(node.typeName)) {
const declaration = reflector.getDeclarationOfIdentifier(node.typeName);
if (declaration !== null) {
// If the type is declared in a different file, we have to check that it can be imported
// into the context file. If they're in the same file, we need to verify that they're
// exported, otherwise TS won't emit it to the .d.ts.
if (declaration.node.getSourceFile() !== contextFile) {
const emittedType = refEmitter.emit(new checker.Reference(declaration.node, declaration.viaModule === checker.AmbientImport ? checker.AmbientImport : null), contextFile, checker.ImportFlags.NoAliasing |
checker.ImportFlags.AllowTypeImports |
checker.ImportFlags.AllowRelativeDtsImports |
checker.ImportFlags.AllowAmbientReferences);
checker.assertSuccessfulReferenceEmit(emittedType, node, 'type');
}
else if (!reflector.isStaticallyExported(declaration.node)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.SYMBOL_NOT_EXPORTED, type, `Symbol must be exported in order to be used as the type of an Input transform function`, [checker.makeRelatedInformation(declaration.node, `The symbol is declared here.`)]);
}
}
}
node.forEachChild(walk);
})(type);
}
/**
* Iterates through all specified class members and attempts to detect
* view and content queries defined.
*
* Queries may be either defined via decorators, or through class member
* initializers for signal-based queries.
*/
function parseQueriesOfClassFields(members, reflector, importTracker, evaluator, isCore) {
const viewQueries = [];
const contentQueries = [];
// For backwards compatibility, decorator-based queries are grouped and
// ordered in a specific way. The order needs to match with what we had in:
// https://github.com/angular/angular/blob/8737544d6963bf664f752de273e919575cca08ac/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts#L94-L111.
const decoratorViewChild = [];
const decoratorViewChildren = [];
const decoratorContentChild = [];
const decoratorContentChildren = [];
for (const member of members) {
const decoratorQuery = tryGetQueryFromFieldDecorator(member, reflector, evaluator, isCore);
const signalQuery = checker.tryParseSignalQueryFromInitializer(member, reflector, importTracker);
if (decoratorQuery !== null && signalQuery !== null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decoratorQuery.decorator.node, `Using @${decoratorQuery.name} with a signal-based query is not allowed.`);
}
const queryNode = decoratorQuery?.decorator.node ?? signalQuery?.call;
if (queryNode !== undefined && member.isStatic) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, queryNode, `Query is incorrectly declared on a static class member.`);
}
if (decoratorQuery !== null) {
switch (decoratorQuery.name) {
case 'ViewChild':
decoratorViewChild.push(decoratorQuery.metadata);
break;
case 'ViewChildren':
decoratorViewChildren.push(decoratorQuery.metadata);
break;
case 'ContentChild':
decoratorContentChild.push(decoratorQuery.metadata);
break;
case 'ContentChildren':
decoratorContentChildren.push(decoratorQuery.metadata);
break;
}
}
else if (signalQuery !== null) {
switch (signalQuery.name) {
case 'viewChild':
case 'viewChildren':
viewQueries.push(signalQuery.metadata);
break;
case 'contentChild':
case 'contentChildren':
contentQueries.push(signalQuery.metadata);
break;
}
}
}
return {
viewQueries: [...viewQueries, ...decoratorViewChild, ...decoratorViewChildren],
contentQueries: [...contentQueries, ...decoratorContentChild, ...decoratorContentChildren],
};
}
/** Parses the `outputs` array of a directive/component. */
function parseOutputsArray(directive, evaluator) {
const metaValues = parseFieldStringArrayValue(directive, 'outputs', evaluator);
return metaValues ? parseMappingStringArray(metaValues) : EMPTY_OBJECT;
}
/** Parses the class members that are outputs. */
function parseOutputFields(clazz, classDecorator, members, isCore, reflector, importTracker, evaluator, outputsFromMeta) {
const outputs = {};
for (const member of members) {
const decoratorOutput = tryParseDecoratorOutput(member, evaluator, isCore);
const initializerOutput = checker.tryParseInitializerBasedOutput(member, reflector, importTracker);
const modelMapping = checker.tryParseSignalModelMapping(member, reflector, importTracker);
if (decoratorOutput !== null && initializerOutput !== null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decoratorOutput.decorator.node, `Using "@Output" with "output()" is not allowed.`);
}
if (decoratorOutput !== null && modelMapping !== null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decoratorOutput.decorator.node, `Using @Output with a model input is not allowed.`);
}
const queryNode = decoratorOutput?.decorator.node ?? initializerOutput?.call ?? modelMapping?.call;
if (queryNode !== undefined && member.isStatic) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, queryNode, `Output is incorrectly declared on a static class member.`);
}
let bindingPropertyName;
if (decoratorOutput !== null) {
bindingPropertyName = decoratorOutput.metadata.bindingPropertyName;
}
else if (initializerOutput !== null) {
bindingPropertyName = initializerOutput.metadata.bindingPropertyName;
}
else if (modelMapping !== null) {
bindingPropertyName = modelMapping.output.bindingPropertyName;
}
else {
continue;
}
// Validate that initializer-based outputs are not accidentally declared
// in the `outputs` class metadata.
if ((initializerOutput !== null || modelMapping !== null) &&
outputsFromMeta.hasOwnProperty(member.name)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, member.node ?? clazz, `Output "${member.name}" is unexpectedly declared in @${classDecorator.name} as well.`);
}
outputs[member.name] = bindingPropertyName;
}
return outputs;
}
/** Attempts to parse a decorator-based @Output. */
function tryParseDecoratorOutput(member, evaluator, isCore) {
const decorator = tryGetDecoratorOnMember(member, 'Output', isCore);
if (decorator === null) {
return null;
}
if (decorator.args !== null && decorator.args.length > 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, `@Output can have at most one argument, got ${decorator.args.length} argument(s)`);
}
const classPropertyName = member.name;
let alias = null;
if (decorator.args?.length === 1) {
const resolvedAlias = evaluator.evaluate(decorator.args[0]);
if (typeof resolvedAlias !== 'string') {
throw createValueHasWrongTypeError(decorator.node, resolvedAlias, `@Output decorator argument must resolve to a string`);
}
alias = resolvedAlias;
}
return {
decorator,
metadata: {
isSignal: false,
classPropertyName,
bindingPropertyName: alias ?? classPropertyName,
},
};
}
function evaluateHostExpressionBindings(hostExpr, evaluator) {
const hostMetaMap = evaluator.evaluate(hostExpr);
if (!(hostMetaMap instanceof Map)) {
throw createValueHasWrongTypeError(hostExpr, hostMetaMap, `Decorator host metadata must be an object`);
}
const hostMetadata = {};
hostMetaMap.forEach((value, key) => {
// Resolve Enum references to their declared value.
if (value instanceof checker.EnumValue) {
value = value.resolved;
}
if (typeof key !== 'string') {
throw createValueHasWrongTypeError(hostExpr, key, `Decorator host metadata must be a string -> string object, but found unparseable key`);
}
if (typeof value == 'string') {
hostMetadata[key] = value;
}
else if (value instanceof checker.DynamicValue) {
hostMetadata[key] = new checker.WrappedNodeExpr(value.node);
}
else {
throw createValueHasWrongTypeError(hostExpr, value, `Decorator host metadata must be a string -> string object, but found unparseable value`);
}
});
const bindings = checker.parseHostBindings(hostMetadata);
const errors = checker.verifyHostBindings(bindings, checker.createSourceSpan(hostExpr));
if (errors.length > 0) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.HOST_BINDING_PARSE_ERROR, getHostBindingErrorNode(errors[0], hostExpr), errors.map((error) => error.msg).join('\n'));
}
return bindings;
}
/**
* Attempts to match a parser error to the host binding expression that caused it.
* @param error Error to match.
* @param hostExpr Expression declaring the host bindings.
*/
function getHostBindingErrorNode(error, hostExpr) {
// In the most common case the `host` object is an object literal with string values. We can
// confidently match the error to its expression by looking at the string value that the parser
// failed to parse and the initializers for each of the properties. If we fail to match, we fall
// back to the old behavior where the error is reported on the entire `host` object.
if (ts__default["default"].isObjectLiteralExpression(hostExpr) && error.relatedError instanceof checker.ParserError) {
for (const prop of hostExpr.properties) {
if (ts__default["default"].isPropertyAssignment(prop) &&
ts__default["default"].isStringLiteralLike(prop.initializer) &&
prop.initializer.text === error.relatedError.input) {
return prop.initializer;
}
}
}
return hostExpr;
}
/**
* Extracts and prepares the host directives metadata from an array literal expression.
* @param rawHostDirectives Expression that defined the `hostDirectives`.
*/
function extractHostDirectives(rawHostDirectives, evaluator, compilationMode) {
const resolved = evaluator.evaluate(rawHostDirectives, checker.forwardRefResolver);
if (!Array.isArray(resolved)) {
throw createValueHasWrongTypeError(rawHostDirectives, resolved, 'hostDirectives must be an array');
}
return resolved.map((value) => {
const hostReference = value instanceof Map ? value.get('directive') : value;
// Diagnostics
if (compilationMode !== checker.CompilationMode.LOCAL) {
if (!(hostReference instanceof checker.Reference)) {
throw createValueHasWrongTypeError(rawHostDirectives, hostReference, 'Host directive must be a reference');
}
if (!checker.isNamedClassDeclaration(hostReference.node)) {
throw createValueHasWrongTypeError(rawHostDirectives, hostReference, 'Host directive reference must be a class');
}
}
let directive;
let nameForErrors = (fieldName) => '@Directive.hostDirectives';
if (compilationMode === checker.CompilationMode.LOCAL && hostReference instanceof checker.DynamicValue) {
// At the moment in local compilation we only support simple array for host directives, i.e.,
// an array consisting of the directive identifiers. We don't support forward refs or other
// expressions applied on externally imported directives. The main reason is simplicity, and
// that almost nobody wants to use host directives this way (e.g., what would be the point of
// forward ref for imported symbols?!)
if (!ts__default["default"].isIdentifier(hostReference.node) &&
!ts__default["default"].isPropertyAccessExpression(hostReference.node)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION, hostReference.node, `In local compilation mode, host directive cannot be an expression. Use an identifier instead`);
}
directive = new checker.WrappedNodeExpr(hostReference.node);
}
else if (hostReference instanceof checker.Reference) {
directive = hostReference;
nameForErrors = (fieldName) => `@Directive.hostDirectives.${directive.node.name.text}.${fieldName}`;
}
else {
throw new Error('Impossible state');
}
const meta = {
directive,
isForwardReference: hostReference instanceof checker.Reference && hostReference.synthetic,
inputs: parseHostDirectivesMapping('inputs', value, nameForErrors('input'), rawHostDirectives),
outputs: parseHostDirectivesMapping('outputs', value, nameForErrors('output'), rawHostDirectives),
};
return meta;
});
}
/**
* Parses the expression that defines the `inputs` or `outputs` of a host directive.
* @param field Name of the field that is being parsed.
* @param resolvedValue Evaluated value of the expression that defined the field.
* @param classReference Reference to the host directive class.
* @param sourceExpression Expression that the host directive is referenced in.
*/
function parseHostDirectivesMapping(field, resolvedValue, nameForErrors, sourceExpression) {
if (resolvedValue instanceof Map && resolvedValue.has(field)) {
const rawInputs = resolvedValue.get(field);
if (isStringArrayOrDie(rawInputs, nameForErrors, sourceExpression)) {
return parseMappingStringArray(rawInputs);
}
}
return null;
}
/** Converts the parsed host directive information into metadata. */
function toHostDirectiveMetadata(hostDirective, context, refEmitter) {
let directive;
if (hostDirective.directive instanceof checker.Reference) {
directive = checker.toR3Reference(hostDirective.directive.node, hostDirective.directive, context, refEmitter);
}
else {
directive = {
value: hostDirective.directive,
type: hostDirective.directive,
};
}
return {
directive,
isForwardReference: hostDirective.isForwardReference,
inputs: hostDirective.inputs || null,
outputs: hostDirective.outputs || null,
};
}
/** Converts the parsed input information into metadata. */
function toR3InputMetadata(mapping) {
return {
classPropertyName: mapping.classPropertyName,
bindingPropertyName: mapping.bindingPropertyName,
required: mapping.required,
transformFunction: mapping.transform !== null ? new checker.WrappedNodeExpr(mapping.transform.node) : null,
isSignal: mapping.isSignal,
};
}
/**
* Represents an Angular directive. Components are represented by `ComponentSymbol`, which inherits
* from this symbol.
*/
class DirectiveSymbol extends SemanticSymbol {
selector;
inputs;
outputs;
exportAs;
typeCheckMeta;
typeParameters;
baseClass = null;
constructor(decl, selector, inputs, outputs, exportAs, typeCheckMeta, typeParameters) {
super(decl);
this.selector = selector;
this.inputs = inputs;
this.outputs = outputs;
this.exportAs = exportAs;
this.typeCheckMeta = typeCheckMeta;
this.typeParameters = typeParameters;
}
isPublicApiAffected(previousSymbol) {
// Note: since components and directives have exactly the same items contributing to their
// public API, it is okay for a directive to change into a component and vice versa without
// the API being affected.
if (!(previousSymbol instanceof DirectiveSymbol)) {
return true;
}
// Directives and components have a public API of:
// 1. Their selector.
// 2. The binding names of their inputs and outputs; a change in ordering is also considered
// to be a change in public API.
// 3. The list of exportAs names and its ordering.
return (this.selector !== previousSymbol.selector ||
!isArrayEqual(this.inputs.propertyNames, previousSymbol.inputs.propertyNames) ||
!isArrayEqual(this.outputs.propertyNames, previousSymbol.outputs.propertyNames) ||
!isArrayEqual(this.exportAs, previousSymbol.exportAs));
}
isTypeCheckApiAffected(previousSymbol) {
// If the public API of the directive has changed, then so has its type-check API.
if (this.isPublicApiAffected(previousSymbol)) {
return true;
}
if (!(previousSymbol instanceof DirectiveSymbol)) {
return true;
}
// The type-check block also depends on the class property names, as writes property bindings
// directly into the backing fields.
if (!isArrayEqual(Array.from(this.inputs), Array.from(previousSymbol.inputs), isInputMappingEqual) ||
!isArrayEqual(Array.from(this.outputs), Array.from(previousSymbol.outputs), isInputOrOutputEqual)) {
return true;
}
// The type parameters of a directive are emitted into the type constructors in the type-check
// block of a component, so if the type parameters are not considered equal then consider the
// type-check API of this directive to be affected.
if (!areTypeParametersEqual(this.typeParameters, previousSymbol.typeParameters)) {
return true;
}
// The type-check metadata is used during TCB code generation, so any changes should invalidate
// prior type-check files.
if (!isTypeCheckMetaEqual(this.typeCheckMeta, previousSymbol.typeCheckMeta)) {
return true;
}
// Changing the base class of a directive means that its inputs/outputs etc may have changed,
// so the type-check block of components that use this directive needs to be regenerated.
if (!isBaseClassEqual(this.baseClass, previousSymbol.baseClass)) {
return true;
}
return false;
}
}
function isInputMappingEqual(current, previous) {
return isInputOrOutputEqual(current, previous) && current.required === previous.required;
}
function isInputOrOutputEqual(current, previous) {
return (current.classPropertyName === previous.classPropertyName &&
current.bindingPropertyName === previous.bindingPropertyName &&
current.isSignal === previous.isSignal);
}
function isTypeCheckMetaEqual(current, previous) {
if (current.hasNgTemplateContextGuard !== previous.hasNgTemplateContextGuard) {
return false;
}
if (current.isGeneric !== previous.isGeneric) {
// Note: changes in the number of type parameters is also considered in
// `areTypeParametersEqual` so this check is technically not needed; it is done anyway for
// completeness in terms of whether the `DirectiveTypeCheckMeta` struct itself compares
// equal or not.
return false;
}
if (!isArrayEqual(current.ngTemplateGuards, previous.ngTemplateGuards, isTemplateGuardEqual)) {
return false;
}
if (!isSetEqual(current.coercedInputFields, previous.coercedInputFields)) {
return false;
}
if (!isSetEqual(current.restrictedInputFields, previous.restrictedInputFields)) {
return false;
}
if (!isSetEqual(current.stringLiteralInputFields, previous.stringLiteralInputFields)) {
return false;
}
if (!isSetEqual(current.undeclaredInputFields, previous.undeclaredInputFields)) {
return false;
}
return true;
}
function isTemplateGuardEqual(current, previous) {
return current.inputName === previous.inputName && current.type === previous.type;
}
function isBaseClassEqual(current, previous) {
if (current === null || previous === null) {
return current === previous;
}
return isSymbolEqual(current, previous);
}
const FIELD_DECORATORS = [
'Input',
'Output',
'ViewChild',
'ViewChildren',
'ContentChild',
'ContentChildren',
'HostBinding',
'HostListener',
];
const LIFECYCLE_HOOKS = new Set([
'ngOnChanges',
'ngOnInit',
'ngOnDestroy',
'ngDoCheck',
'ngAfterViewInit',
'ngAfterViewChecked',
'ngAfterContentInit',
'ngAfterContentChecked',
]);
class DirectiveDecoratorHandler {
reflector;
evaluator;
metaRegistry;
scopeRegistry;
metaReader;
injectableRegistry;
refEmitter;
referencesRegistry;
isCore;
strictCtorDeps;
semanticDepGraphUpdater;
annotateForClosureCompiler;
perf;
importTracker;
includeClassMetadata;
compilationMode;
jitDeclarationRegistry;
strictStandalone;
implicitStandaloneValue;
constructor(reflector, evaluator, metaRegistry, scopeRegistry, metaReader, injectableRegistry, refEmitter, referencesRegistry, isCore, strictCtorDeps, semanticDepGraphUpdater, annotateForClosureCompiler, perf, importTracker, includeClassMetadata, compilationMode, jitDeclarationRegistry, strictStandalone, implicitStandaloneValue) {
this.reflector = reflector;
this.evaluator = evaluator;
this.metaRegistry = metaRegistry;
this.scopeRegistry = scopeRegistry;
this.metaReader = metaReader;
this.injectableRegistry = injectableRegistry;
this.refEmitter = refEmitter;
this.referencesRegistry = referencesRegistry;
this.isCore = isCore;
this.strictCtorDeps = strictCtorDeps;
this.semanticDepGraphUpdater = semanticDepGraphUpdater;
this.annotateForClosureCompiler = annotateForClosureCompiler;
this.perf = perf;
this.importTracker = importTracker;
this.includeClassMetadata = includeClassMetadata;
this.compilationMode = compilationMode;
this.jitDeclarationRegistry = jitDeclarationRegistry;
this.strictStandalone = strictStandalone;
this.implicitStandaloneValue = implicitStandaloneValue;
}
precedence = checker.HandlerPrecedence.PRIMARY;
name = 'DirectiveDecoratorHandler';
detect(node, decorators) {
// If a class is undecorated but uses Angular features, we detect it as an
// abstract directive. This is an unsupported pattern as of v10, but we want
// to still detect these patterns so that we can report diagnostics.
if (!decorators) {
const angularField = this.findClassFieldWithAngularFeatures(node);
return angularField
? { trigger: angularField.node, decorator: null, metadata: null }
: undefined;
}
else {
const decorator = checker.findAngularDecorator(decorators, 'Directive', this.isCore);
return decorator ? { trigger: decorator.node, decorator, metadata: decorator } : undefined;
}
}
analyze(node, decorator) {
// Skip processing of the class declaration if compilation of undecorated classes
// with Angular features is disabled. Previously in ngtsc, such classes have always
// been processed, but we want to enforce a consistent decorator mental model.
// See: https://v9.angular.io/guide/migration-undecorated-classes.
if (decorator === null) {
// If compiling @angular/core, skip the diagnostic as core occasionally hand-writes
// definitions.
if (this.isCore) {
return {};
}
return { diagnostics: [getUndecoratedClassWithAngularFeaturesDiagnostic(node)] };
}
this.perf.eventCount(checker.PerfEvent.AnalyzeDirective);
const directiveResult = extractDirectiveMetadata(node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter, this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode,
/* defaultSelector */ null, this.strictStandalone, this.implicitStandaloneValue);
// `extractDirectiveMetadata` returns `jitForced = true` when the `@Directive` has
// set `jit: true`. In this case, compilation of the decorator is skipped. Returning
// an empty object signifies that no analysis was produced.
if (directiveResult.jitForced) {
this.jitDeclarationRegistry.jitDeclarations.add(node);
return {};
}
const analysis = directiveResult.metadata;
let providersRequiringFactory = null;
if (directiveResult !== undefined && directiveResult.decorator.has('providers')) {
providersRequiringFactory = checker.resolveProvidersRequiringFactory(directiveResult.decorator.get('providers'), this.reflector, this.evaluator);
}
return {
analysis: {
inputs: directiveResult.inputs,
inputFieldNamesFromMetadataArray: directiveResult.inputFieldNamesFromMetadataArray,
outputs: directiveResult.outputs,
meta: analysis,
hostDirectives: directiveResult.hostDirectives,
rawHostDirectives: directiveResult.rawHostDirectives,
classMetadata: this.includeClassMetadata
? extractClassMetadata(node, this.reflector, this.isCore, this.annotateForClosureCompiler)
: null,
baseClass: checker.readBaseClass(node, this.reflector, this.evaluator),
typeCheckMeta: checker.extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
providersRequiringFactory,
isPoisoned: false,
isStructural: directiveResult.isStructural,
decorator: decorator?.node ?? null,
},
};
}
symbol(node, analysis) {
const typeParameters = extractSemanticTypeParameters(node);
return new DirectiveSymbol(node, analysis.meta.selector, analysis.inputs, analysis.outputs, analysis.meta.exportAs, analysis.typeCheckMeta, typeParameters);
}
register(node, analysis) {
// Register this directive's information with the `MetadataRegistry`. This ensures that
// the information about the directive is available during the compile() phase.
const ref = new checker.Reference(node);
this.metaRegistry.registerDirectiveMetadata({
kind: checker.MetaKind.Directive,
matchSource: checker.MatchSource.Selector,
ref,
name: node.name.text,
selector: analysis.meta.selector,
exportAs: analysis.meta.exportAs,
inputs: analysis.inputs,
inputFieldNamesFromMetadataArray: analysis.inputFieldNamesFromMetadataArray,
outputs: analysis.outputs,
queries: analysis.meta.queries.map((query) => query.propertyName),
isComponent: false,
baseClass: analysis.baseClass,
hostDirectives: analysis.hostDirectives,
...analysis.typeCheckMeta,
isPoisoned: analysis.isPoisoned,
isStructural: analysis.isStructural,
animationTriggerNames: null,
isStandalone: analysis.meta.isStandalone,
isSignal: analysis.meta.isSignal,
imports: null,
rawImports: null,
deferredImports: null,
schemas: null,
ngContentSelectors: null,
decorator: analysis.decorator,
preserveWhitespaces: false,
// Directives analyzed within our own compilation are not _assumed_ to export providers.
// Instead, we statically analyze their imports to make a direct determination.
assumedToExportProviders: false,
isExplicitlyDeferred: false,
});
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.meta.deps,
});
}
resolve(node, analysis, symbol) {
if (this.compilationMode === checker.CompilationMode.LOCAL) {
return {};
}
if (this.semanticDepGraphUpdater !== null && analysis.baseClass instanceof checker.Reference) {
symbol.baseClass = this.semanticDepGraphUpdater.getSymbol(analysis.baseClass.node);
}
const diagnostics = [];
if (analysis.providersRequiringFactory !== null &&
analysis.meta.providers instanceof checker.WrappedNodeExpr) {
const providerDiagnostics = getProviderDiagnostics(analysis.providersRequiringFactory, analysis.meta.providers.node, this.injectableRegistry);
diagnostics.push(...providerDiagnostics);
}
const directiveDiagnostics = getDirectiveDiagnostics(node, this.injectableRegistry, this.evaluator, this.reflector, this.scopeRegistry, this.strictCtorDeps, 'Directive');
if (directiveDiagnostics !== null) {
diagnostics.push(...directiveDiagnostics);
}
const hostDirectivesDiagnotics = analysis.hostDirectives && analysis.rawHostDirectives
? validateHostDirectives(analysis.rawHostDirectives, analysis.hostDirectives, this.metaReader)
: null;
if (hostDirectivesDiagnotics !== null) {
diagnostics.push(...hostDirectivesDiagnotics);
}
return { diagnostics: diagnostics.length > 0 ? diagnostics : undefined };
}
compileFull(node, analysis, resolution, pool) {
const fac = compileNgFactoryDefField(checker.toFactoryMetadata(analysis.meta, checker.FactoryTarget.Directive));
const def = checker.compileDirectiveFromMetadata(analysis.meta, pool, checker.makeBindingParser());
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const classMetadata = analysis.classMetadata !== null
? compileClassMetadata(analysis.classMetadata).toStmt()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */);
}
compilePartial(node, analysis, resolution) {
const fac = compileDeclareFactory(checker.toFactoryMetadata(analysis.meta, checker.FactoryTarget.Directive));
const def = compileDeclareDirectiveFromMetadata(analysis.meta);
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const classMetadata = analysis.classMetadata !== null
? compileDeclareClassMetadata(analysis.classMetadata).toStmt()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */);
}
compileLocal(node, analysis, resolution, pool) {
const fac = compileNgFactoryDefField(checker.toFactoryMetadata(analysis.meta, checker.FactoryTarget.Directive));
const def = checker.compileDirectiveFromMetadata(analysis.meta, pool, checker.makeBindingParser());
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const classMetadata = analysis.classMetadata !== null
? compileClassMetadata(analysis.classMetadata).toStmt()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */);
}
/**
* Checks if a given class uses Angular features and returns the TypeScript node
* that indicated the usage. Classes are considered using Angular features if they
* contain class members that are either decorated with a known Angular decorator,
* or if they correspond to a known Angular lifecycle hook.
*/
findClassFieldWithAngularFeatures(node) {
return this.reflector.getMembersOfClass(node).find((member) => {
if (!member.isStatic &&
member.kind === checker.ClassMemberKind.Method &&
LIFECYCLE_HOOKS.has(member.name)) {
return true;
}
if (member.decorators) {
return member.decorators.some((decorator) => FIELD_DECORATORS.some((decoratorName) => checker.isAngularDecorator(decorator, decoratorName, this.isCore)));
}
return false;
});
}
}
/**
* Creates a foreign function resolver to detect a `ModuleWithProviders` type in a return type
* position of a function or method declaration. A `SyntheticValue` is produced if such a return
* type is recognized.
*
* @param reflector The reflection host to use for analyzing the syntax.
* @param isCore Whether the @angular/core package is being compiled.
*/
function createModuleWithProvidersResolver(reflector, isCore) {
/**
* Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form:
* `ModuleWithProviders`
* @param type The type to reflect on.
* @returns the identifier of the NgModule type if found, or null otherwise.
*/
function _reflectModuleFromTypeParam(type, node) {
// Examine the type of the function to see if it's a ModuleWithProviders reference.
if (!ts__default["default"].isTypeReferenceNode(type)) {
return null;
}
const typeName = (type &&
((ts__default["default"].isIdentifier(type.typeName) && type.typeName) ||
(ts__default["default"].isQualifiedName(type.typeName) && type.typeName.right))) ||
null;
if (typeName === null) {
return null;
}
// Look at the type itself to see where it comes from.
const id = reflector.getImportOfIdentifier(typeName);
// If it's not named ModuleWithProviders, bail.
if (id === null || id.name !== 'ModuleWithProviders') {
return null;
}
// If it's not from @angular/core, bail.
if (!isCore && id.from !== '@angular/core') {
return null;
}
// If there's no type parameter specified, bail.
if (type.typeArguments === undefined || type.typeArguments.length !== 1) {
const parent = ts__default["default"].isMethodDeclaration(node) && ts__default["default"].isClassDeclaration(node.parent) ? node.parent : null;
const symbolName = (parent && parent.name ? parent.name.getText() + '.' : '') +
(node.name ? node.name.getText() : 'anonymous');
throw new checker.FatalDiagnosticError(checker.ErrorCode.NGMODULE_MODULE_WITH_PROVIDERS_MISSING_GENERIC, type, `${symbolName} returns a ModuleWithProviders type without a generic type argument. ` +
`Please add a generic type argument to the ModuleWithProviders type. If this ` +
`occurrence is in library code you don't control, please contact the library authors.`);
}
const arg = type.typeArguments[0];
return checker.typeNodeToValueExpr(arg);
}
/**
* Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form:
* `A|B|{ngModule: T}|C`.
* @param type The type to reflect on.
* @returns the identifier of the NgModule type if found, or null otherwise.
*/
function _reflectModuleFromLiteralType(type) {
if (!ts__default["default"].isIntersectionTypeNode(type)) {
return null;
}
for (const t of type.types) {
if (ts__default["default"].isTypeLiteralNode(t)) {
for (const m of t.members) {
const ngModuleType = (ts__default["default"].isPropertySignature(m) &&
ts__default["default"].isIdentifier(m.name) &&
m.name.text === 'ngModule' &&
m.type) ||
null;
let ngModuleExpression = null;
// Handle `: typeof X` or `: X` cases.
if (ngModuleType !== null && ts__default["default"].isTypeQueryNode(ngModuleType)) {
ngModuleExpression = checker.entityNameToValue(ngModuleType.exprName);
}
else if (ngModuleType !== null) {
ngModuleExpression = checker.typeNodeToValueExpr(ngModuleType);
}
if (ngModuleExpression) {
return ngModuleExpression;
}
}
}
}
return null;
}
return (fn, callExpr, resolve, unresolvable) => {
const rawType = fn.node.type;
if (rawType === undefined) {
return unresolvable;
}
const type = _reflectModuleFromTypeParam(rawType, fn.node) ?? _reflectModuleFromLiteralType(rawType);
if (type === null) {
return unresolvable;
}
const ngModule = resolve(type);
if (!(ngModule instanceof checker.Reference) || !checker.isNamedClassDeclaration(ngModule.node)) {
return unresolvable;
}
return new checker.SyntheticValue({
ngModule: ngModule,
mwpCall: callExpr,
});
};
}
function isResolvedModuleWithProviders(sv) {
return (typeof sv.value === 'object' &&
sv.value != null &&
sv.value.hasOwnProperty('ngModule') &&
sv.value.hasOwnProperty('mwpCall'));
}
/**
* Represents an Angular NgModule.
*/
class NgModuleSymbol extends SemanticSymbol {
hasProviders;
remotelyScopedComponents = [];
/**
* `SemanticSymbol`s of the transitive imports of this NgModule which came from imported
* standalone components.
*
* Standalone components are excluded/included in the `InjectorDef` emit output of the NgModule
* based on whether the compiler can prove that their transitive imports may contain exported
* providers, so a change in this set of symbols may affect the compilation output of this
* NgModule.
*/
transitiveImportsFromStandaloneComponents = new Set();
constructor(decl, hasProviders) {
super(decl);
this.hasProviders = hasProviders;
}
isPublicApiAffected(previousSymbol) {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
// Changes in the provider status of this NgModule affect downstream dependencies, which may
// consider provider status in their own emits.
if (previousSymbol.hasProviders !== this.hasProviders) {
return true;
}
return false;
}
isEmitAffected(previousSymbol) {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
// compare our remotelyScopedComponents to the previous symbol
if (previousSymbol.remotelyScopedComponents.length !== this.remotelyScopedComponents.length) {
return true;
}
for (const currEntry of this.remotelyScopedComponents) {
const prevEntry = previousSymbol.remotelyScopedComponents.find((prevEntry) => {
return isSymbolEqual(prevEntry.component, currEntry.component);
});
if (prevEntry === undefined) {
// No previous entry was found, which means that this component became remotely scoped and
// hence this NgModule needs to be re-emitted.
return true;
}
if (!isArrayEqual(currEntry.usedDirectives, prevEntry.usedDirectives, isReferenceEqual)) {
// The list of used directives or their order has changed. Since this NgModule emits
// references to the list of used directives, it should be re-emitted to update this list.
// Note: the NgModule does not have to be re-emitted when any of the directives has had
// their public API changed, as the NgModule only emits a reference to the symbol by its
// name. Therefore, testing for symbol equality is sufficient.
return true;
}
if (!isArrayEqual(currEntry.usedPipes, prevEntry.usedPipes, isReferenceEqual)) {
return true;
}
}
if (previousSymbol.transitiveImportsFromStandaloneComponents.size !==
this.transitiveImportsFromStandaloneComponents.size) {
return true;
}
const previousImports = Array.from(previousSymbol.transitiveImportsFromStandaloneComponents);
for (const transitiveImport of this.transitiveImportsFromStandaloneComponents) {
const prevEntry = previousImports.find((prevEntry) => isSymbolEqual(prevEntry, transitiveImport));
if (prevEntry === undefined) {
return true;
}
if (transitiveImport.isPublicApiAffected(prevEntry)) {
return true;
}
}
return false;
}
isTypeCheckApiAffected(previousSymbol) {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
return false;
}
addRemotelyScopedComponent(component, usedDirectives, usedPipes) {
this.remotelyScopedComponents.push({ component, usedDirectives, usedPipes });
}
addTransitiveImportFromStandaloneComponent(importedSymbol) {
this.transitiveImportsFromStandaloneComponents.add(importedSymbol);
}
}
/**
* Compiles @NgModule annotations to ngModuleDef fields.
*/
class NgModuleDecoratorHandler {
reflector;
evaluator;
metaReader;
metaRegistry;
scopeRegistry;
referencesRegistry;
exportedProviderStatusResolver;
semanticDepGraphUpdater;
isCore;
refEmitter;
annotateForClosureCompiler;
onlyPublishPublicTypings;
injectableRegistry;
perf;
includeClassMetadata;
includeSelectorScope;
compilationMode;
localCompilationExtraImportsTracker;
jitDeclarationRegistry;
constructor(reflector, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, exportedProviderStatusResolver, semanticDepGraphUpdater, isCore, refEmitter, annotateForClosureCompiler, onlyPublishPublicTypings, injectableRegistry, perf, includeClassMetadata, includeSelectorScope, compilationMode, localCompilationExtraImportsTracker, jitDeclarationRegistry) {
this.reflector = reflector;
this.evaluator = evaluator;
this.metaReader = metaReader;
this.metaRegistry = metaRegistry;
this.scopeRegistry = scopeRegistry;
this.referencesRegistry = referencesRegistry;
this.exportedProviderStatusResolver = exportedProviderStatusResolver;
this.semanticDepGraphUpdater = semanticDepGraphUpdater;
this.isCore = isCore;
this.refEmitter = refEmitter;
this.annotateForClosureCompiler = annotateForClosureCompiler;
this.onlyPublishPublicTypings = onlyPublishPublicTypings;
this.injectableRegistry = injectableRegistry;
this.perf = perf;
this.includeClassMetadata = includeClassMetadata;
this.includeSelectorScope = includeSelectorScope;
this.compilationMode = compilationMode;
this.localCompilationExtraImportsTracker = localCompilationExtraImportsTracker;
this.jitDeclarationRegistry = jitDeclarationRegistry;
}
precedence = checker.HandlerPrecedence.PRIMARY;
name = 'NgModuleDecoratorHandler';
detect(node, decorators) {
if (!decorators) {
return undefined;
}
const decorator = checker.findAngularDecorator(decorators, 'NgModule', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,
decorator: decorator,
metadata: decorator,
};
}
else {
return undefined;
}
}
analyze(node, decorator) {
this.perf.eventCount(checker.PerfEvent.AnalyzeNgModule);
const name = node.name.text;
if (decorator.args === null || decorator.args.length > 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, `Incorrect number of arguments to @NgModule decorator`);
}
// @NgModule can be invoked without arguments. In case it is, pretend as if a blank object
// literal was specified. This simplifies the code below.
const meta = decorator.args.length === 1
? checker.unwrapExpression(decorator.args[0])
: ts__default["default"].factory.createObjectLiteralExpression([]);
if (!ts__default["default"].isObjectLiteralExpression(meta)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, '@NgModule argument must be an object literal');
}
const ngModule = checker.reflectObjectLiteral(meta);
if (ngModule.has('jit')) {
this.jitDeclarationRegistry.jitDeclarations.add(node);
// The only allowed value is true, so there's no need to expand further.
return {};
}
const moduleResolvers = checker.combineResolvers([
createModuleWithProvidersResolver(this.reflector, this.isCore),
checker.forwardRefResolver,
]);
const diagnostics = [];
// Resolving declarations
let declarationRefs = [];
const rawDeclarations = ngModule.get('declarations') ?? null;
if (rawDeclarations !== null) {
const declarationMeta = this.evaluator.evaluate(rawDeclarations, checker.forwardRefResolver);
declarationRefs = this.resolveTypeList(rawDeclarations, declarationMeta, name, 'declarations', 0, this.compilationMode === checker.CompilationMode.LOCAL).references;
// Look through the declarations to make sure they're all a part of the current compilation.
for (const ref of declarationRefs) {
if (ref.node.getSourceFile().isDeclarationFile) {
const errorNode = ref.getOriginForDiagnostics(rawDeclarations);
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode, `Cannot declare '${ref.node.name.text}' in an NgModule as it's not a part of the current compilation.`, [checker.makeRelatedInformation(ref.node.name, `'${ref.node.name.text}' is declared here.`)]));
}
}
}
if (diagnostics.length > 0) {
return { diagnostics };
}
// Resolving imports
let importRefs = [];
let rawImports = ngModule.get('imports') ?? null;
if (rawImports !== null) {
const importsMeta = this.evaluator.evaluate(rawImports, moduleResolvers);
const result = this.resolveTypeList(rawImports, importsMeta, name, 'imports', 0, this.compilationMode === checker.CompilationMode.LOCAL);
if (this.compilationMode === checker.CompilationMode.LOCAL &&
this.localCompilationExtraImportsTracker !== null) {
// For generating extra imports in local mode, the NgModule imports that are from external
// files (i.e., outside of the compilation unit) are to be added to all the files in the
// compilation unit. This is because any external component that is a dependency of some
// component in the compilation unit must be imported by one of these NgModule's external
// imports (or the external component cannot be a dependency of that internal component).
// This approach can be further optimized by adding these NgModule external imports to a
// subset of files in the compilation unit and not all. See comments in {@link
// LocalCompilationExtraImportsTracker} and {@link
// LocalCompilationExtraImportsTracker#addGlobalImportFromIdentifier} for more details.
for (const d of result.dynamicValues) {
this.localCompilationExtraImportsTracker.addGlobalImportFromIdentifier(d.node);
}
}
importRefs = result.references;
}
// Resolving exports
let exportRefs = [];
const rawExports = ngModule.get('exports') ?? null;
if (rawExports !== null) {
const exportsMeta = this.evaluator.evaluate(rawExports, moduleResolvers);
exportRefs = this.resolveTypeList(rawExports, exportsMeta, name, 'exports', 0, this.compilationMode === checker.CompilationMode.LOCAL).references;
this.referencesRegistry.add(node, ...exportRefs);
}
// Resolving bootstrap
let bootstrapRefs = [];
const rawBootstrap = ngModule.get('bootstrap') ?? null;
if (this.compilationMode !== checker.CompilationMode.LOCAL && rawBootstrap !== null) {
const bootstrapMeta = this.evaluator.evaluate(rawBootstrap, checker.forwardRefResolver);
bootstrapRefs = this.resolveTypeList(rawBootstrap, bootstrapMeta, name, 'bootstrap', 0,
/* allowUnresolvedReferences */ false).references;
// Verify that the `@NgModule.bootstrap` list doesn't have Standalone Components.
for (const ref of bootstrapRefs) {
const dirMeta = this.metaReader.getDirectiveMetadata(ref);
if (dirMeta?.isStandalone) {
diagnostics.push(makeStandaloneBootstrapDiagnostic(node, ref, rawBootstrap));
}
}
}
const schemas = this.compilationMode !== checker.CompilationMode.LOCAL && ngModule.has('schemas')
? extractSchemas(ngModule.get('schemas'), this.evaluator, 'NgModule')
: [];
let id = null;
if (ngModule.has('id')) {
const idExpr = ngModule.get('id');
if (!isModuleIdExpression(idExpr)) {
id = new checker.WrappedNodeExpr(idExpr);
}
else {
const diag = checker.makeDiagnostic(checker.ErrorCode.WARN_NGMODULE_ID_UNNECESSARY, idExpr, `Using 'module.id' for NgModule.id is a common anti-pattern that is ignored by the Angular compiler.`);
diag.category = ts__default["default"].DiagnosticCategory.Warning;
diagnostics.push(diag);
}
}
const valueContext = node.getSourceFile();
const exportedNodes = new Set(exportRefs.map((ref) => ref.node));
const declarations = [];
const exportedDeclarations = [];
const bootstrap = bootstrapRefs.map((bootstrap) => this._toR3Reference(bootstrap.getOriginForDiagnostics(meta, node.name), bootstrap, valueContext));
for (const ref of declarationRefs) {
const decl = this._toR3Reference(ref.getOriginForDiagnostics(meta, node.name), ref, valueContext);
declarations.push(decl);
if (exportedNodes.has(ref.node)) {
exportedDeclarations.push(decl.type);
}
}
const imports = importRefs.map((imp) => this._toR3Reference(imp.getOriginForDiagnostics(meta, node.name), imp, valueContext));
const exports = exportRefs.map((exp) => this._toR3Reference(exp.getOriginForDiagnostics(meta, node.name), exp, valueContext));
const isForwardReference = (ref) => checker.isExpressionForwardReference(ref.value, node.name, valueContext);
const containsForwardDecls = bootstrap.some(isForwardReference) ||
declarations.some(isForwardReference) ||
imports.some(isForwardReference) ||
exports.some(isForwardReference);
const type = checker.wrapTypeReference(this.reflector, node);
let ngModuleMetadata;
if (this.compilationMode === checker.CompilationMode.LOCAL) {
ngModuleMetadata = {
kind: checker.R3NgModuleMetadataKind.Local,
type,
bootstrapExpression: rawBootstrap ? new checker.WrappedNodeExpr(rawBootstrap) : null,
declarationsExpression: rawDeclarations ? new checker.WrappedNodeExpr(rawDeclarations) : null,
exportsExpression: rawExports ? new checker.WrappedNodeExpr(rawExports) : null,
importsExpression: rawImports ? new checker.WrappedNodeExpr(rawImports) : null,
id,
// Use `ɵɵsetNgModuleScope` to patch selector scopes onto the generated definition in a
// tree-shakeable way.
selectorScopeMode: checker.R3SelectorScopeMode.SideEffect,
// TODO: to be implemented as a part of FW-1004.
schemas: [],
};
}
else {
ngModuleMetadata = {
kind: checker.R3NgModuleMetadataKind.Global,
type,
bootstrap,
declarations,
publicDeclarationTypes: this.onlyPublishPublicTypings ? exportedDeclarations : null,
exports,
imports,
// Imported types are generally private, so include them unless restricting the .d.ts emit
// to only public types.
includeImportTypes: !this.onlyPublishPublicTypings,
containsForwardDecls,
id,
// Use `ɵɵsetNgModuleScope` to patch selector scopes onto the generated definition in a
// tree-shakeable way.
selectorScopeMode: this.includeSelectorScope
? checker.R3SelectorScopeMode.SideEffect
: checker.R3SelectorScopeMode.Omit,
// TODO: to be implemented as a part of FW-1004.
schemas: [],
};
}
const rawProviders = ngModule.has('providers') ? ngModule.get('providers') : null;
let wrappedProviders = null;
// In most cases the providers will be an array literal. Check if it has any elements
// and don't include the providers if it doesn't which saves us a few bytes.
if (rawProviders !== null &&
(!ts__default["default"].isArrayLiteralExpression(rawProviders) || rawProviders.elements.length > 0)) {
wrappedProviders = new checker.WrappedNodeExpr(this.annotateForClosureCompiler
? checker.wrapFunctionExpressionsInParens(rawProviders)
: rawProviders);
}
const topLevelImports = [];
if (this.compilationMode !== checker.CompilationMode.LOCAL && ngModule.has('imports')) {
const rawImports = checker.unwrapExpression(ngModule.get('imports'));
let topLevelExpressions = [];
if (ts__default["default"].isArrayLiteralExpression(rawImports)) {
for (const element of rawImports.elements) {
if (ts__default["default"].isSpreadElement(element)) {
// Because `imports` allows nested arrays anyway, a spread expression (`...foo`) can be
// treated the same as a direct reference to `foo`.
topLevelExpressions.push(element.expression);
continue;
}
topLevelExpressions.push(element);
}
}
else {
// Treat the whole `imports` expression as top-level.
topLevelExpressions.push(rawImports);
}
let absoluteIndex = 0;
for (const importExpr of topLevelExpressions) {
const resolved = this.evaluator.evaluate(importExpr, moduleResolvers);
const { references, hasModuleWithProviders } = this.resolveTypeList(importExpr, [resolved], node.name.text, 'imports', absoluteIndex,
/* allowUnresolvedReferences */ false);
absoluteIndex += references.length;
topLevelImports.push({
expression: importExpr,
resolvedReferences: references,
hasModuleWithProviders,
});
}
}
const injectorMetadata = {
name,
type,
providers: wrappedProviders,
imports: [],
};
if (this.compilationMode === checker.CompilationMode.LOCAL) {
// Adding NgModule's raw imports/exports to the injector's imports field in local compilation
// mode.
for (const exp of [rawImports, rawExports]) {
if (exp === null) {
continue;
}
if (ts__default["default"].isArrayLiteralExpression(exp)) {
// If array expression then add it entry-by-entry to the injector imports
if (exp.elements) {
injectorMetadata.imports.push(...exp.elements.map((n) => new checker.WrappedNodeExpr(n)));
}
}
else {
// if not array expression then add it as is to the injector's imports field.
injectorMetadata.imports.push(new checker.WrappedNodeExpr(exp));
}
}
}
const factoryMetadata = {
name,
type,
typeArgumentCount: 0,
deps: getValidConstructorDependencies(node, this.reflector, this.isCore),
target: checker.FactoryTarget.NgModule,
};
// Remote scoping is used when adding imports to a component file would create a cycle. In such
// circumstances the component scope is monkey-patched from the NgModule file instead.
//
// However, if the NgModule itself has a cycle with the desired component/directive
// reference(s), then we need to be careful. This can happen for example if an NgModule imports
// a standalone component and the component also imports the NgModule.
//
// In this case, it'd be tempting to rely on the compiler's cycle detector to automatically put
// such circular references behind a function/closure. This requires global knowledge of the
// import graph though, and we don't want to depend on such techniques for new APIs like
// standalone components.
//
// Instead, we look for `forwardRef`s in the NgModule dependencies - an explicit signal from the
// user that a reference may not be defined until a circular import is resolved. If an NgModule
// contains forward-referenced declarations or imports, we assume that remotely scoped
// components should also guard against cycles using a closure-wrapped scope.
//
// The actual detection here is done heuristically. The compiler doesn't actually know whether
// any given `Reference` came from a `forwardRef`, but it does know when a `Reference` came from
// a `ForeignFunctionResolver` _like_ the `forwardRef` resolver. So we know when it's safe to
// not use a closure, and will use one just in case otherwise.
const remoteScopesMayRequireCycleProtection = declarationRefs.some(isSyntheticReference) || importRefs.some(isSyntheticReference);
return {
diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
analysis: {
id,
schemas,
mod: ngModuleMetadata,
inj: injectorMetadata,
fac: factoryMetadata,
declarations: declarationRefs,
rawDeclarations,
imports: topLevelImports,
rawImports,
importRefs,
exports: exportRefs,
rawExports,
providers: rawProviders,
providersRequiringFactory: rawProviders
? checker.resolveProvidersRequiringFactory(rawProviders, this.reflector, this.evaluator)
: null,
classMetadata: this.includeClassMetadata
? extractClassMetadata(node, this.reflector, this.isCore, this.annotateForClosureCompiler)
: null,
factorySymbolName: node.name.text,
remoteScopesMayRequireCycleProtection,
decorator: decorator?.node ?? null,
},
};
}
symbol(node, analysis) {
return new NgModuleSymbol(node, analysis.providers !== null);
}
register(node, analysis) {
// Register this module's information with the LocalModuleScopeRegistry. This ensures that
// during the compile() phase, the module's metadata is available for selector scope
// computation.
this.metaRegistry.registerNgModuleMetadata({
kind: checker.MetaKind.NgModule,
ref: new checker.Reference(node),
schemas: analysis.schemas,
declarations: analysis.declarations,
imports: analysis.importRefs,
exports: analysis.exports,
rawDeclarations: analysis.rawDeclarations,
rawImports: analysis.rawImports,
rawExports: analysis.rawExports,
decorator: analysis.decorator,
mayDeclareProviders: analysis.providers !== null,
isPoisoned: false,
});
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.fac.deps,
});
}
resolve(node, analysis) {
if (this.compilationMode === checker.CompilationMode.LOCAL) {
return {};
}
const scope = this.scopeRegistry.getScopeOfModule(node);
const diagnostics = [];
const scopeDiagnostics = this.scopeRegistry.getDiagnosticsOfModule(node);
if (scopeDiagnostics !== null) {
diagnostics.push(...scopeDiagnostics);
}
if (analysis.providersRequiringFactory !== null) {
const providerDiagnostics = getProviderDiagnostics(analysis.providersRequiringFactory, analysis.providers, this.injectableRegistry);
diagnostics.push(...providerDiagnostics);
}
const data = {
injectorImports: [],
};
// Add all top-level imports from the `imports` field to the injector imports.
for (const topLevelImport of analysis.imports) {
if (topLevelImport.hasModuleWithProviders) {
// We have no choice but to emit expressions which contain MWPs, as we cannot filter on
// individual references.
data.injectorImports.push(new checker.WrappedNodeExpr(topLevelImport.expression));
continue;
}
const refsToEmit = [];
let symbol = null;
if (this.semanticDepGraphUpdater !== null) {
const sym = this.semanticDepGraphUpdater.getSymbol(node);
if (sym instanceof NgModuleSymbol) {
symbol = sym;
}
}
for (const ref of topLevelImport.resolvedReferences) {
const dirMeta = this.metaReader.getDirectiveMetadata(ref);
if (dirMeta !== null) {
if (!dirMeta.isComponent) {
// Skip emit of directives in imports - directives can't carry providers.
continue;
}
// Check whether this component has providers.
const mayExportProviders = this.exportedProviderStatusResolver.mayExportProviders(dirMeta.ref, (importRef) => {
// We need to keep track of which transitive imports were used to decide
// `mayExportProviders`, since if those change in a future compilation this
// NgModule will need to be re-emitted.
if (symbol !== null && this.semanticDepGraphUpdater !== null) {
const importSymbol = this.semanticDepGraphUpdater.getSymbol(importRef.node);
symbol.addTransitiveImportFromStandaloneComponent(importSymbol);
}
});
if (!mayExportProviders) {
// Skip emit of components that don't carry providers.
continue;
}
}
const pipeMeta = dirMeta === null ? this.metaReader.getPipeMetadata(ref) : null;
if (pipeMeta !== null) {
// Skip emit of pipes in imports - pipes can't carry providers.
continue;
}
refsToEmit.push(ref);
}
if (refsToEmit.length === topLevelImport.resolvedReferences.length) {
// All references within this top-level import should be emitted, so just use the user's
// expression.
data.injectorImports.push(new checker.WrappedNodeExpr(topLevelImport.expression));
}
else {
// Some references have been filtered out. Emit references to individual classes.
const context = node.getSourceFile();
for (const ref of refsToEmit) {
const emittedRef = this.refEmitter.emit(ref, context);
checker.assertSuccessfulReferenceEmit(emittedRef, topLevelImport.expression, 'class');
data.injectorImports.push(emittedRef.expression);
}
}
}
if (scope !== null && !scope.compilation.isPoisoned) {
// Using the scope information, extend the injector's imports using the modules that are
// specified as module exports.
const context = checker.getSourceFile(node);
for (const exportRef of analysis.exports) {
if (isNgModule(exportRef.node, scope.compilation)) {
const type = this.refEmitter.emit(exportRef, context);
checker.assertSuccessfulReferenceEmit(type, node, 'NgModule');
data.injectorImports.push(type.expression);
}
}
for (const decl of analysis.declarations) {
const dirMeta = this.metaReader.getDirectiveMetadata(decl);
if (dirMeta !== null) {
const refType = dirMeta.isComponent ? 'Component' : 'Directive';
if (dirMeta.selector === null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DIRECTIVE_MISSING_SELECTOR, decl.node, `${refType} ${decl.node.name.text} has no selector, please add it!`);
}
continue;
}
}
}
if (diagnostics.length > 0) {
return { diagnostics };
}
if (scope === null ||
scope.compilation.isPoisoned ||
scope.exported.isPoisoned ||
scope.reexports === null) {
return { data };
}
else {
return {
data,
reexports: scope.reexports,
};
}
}
compileFull(node, { inj, mod, fac, classMetadata, declarations, remoteScopesMayRequireCycleProtection, }, { injectorImports }) {
const factoryFn = compileNgFactoryDefField(fac);
const ngInjectorDef = checker.compileInjector({
...inj,
imports: injectorImports,
});
const ngModuleDef = checker.compileNgModule(mod);
const statements = ngModuleDef.statements;
const metadata = classMetadata !== null ? compileClassMetadata(classMetadata) : null;
this.insertMetadataStatement(statements, metadata);
this.appendRemoteScopingStatements(statements, node, declarations, remoteScopesMayRequireCycleProtection);
return this.compileNgModule(factoryFn, ngInjectorDef, ngModuleDef);
}
compilePartial(node, { inj, fac, mod, classMetadata }, { injectorImports }) {
const factoryFn = compileDeclareFactory(fac);
const injectorDef = compileDeclareInjectorFromMetadata({
...inj,
imports: injectorImports,
});
const ngModuleDef = compileDeclareNgModuleFromMetadata(mod);
const metadata = classMetadata !== null ? compileDeclareClassMetadata(classMetadata) : null;
this.insertMetadataStatement(ngModuleDef.statements, metadata);
// NOTE: no remote scoping required as this is banned in partial compilation.
return this.compileNgModule(factoryFn, injectorDef, ngModuleDef);
}
compileLocal(node, { inj, mod, fac, classMetadata, declarations, remoteScopesMayRequireCycleProtection, }) {
const factoryFn = compileNgFactoryDefField(fac);
const ngInjectorDef = checker.compileInjector({
...inj,
});
const ngModuleDef = checker.compileNgModule(mod);
const statements = ngModuleDef.statements;
const metadata = classMetadata !== null ? compileClassMetadata(classMetadata) : null;
this.insertMetadataStatement(statements, metadata);
this.appendRemoteScopingStatements(statements, node, declarations, remoteScopesMayRequireCycleProtection);
return this.compileNgModule(factoryFn, ngInjectorDef, ngModuleDef);
}
/**
* Add class metadata statements, if provided, to the `ngModuleStatements`.
*/
insertMetadataStatement(ngModuleStatements, metadata) {
if (metadata !== null) {
ngModuleStatements.unshift(metadata.toStmt());
}
}
/**
* Add remote scoping statements, as needed, to the `ngModuleStatements`.
*/
appendRemoteScopingStatements(ngModuleStatements, node, declarations, remoteScopesMayRequireCycleProtection) {
// Local compilation mode generates its own runtimes to compute the dependencies. So there no
// need to add remote scope statements (which also conflicts with local compilation runtimes)
if (this.compilationMode === checker.CompilationMode.LOCAL) {
return;
}
const context = checker.getSourceFile(node);
for (const decl of declarations) {
const remoteScope = this.scopeRegistry.getRemoteScope(decl.node);
if (remoteScope !== null) {
const directives = remoteScope.directives.map((directive) => {
const type = this.refEmitter.emit(directive, context);
checker.assertSuccessfulReferenceEmit(type, node, 'directive');
return type.expression;
});
const pipes = remoteScope.pipes.map((pipe) => {
const type = this.refEmitter.emit(pipe, context);
checker.assertSuccessfulReferenceEmit(type, node, 'pipe');
return type.expression;
});
const directiveArray = new checker.LiteralArrayExpr(directives);
const pipesArray = new checker.LiteralArrayExpr(pipes);
const directiveExpr = remoteScopesMayRequireCycleProtection && directives.length > 0
? new checker.FunctionExpr([], [new checker.ReturnStatement(directiveArray)])
: directiveArray;
const pipesExpr = remoteScopesMayRequireCycleProtection && pipes.length > 0
? new checker.FunctionExpr([], [new checker.ReturnStatement(pipesArray)])
: pipesArray;
const componentType = this.refEmitter.emit(decl, context);
checker.assertSuccessfulReferenceEmit(componentType, node, 'component');
const declExpr = componentType.expression;
const setComponentScope = new checker.ExternalExpr(checker.Identifiers.setComponentScope);
const callExpr = new checker.InvokeFunctionExpr(setComponentScope, [
declExpr,
directiveExpr,
pipesExpr,
]);
ngModuleStatements.push(callExpr.toStmt());
}
}
}
compileNgModule(factoryFn, injectorDef, ngModuleDef) {
const res = [
factoryFn,
{
name: 'ɵmod',
initializer: ngModuleDef.expression,
statements: ngModuleDef.statements,
type: ngModuleDef.type,
deferrableImports: null,
},
{
name: 'ɵinj',
initializer: injectorDef.expression,
statements: injectorDef.statements,
type: injectorDef.type,
deferrableImports: null,
},
];
return res;
}
_toR3Reference(origin, valueRef, valueContext) {
if (valueRef.hasOwningModuleGuess) {
return checker.toR3Reference(origin, valueRef, valueContext, this.refEmitter);
}
else {
return checker.toR3Reference(origin, valueRef, valueContext, this.refEmitter);
}
}
// Verify that a "Declaration" reference is a `ClassDeclaration` reference.
isClassDeclarationReference(ref) {
return this.reflector.isClass(ref.node);
}
/**
* Compute a list of `Reference`s from a resolved metadata value.
*/
resolveTypeList(expr, resolvedList, className, arrayName, absoluteIndex, allowUnresolvedReferences) {
let hasModuleWithProviders = false;
const refList = [];
const dynamicValueSet = new Set();
if (!Array.isArray(resolvedList)) {
if (allowUnresolvedReferences) {
return {
references: [],
hasModuleWithProviders: false,
dynamicValues: [],
};
}
throw createValueHasWrongTypeError(expr, resolvedList, `Expected array when reading the NgModule.${arrayName} of ${className}`);
}
for (let idx = 0; idx < resolvedList.length; idx++) {
let entry = resolvedList[idx];
// Unwrap ModuleWithProviders for modules that are locally declared (and thus static
// resolution was able to descend into the function and return an object literal, a Map).
if (entry instanceof checker.SyntheticValue && isResolvedModuleWithProviders(entry)) {
entry = entry.value.ngModule;
hasModuleWithProviders = true;
}
else if (entry instanceof Map && entry.has('ngModule')) {
entry = entry.get('ngModule');
hasModuleWithProviders = true;
}
if (Array.isArray(entry)) {
// Recurse into nested arrays.
const recursiveResult = this.resolveTypeList(expr, entry, className, arrayName, absoluteIndex, allowUnresolvedReferences);
refList.push(...recursiveResult.references);
for (const d of recursiveResult.dynamicValues) {
dynamicValueSet.add(d);
}
absoluteIndex += recursiveResult.references.length;
hasModuleWithProviders = hasModuleWithProviders || recursiveResult.hasModuleWithProviders;
}
else if (entry instanceof checker.Reference) {
if (!this.isClassDeclarationReference(entry)) {
throw createValueHasWrongTypeError(entry.node, entry, `Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${className} is not a class`);
}
refList.push(entry);
absoluteIndex += 1;
}
else if (entry instanceof checker.DynamicValue && allowUnresolvedReferences) {
dynamicValueSet.add(entry);
continue;
}
else {
// TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array.
throw createValueHasWrongTypeError(expr, entry, `Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${className} is not a reference`);
}
}
return {
references: refList,
hasModuleWithProviders,
dynamicValues: [...dynamicValueSet],
};
}
}
function isNgModule(node, compilation) {
return !compilation.dependencies.some((dep) => dep.ref.node === node);
}
/**
* Checks whether the given `ts.Expression` is the expression `module.id`.
*/
function isModuleIdExpression(expr) {
return (ts__default["default"].isPropertyAccessExpression(expr) &&
ts__default["default"].isIdentifier(expr.expression) &&
expr.expression.text === 'module' &&
expr.name.text === 'id');
}
/**
* Helper method to produce a diagnostics for a situation when a standalone component
* is referenced in the `@NgModule.bootstrap` array.
*/
function makeStandaloneBootstrapDiagnostic(ngModuleClass, bootstrappedClassRef, rawBootstrapExpr) {
const componentClassName = bootstrappedClassRef.node.name.text;
// Note: this error message should be aligned with the one produced by JIT.
const message = //
`The \`${componentClassName}\` class is a standalone component, which can ` +
`not be used in the \`@NgModule.bootstrap\` array. Use the \`bootstrapApplication\` ` +
`function for bootstrap instead.`;
const relatedInformation = [
checker.makeRelatedInformation(ngModuleClass, `The 'bootstrap' array is present on this NgModule.`),
];
return checker.makeDiagnostic(checker.ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE, getDiagnosticNode(bootstrappedClassRef, rawBootstrapExpr), message, relatedInformation);
}
function isSyntheticReference(ref) {
return ref.synthetic;
}
/**
* Generate a diagnostic related information object that describes a potential cyclic import path.
*/
function makeCyclicImportInfo(ref, type, cycle) {
const name = ref.debugName || '(unknown)';
const path = cycle
.getPath()
.map((sf) => sf.fileName)
.join(' -> ');
const message = `The ${type} '${name}' is used in the template but importing it would create a cycle: `;
return checker.makeRelatedInformation(ref.node, message + path);
}
/**
* Checks whether a selector is a valid custom element tag name.
* Based loosely on https://github.com/sindresorhus/validate-element-name.
*/
function checkCustomElementSelectorForErrors(selector) {
// Avoid flagging components with an attribute or class selector. This isn't bulletproof since it
// won't catch cases like `foo[]bar`, but we don't need it to be. This is mainly to avoid flagging
// something like `foo-bar[baz]` incorrectly.
if (selector.includes('.') || (selector.includes('[') && selector.includes(']'))) {
return null;
}
if (!/^[a-z]/.test(selector)) {
return 'Selector of a ShadowDom-encapsulated component must start with a lower case letter.';
}
if (/[A-Z]/.test(selector)) {
return 'Selector of a ShadowDom-encapsulated component must all be in lower case.';
}
if (!selector.includes('-')) {
return 'Selector of a component that uses ViewEncapsulation.ShadowDom must contain a hyphen.';
}
return null;
}
/** Determines the node to use for debugging purposes for the given TemplateDeclaration. */
function getTemplateDeclarationNodeForError(declaration) {
return declaration.isInline ? declaration.expression : declaration.templateUrlExpression;
}
function extractTemplate(node, template, evaluator, depTracker, resourceLoader, options, compilationMode) {
if (template.isInline) {
let sourceStr;
let sourceParseRange = null;
let templateContent;
let sourceMapping;
let escapedString = false;
let sourceMapUrl;
// We only support SourceMaps for inline templates that are simple string literals.
if (ts__default["default"].isStringLiteral(template.expression) ||
ts__default["default"].isNoSubstitutionTemplateLiteral(template.expression)) {
// the start and end of the `templateExpr` node includes the quotation marks, which we must
// strip
sourceParseRange = getTemplateRange(template.expression);
sourceStr = template.expression.getSourceFile().text;
templateContent = template.expression.text;
escapedString = true;
sourceMapping = {
type: 'direct',
node: template.expression,
};
sourceMapUrl = template.resolvedTemplateUrl;
}
else {
const resolvedTemplate = evaluator.evaluate(template.expression);
// The identifier used for @Component.template cannot be resolved in local compilation mode. An error specific to this situation is generated.
assertLocalCompilationUnresolvedConst(compilationMode, resolvedTemplate, template.expression, 'Unresolved identifier found for @Component.template field! ' +
'Did you import this identifier from a file outside of the compilation unit? ' +
'This is not allowed when Angular compiler runs in local mode. ' +
'Possible solutions: 1) Move the declaration into a file within the ' +
'compilation unit, 2) Inline the template, 3) Move the template into ' +
'a separate .html file and include it using @Component.templateUrl');
if (typeof resolvedTemplate !== 'string') {
throw createValueHasWrongTypeError(template.expression, resolvedTemplate, 'template must be a string');
}
// We do not parse the template directly from the source file using a lexer range, so
// the template source and content are set to the statically resolved template.
sourceStr = resolvedTemplate;
templateContent = resolvedTemplate;
sourceMapping = {
type: 'indirect',
node: template.expression,
componentClass: node,
template: templateContent,
};
// Indirect templates cannot be mapped to a particular byte range of any input file, since
// they're computed by expressions that may span many files. Don't attempt to map them back
// to a given file.
sourceMapUrl = null;
}
return {
...parseExtractedTemplate(template, sourceStr, sourceParseRange, escapedString, sourceMapUrl, options),
content: templateContent,
sourceMapping,
declaration: template,
};
}
else {
const templateContent = resourceLoader.load(template.resolvedTemplateUrl);
if (depTracker !== null) {
depTracker.addResourceDependency(node.getSourceFile(), checker.absoluteFrom(template.resolvedTemplateUrl));
}
return {
...parseExtractedTemplate(template,
/* sourceStr */ templateContent,
/* sourceParseRange */ null,
/* escapedString */ false,
/* sourceMapUrl */ template.resolvedTemplateUrl, options),
content: templateContent,
sourceMapping: {
type: 'external',
componentClass: node,
node: template.templateUrlExpression,
template: templateContent,
templateUrl: template.resolvedTemplateUrl,
},
declaration: template,
};
}
}
function parseExtractedTemplate(template, sourceStr, sourceParseRange, escapedString, sourceMapUrl, options) {
// We always normalize line endings if the template has been escaped (i.e. is inline).
const i18nNormalizeLineEndingsInICUs = escapedString || options.i18nNormalizeLineEndingsInICUs;
const commonParseOptions = {
interpolationConfig: template.interpolationConfig,
range: sourceParseRange ?? undefined,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs,
alwaysAttemptHtmlToR3AstConversion: options.usePoisonedData,
escapedString,
enableBlockSyntax: options.enableBlockSyntax,
enableLetSyntax: options.enableLetSyntax,
};
const parsedTemplate = checker.parseTemplate(sourceStr, sourceMapUrl ?? '', {
...commonParseOptions,
preserveWhitespaces: template.preserveWhitespaces,
preserveSignificantWhitespace: options.preserveSignificantWhitespace,
});
// Unfortunately, the primary parse of the template above may not contain accurate source map
// information. If used directly, it would result in incorrect code locations in template
// errors, etc. There are three main problems:
//
// 1. `preserveWhitespaces: false` or `preserveSignificantWhitespace: false` annihilates the
// correctness of template source mapping, as the whitespace transformation changes the
// contents of HTML text nodes before they're parsed into Angular expressions.
// 2. `preserveLineEndings: false` causes growing misalignments in templates that use '\r\n'
// line endings, by normalizing them to '\n'.
// 3. By default, the template parser strips leading trivia characters (like spaces, tabs, and
// newlines). This also destroys source mapping information.
//
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
// with the above options set to preserve source mappings.
const { nodes: diagNodes } = checker.parseTemplate(sourceStr, sourceMapUrl ?? '', {
...commonParseOptions,
preserveWhitespaces: true,
preserveLineEndings: true,
preserveSignificantWhitespace: true,
leadingTriviaChars: [],
});
return {
...parsedTemplate,
diagNodes,
file: new checker.ParseSourceFile(sourceStr, sourceMapUrl ?? ''),
};
}
function parseTemplateDeclaration(node, decorator, component, containingFile, evaluator, depTracker, resourceLoader, defaultPreserveWhitespaces) {
let preserveWhitespaces = defaultPreserveWhitespaces;
if (component.has('preserveWhitespaces')) {
const expr = component.get('preserveWhitespaces');
const value = evaluator.evaluate(expr);
if (typeof value !== 'boolean') {
throw createValueHasWrongTypeError(expr, value, 'preserveWhitespaces must be a boolean');
}
preserveWhitespaces = value;
}
let interpolationConfig = checker.DEFAULT_INTERPOLATION_CONFIG;
if (component.has('interpolation')) {
const expr = component.get('interpolation');
const value = evaluator.evaluate(expr);
if (!Array.isArray(value) ||
value.length !== 2 ||
!value.every((element) => typeof element === 'string')) {
throw createValueHasWrongTypeError(expr, value, 'interpolation must be an array with 2 elements of string type');
}
interpolationConfig = checker.InterpolationConfig.fromArray(value);
}
if (component.has('templateUrl')) {
const templateUrlExpr = component.get('templateUrl');
const templateUrl = evaluator.evaluate(templateUrlExpr);
if (typeof templateUrl !== 'string') {
throw createValueHasWrongTypeError(templateUrlExpr, templateUrl, 'templateUrl must be a string');
}
try {
const resourceUrl = resourceLoader.resolve(templateUrl, containingFile);
return {
isInline: false,
interpolationConfig,
preserveWhitespaces,
templateUrl,
templateUrlExpression: templateUrlExpr,
resolvedTemplateUrl: resourceUrl,
};
}
catch (e) {
if (depTracker !== null) {
// The analysis of this file cannot be re-used if the template URL could
// not be resolved. Future builds should re-analyze and re-attempt resolution.
depTracker.recordDependencyAnalysisFailure(node.getSourceFile());
}
throw makeResourceNotFoundError(templateUrl, templateUrlExpr, 0 /* ResourceTypeForDiagnostics.Template */);
}
}
else if (component.has('template')) {
return {
isInline: true,
interpolationConfig,
preserveWhitespaces,
expression: component.get('template'),
templateUrl: containingFile,
resolvedTemplateUrl: containingFile,
};
}
else {
throw new checker.FatalDiagnosticError(checker.ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, 'component is missing a template');
}
}
function preloadAndParseTemplate(evaluator, resourceLoader, depTracker, preanalyzeTemplateCache, node, decorator, component, containingFile, defaultPreserveWhitespaces, options, compilationMode) {
if (component.has('templateUrl')) {
// Extract the templateUrl and preload it.
const templateUrlExpr = component.get('templateUrl');
const templateUrl = evaluator.evaluate(templateUrlExpr);
if (typeof templateUrl !== 'string') {
throw createValueHasWrongTypeError(templateUrlExpr, templateUrl, 'templateUrl must be a string');
}
try {
const resourceUrl = resourceLoader.resolve(templateUrl, containingFile);
const templatePromise = resourceLoader.preload(resourceUrl, {
type: 'template',
containingFile,
className: node.name.text,
});
// If the preload worked, then actually load and parse the template, and wait for any
// style URLs to resolve.
if (templatePromise !== undefined) {
return templatePromise.then(() => {
const templateDecl = parseTemplateDeclaration(node, decorator, component, containingFile, evaluator, depTracker, resourceLoader, defaultPreserveWhitespaces);
const template = extractTemplate(node, templateDecl, evaluator, depTracker, resourceLoader, options, compilationMode);
preanalyzeTemplateCache.set(node, template);
return template;
});
}
else {
return Promise.resolve(null);
}
}
catch (e) {
if (depTracker !== null) {
// The analysis of this file cannot be re-used if the template URL could
// not be resolved. Future builds should re-analyze and re-attempt resolution.
depTracker.recordDependencyAnalysisFailure(node.getSourceFile());
}
throw makeResourceNotFoundError(templateUrl, templateUrlExpr, 0 /* ResourceTypeForDiagnostics.Template */);
}
}
else {
const templateDecl = parseTemplateDeclaration(node, decorator, component, containingFile, evaluator, depTracker, resourceLoader, defaultPreserveWhitespaces);
const template = extractTemplate(node, templateDecl, evaluator, depTracker, resourceLoader, options, compilationMode);
preanalyzeTemplateCache.set(node, template);
return Promise.resolve(template);
}
}
function getTemplateRange(templateExpr) {
const startPos = templateExpr.getStart() + 1;
const { line, character } = ts__default["default"].getLineAndCharacterOfPosition(templateExpr.getSourceFile(), startPos);
return {
startPos,
startLine: line,
startCol: character,
endPos: templateExpr.getEnd() - 1,
};
}
function makeResourceNotFoundError(file, nodeForError, resourceType) {
let errorText;
switch (resourceType) {
case 0 /* ResourceTypeForDiagnostics.Template */:
errorText = `Could not find template file '${file}'.`;
break;
case 1 /* ResourceTypeForDiagnostics.StylesheetFromTemplate */:
errorText = `Could not find stylesheet file '${file}' linked from the template.`;
break;
case 2 /* ResourceTypeForDiagnostics.StylesheetFromDecorator */:
errorText = `Could not find stylesheet file '${file}'.`;
break;
}
return new checker.FatalDiagnosticError(checker.ErrorCode.COMPONENT_RESOURCE_NOT_FOUND, nodeForError, errorText);
}
/**
* Transforms the given decorator to inline external resources. i.e. if the decorator
* resolves to `@Component`, the `templateUrl` and `styleUrls` metadata fields will be
* transformed to their semantically-equivalent inline variants.
*
* This method is used for serializing decorators into the class metadata. The emitted
* class metadata should not refer to external resources as this would be inconsistent
* with the component definitions/declarations which already inline external resources.
*
* Additionally, the references to external resources would require libraries to ship
* external resources exclusively for the class metadata.
*/
function transformDecoratorResources(dec, component, styles, template) {
if (dec.name !== 'Component') {
return dec;
}
// If no external resources are referenced, preserve the original decorator
// for the best source map experience when the decorator is emitted in TS.
if (!component.has('templateUrl') &&
!component.has('styleUrls') &&
!component.has('styleUrl') &&
!component.has('styles')) {
return dec;
}
const metadata = new Map(component);
// Set the `template` property if the `templateUrl` property is set.
if (metadata.has('templateUrl')) {
metadata.delete('templateUrl');
metadata.set('template', ts__default["default"].factory.createStringLiteral(template.content));
}
if (metadata.has('styleUrls') || metadata.has('styleUrl') || metadata.has('styles')) {
metadata.delete('styles');
metadata.delete('styleUrls');
metadata.delete('styleUrl');
if (styles.length > 0) {
const styleNodes = styles.reduce((result, style) => {
if (style.trim().length > 0) {
result.push(ts__default["default"].factory.createStringLiteral(style));
}
return result;
}, []);
if (styleNodes.length > 0) {
metadata.set('styles', ts__default["default"].factory.createArrayLiteralExpression(styleNodes));
}
}
}
// Convert the metadata to TypeScript AST object literal element nodes.
const newMetadataFields = [];
for (const [name, value] of metadata.entries()) {
newMetadataFields.push(ts__default["default"].factory.createPropertyAssignment(name, value));
}
// Return the original decorator with the overridden metadata argument.
return { ...dec, args: [ts__default["default"].factory.createObjectLiteralExpression(newMetadataFields)] };
}
function extractComponentStyleUrls(evaluator, component) {
const styleUrlsExpr = component.get('styleUrls');
const styleUrlExpr = component.get('styleUrl');
if (styleUrlsExpr !== undefined && styleUrlExpr !== undefined) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.COMPONENT_INVALID_STYLE_URLS, styleUrlExpr, '@Component cannot define both `styleUrl` and `styleUrls`. ' +
'Use `styleUrl` if the component has one stylesheet, or `styleUrls` if it has multiple');
}
if (styleUrlsExpr !== undefined) {
return extractStyleUrlsFromExpression(evaluator, component.get('styleUrls'));
}
if (styleUrlExpr !== undefined) {
const styleUrl = evaluator.evaluate(styleUrlExpr);
if (typeof styleUrl !== 'string') {
throw createValueHasWrongTypeError(styleUrlExpr, styleUrl, 'styleUrl must be a string');
}
return [
{
url: styleUrl,
source: 2 /* ResourceTypeForDiagnostics.StylesheetFromDecorator */,
expression: styleUrlExpr,
},
];
}
return [];
}
function extractStyleUrlsFromExpression(evaluator, styleUrlsExpr) {
const styleUrls = [];
if (ts__default["default"].isArrayLiteralExpression(styleUrlsExpr)) {
for (const styleUrlExpr of styleUrlsExpr.elements) {
if (ts__default["default"].isSpreadElement(styleUrlExpr)) {
styleUrls.push(...extractStyleUrlsFromExpression(evaluator, styleUrlExpr.expression));
}
else {
const styleUrl = evaluator.evaluate(styleUrlExpr);
if (typeof styleUrl !== 'string') {
throw createValueHasWrongTypeError(styleUrlExpr, styleUrl, 'styleUrl must be a string');
}
styleUrls.push({
url: styleUrl,
source: 2 /* ResourceTypeForDiagnostics.StylesheetFromDecorator */,
expression: styleUrlExpr,
});
}
}
}
else {
const evaluatedStyleUrls = evaluator.evaluate(styleUrlsExpr);
if (!isStringArray(evaluatedStyleUrls)) {
throw createValueHasWrongTypeError(styleUrlsExpr, evaluatedStyleUrls, 'styleUrls must be an array of strings');
}
for (const styleUrl of evaluatedStyleUrls) {
styleUrls.push({
url: styleUrl,
source: 2 /* ResourceTypeForDiagnostics.StylesheetFromDecorator */,
expression: styleUrlsExpr,
});
}
}
return styleUrls;
}
function extractInlineStyleResources(component) {
const styles = new Set();
function stringLiteralElements(array) {
return array.elements.filter((e) => ts__default["default"].isStringLiteralLike(e));
}
const stylesExpr = component.get('styles');
if (stylesExpr !== undefined) {
if (ts__default["default"].isArrayLiteralExpression(stylesExpr)) {
for (const expression of stringLiteralElements(stylesExpr)) {
styles.add({ path: null, expression });
}
}
else if (ts__default["default"].isStringLiteralLike(stylesExpr)) {
styles.add({ path: null, expression: stylesExpr });
}
}
return styles;
}
function _extractTemplateStyleUrls(template) {
if (template.styleUrls === null) {
return [];
}
const expression = getTemplateDeclarationNodeForError(template.declaration);
return template.styleUrls.map((url) => ({
url,
source: 1 /* ResourceTypeForDiagnostics.StylesheetFromTemplate */,
expression,
}));
}
/**
* Represents an Angular component.
*/
class ComponentSymbol extends DirectiveSymbol {
usedDirectives = [];
usedPipes = [];
isRemotelyScoped = false;
isEmitAffected(previousSymbol, publicApiAffected) {
if (!(previousSymbol instanceof ComponentSymbol)) {
return true;
}
// Create an equality function that considers symbols equal if they represent the same
// declaration, but only if the symbol in the current compilation does not have its public API
// affected.
const isSymbolUnaffected = (current, previous) => isReferenceEqual(current, previous) && !publicApiAffected.has(current.symbol);
// The emit of a component is affected if either of the following is true:
// 1. The component used to be remotely scoped but no longer is, or vice versa.
// 2. The list of used directives has changed or any of those directives have had their public
// API changed. If the used directives have been reordered but not otherwise affected then
// the component must still be re-emitted, as this may affect directive instantiation order.
// 3. The list of used pipes has changed, or any of those pipes have had their public API
// changed.
return (this.isRemotelyScoped !== previousSymbol.isRemotelyScoped ||
!isArrayEqual(this.usedDirectives, previousSymbol.usedDirectives, isSymbolUnaffected) ||
!isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isSymbolUnaffected));
}
isTypeCheckBlockAffected(previousSymbol, typeCheckApiAffected) {
if (!(previousSymbol instanceof ComponentSymbol)) {
return true;
}
// To verify that a used directive is not affected we need to verify that its full inheritance
// chain is not present in `typeCheckApiAffected`.
const isInheritanceChainAffected = (symbol) => {
let currentSymbol = symbol;
while (currentSymbol instanceof DirectiveSymbol) {
if (typeCheckApiAffected.has(currentSymbol)) {
return true;
}
currentSymbol = currentSymbol.baseClass;
}
return false;
};
// Create an equality function that considers directives equal if they represent the same
// declaration and if the symbol and all symbols it inherits from in the current compilation
// do not have their type-check API affected.
const isDirectiveUnaffected = (current, previous) => isReferenceEqual(current, previous) && !isInheritanceChainAffected(current.symbol);
// Create an equality function that considers pipes equal if they represent the same
// declaration and if the symbol in the current compilation does not have its type-check
// API affected.
const isPipeUnaffected = (current, previous) => isReferenceEqual(current, previous) && !typeCheckApiAffected.has(current.symbol);
// The emit of a type-check block of a component is affected if either of the following is true:
// 1. The list of used directives has changed or any of those directives have had their
// type-check API changed.
// 2. The list of used pipes has changed, or any of those pipes have had their type-check API
// changed.
return (!isArrayEqual(this.usedDirectives, previousSymbol.usedDirectives, isDirectiveUnaffected) ||
!isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isPipeUnaffected));
}
}
/**
* Collect the animation names from the static evaluation result.
* @param value the static evaluation result of the animations
* @param animationTriggerNames the animation names collected and whether some names could not be
* statically evaluated.
*/
function collectAnimationNames(value, animationTriggerNames) {
if (value instanceof Map) {
const name = value.get('name');
if (typeof name === 'string') {
animationTriggerNames.staticTriggerNames.push(name);
}
else {
animationTriggerNames.includesDynamicAnimations = true;
}
}
else if (Array.isArray(value)) {
for (const resolvedValue of value) {
collectAnimationNames(resolvedValue, animationTriggerNames);
}
}
else {
animationTriggerNames.includesDynamicAnimations = true;
}
}
function isAngularAnimationsReference(reference, symbolName) {
return (reference.ownedByModuleGuess === '@angular/animations' && reference.debugName === symbolName);
}
const animationTriggerResolver = (fn, node, resolve, unresolvable) => {
const animationTriggerMethodName = 'trigger';
if (!isAngularAnimationsReference(fn, animationTriggerMethodName)) {
return unresolvable;
}
const triggerNameExpression = node.arguments[0];
if (!triggerNameExpression) {
return unresolvable;
}
const res = new Map();
res.set('name', resolve(triggerNameExpression));
return res;
};
function validateAndFlattenComponentImports(imports, expr, isDeferred) {
const flattened = [];
const errorMessage = isDeferred
? `'deferredImports' must be an array of components, directives, or pipes.`
: `'imports' must be an array of components, directives, pipes, or NgModules.`;
if (!Array.isArray(imports)) {
const error = createValueHasWrongTypeError(expr, imports, errorMessage).toDiagnostic();
return {
imports: [],
diagnostics: [error],
};
}
const diagnostics = [];
for (const ref of imports) {
if (Array.isArray(ref)) {
const { imports: childImports, diagnostics: childDiagnostics } = validateAndFlattenComponentImports(ref, expr, isDeferred);
flattened.push(...childImports);
diagnostics.push(...childDiagnostics);
}
else if (ref instanceof checker.Reference) {
if (checker.isNamedClassDeclaration(ref.node)) {
flattened.push(ref);
}
else {
diagnostics.push(createValueHasWrongTypeError(ref.getOriginForDiagnostics(expr), ref, errorMessage).toDiagnostic());
}
}
else if (isLikelyModuleWithProviders(ref)) {
let origin = expr;
if (ref instanceof checker.SyntheticValue) {
// The `ModuleWithProviders` type originated from a foreign function declaration, in which
// case the original foreign call is available which is used to get a more accurate origin
// node that points at the specific call expression.
origin = checker.getOriginNodeForDiagnostics(ref.value.mwpCall, expr);
}
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.COMPONENT_UNKNOWN_IMPORT, origin, `Component imports contains a ModuleWithProviders value, likely the result of a 'Module.forRoot()'-style call. ` +
`These calls are not used to configure components and are not valid in standalone component imports - ` +
`consider importing them in the application bootstrap instead.`));
}
else {
diagnostics.push(createValueHasWrongTypeError(expr, imports, errorMessage).toDiagnostic());
}
}
return { imports: flattened, diagnostics };
}
/**
* Inspects `value` to determine if it resembles a `ModuleWithProviders` value. This is an
* approximation only suitable for error reporting as any resolved object with an `ngModule`
* key is considered a `ModuleWithProviders`.
*/
function isLikelyModuleWithProviders(value) {
if (value instanceof checker.SyntheticValue && isResolvedModuleWithProviders(value)) {
// This is a `ModuleWithProviders` as extracted from a foreign function call.
return true;
}
if (value instanceof Map && value.has('ngModule')) {
// A resolved `Map` with `ngModule` property would have been extracted from locally declared
// functions that return a `ModuleWithProviders` object.
return true;
}
return false;
}
const TS_EXTENSIONS = /\.tsx?$/i;
/**
* Replace the .ts or .tsx extension of a file with the shim filename suffix.
*/
function makeShimFileName(fileName, suffix) {
return checker.absoluteFrom(fileName.replace(TS_EXTENSIONS, suffix));
}
/**
* Generates and tracks shim files for each original `ts.SourceFile`.
*
* The `ShimAdapter` provides an API that's designed to be used by a `ts.CompilerHost`
* implementation and allows it to include synthetic "shim" files in the program that's being
* created. It works for both freshly created programs as well as with reuse of an older program
* (which already may contain shim files and thus have a different creation flow).
*/
class ShimAdapter {
delegate;
/**
* A map of shim file names to the `ts.SourceFile` generated for those shims.
*/
shims = new Map();
/**
* A map of shim file names to existing shims which were part of a previous iteration of this
* program.
*
* Not all of these shims will be inherited into this program.
*/
priorShims = new Map();
/**
* File names which are already known to not be shims.
*
* This allows for short-circuit returns without the expense of running regular expressions
* against the filename repeatedly.
*/
notShims = new Set();
/**
* The shim generators supported by this adapter as well as extra precalculated data facilitating
* their use.
*/
generators = [];
/**
* A `Set` of shim `ts.SourceFile`s which should not be emitted.
*/
ignoreForEmit = new Set();
/**
* A list of extra filenames which should be considered inputs to program creation.
*
* This includes any top-level shims generated for the program, as well as per-file shim names for
* those files which are included in the root files of the program.
*/
extraInputFiles;
/**
* Extension prefixes of all installed per-file shims.
*/
extensionPrefixes = [];
constructor(delegate, tsRootFiles, topLevelGenerators, perFileGenerators, oldProgram) {
this.delegate = delegate;
// Initialize `this.generators` with a regex that matches each generator's paths.
for (const gen of perFileGenerators) {
// This regex matches paths for shims from this generator. The first (and only) capture group
// extracts the filename prefix, which can be used to find the original file that was used to
// generate this shim.
const pattern = `^(.*)\\.${gen.extensionPrefix}\\.ts$`;
const regexp = new RegExp(pattern, 'i');
this.generators.push({
generator: gen,
test: regexp,
suffix: `.${gen.extensionPrefix}.ts`,
});
this.extensionPrefixes.push(gen.extensionPrefix);
}
// Process top-level generators and pre-generate their shims. Accumulate the list of filenames
// as extra input files.
const extraInputFiles = [];
for (const gen of topLevelGenerators) {
const sf = gen.makeTopLevelShim();
checker.sfExtensionData(sf).isTopLevelShim = true;
if (!gen.shouldEmit) {
this.ignoreForEmit.add(sf);
}
const fileName = checker.absoluteFromSourceFile(sf);
this.shims.set(fileName, sf);
extraInputFiles.push(fileName);
}
// Add to that list the per-file shims associated with each root file. This is needed because
// reference tagging alone may not work in TS compilations that have `noResolve` set. Such
// compilations rely on the list of input files completely describing the program.
for (const rootFile of tsRootFiles) {
for (const gen of this.generators) {
extraInputFiles.push(makeShimFileName(rootFile, gen.suffix));
}
}
this.extraInputFiles = extraInputFiles;
// If an old program is present, extract all per-file shims into a map, which will be used to
// generate new versions of those shims.
if (oldProgram !== null) {
for (const oldSf of oldProgram.getSourceFiles()) {
if (oldSf.isDeclarationFile || !checker.isFileShimSourceFile(oldSf)) {
continue;
}
this.priorShims.set(checker.absoluteFromSourceFile(oldSf), oldSf);
}
}
}
/**
* Produce a shim `ts.SourceFile` if `fileName` refers to a shim file which should exist in the
* program.
*
* If `fileName` does not refer to a potential shim file, `null` is returned. If a corresponding
* base file could not be determined, `undefined` is returned instead.
*/
maybeGenerate(fileName) {
// Fast path: either this filename has been proven not to be a shim before, or it is a known
// shim and no generation is required.
if (this.notShims.has(fileName)) {
return null;
}
else if (this.shims.has(fileName)) {
return this.shims.get(fileName);
}
// .d.ts files can't be shims.
if (checker.isDtsPath(fileName)) {
this.notShims.add(fileName);
return null;
}
// This is the first time seeing this path. Try to match it against a shim generator.
for (const record of this.generators) {
const match = record.test.exec(fileName);
if (match === null) {
continue;
}
// The path matched. Extract the filename prefix without the extension.
const prefix = match[1];
// This _might_ be a shim, if an underlying base file exists. The base file might be .ts or
// .tsx.
let baseFileName = checker.absoluteFrom(prefix + '.ts');
// Retrieve the original file for which the shim will be generated.
let inputFile = this.delegate.getSourceFile(baseFileName, ts__default["default"].ScriptTarget.Latest);
if (inputFile === undefined) {
// No .ts file by that name - try .tsx.
baseFileName = checker.absoluteFrom(prefix + '.tsx');
inputFile = this.delegate.getSourceFile(baseFileName, ts__default["default"].ScriptTarget.Latest);
}
if (inputFile === undefined || checker.isShim(inputFile)) {
// This isn't a shim after all since there is no original file which would have triggered
// its generation, even though the path is right. There are a few reasons why this could
// occur:
//
// * when resolving an import to an .ngfactory.d.ts file, the module resolution algorithm
// will first look for an .ngfactory.ts file in its place, which will be requested here.
// * when the user writes a bad import.
// * when a file is present in one compilation and removed in the next incremental step.
//
// Note that this does not add the filename to `notShims`, so this path is not cached.
// That's okay as these cases above are edge cases and do not occur regularly in normal
// operations.
return undefined;
}
// Actually generate and cache the shim.
return this.generateSpecific(fileName, record.generator, inputFile);
}
// No generator matched.
this.notShims.add(fileName);
return null;
}
generateSpecific(fileName, generator, inputFile) {
let priorShimSf = null;
if (this.priorShims.has(fileName)) {
// In the previous program a shim with this name already existed. It's passed to the shim
// generator which may reuse it instead of generating a fresh shim.
priorShimSf = this.priorShims.get(fileName);
this.priorShims.delete(fileName);
}
const shimSf = generator.generateShimForFile(inputFile, fileName, priorShimSf);
// Mark the new generated source file as a shim that originated from this generator.
checker.sfExtensionData(shimSf).fileShim = {
extension: generator.extensionPrefix,
generatedFrom: checker.absoluteFromSourceFile(inputFile),
};
if (!generator.shouldEmit) {
this.ignoreForEmit.add(shimSf);
}
this.shims.set(fileName, shimSf);
return shimSf;
}
}
/**
* Manipulates the `referencedFiles` property of `ts.SourceFile`s to add references to shim files
* for each original source file, causing the shims to be loaded into the program as well.
*
* `ShimReferenceTagger`s are intended to operate during program creation only.
*/
class ShimReferenceTagger {
suffixes;
/**
* Tracks which original files have been processed and had shims generated if necessary.
*
* This is used to avoid generating shims twice for the same file.
*/
tagged = new Set();
/**
* Whether shim tagging is currently being performed.
*/
enabled = true;
constructor(shimExtensions) {
this.suffixes = shimExtensions.map((extension) => `.${extension}.ts`);
}
/**
* Tag `sf` with any needed references if it's not a shim itself.
*/
tag(sf) {
if (!this.enabled ||
sf.isDeclarationFile ||
checker.isShim(sf) ||
this.tagged.has(sf) ||
!checker.isNonDeclarationTsPath(sf.fileName)) {
return;
}
const ext = checker.sfExtensionData(sf);
// If this file has never been tagged before, capture its `referencedFiles` in the extension
// data.
if (ext.originalReferencedFiles === null) {
ext.originalReferencedFiles = sf.referencedFiles;
}
const referencedFiles = [...ext.originalReferencedFiles];
const sfPath = checker.absoluteFromSourceFile(sf);
for (const suffix of this.suffixes) {
referencedFiles.push({
fileName: makeShimFileName(sfPath, suffix),
pos: 0,
end: 0,
});
}
ext.taggedReferenceFiles = referencedFiles;
sf.referencedFiles = referencedFiles;
this.tagged.add(sf);
}
/**
* Disable the `ShimReferenceTagger` and free memory associated with tracking tagged files.
*/
finalize() {
this.enabled = false;
this.tagged.clear();
}
}
/**
* Delegates all methods of `ts.CompilerHost` to a delegate, with the exception of
* `getSourceFile`, `fileExists` and `writeFile` which are implemented in `TypeCheckProgramHost`.
*
* If a new method is added to `ts.CompilerHost` which is not delegated, a type error will be
* generated for this class.
*/
class DelegatingCompilerHost$1 {
delegate;
createHash;
directoryExists;
getCancellationToken;
getCanonicalFileName;
getCurrentDirectory;
getDefaultLibFileName;
getDefaultLibLocation;
getDirectories;
getEnvironmentVariable;
getNewLine;
getParsedCommandLine;
getSourceFileByPath;
readDirectory;
readFile;
realpath;
resolveModuleNames;
resolveTypeReferenceDirectives;
trace;
useCaseSensitiveFileNames;
getModuleResolutionCache;
hasInvalidatedResolutions;
resolveModuleNameLiterals;
resolveTypeReferenceDirectiveReferences;
// jsDocParsingMode is not a method like the other elements above
// TODO: ignore usage can be dropped once 5.2 support is dropped
get jsDocParsingMode() {
// @ts-ignore
return this.delegate.jsDocParsingMode;
}
set jsDocParsingMode(mode) {
// @ts-ignore
this.delegate.jsDocParsingMode = mode;
}
constructor(delegate) {
// Excluded are 'getSourceFile', 'fileExists' and 'writeFile', which are actually implemented by
// `TypeCheckProgramHost` below.
this.delegate = delegate;
this.createHash = this.delegateMethod('createHash');
this.directoryExists = this.delegateMethod('directoryExists');
this.getCancellationToken = this.delegateMethod('getCancellationToken');
this.getCanonicalFileName = this.delegateMethod('getCanonicalFileName');
this.getCurrentDirectory = this.delegateMethod('getCurrentDirectory');
this.getDefaultLibFileName = this.delegateMethod('getDefaultLibFileName');
this.getDefaultLibLocation = this.delegateMethod('getDefaultLibLocation');
this.getDirectories = this.delegateMethod('getDirectories');
this.getEnvironmentVariable = this.delegateMethod('getEnvironmentVariable');
this.getNewLine = this.delegateMethod('getNewLine');
this.getParsedCommandLine = this.delegateMethod('getParsedCommandLine');
this.getSourceFileByPath = this.delegateMethod('getSourceFileByPath');
this.readDirectory = this.delegateMethod('readDirectory');
this.readFile = this.delegateMethod('readFile');
this.realpath = this.delegateMethod('realpath');
this.resolveModuleNames = this.delegateMethod('resolveModuleNames');
this.resolveTypeReferenceDirectives = this.delegateMethod('resolveTypeReferenceDirectives');
this.trace = this.delegateMethod('trace');
this.useCaseSensitiveFileNames = this.delegateMethod('useCaseSensitiveFileNames');
this.getModuleResolutionCache = this.delegateMethod('getModuleResolutionCache');
this.hasInvalidatedResolutions = this.delegateMethod('hasInvalidatedResolutions');
this.resolveModuleNameLiterals = this.delegateMethod('resolveModuleNameLiterals');
this.resolveTypeReferenceDirectiveReferences = this.delegateMethod('resolveTypeReferenceDirectiveReferences');
}
delegateMethod(name) {
return this.delegate[name] !== undefined
? this.delegate[name].bind(this.delegate)
: undefined;
}
}
/**
* A `ts.CompilerHost` which augments source files.
*/
class UpdatedProgramHost extends DelegatingCompilerHost$1 {
originalProgram;
shimExtensionPrefixes;
/**
* Map of source file names to `ts.SourceFile` instances.
*/
sfMap;
/**
* The `ShimReferenceTagger` responsible for tagging `ts.SourceFile`s loaded via this host.
*
* The `UpdatedProgramHost` is used in the creation of a new `ts.Program`. Even though this new
* program is based on a prior one, TypeScript will still start from the root files and enumerate
* all source files to include in the new program. This means that just like during the original
* program's creation, these source files must be tagged with references to per-file shims in
* order for those shims to be loaded, and then cleaned up afterwards. Thus the
* `UpdatedProgramHost` has its own `ShimReferenceTagger` to perform this function.
*/
shimTagger;
constructor(sfMap, originalProgram, delegate, shimExtensionPrefixes) {
super(delegate);
this.originalProgram = originalProgram;
this.shimExtensionPrefixes = shimExtensionPrefixes;
this.shimTagger = new ShimReferenceTagger(this.shimExtensionPrefixes);
this.sfMap = sfMap;
}
getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) {
// Try to use the same `ts.SourceFile` as the original program, if possible. This guarantees
// that program reuse will be as efficient as possible.
let delegateSf = this.originalProgram.getSourceFile(fileName);
if (delegateSf === undefined) {
// Something went wrong and a source file is being requested that's not in the original
// program. Just in case, try to retrieve it from the delegate.
delegateSf = this.delegate.getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
}
if (delegateSf === undefined) {
return undefined;
}
// Look for replacements.
let sf;
if (this.sfMap.has(fileName)) {
sf = this.sfMap.get(fileName);
checker.copyFileShimData(delegateSf, sf);
}
else {
sf = delegateSf;
}
// TypeScript doesn't allow returning redirect source files. To avoid unforeseen errors we
// return the original source file instead of the redirect target.
sf = checker.toUnredirectedSourceFile(sf);
this.shimTagger.tag(sf);
return sf;
}
postProgramCreationCleanup() {
this.shimTagger.finalize();
}
writeFile() {
throw new Error(`TypeCheckProgramHost should never write files`);
}
fileExists(fileName) {
return this.sfMap.has(fileName) || this.delegate.fileExists(fileName);
}
}
/**
* Updates a `ts.Program` instance with a new one that incorporates specific changes, using the
* TypeScript compiler APIs for incremental program creation.
*/
class TsCreateProgramDriver {
originalProgram;
originalHost;
options;
shimExtensionPrefixes;
/**
* A map of source file paths to replacement `ts.SourceFile`s for those paths.
*
* Effectively, this tracks the delta between the user's program (represented by the
* `originalHost`) and the template type-checking program being managed.
*/
sfMap = new Map();
program;
constructor(originalProgram, originalHost, options, shimExtensionPrefixes) {
this.originalProgram = originalProgram;
this.originalHost = originalHost;
this.options = options;
this.shimExtensionPrefixes = shimExtensionPrefixes;
this.program = this.originalProgram;
}
supportsInlineOperations = true;
getProgram() {
return this.program;
}
updateFiles(contents, updateMode) {
if (contents.size === 0) {
// No changes have been requested. Is it safe to skip updating entirely?
// If UpdateMode is Incremental, then yes. If UpdateMode is Complete, then it's safe to skip
// only if there are no active changes already (that would be cleared by the update).
if (updateMode !== checker.UpdateMode.Complete || this.sfMap.size === 0) {
// No changes would be made to the `ts.Program` anyway, so it's safe to do nothing here.
return;
}
}
if (updateMode === checker.UpdateMode.Complete) {
this.sfMap.clear();
}
for (const [filePath, { newText, originalFile }] of contents.entries()) {
const sf = ts__default["default"].createSourceFile(filePath, newText, ts__default["default"].ScriptTarget.Latest, true);
if (originalFile !== null) {
sf[checker.NgOriginalFile] = originalFile;
}
this.sfMap.set(filePath, sf);
}
const host = new UpdatedProgramHost(this.sfMap, this.originalProgram, this.originalHost, this.shimExtensionPrefixes);
const oldProgram = this.program;
// Retag the old program's `ts.SourceFile`s with shim tags, to allow TypeScript to reuse the
// most data.
checker.retagAllTsFiles(oldProgram);
this.program = ts__default["default"].createProgram({
host,
rootNames: this.program.getRootFileNames(),
options: this.options,
oldProgram,
});
host.postProgramCreationCleanup();
// Only untag the old program. The new program needs to keep the tagged files, because as of
// TS 5.5 not having the files tagged while producing diagnostics can lead to errors. See:
// https://github.com/microsoft/TypeScript/pull/58398
checker.untagAllTsFiles(oldProgram);
}
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Determines the file-level dependencies that the HMR initializer needs to capture and pass along.
* @param sourceFile File in which the file is being compiled.
* @param definition Compiled component definition.
* @param factory Compiled component factory.
* @param classMetadata Compiled `setClassMetadata` expression, if any.
* @param debugInfo Compiled `setClassDebugInfo` expression, if any.
*/
function extractHmrDependencies(node, definition, factory, classMetadata, debugInfo) {
const name = ts__default["default"].isClassDeclaration(node) && node.name ? node.name.text : null;
const visitor = new PotentialTopLevelReadsVisitor();
const sourceFile = node.getSourceFile();
// Visit all of the compiled expression to look for potential
// local references that would have to be retained.
definition.expression.visitExpression(visitor, null);
definition.statements.forEach((statement) => statement.visitStatement(visitor, null));
factory.initializer?.visitExpression(visitor, null);
factory.statements.forEach((statement) => statement.visitStatement(visitor, null));
classMetadata?.visitStatement(visitor, null);
debugInfo?.visitStatement(visitor, null);
// Filter out only the references to defined top-level symbols. This allows us to ignore local
// variables inside of functions. Note that we filter out the class name since it is always
// defined and it saves us having to repeat this logic wherever the locals are consumed.
const availableTopLevel = getTopLevelDeclarationNames(sourceFile);
return {
local: Array.from(visitor.allReads).filter((r) => r !== name && availableTopLevel.has(r)),
external: Array.from(visitor.namespaceReads, (name, index) => ({
moduleName: name,
assignedName: `ɵhmr${index}`,
})),
};
}
/**
* Gets the names of all top-level declarations within the file (imports, declared classes etc).
* @param sourceFile File in which to search for locals.
*/
function getTopLevelDeclarationNames(sourceFile) {
const results = new Set();
// Only look through the top-level statements.
for (const node of sourceFile.statements) {
// Class, function and const enum declarations need to be captured since they correspond
// to runtime code. Intentionally excludes interfaces and type declarations.
if (ts__default["default"].isClassDeclaration(node) ||
ts__default["default"].isFunctionDeclaration(node) ||
(ts__default["default"].isEnumDeclaration(node) &&
!node.modifiers?.some((m) => m.kind === ts__default["default"].SyntaxKind.ConstKeyword))) {
if (node.name) {
results.add(node.name.text);
}
continue;
}
// Variable declarations.
if (ts__default["default"].isVariableStatement(node)) {
for (const decl of node.declarationList.declarations) {
trackBindingName(decl.name, results);
}
continue;
}
// Import declarations.
if (ts__default["default"].isImportDeclaration(node) && node.importClause) {
const importClause = node.importClause;
// Skip over type-only imports since they won't be emitted to JS.
if (importClause.isTypeOnly) {
continue;
}
// import foo from 'foo'
if (importClause.name) {
results.add(importClause.name.text);
}
if (importClause.namedBindings) {
const namedBindings = importClause.namedBindings;
if (ts__default["default"].isNamespaceImport(namedBindings)) {
// import * as foo from 'foo';
results.add(namedBindings.name.text);
}
else {
// import {foo} from 'foo';
namedBindings.elements.forEach((el) => {
if (!el.isTypeOnly) {
results.add(el.name.text);
}
});
}
}
continue;
}
}
return results;
}
/**
* Adds all the variables declared through a `ts.BindingName` to a set of results.
* @param node Node from which to start searching for variables.
* @param results Set to which to add the matches.
*/
function trackBindingName(node, results) {
if (ts__default["default"].isIdentifier(node)) {
results.add(node.text);
}
else {
for (const el of node.elements) {
if (!ts__default["default"].isOmittedExpression(el)) {
trackBindingName(el.name, results);
}
}
}
}
/**
* Visitor that will traverse an AST looking for potential top-level variable reads.
* The reads are "potential", because the visitor doesn't account for local variables
* inside functions.
*/
class PotentialTopLevelReadsVisitor extends checker.RecursiveAstVisitor {
allReads = new Set();
namespaceReads = new Set();
visitExternalExpr(ast, context) {
if (ast.value.moduleName !== null) {
this.namespaceReads.add(ast.value.moduleName);
}
super.visitExternalExpr(ast, context);
}
visitReadVarExpr(ast, context) {
this.allReads.add(ast.name);
super.visitReadVarExpr(ast, context);
}
visitWrappedNodeExpr(ast, context) {
if (this.isTypeScriptNode(ast.node)) {
this.addAllTopLevelIdentifiers(ast.node);
}
super.visitWrappedNodeExpr(ast, context);
}
/**
* Traverses a TypeScript AST and tracks all the top-level reads.
* @param node Node from which to start the traversal.
*/
addAllTopLevelIdentifiers = (node) => {
if (ts__default["default"].isIdentifier(node) && this.isTopLevelIdentifierReference(node)) {
this.allReads.add(node.text);
}
else {
ts__default["default"].forEachChild(node, this.addAllTopLevelIdentifiers);
}
};
/**
* TypeScript identifiers are used both when referring to a variable (e.g. `console.log(foo)`)
* and for names (e.g. `{foo: 123}`). This function determines if the identifier is a top-level
* variable read, rather than a nested name.
* @param node Identifier to check.
*/
isTopLevelIdentifierReference(node) {
const parent = node.parent;
// The parent might be undefined for a synthetic node or if `setParentNodes` is set to false
// when the SourceFile was created. We can account for such cases using the type checker, at
// the expense of performance. At the moment of writing, we're keeping it simple since the
// compiler sets `setParentNodes: true`.
if (!parent) {
return false;
}
// Identifier referenced at the top level. Unlikely.
if (ts__default["default"].isSourceFile(parent) ||
(ts__default["default"].isExpressionStatement(parent) && parent.expression === node)) {
return true;
}
// Identifier used inside a call is only top-level if it's an argument.
// This also covers decorators since their expression is usually a call.
if (ts__default["default"].isCallExpression(parent)) {
return parent.expression === node || parent.arguments.includes(node);
}
// Identifier used in a property read is only top-level if it's the expression.
if (ts__default["default"].isPropertyAccessExpression(parent)) {
return parent.expression === node;
}
// Identifier used in an array is only top-level if it's one of the elements.
if (ts__default["default"].isArrayLiteralExpression(parent)) {
return parent.elements.includes(node);
}
// Identifier in a property assignment is only top level if it's the initializer.
if (ts__default["default"].isPropertyAssignment(parent)) {
return parent.initializer === node;
}
// Identifier in a class is only top level if it's the name.
if (ts__default["default"].isClassDeclaration(parent)) {
return parent.name === node;
}
// Otherwise it's not top-level.
return false;
}
/** Checks if a value is a TypeScript AST node. */
isTypeScriptNode(value) {
// If this is too permissive, we can also check for `getSourceFile`. This code runs
// on a narrow set of use cases so checking for `kind` should be enough.
return !!value && typeof value.kind === 'number';
}
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Extracts the HMR metadata for a class declaration.
* @param clazz Class being analyzed.
* @param reflection Reflection host.
* @param compilerHost Compiler host to use when resolving file names.
* @param rootDirs Root directories configured by the user.
* @param definition Analyzed component definition.
* @param factory Analyzed component factory.
* @param classMetadata Analyzed `setClassMetadata` expression, if any.
* @param debugInfo Analyzed `setClassDebugInfo` expression, if any.
*/
function extractHmrMetatadata(clazz, reflection, compilerHost, rootDirs, definition, factory, classMetadata, debugInfo) {
if (!reflection.isClass(clazz)) {
return null;
}
const sourceFile = clazz.getSourceFile();
const filePath = getProjectRelativePath(sourceFile, rootDirs, compilerHost) ||
compilerHost.getCanonicalFileName(sourceFile.fileName);
const dependencies = extractHmrDependencies(clazz, definition, factory, classMetadata, debugInfo);
const meta = {
type: new checker.WrappedNodeExpr(clazz.name),
className: clazz.name.text,
filePath,
localDependencies: dependencies.local,
namespaceDependencies: dependencies.external,
};
return meta;
}
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Gets the declaration for the function that replaces the metadata of a class during HMR.
* @param compilationResults Code generated for the class during compilation.
* @param meta HMR metadata about the class.
* @param sourceFile File in which the class is defined.
*/
function getHmrUpdateDeclaration(compilationResults, constantStatements, meta, sourceFile) {
const namespaceSpecifiers = meta.namespaceDependencies.reduce((result, current) => {
result.set(current.moduleName, current.assignedName);
return result;
}, new Map());
const importRewriter = new HmrModuleImportRewriter(namespaceSpecifiers);
const importManager = new checker.ImportManager({
...checker.presetImportManagerForceNamespaceImports,
rewriter: importRewriter,
});
const callback = compileHmrUpdateCallback(compilationResults, constantStatements, meta);
const node = checker.translateStatement(sourceFile, callback, importManager);
// The output AST doesn't support modifiers so we have to emit to
// TS and then update the declaration to add `export default`.
return ts__default["default"].factory.updateFunctionDeclaration(node, [
ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.ExportKeyword),
ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.DefaultKeyword),
], node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body);
}
class HmrModuleImportRewriter {
lookup;
constructor(lookup) {
this.lookup = lookup;
}
rewriteNamespaceImportIdentifier(specifier, moduleName) {
return this.lookup.has(moduleName) ? this.lookup.get(moduleName) : specifier;
}
rewriteSymbol(symbol) {
return symbol;
}
rewriteSpecifier(specifier) {
return specifier;
}
}
const EMPTY_ARRAY = [];
const isUsedDirective = (decl) => decl.kind === checker.R3TemplateDependencyKind.Directive;
const isUsedPipe = (decl) => decl.kind === checker.R3TemplateDependencyKind.Pipe;
/**
* `DecoratorHandler` which handles the `@Component` annotation.
*/
class ComponentDecoratorHandler {
reflector;
evaluator;
metaRegistry;
metaReader;
scopeReader;
compilerHost;
scopeRegistry;
typeCheckScopeRegistry;
resourceRegistry;
isCore;
strictCtorDeps;
resourceLoader;
rootDirs;
defaultPreserveWhitespaces;
i18nUseExternalIds;
enableI18nLegacyMessageIdFormat;
usePoisonedData;
i18nNormalizeLineEndingsInICUs;
moduleResolver;
cycleAnalyzer;
cycleHandlingStrategy;
refEmitter;
referencesRegistry;
depTracker;
injectableRegistry;
semanticDepGraphUpdater;
annotateForClosureCompiler;
perf;
hostDirectivesResolver;
importTracker;
includeClassMetadata;
compilationMode;
deferredSymbolTracker;
forbidOrphanRendering;
enableBlockSyntax;
enableLetSyntax;
externalRuntimeStyles;
localCompilationExtraImportsTracker;
jitDeclarationRegistry;
i18nPreserveSignificantWhitespace;
strictStandalone;
enableHmr;
implicitStandaloneValue;
constructor(reflector, evaluator, metaRegistry, metaReader, scopeReader, compilerHost, scopeRegistry, typeCheckScopeRegistry, resourceRegistry, isCore, strictCtorDeps, resourceLoader, rootDirs, defaultPreserveWhitespaces, i18nUseExternalIds, enableI18nLegacyMessageIdFormat, usePoisonedData, i18nNormalizeLineEndingsInICUs, moduleResolver, cycleAnalyzer, cycleHandlingStrategy, refEmitter, referencesRegistry, depTracker, injectableRegistry, semanticDepGraphUpdater, annotateForClosureCompiler, perf, hostDirectivesResolver, importTracker, includeClassMetadata, compilationMode, deferredSymbolTracker, forbidOrphanRendering, enableBlockSyntax, enableLetSyntax, externalRuntimeStyles, localCompilationExtraImportsTracker, jitDeclarationRegistry, i18nPreserveSignificantWhitespace, strictStandalone, enableHmr, implicitStandaloneValue) {
this.reflector = reflector;
this.evaluator = evaluator;
this.metaRegistry = metaRegistry;
this.metaReader = metaReader;
this.scopeReader = scopeReader;
this.compilerHost = compilerHost;
this.scopeRegistry = scopeRegistry;
this.typeCheckScopeRegistry = typeCheckScopeRegistry;
this.resourceRegistry = resourceRegistry;
this.isCore = isCore;
this.strictCtorDeps = strictCtorDeps;
this.resourceLoader = resourceLoader;
this.rootDirs = rootDirs;
this.defaultPreserveWhitespaces = defaultPreserveWhitespaces;
this.i18nUseExternalIds = i18nUseExternalIds;
this.enableI18nLegacyMessageIdFormat = enableI18nLegacyMessageIdFormat;
this.usePoisonedData = usePoisonedData;
this.i18nNormalizeLineEndingsInICUs = i18nNormalizeLineEndingsInICUs;
this.moduleResolver = moduleResolver;
this.cycleAnalyzer = cycleAnalyzer;
this.cycleHandlingStrategy = cycleHandlingStrategy;
this.refEmitter = refEmitter;
this.referencesRegistry = referencesRegistry;
this.depTracker = depTracker;
this.injectableRegistry = injectableRegistry;
this.semanticDepGraphUpdater = semanticDepGraphUpdater;
this.annotateForClosureCompiler = annotateForClosureCompiler;
this.perf = perf;
this.hostDirectivesResolver = hostDirectivesResolver;
this.importTracker = importTracker;
this.includeClassMetadata = includeClassMetadata;
this.compilationMode = compilationMode;
this.deferredSymbolTracker = deferredSymbolTracker;
this.forbidOrphanRendering = forbidOrphanRendering;
this.enableBlockSyntax = enableBlockSyntax;
this.enableLetSyntax = enableLetSyntax;
this.externalRuntimeStyles = externalRuntimeStyles;
this.localCompilationExtraImportsTracker = localCompilationExtraImportsTracker;
this.jitDeclarationRegistry = jitDeclarationRegistry;
this.i18nPreserveSignificantWhitespace = i18nPreserveSignificantWhitespace;
this.strictStandalone = strictStandalone;
this.enableHmr = enableHmr;
this.implicitStandaloneValue = implicitStandaloneValue;
this.extractTemplateOptions = {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs,
usePoisonedData: this.usePoisonedData,
enableBlockSyntax: this.enableBlockSyntax,
enableLetSyntax: this.enableLetSyntax,
preserveSignificantWhitespace: this.i18nPreserveSignificantWhitespace,
};
// Dependencies can't be deferred during HMR, because the HMR update module can't have
// dynamic imports and its dependencies need to be passed in directly. If dependencies
// are deferred, their imports will be deleted so we won't may lose the reference to them.
this.canDeferDeps = !enableHmr;
}
literalCache = new Map();
elementSchemaRegistry = new checker.DomElementSchemaRegistry();
/**
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
* any potential tags which might need to be loaded. This cache ensures that work is not
* thrown away, and the parsed template is reused during the analyze phase.
*/
preanalyzeTemplateCache = new Map();
preanalyzeStylesCache = new Map();
/** Whether generated code for a component can defer its dependencies. */
canDeferDeps;
extractTemplateOptions;
precedence = checker.HandlerPrecedence.PRIMARY;
name = 'ComponentDecoratorHandler';
detect(node, decorators) {
if (!decorators) {
return undefined;
}
const decorator = checker.findAngularDecorator(decorators, 'Component', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,
decorator,
metadata: decorator,
};
}
else {
return undefined;
}
}
preanalyze(node, decorator) {
// In preanalyze, resource URLs associated with the component are asynchronously preloaded via
// the resourceLoader. This is the only time async operations are allowed for a component.
// These resources are:
//
// - the templateUrl, if there is one
// - any styleUrls if present
// - any stylesheets referenced from tags in the template itself
//
// As a result of the last one, the template must be parsed as part of preanalysis to extract
// tags, which may involve waiting for the templateUrl to be resolved first.
// If preloading isn't possible, then skip this step.
if (!this.resourceLoader.canPreload) {
return undefined;
}
const meta = resolveLiteral(decorator, this.literalCache);
const component = checker.reflectObjectLiteral(meta);
const containingFile = node.getSourceFile().fileName;
const resolveStyleUrl = (styleUrl) => {
try {
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
return this.resourceLoader.preload(resourceUrl, {
type: 'style',
containingFile,
className: node.name.text,
});
}
catch {
// Don't worry about failures to preload. We can handle this problem during analysis by
// producing a diagnostic.
return undefined;
}
};
// A Promise that waits for the template and all ed styles within it to be preloaded.
const templateAndTemplateStyleResources = preloadAndParseTemplate(this.evaluator, this.resourceLoader, this.depTracker, this.preanalyzeTemplateCache, node, decorator, component, containingFile, this.defaultPreserveWhitespaces, this.extractTemplateOptions, this.compilationMode).then((template) => {
if (template === null) {
return { templateStyles: [], templateStyleUrls: [] };
}
let templateUrl;
if (template.sourceMapping.type === 'external') {
templateUrl = template.sourceMapping.templateUrl;
}
return {
templateUrl,
templateStyles: template.styles,
templateStyleUrls: template.styleUrls,
};
});
// Extract all the styleUrls in the decorator.
const componentStyleUrls = extractComponentStyleUrls(this.evaluator, component);
return templateAndTemplateStyleResources.then(async (templateInfo) => {
// Extract inline styles, process, and cache for use in synchronous analyze phase
let styles = null;
// Order plus className allows inline styles to be identified per component by a preprocessor
let orderOffset = 0;
const rawStyles = parseDirectiveStyles(component, this.evaluator, this.compilationMode);
if (rawStyles?.length) {
styles = await Promise.all(rawStyles.map((style) => this.resourceLoader.preprocessInline(style, {
type: 'style',
containingFile,
order: orderOffset++,
className: node.name.text,
})));
}
if (templateInfo.templateStyles) {
styles ??= [];
styles.push(...(await Promise.all(templateInfo.templateStyles.map((style) => this.resourceLoader.preprocessInline(style, {
type: 'style',
containingFile: templateInfo.templateUrl ?? containingFile,
order: orderOffset++,
className: node.name.text,
})))));
}
this.preanalyzeStylesCache.set(node, styles);
if (this.externalRuntimeStyles) {
// No preanalysis required for style URLs with external runtime styles
return;
}
// Wait for both the template and all styleUrl resources to resolve.
await Promise.all([
...componentStyleUrls.map((styleUrl) => resolveStyleUrl(styleUrl.url)),
...templateInfo.templateStyleUrls.map((url) => resolveStyleUrl(url)),
]);
});
}
analyze(node, decorator) {
this.perf.eventCount(checker.PerfEvent.AnalyzeComponent);
const containingFile = node.getSourceFile().fileName;
this.literalCache.delete(decorator);
let diagnostics;
let isPoisoned = false;
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
// on it.
const directiveResult = extractDirectiveMetadata(node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter, this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode, this.elementSchemaRegistry.getDefaultComponentElementName(), this.strictStandalone, this.implicitStandaloneValue);
// `extractDirectiveMetadata` returns `jitForced = true` when the `@Component` has
// set `jit: true`. In this case, compilation of the decorator is skipped. Returning
// an empty object signifies that no analysis was produced.
if (directiveResult.jitForced) {
this.jitDeclarationRegistry.jitDeclarations.add(node);
return {};
}
// Next, read the `@Component`-specific fields.
const { decorator: component, metadata, inputs, outputs, hostDirectives, rawHostDirectives, } = directiveResult;
const encapsulation = (this.compilationMode !== checker.CompilationMode.LOCAL
? resolveEnumValue(this.evaluator, component, 'encapsulation', 'ViewEncapsulation')
: resolveEncapsulationEnumValueLocally(component.get('encapsulation'))) ??
checker.ViewEncapsulation.Emulated;
let changeDetection = null;
if (this.compilationMode !== checker.CompilationMode.LOCAL) {
changeDetection = resolveEnumValue(this.evaluator, component, 'changeDetection', 'ChangeDetectionStrategy');
}
else if (component.has('changeDetection')) {
changeDetection = new checker.WrappedNodeExpr(component.get('changeDetection'));
}
let animations = null;
let animationTriggerNames = null;
if (component.has('animations')) {
const animationExpression = component.get('animations');
animations = new checker.WrappedNodeExpr(animationExpression);
const animationsValue = this.evaluator.evaluate(animationExpression, animationTriggerResolver);
animationTriggerNames = { includesDynamicAnimations: false, staticTriggerNames: [] };
collectAnimationNames(animationsValue, animationTriggerNames);
}
// Go through the root directories for this project, and select the one with the smallest
// relative path representation.
const relativeContextFilePath = this.rootDirs.reduce((previous, rootDir) => {
const candidate = checker.relative(checker.absoluteFrom(rootDir), checker.absoluteFrom(containingFile));
if (previous === undefined || candidate.length < previous.length) {
return candidate;
}
else {
return previous;
}
}, undefined);
// Note that we could technically combine the `viewProvidersRequiringFactory` and
// `providersRequiringFactory` into a single set, but we keep the separate so that
// we can distinguish where an error is coming from when logging the diagnostics in `resolve`.
let viewProvidersRequiringFactory = null;
let providersRequiringFactory = null;
let wrappedViewProviders = null;
if (component.has('viewProviders')) {
const viewProviders = component.get('viewProviders');
viewProvidersRequiringFactory = checker.resolveProvidersRequiringFactory(viewProviders, this.reflector, this.evaluator);
wrappedViewProviders = new checker.WrappedNodeExpr(this.annotateForClosureCompiler
? checker.wrapFunctionExpressionsInParens(viewProviders)
: viewProviders);
}
if (component.has('providers')) {
providersRequiringFactory = checker.resolveProvidersRequiringFactory(component.get('providers'), this.reflector, this.evaluator);
}
let resolvedImports = null;
let resolvedDeferredImports = null;
let rawImports = component.get('imports') ?? null;
let rawDeferredImports = component.get('deferredImports') ?? null;
if ((rawImports || rawDeferredImports) && !metadata.isStandalone) {
if (diagnostics === undefined) {
diagnostics = [];
}
const importsField = rawImports ? 'imports' : 'deferredImports';
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.COMPONENT_NOT_STANDALONE, component.get(importsField), `'${importsField}' is only valid on a component that is standalone.`, [
checker.makeRelatedInformation(node.name, `Did you forget to add 'standalone: true' to this @Component?`),
]));
// Poison the component so that we don't spam further template type-checking errors that
// result from misconfigured imports.
isPoisoned = true;
}
else if (this.compilationMode !== checker.CompilationMode.LOCAL &&
(rawImports || rawDeferredImports)) {
const importResolvers = checker.combineResolvers([
createModuleWithProvidersResolver(this.reflector, this.isCore),
checker.forwardRefResolver,
]);
const importDiagnostics = [];
if (rawImports) {
const expr = rawImports;
const imported = this.evaluator.evaluate(expr, importResolvers);
const { imports: flattened, diagnostics } = validateAndFlattenComponentImports(imported, expr, false /* isDeferred */);
importDiagnostics.push(...diagnostics);
resolvedImports = flattened;
rawImports = expr;
}
if (rawDeferredImports) {
const expr = rawDeferredImports;
const imported = this.evaluator.evaluate(expr, importResolvers);
const { imports: flattened, diagnostics } = validateAndFlattenComponentImports(imported, expr, true /* isDeferred */);
importDiagnostics.push(...diagnostics);
resolvedDeferredImports = flattened;
rawDeferredImports = expr;
}
if (importDiagnostics.length > 0) {
isPoisoned = true;
if (diagnostics === undefined) {
diagnostics = [];
}
diagnostics.push(...importDiagnostics);
}
}
let schemas = null;
if (component.has('schemas') && !metadata.isStandalone) {
if (diagnostics === undefined) {
diagnostics = [];
}
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.COMPONENT_NOT_STANDALONE, component.get('schemas'), `'schemas' is only valid on a component that is standalone.`));
}
else if (this.compilationMode !== checker.CompilationMode.LOCAL && component.has('schemas')) {
schemas = extractSchemas(component.get('schemas'), this.evaluator, 'Component');
}
else if (metadata.isStandalone) {
schemas = [];
}
// Parse the template.
// If a preanalyze phase was executed, the template may already exist in parsed form, so check
// the preanalyzeTemplateCache.
// Extract a closure of the template parsing code so that it can be reparsed with different
// options if needed, like in the indexing pipeline.
let template;
if (this.preanalyzeTemplateCache.has(node)) {
// The template was parsed in preanalyze. Use it and delete it to save memory.
const preanalyzed = this.preanalyzeTemplateCache.get(node);
this.preanalyzeTemplateCache.delete(node);
template = preanalyzed;
}
else {
const templateDecl = parseTemplateDeclaration(node, decorator, component, containingFile, this.evaluator, this.depTracker, this.resourceLoader, this.defaultPreserveWhitespaces);
template = extractTemplate(node, templateDecl, this.evaluator, this.depTracker, this.resourceLoader, {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs,
usePoisonedData: this.usePoisonedData,
enableBlockSyntax: this.enableBlockSyntax,
enableLetSyntax: this.enableLetSyntax,
preserveSignificantWhitespace: this.i18nPreserveSignificantWhitespace,
}, this.compilationMode);
if (this.compilationMode === checker.CompilationMode.LOCAL &&
template.errors &&
template.errors.length > 0) {
// Template errors are handled at the type check phase. But we skip this phase in local compilation mode. As a result we need to handle the errors now and add them to the diagnostics.
if (diagnostics === undefined) {
diagnostics = [];
}
diagnostics.push(...checker.getTemplateDiagnostics(template.errors, '', // Template ID is required as part of the template type check, mainly for mapping the template to its component class. But here we are generating the diagnostic outside of the type check context, and so we skip the template ID.
template.sourceMapping));
}
}
const templateResource = template.declaration.isInline
? { path: null, expression: component.get('template') }
: {
path: checker.absoluteFrom(template.declaration.resolvedTemplateUrl),
expression: template.sourceMapping.node,
};
// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
// precede inline styles, and styles defined in the template override styles defined in the
// component.
let styles = [];
const externalStyles = [];
const styleResources = extractInlineStyleResources(component);
const styleUrls = [
...extractComponentStyleUrls(this.evaluator, component),
..._extractTemplateStyleUrls(template),
];
for (const styleUrl of styleUrls) {
try {
const resourceUrl = this.resourceLoader.resolve(styleUrl.url, containingFile);
if (this.externalRuntimeStyles) {
// External runtime styles are not considered disk-based and may not actually exist on disk
externalStyles.push(resourceUrl);
continue;
}
if (styleUrl.source === 2 /* ResourceTypeForDiagnostics.StylesheetFromDecorator */ &&
ts__default["default"].isStringLiteralLike(styleUrl.expression)) {
// Only string literal values from the decorator are considered style resources
styleResources.add({
path: checker.absoluteFrom(resourceUrl),
expression: styleUrl.expression,
});
}
const resourceStr = this.resourceLoader.load(resourceUrl);
styles.push(resourceStr);
if (this.depTracker !== null) {
this.depTracker.addResourceDependency(node.getSourceFile(), checker.absoluteFrom(resourceUrl));
}
}
catch {
if (this.depTracker !== null) {
// The analysis of this file cannot be re-used if one of the style URLs could
// not be resolved or loaded. Future builds should re-analyze and re-attempt
// resolution/loading.
this.depTracker.recordDependencyAnalysisFailure(node.getSourceFile());
}
if (diagnostics === undefined) {
diagnostics = [];
}
const resourceType = styleUrl.source === 2 /* ResourceTypeForDiagnostics.StylesheetFromDecorator */
? 2 /* ResourceTypeForDiagnostics.StylesheetFromDecorator */
: 1 /* ResourceTypeForDiagnostics.StylesheetFromTemplate */;
diagnostics.push(makeResourceNotFoundError(styleUrl.url, styleUrl.expression, resourceType).toDiagnostic());
}
}
if (encapsulation === checker.ViewEncapsulation.ShadowDom && metadata.selector !== null) {
const selectorError = checkCustomElementSelectorForErrors(metadata.selector);
if (selectorError !== null) {
if (diagnostics === undefined) {
diagnostics = [];
}
diagnostics.push(checker.makeDiagnostic(checker.ErrorCode.COMPONENT_INVALID_SHADOW_DOM_SELECTOR, component.get('selector'), selectorError));
}
}
// If inline styles were preprocessed use those
let inlineStyles = null;
if (this.preanalyzeStylesCache.has(node)) {
inlineStyles = this.preanalyzeStylesCache.get(node);
this.preanalyzeStylesCache.delete(node);
if (inlineStyles?.length) {
if (this.externalRuntimeStyles) {
// When external runtime styles is enabled, a list of URLs is provided
externalStyles.push(...inlineStyles);
}
else {
styles.push(...inlineStyles);
}
}
}
else {
// Preprocessing is only supported asynchronously
// If no style cache entry is present asynchronous preanalyze was not executed.
// This protects against accidental differences in resource contents when preanalysis
// is not used with a provided transformResource hook on the ResourceHost.
if (this.resourceLoader.canPreprocess) {
throw new Error('Inline resource processing requires asynchronous preanalyze.');
}
if (component.has('styles')) {
const litStyles = parseDirectiveStyles(component, this.evaluator, this.compilationMode);
if (litStyles !== null) {
inlineStyles = [...litStyles];
styles.push(...litStyles);
}
}
if (template.styles.length > 0) {
styles.push(...template.styles);
}
}
// Collect all explicitly deferred symbols from the `@Component.deferredImports` field
// (if it exists) and populate the `DeferredSymbolTracker` state. These operations are safe
// for the local compilation mode, since they don't require accessing/resolving symbols
// outside of the current source file.
let explicitlyDeferredTypes = null;
if (metadata.isStandalone && rawDeferredImports !== null) {
const deferredTypes = this.collectExplicitlyDeferredSymbols(rawDeferredImports);
for (const [deferredType, importDetails] of deferredTypes) {
explicitlyDeferredTypes ??= [];
explicitlyDeferredTypes.push({
symbolName: importDetails.name,
importPath: importDetails.from,
isDefaultImport: isDefaultImport(importDetails.node),
});
this.deferredSymbolTracker.markAsDeferrableCandidate(deferredType, importDetails.node, node, true /* isExplicitlyDeferred */);
}
}
const output = {
analysis: {
baseClass: checker.readBaseClass(node, this.reflector, this.evaluator),
inputs,
inputFieldNamesFromMetadataArray: directiveResult.inputFieldNamesFromMetadataArray,
outputs,
hostDirectives,
rawHostDirectives,
meta: {
...metadata,
template,
encapsulation,
changeDetection,
interpolation: template.interpolationConfig ?? checker.DEFAULT_INTERPOLATION_CONFIG,
styles,
externalStyles,
// These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized.
animations,
viewProviders: wrappedViewProviders,
i18nUseExternalIds: this.i18nUseExternalIds,
relativeContextFilePath,
rawImports: rawImports !== null ? new checker.WrappedNodeExpr(rawImports) : undefined,
},
typeCheckMeta: checker.extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
classMetadata: this.includeClassMetadata
? extractClassMetadata(node, this.reflector, this.isCore, this.annotateForClosureCompiler, (dec) => transformDecoratorResources(dec, component, styles, template))
: null,
classDebugInfo: extractClassDebugInfo(node, this.reflector, this.compilerHost, this.rootDirs,
/* forbidOrphanRenderering */ this.forbidOrphanRendering),
template,
providersRequiringFactory,
viewProvidersRequiringFactory,
inlineStyles,
styleUrls,
resources: {
styles: styleResources,
template: templateResource,
},
isPoisoned,
animationTriggerNames,
rawImports,
resolvedImports,
rawDeferredImports,
resolvedDeferredImports,
explicitlyDeferredTypes,
schemas,
decorator: decorator?.node ?? null,
},
diagnostics,
};
return output;
}
symbol(node, analysis) {
const typeParameters = extractSemanticTypeParameters(node);
return new ComponentSymbol(node, analysis.meta.selector, analysis.inputs, analysis.outputs, analysis.meta.exportAs, analysis.typeCheckMeta, typeParameters);
}
register(node, analysis) {
// Register this component's information with the `MetadataRegistry`. This ensures that
// the information about the component is available during the compile() phase.
const ref = new checker.Reference(node);
this.metaRegistry.registerDirectiveMetadata({
kind: checker.MetaKind.Directive,
matchSource: checker.MatchSource.Selector,
ref,
name: node.name.text,
selector: analysis.meta.selector,
exportAs: analysis.meta.exportAs,
inputs: analysis.inputs,
inputFieldNamesFromMetadataArray: analysis.inputFieldNamesFromMetadataArray,
outputs: analysis.outputs,
queries: analysis.meta.queries.map((query) => query.propertyName),
isComponent: true,
baseClass: analysis.baseClass,
hostDirectives: analysis.hostDirectives,
...analysis.typeCheckMeta,
isPoisoned: analysis.isPoisoned,
isStructural: false,
isStandalone: analysis.meta.isStandalone,
isSignal: analysis.meta.isSignal,
imports: analysis.resolvedImports,
rawImports: analysis.rawImports,
deferredImports: analysis.resolvedDeferredImports,
animationTriggerNames: analysis.animationTriggerNames,
schemas: analysis.schemas,
decorator: analysis.decorator,
assumedToExportProviders: false,
ngContentSelectors: analysis.template.ngContentSelectors,
preserveWhitespaces: analysis.template.preserveWhitespaces ?? false,
isExplicitlyDeferred: false,
});
this.resourceRegistry.registerResources(analysis.resources, node);
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.meta.deps,
});
}
index(context, node, analysis) {
if (analysis.isPoisoned && !this.usePoisonedData) {
return null;
}
const scope = this.scopeReader.getScopeForComponent(node);
const selector = analysis.meta.selector;
const matcher = new checker.SelectorMatcher();
if (scope !== null) {
let { dependencies, isPoisoned } = scope.kind === checker.ComponentScopeKind.NgModule ? scope.compilation : scope;
if ((isPoisoned || (scope.kind === checker.ComponentScopeKind.NgModule && scope.exported.isPoisoned)) &&
!this.usePoisonedData) {
// Don't bother indexing components which had erroneous scopes, unless specifically
// requested.
return null;
}
for (const dep of dependencies) {
if (dep.kind === checker.MetaKind.Directive && dep.selector !== null) {
matcher.addSelectables(checker.CssSelector.parse(dep.selector), [
...this.hostDirectivesResolver.resolve(dep),
dep,
]);
}
}
}
const binder = new checker.R3TargetBinder(matcher);
const boundTemplate = binder.bind({ template: analysis.template.diagNodes });
context.addComponent({
declaration: node,
selector,
boundTemplate,
templateMeta: {
isInline: analysis.template.declaration.isInline,
file: analysis.template.file,
},
});
return null;
}
typeCheck(ctx, node, meta) {
if (this.typeCheckScopeRegistry === null || !ts__default["default"].isClassDeclaration(node)) {
return;
}
if (meta.isPoisoned && !this.usePoisonedData) {
return;
}
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
if (scope.isPoisoned && !this.usePoisonedData) {
// Don't type-check components that had errors in their scopes, unless requested.
return;
}
const binder = new checker.R3TargetBinder(scope.matcher);
ctx.addTemplate(new checker.Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas, meta.template.sourceMapping, meta.template.file, meta.template.errors, meta.meta.isStandalone, meta.meta.template.preserveWhitespaces ?? false);
}
extendedTemplateCheck(component, extendedTemplateChecker) {
return extendedTemplateChecker.getDiagnosticsForComponent(component);
}
templateSemanticsCheck(component, templateSemanticsChecker) {
return templateSemanticsChecker.getDiagnosticsForComponent(component);
}
resolve(node, analysis, symbol) {
const metadata = analysis.meta;
const diagnostics = [];
const context = checker.getSourceFile(node);
// Check if there are some import declarations that contain symbols used within
// the `@Component.deferredImports` field, but those imports contain other symbols
// and thus the declaration can not be removed. This diagnostics is shared between local and
// global compilation modes.
const nonRemovableImports = this.deferredSymbolTracker.getNonRemovableDeferredImports(context, node);
if (nonRemovableImports.length > 0) {
for (const importDecl of nonRemovableImports) {
const diagnostic = checker.makeDiagnostic(checker.ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, importDecl, `This import contains symbols that are used both inside and outside of the ` +
`\`@Component.deferredImports\` fields in the file. This renders all these ` +
`defer imports useless as this import remains and its module is eagerly loaded. ` +
`To fix this, make sure that all symbols from the import are *only* used within ` +
`\`@Component.deferredImports\` arrays and there are no other references to those ` +
`symbols present in this file.`);
diagnostics.push(diagnostic);
}
return { diagnostics };
}
let data;
if (this.compilationMode === checker.CompilationMode.LOCAL) {
// Initial value in local compilation mode.
data = {
declarations: EMPTY_ARRAY,
declarationListEmitMode: !analysis.meta.isStandalone || analysis.rawImports !== null
? 3 /* DeclarationListEmitMode.RuntimeResolved */
: 0 /* DeclarationListEmitMode.Direct */,
deferPerBlockDependencies: this.locateDeferBlocksWithoutScope(analysis.template),
deferBlockDepsEmitMode: 1 /* DeferBlockDepsEmitMode.PerComponent */,
deferrableDeclToImportDecl: new Map(),
deferPerComponentDependencies: analysis.explicitlyDeferredTypes ?? [],
};
if (this.localCompilationExtraImportsTracker === null) {
// In local compilation mode the resolve phase is only needed for generating extra imports.
// Otherwise we can skip it.
return { data };
}
}
else {
// Initial value in global compilation mode.
data = {
declarations: EMPTY_ARRAY,
declarationListEmitMode: 0 /* DeclarationListEmitMode.Direct */,
deferPerBlockDependencies: new Map(),
deferBlockDepsEmitMode: 0 /* DeferBlockDepsEmitMode.PerBlock */,
deferrableDeclToImportDecl: new Map(),
deferPerComponentDependencies: [],
};
}
if (this.semanticDepGraphUpdater !== null && analysis.baseClass instanceof checker.Reference) {
symbol.baseClass = this.semanticDepGraphUpdater.getSymbol(analysis.baseClass.node);
}
if (analysis.isPoisoned && !this.usePoisonedData) {
return {};
}
const scope = this.scopeReader.getScopeForComponent(node);
if (scope !== null) {
// Replace the empty components and directives from the analyze() step with a fully expanded
// scope. This is possible now because during resolve() the whole compilation unit has been
// fully analyzed.
//
// First it needs to be determined if actually importing the directives/pipes used in the
// template would create a cycle. Currently ngtsc refuses to generate cycles, so an option
// known as "remote scoping" is used if a cycle would be created. In remote scoping, the
// module file sets the directives/pipes on the ɵcmp of the component, without
// requiring new imports (but also in a way that breaks tree shaking).
//
// Determining this is challenging, because the TemplateDefinitionBuilder is responsible for
// matching directives and pipes in the template; however, that doesn't run until the actual
// compile() step. It's not possible to run template compilation sooner as it requires the
// ConstantPool for the overall file being compiled (which isn't available until the
// transform step).
//
// Instead, directives/pipes are matched independently here, using the R3TargetBinder. This
// is an alternative implementation of template matching which is used for template
// type-checking and will eventually replace matching in the TemplateDefinitionBuilder.
const isModuleScope = scope.kind === checker.ComponentScopeKind.NgModule;
// Dependencies coming from the regular `imports` field.
const dependencies = isModuleScope ? scope.compilation.dependencies : scope.dependencies;
// Dependencies from the `@Component.deferredImports` field.
const explicitlyDeferredDependencies = getExplicitlyDeferredDeps(scope);
// Mark the component is an NgModule-based component with its NgModule in a different file
// then mark this file for extra import generation
if (isModuleScope && context.fileName !== checker.getSourceFile(scope.ngModule).fileName) {
this.localCompilationExtraImportsTracker?.markFileForExtraImportGeneration(context);
}
// Make sure that `@Component.imports` and `@Component.deferredImports` do not have
// the same dependencies.
if (metadata.isStandalone &&
analysis.rawDeferredImports !== null &&
explicitlyDeferredDependencies.length > 0) {
const diagnostic = validateNoImportOverlap(dependencies, explicitlyDeferredDependencies, analysis.rawDeferredImports);
if (diagnostic !== null) {
diagnostics.push(diagnostic);
}
}
// Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are
// later fed to the TemplateDefinitionBuilder.
const binder = createTargetBinder(dependencies);
const pipes = extractPipes(dependencies);
let allDependencies = dependencies;
let deferBlockBinder = binder;
// If there are any explicitly deferred dependencies (via `@Component.deferredImports`),
// re-compute the list of dependencies and create a new binder for defer blocks.
if (explicitlyDeferredDependencies.length > 0) {
allDependencies = [...explicitlyDeferredDependencies, ...dependencies];
deferBlockBinder = createTargetBinder(allDependencies);
}
// Next, the component template AST is bound using the R3TargetBinder. This produces a
// BoundTarget, which is similar to a ts.TypeChecker.
const bound = binder.bind({ template: metadata.template.nodes });
// Find all defer blocks used in the template and for each block
// bind its own scope.
const deferBlocks = new Map();
for (const deferBlock of bound.getDeferBlocks()) {
deferBlocks.set(deferBlock, deferBlockBinder.bind({ template: deferBlock.children }));
}
// Register all Directives and Pipes used at the top level (outside
// of any defer blocks), which would be eagerly referenced.
const eagerlyUsed = new Set();
for (const dir of bound.getEagerlyUsedDirectives()) {
eagerlyUsed.add(dir.ref.node);
}
for (const name of bound.getEagerlyUsedPipes()) {
if (!pipes.has(name)) {
continue;
}
eagerlyUsed.add(pipes.get(name).ref.node);
}
// Set of Directives and Pipes used across the entire template,
// including all defer blocks.
const wholeTemplateUsed = new Set(eagerlyUsed);
for (const bound of deferBlocks.values()) {
for (const dir of bound.getEagerlyUsedDirectives()) {
wholeTemplateUsed.add(dir.ref.node);
}
for (const name of bound.getEagerlyUsedPipes()) {
if (!pipes.has(name)) {
continue;
}
wholeTemplateUsed.add(pipes.get(name).ref.node);
}
}
const declarations = new Map();
// Transform the dependencies list, filtering out unused dependencies.
for (const dep of allDependencies) {
// Only emit references to each dependency once.
if (declarations.has(dep.ref.node)) {
continue;
}
switch (dep.kind) {
case checker.MetaKind.Directive:
if (!wholeTemplateUsed.has(dep.ref.node) || dep.matchSource !== checker.MatchSource.Selector) {
continue;
}
const dirType = this.refEmitter.emit(dep.ref, context);
checker.assertSuccessfulReferenceEmit(dirType, node.name, dep.isComponent ? 'component' : 'directive');
declarations.set(dep.ref.node, {
kind: checker.R3TemplateDependencyKind.Directive,
ref: dep.ref,
type: dirType.expression,
importedFile: dirType.importedFile,
selector: dep.selector,
inputs: dep.inputs.propertyNames,
outputs: dep.outputs.propertyNames,
exportAs: dep.exportAs,
isComponent: dep.isComponent,
});
break;
case checker.MetaKind.Pipe:
if (!wholeTemplateUsed.has(dep.ref.node)) {
continue;
}
const pipeType = this.refEmitter.emit(dep.ref, context);
checker.assertSuccessfulReferenceEmit(pipeType, node.name, 'pipe');
declarations.set(dep.ref.node, {
kind: checker.R3TemplateDependencyKind.Pipe,
type: pipeType.expression,
name: dep.name,
ref: dep.ref,
importedFile: pipeType.importedFile,
});
break;
case checker.MetaKind.NgModule:
const ngModuleType = this.refEmitter.emit(dep.ref, context);
checker.assertSuccessfulReferenceEmit(ngModuleType, node.name, 'NgModule');
declarations.set(dep.ref.node, {
kind: checker.R3TemplateDependencyKind.NgModule,
type: ngModuleType.expression,
importedFile: ngModuleType.importedFile,
});
break;
}
}
const getSemanticReference = (decl) => this.semanticDepGraphUpdater.getSemanticReference(decl.ref.node, decl.type);
if (this.semanticDepGraphUpdater !== null) {
symbol.usedDirectives = Array.from(declarations.values())
.filter(isUsedDirective)
.map(getSemanticReference);
symbol.usedPipes = Array.from(declarations.values())
.filter(isUsedPipe)
.map(getSemanticReference);
}
const eagerDeclarations = Array.from(declarations.values()).filter((decl) => decl.kind === checker.R3TemplateDependencyKind.NgModule || eagerlyUsed.has(decl.ref.node));
// Process information related to defer blocks
if (this.compilationMode !== checker.CompilationMode.LOCAL) {
this.resolveDeferBlocks(node, deferBlocks, declarations, data, analysis, eagerlyUsed);
}
const cyclesFromDirectives = new Map();
const cyclesFromPipes = new Map();
// Scan through the directives/pipes actually used in the template and check whether any
// import which needs to be generated would create a cycle. This check is skipped for
// standalone components as the dependencies of a standalone component have already been
// imported directly by the user, so Angular won't introduce any imports that aren't already
// in the user's program.
if (!metadata.isStandalone) {
for (const usedDep of eagerDeclarations) {
const cycle = this._checkForCyclicImport(usedDep.importedFile, usedDep.type, context);
if (cycle !== null) {
switch (usedDep.kind) {
case checker.R3TemplateDependencyKind.Directive:
cyclesFromDirectives.set(usedDep, cycle);
break;
case checker.R3TemplateDependencyKind.Pipe:
cyclesFromPipes.set(usedDep, cycle);
break;
}
}
}
}
// Check whether any usages of standalone components in imports requires the dependencies
// array to be wrapped in a closure. This check is technically a heuristic as there's no
// direct way to check whether a `Reference` came from a `forwardRef`. Instead, we check if
// the reference is `synthetic`, implying it came from _any_ foreign function resolver,
// including the `forwardRef` resolver.
const standaloneImportMayBeForwardDeclared = analysis.resolvedImports !== null && analysis.resolvedImports.some((ref) => ref.synthetic);
const cycleDetected = cyclesFromDirectives.size !== 0 || cyclesFromPipes.size !== 0;
if (!cycleDetected) {
// No cycle was detected. Record the imports that need to be created in the cycle detector
// so that future cyclic import checks consider their production.
for (const { type, importedFile } of eagerDeclarations) {
this.maybeRecordSyntheticImport(importedFile, type, context);
}
// Check whether the dependencies arrays in ɵcmp need to be wrapped in a closure.
// This is required if any dependency reference is to a declaration in the same file
// but declared after this component.
const declarationIsForwardDeclared = eagerDeclarations.some((decl) => checker.isExpressionForwardReference(decl.type, node.name, context));
if (this.compilationMode !== checker.CompilationMode.LOCAL &&
(declarationIsForwardDeclared || standaloneImportMayBeForwardDeclared)) {
data.declarationListEmitMode = 1 /* DeclarationListEmitMode.Closure */;
}
data.declarations = eagerDeclarations;
// Register extra local imports.
if (this.compilationMode === checker.CompilationMode.LOCAL &&
this.localCompilationExtraImportsTracker !== null) {
// In global compilation mode `eagerDeclarations` contains "all" the component
// dependencies, whose import statements will be added to the file. In local compilation
// mode `eagerDeclarations` only includes the "local" dependencies, meaning those that are
// declared inside this compilation unit.Here the import info of these local dependencies
// are added to the tracker so that we can generate extra imports representing these local
// dependencies. For non-local dependencies we use another technique of adding some
// best-guess extra imports globally to all files using
// `localCompilationExtraImportsTracker.addGlobalImportFromIdentifier`.
for (const { type } of eagerDeclarations) {
if (type instanceof checker.ExternalExpr && type.value.moduleName) {
this.localCompilationExtraImportsTracker.addImportForFile(context, type.value.moduleName);
}
}
}
}
else {
if (this.cycleHandlingStrategy === 0 /* CycleHandlingStrategy.UseRemoteScoping */) {
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
// create a cycle. Instead, mark this component as requiring remote scoping, so that the
// NgModule file will take care of setting the directives for the component.
this.scopeRegistry.setComponentRemoteScope(node, eagerDeclarations.filter(isUsedDirective).map((dir) => dir.ref), eagerDeclarations.filter(isUsedPipe).map((pipe) => pipe.ref));
symbol.isRemotelyScoped = true;
// If a semantic graph is being tracked, record the fact that this component is remotely
// scoped with the declaring NgModule symbol as the NgModule's emit becomes dependent on
// the directive/pipe usages of this component.
if (this.semanticDepGraphUpdater !== null &&
scope.kind === checker.ComponentScopeKind.NgModule &&
scope.ngModule !== null) {
const moduleSymbol = this.semanticDepGraphUpdater.getSymbol(scope.ngModule);
if (!(moduleSymbol instanceof NgModuleSymbol)) {
throw new Error(`AssertionError: Expected ${scope.ngModule.name} to be an NgModuleSymbol.`);
}
moduleSymbol.addRemotelyScopedComponent(symbol, symbol.usedDirectives, symbol.usedPipes);
}
}
else {
// We are not able to handle this cycle so throw an error.
const relatedMessages = [];
for (const [dir, cycle] of cyclesFromDirectives) {
relatedMessages.push(makeCyclicImportInfo(dir.ref, dir.isComponent ? 'component' : 'directive', cycle));
}
for (const [pipe, cycle] of cyclesFromPipes) {
relatedMessages.push(makeCyclicImportInfo(pipe.ref, 'pipe', cycle));
}
throw new checker.FatalDiagnosticError(checker.ErrorCode.IMPORT_CYCLE_DETECTED, node, 'One or more import cycles would need to be created to compile this component, ' +
'which is not supported by the current compiler configuration.', relatedMessages);
}
}
}
else {
// If there is no scope, we can still use the binder to retrieve *some* information about the
// deferred blocks.
data.deferPerBlockDependencies = this.locateDeferBlocksWithoutScope(metadata.template);
}
// Run diagnostics only in global mode.
if (this.compilationMode !== checker.CompilationMode.LOCAL) {
// Validate `@Component.imports` and `@Component.deferredImports` fields.
if (analysis.resolvedImports !== null && analysis.rawImports !== null) {
const importDiagnostics = validateStandaloneImports(analysis.resolvedImports, analysis.rawImports, this.metaReader, this.scopeReader, false /* isDeferredImport */);
diagnostics.push(...importDiagnostics);
}
if (analysis.resolvedDeferredImports !== null && analysis.rawDeferredImports !== null) {
const importDiagnostics = validateStandaloneImports(analysis.resolvedDeferredImports, analysis.rawDeferredImports, this.metaReader, this.scopeReader, true /* isDeferredImport */);
diagnostics.push(...importDiagnostics);
}
if (analysis.providersRequiringFactory !== null &&
analysis.meta.providers instanceof checker.WrappedNodeExpr) {
const providerDiagnostics = getProviderDiagnostics(analysis.providersRequiringFactory, analysis.meta.providers.node, this.injectableRegistry);
diagnostics.push(...providerDiagnostics);
}
if (analysis.viewProvidersRequiringFactory !== null &&
analysis.meta.viewProviders instanceof checker.WrappedNodeExpr) {
const viewProviderDiagnostics = getProviderDiagnostics(analysis.viewProvidersRequiringFactory, analysis.meta.viewProviders.node, this.injectableRegistry);
diagnostics.push(...viewProviderDiagnostics);
}
const directiveDiagnostics = getDirectiveDiagnostics(node, this.injectableRegistry, this.evaluator, this.reflector, this.scopeRegistry, this.strictCtorDeps, 'Component');
if (directiveDiagnostics !== null) {
diagnostics.push(...directiveDiagnostics);
}
const hostDirectivesDiagnostics = analysis.hostDirectives && analysis.rawHostDirectives
? validateHostDirectives(analysis.rawHostDirectives, analysis.hostDirectives, this.metaReader)
: null;
if (hostDirectivesDiagnostics !== null) {
diagnostics.push(...hostDirectivesDiagnostics);
}
}
if (diagnostics.length > 0) {
return { diagnostics };
}
return { data };
}
xi18n(ctx, node, analysis) {
ctx.updateFromTemplate(analysis.template.content, analysis.template.declaration.resolvedTemplateUrl, analysis.template.interpolationConfig ?? checker.DEFAULT_INTERPOLATION_CONFIG);
}
updateResources(node, analysis) {
const containingFile = node.getSourceFile().fileName;
// If the template is external, re-parse it.
const templateDecl = analysis.template.declaration;
if (!templateDecl.isInline) {
analysis.template = extractTemplate(node, templateDecl, this.evaluator, this.depTracker, this.resourceLoader, this.extractTemplateOptions, this.compilationMode);
}
// Update any external stylesheets and rebuild the combined 'styles' list.
// TODO(alxhub): write tests for styles when the primary compiler uses the updateResources
// path
let styles = [];
if (analysis.styleUrls !== null) {
for (const styleUrl of analysis.styleUrls) {
try {
const resolvedStyleUrl = this.resourceLoader.resolve(styleUrl.url, containingFile);
const styleText = this.resourceLoader.load(resolvedStyleUrl);
styles.push(styleText);
}
catch (e) {
// Resource resolve failures should already be in the diagnostics list from the analyze
// stage. We do not need to do anything with them when updating resources.
}
}
}
if (analysis.inlineStyles !== null) {
for (const styleText of analysis.inlineStyles) {
styles.push(styleText);
}
}
for (const styleText of analysis.template.styles) {
styles.push(styleText);
}
analysis.meta.styles = styles.filter((s) => s.trim().length > 0);
}
compileFull(node, analysis, resolution, pool) {
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
return [];
}
const perComponentDeferredDeps = this.canDeferDeps
? this.resolveAllDeferredDependencies(resolution)
: null;
const meta = {
...analysis.meta,
...resolution,
defer: this.compileDeferBlocks(resolution),
};
const fac = compileNgFactoryDefField(checker.toFactoryMetadata(meta, checker.FactoryTarget.Component));
if (perComponentDeferredDeps !== null) {
removeDeferrableTypesFromComponentDecorator(analysis, perComponentDeferredDeps);
}
const def = checker.compileComponentFromMetadata(meta, pool, checker.makeBindingParser());
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const classMetadata = analysis.classMetadata !== null
? compileComponentClassMetadata(analysis.classMetadata, perComponentDeferredDeps).toStmt()
: null;
const debugInfo = analysis.classDebugInfo !== null
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
: null;
const hmrMeta = this.enableHmr
? extractHmrMetatadata(node, this.reflector, this.compilerHost, this.rootDirs, def, fac, classMetadata, debugInfo)
: null;
const hmrInitializer = hmrMeta ? compileHmrInitializer(hmrMeta).toStmt() : null;
const deferrableImports = this.canDeferDeps
? this.deferredSymbolTracker.getDeferrableImportDecls()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports, debugInfo, hmrInitializer);
}
compilePartial(node, analysis, resolution) {
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
return [];
}
const templateInfo = {
content: analysis.template.content,
sourceUrl: analysis.template.declaration.resolvedTemplateUrl,
isInline: analysis.template.declaration.isInline,
inlineTemplateLiteralExpression: analysis.template.sourceMapping.type === 'direct'
? new checker.WrappedNodeExpr(analysis.template.sourceMapping.node)
: null,
};
const perComponentDeferredDeps = this.canDeferDeps
? this.resolveAllDeferredDependencies(resolution)
: null;
const meta = {
...analysis.meta,
...resolution,
defer: this.compileDeferBlocks(resolution),
};
const fac = compileDeclareFactory(checker.toFactoryMetadata(meta, checker.FactoryTarget.Component));
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const def = compileDeclareComponentFromMetadata(meta, analysis.template, templateInfo);
const classMetadata = analysis.classMetadata !== null
? compileComponentDeclareClassMetadata(analysis.classMetadata, perComponentDeferredDeps).toStmt()
: null;
const hmrMeta = this.enableHmr
? extractHmrMetatadata(node, this.reflector, this.compilerHost, this.rootDirs, def, fac, classMetadata, null)
: null;
const hmrInitializer = hmrMeta ? compileHmrInitializer(hmrMeta).toStmt() : null;
const deferrableImports = this.canDeferDeps
? this.deferredSymbolTracker.getDeferrableImportDecls()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports, null, hmrInitializer);
}
compileLocal(node, analysis, resolution, pool) {
// In the local compilation mode we can only rely on the information available
// within the `@Component.deferredImports` array, because in this mode compiler
// doesn't have information on which dependencies belong to which defer blocks.
const deferrableTypes = this.canDeferDeps ? analysis.explicitlyDeferredTypes : null;
const meta = {
...analysis.meta,
...resolution,
defer: this.compileDeferBlocks(resolution),
};
if (deferrableTypes !== null) {
removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes);
}
const fac = compileNgFactoryDefField(checker.toFactoryMetadata(meta, checker.FactoryTarget.Component));
const def = checker.compileComponentFromMetadata(meta, pool, checker.makeBindingParser());
const inputTransformFields = compileInputTransformFields(analysis.inputs);
const classMetadata = analysis.classMetadata !== null
? compileComponentClassMetadata(analysis.classMetadata, deferrableTypes).toStmt()
: null;
const debugInfo = analysis.classDebugInfo !== null
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
: null;
const hmrMeta = this.enableHmr
? extractHmrMetatadata(node, this.reflector, this.compilerHost, this.rootDirs, def, fac, classMetadata, debugInfo)
: null;
const hmrInitializer = hmrMeta ? compileHmrInitializer(hmrMeta).toStmt() : null;
const deferrableImports = this.canDeferDeps
? this.deferredSymbolTracker.getDeferrableImportDecls()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports, debugInfo, hmrInitializer);
}
compileHmrUpdateDeclaration(node, analysis, resolution) {
if (analysis.template.errors !== null && analysis.template.errors.length > 0) {
return null;
}
// Create a brand-new constant pool since there shouldn't be any constant sharing.
const pool = new checker.ConstantPool();
const meta = {
...analysis.meta,
...resolution,
defer: this.compileDeferBlocks(resolution),
};
const fac = compileNgFactoryDefField(checker.toFactoryMetadata(meta, checker.FactoryTarget.Component));
const def = checker.compileComponentFromMetadata(meta, pool, checker.makeBindingParser());
const classMetadata = analysis.classMetadata !== null
? compileComponentClassMetadata(analysis.classMetadata, null).toStmt()
: null;
const debugInfo = analysis.classDebugInfo !== null
? compileClassDebugInfo(analysis.classDebugInfo).toStmt()
: null;
const hmrMeta = this.enableHmr
? extractHmrMetatadata(node, this.reflector, this.compilerHost, this.rootDirs, def, fac, classMetadata, debugInfo)
: null;
const res = checker.compileResults(fac, def, classMetadata, 'ɵcmp', null, null, debugInfo, null);
return hmrMeta === null || res.length === 0
? null
: getHmrUpdateDeclaration(res, pool.statements, hmrMeta, node.getSourceFile());
}
/**
* Locates defer blocks in case scope information is not available.
* For example, this happens in the local compilation mode.
*/
locateDeferBlocksWithoutScope(template) {
const deferBlocks = new Map();
const directivelessBinder = new checker.R3TargetBinder(new checker.SelectorMatcher());
const bound = directivelessBinder.bind({ template: template.nodes });
const deferredBlocks = bound.getDeferBlocks();
for (const block of deferredBlocks) {
// We can't determine the dependencies without a scope so we leave them empty.
deferBlocks.set(block, []);
}
return deferBlocks;
}
/**
* Computes a list of deferrable symbols based on dependencies from
* the `@Component.imports` field and their usage in `@defer` blocks.
*/
resolveAllDeferredDependencies(resolution) {
const deferrableTypes = [];
// Go over all dependencies of all defer blocks and update the value of
// the `isDeferrable` flag and the `importPath` to reflect the current
// state after visiting all components during the `resolve` phase.
for (const [_, deps] of resolution.deferPerBlockDependencies) {
for (const deferBlockDep of deps) {
const importDecl = resolution.deferrableDeclToImportDecl.get(deferBlockDep.declaration.node) ?? null;
if (importDecl !== null && this.deferredSymbolTracker.canDefer(importDecl)) {
deferBlockDep.isDeferrable = true;
deferBlockDep.importPath = importDecl.moduleSpecifier.text;
deferBlockDep.isDefaultImport = isDefaultImport(importDecl);
deferrableTypes.push(deferBlockDep);
}
}
}
return deferrableTypes;
}
/**
* Collects deferrable symbols from the `@Component.deferredImports` field.
*/
collectExplicitlyDeferredSymbols(rawDeferredImports) {
const deferredTypes = new Map();
if (!ts__default["default"].isArrayLiteralExpression(rawDeferredImports)) {
return deferredTypes;
}
for (const element of rawDeferredImports.elements) {
const node = checker.tryUnwrapForwardRef(element, this.reflector) || element;
if (!ts__default["default"].isIdentifier(node)) {
// Can't defer-load non-literal references.
continue;
}
const imp = this.reflector.getImportOfIdentifier(node);
if (imp !== null) {
deferredTypes.set(node, imp);
}
}
return deferredTypes;
}
/**
* Check whether adding an import from `origin` to the source-file corresponding to `expr` would
* create a cyclic import.
*
* @returns a `Cycle` object if a cycle would be created, otherwise `null`.
*/
_checkForCyclicImport(importedFile, expr, origin) {
const imported = checker.resolveImportedFile(this.moduleResolver, importedFile, expr, origin);
if (imported === null) {
return null;
}
// Check whether the import is legal.
return this.cycleAnalyzer.wouldCreateCycle(origin, imported);
}
maybeRecordSyntheticImport(importedFile, expr, origin) {
const imported = checker.resolveImportedFile(this.moduleResolver, importedFile, expr, origin);
if (imported === null) {
return;
}
this.cycleAnalyzer.recordSyntheticImport(origin, imported);
}
/**
* Resolves information about defer blocks dependencies to make it
* available for the final `compile` step.
*/
resolveDeferBlocks(componentClassDecl, deferBlocks, deferrableDecls, resolutionData, analysisData, eagerlyUsedDecls) {
// Collect all deferred decls from all defer blocks from the entire template
// to intersect with the information from the `imports` field of a particular
// Component.
const allDeferredDecls = new Set();
for (const [deferBlock, bound] of deferBlocks) {
const usedDirectives = new Set(bound.getEagerlyUsedDirectives().map((d) => d.ref.node));
const usedPipes = new Set(bound.getEagerlyUsedPipes());
let deps;
if (resolutionData.deferPerBlockDependencies.has(deferBlock)) {
deps = resolutionData.deferPerBlockDependencies.get(deferBlock);
}
else {
deps = [];
resolutionData.deferPerBlockDependencies.set(deferBlock, deps);
}
for (const decl of Array.from(deferrableDecls.values())) {
if (decl.kind === checker.R3TemplateDependencyKind.NgModule) {
continue;
}
if (decl.kind === checker.R3TemplateDependencyKind.Directive &&
!usedDirectives.has(decl.ref.node)) {
continue;
}
if (decl.kind === checker.R3TemplateDependencyKind.Pipe && !usedPipes.has(decl.name)) {
continue;
}
// Collect initial information about this dependency.
// `isDeferrable`, `importPath` and `isDefaultImport` will be
// added later during the `compile` step.
deps.push({
typeReference: decl.type,
symbolName: decl.ref.node.name.text,
isDeferrable: false,
importPath: null,
isDefaultImport: false,
declaration: decl.ref,
});
allDeferredDecls.add(decl.ref.node);
}
}
// For standalone components with the `imports` and `deferredImports` fields -
// inspect the list of referenced symbols and mark the ones used in defer blocks
// as potential candidates for defer loading.
if (analysisData.meta.isStandalone) {
if (analysisData.rawImports !== null) {
this.registerDeferrableCandidates(componentClassDecl, analysisData.rawImports, false /* isDeferredImport */, allDeferredDecls, eagerlyUsedDecls, resolutionData);
}
if (analysisData.rawDeferredImports !== null) {
this.registerDeferrableCandidates(componentClassDecl, analysisData.rawDeferredImports, true /* isDeferredImport */, allDeferredDecls, eagerlyUsedDecls, resolutionData);
}
}
}
/**
* Inspects provided imports expression (either `@Component.imports` or
* `@Component.deferredImports`) and registers imported types as deferrable
* candidates.
*/
registerDeferrableCandidates(componentClassDecl, importsExpr, isDeferredImport, allDeferredDecls, eagerlyUsedDecls, resolutionData) {
if (!ts__default["default"].isArrayLiteralExpression(importsExpr)) {
return;
}
for (const element of importsExpr.elements) {
const node = checker.tryUnwrapForwardRef(element, this.reflector) || element;
if (!ts__default["default"].isIdentifier(node)) {
// Can't defer-load non-literal references.
continue;
}
const imp = this.reflector.getImportOfIdentifier(node);
if (imp === null) {
// Can't defer-load symbols which aren't imported.
continue;
}
const decl = this.reflector.getDeclarationOfIdentifier(node);
if (decl === null) {
// Can't defer-load symbols which don't exist.
continue;
}
if (!checker.isNamedClassDeclaration(decl.node)) {
// Can't defer-load symbols which aren't classes.
continue;
}
// Are we even trying to defer-load this symbol?
if (!allDeferredDecls.has(decl.node)) {
continue;
}
if (eagerlyUsedDecls.has(decl.node)) {
// Can't defer-load symbols that are eagerly referenced as a dependency
// in a template outside of a defer block.
continue;
}
// Is it a standalone directive/component?
const dirMeta = this.metaReader.getDirectiveMetadata(new checker.Reference(decl.node));
if (dirMeta !== null && !dirMeta.isStandalone) {
continue;
}
// Is it a standalone pipe?
const pipeMeta = this.metaReader.getPipeMetadata(new checker.Reference(decl.node));
if (pipeMeta !== null && !pipeMeta.isStandalone) {
continue;
}
if (dirMeta === null && pipeMeta === null) {
// This is not a directive or a pipe.
continue;
}
// Keep track of how this class made it into the current source file
// (which ts.ImportDeclaration was used for this symbol).
resolutionData.deferrableDeclToImportDecl.set(decl.node, imp.node);
this.deferredSymbolTracker.markAsDeferrableCandidate(node, imp.node, componentClassDecl, isDeferredImport);
}
}
compileDeferBlocks(resolution) {
const { deferBlockDepsEmitMode: mode, deferPerBlockDependencies: perBlockDeps, deferPerComponentDependencies: perComponentDeps, } = resolution;
if (mode === 0 /* DeferBlockDepsEmitMode.PerBlock */) {
if (!perBlockDeps) {
throw new Error('Internal error: deferPerBlockDependencies must be present when compiling in PerBlock mode');
}
const blocks = new Map();
for (const [block, dependencies] of perBlockDeps) {
blocks.set(block, dependencies.length === 0 ? null : checker.compileDeferResolverFunction({ mode, dependencies }));
}
return { mode, blocks };
}
if (mode === 1 /* DeferBlockDepsEmitMode.PerComponent */) {
if (!perComponentDeps) {
throw new Error('Internal error: deferPerComponentDependencies must be present in PerComponent mode');
}
return {
mode,
dependenciesFn: perComponentDeps.length === 0
? null
: checker.compileDeferResolverFunction({ mode, dependencies: perComponentDeps }),
};
}
throw new Error(`Invalid deferBlockDepsEmitMode. Cannot compile deferred block metadata.`);
}
}
/**
* Creates an instance of a target binder based on provided dependencies.
*/
function createTargetBinder(dependencies) {
const matcher = new checker.SelectorMatcher();
for (const dep of dependencies) {
if (dep.kind === checker.MetaKind.Directive && dep.selector !== null) {
matcher.addSelectables(checker.CssSelector.parse(dep.selector), [dep]);
}
}
return new checker.R3TargetBinder(matcher);
}
/**
* Returns the list of dependencies from `@Component.deferredImports` if provided.
*/
function getExplicitlyDeferredDeps(scope) {
return scope.kind === checker.ComponentScopeKind.NgModule
? []
: scope.deferredDependencies;
}
function extractPipes(dependencies) {
const pipes = new Map();
for (const dep of dependencies) {
if (dep.kind === checker.MetaKind.Pipe) {
pipes.set(dep.name, dep);
}
}
return pipes;
}
/**
* Drop references to existing imports for deferrable symbols that should be present
* in the `setClassMetadataAsync` call. Otherwise, an import declaration gets retained.
*/
function removeDeferrableTypesFromComponentDecorator(analysis, deferrableTypes) {
if (analysis.classMetadata) {
const deferrableSymbols = new Set(deferrableTypes.map((t) => t.symbolName));
const rewrittenDecoratorsNode = removeIdentifierReferences(analysis.classMetadata.decorators.node, deferrableSymbols);
analysis.classMetadata.decorators = new checker.WrappedNodeExpr(rewrittenDecoratorsNode);
}
}
/**
* Validates that `@Component.imports` and `@Component.deferredImports` do not have
* overlapping dependencies.
*/
function validateNoImportOverlap(eagerDeps, deferredDeps, rawDeferredImports) {
let diagnostic = null;
const eagerDepsSet = new Set();
for (const eagerDep of eagerDeps) {
eagerDepsSet.add(eagerDep.ref.node);
}
for (const deferredDep of deferredDeps) {
if (eagerDepsSet.has(deferredDep.ref.node)) {
const classInfo = deferredDep.ref.debugName
? `The \`${deferredDep.ref.debugName}\``
: 'One of the dependencies';
diagnostic = checker.makeDiagnostic(checker.ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, getDiagnosticNode(deferredDep.ref, rawDeferredImports), `\`${classInfo}\` is imported via both \`@Component.imports\` and ` +
`\`@Component.deferredImports\`. To fix this, make sure that ` +
`dependencies are imported only once.`);
break;
}
}
return diagnostic;
}
function validateStandaloneImports(importRefs, importExpr, metaReader, scopeReader, isDeferredImport) {
const diagnostics = [];
for (const ref of importRefs) {
const dirMeta = metaReader.getDirectiveMetadata(ref);
if (dirMeta !== null) {
if (!dirMeta.isStandalone) {
// Directly importing a directive that's not standalone is an error.
diagnostics.push(makeNotStandaloneDiagnostic(scopeReader, ref, importExpr, dirMeta.isComponent ? 'component' : 'directive'));
}
continue;
}
const pipeMeta = metaReader.getPipeMetadata(ref);
if (pipeMeta !== null) {
if (!pipeMeta.isStandalone) {
diagnostics.push(makeNotStandaloneDiagnostic(scopeReader, ref, importExpr, 'pipe'));
}
continue;
}
const ngModuleMeta = metaReader.getNgModuleMetadata(ref);
if (!isDeferredImport && ngModuleMeta !== null) {
// Importing NgModules is always legal in `@Component.imports`,
// but not supported in `@Component.deferredImports`.
continue;
}
// Make an error?
const error = isDeferredImport
? makeUnknownComponentDeferredImportDiagnostic(ref, importExpr)
: makeUnknownComponentImportDiagnostic(ref, importExpr);
diagnostics.push(error);
}
return diagnostics;
}
/** Returns whether an ImportDeclaration is a default import. */
function isDefaultImport(node) {
return node.importClause !== undefined && node.importClause.namedBindings === undefined;
}
/**
* Adapts the `compileInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
class InjectableDecoratorHandler {
reflector;
evaluator;
isCore;
strictCtorDeps;
injectableRegistry;
perf;
includeClassMetadata;
compilationMode;
errorOnDuplicateProv;
constructor(reflector, evaluator, isCore, strictCtorDeps, injectableRegistry, perf, includeClassMetadata, compilationMode,
/**
* What to do if the injectable already contains a ɵprov property.
*
* If true then an error diagnostic is reported.
* If false then there is no error and a new ɵprov property is not added.
*/
errorOnDuplicateProv = true) {
this.reflector = reflector;
this.evaluator = evaluator;
this.isCore = isCore;
this.strictCtorDeps = strictCtorDeps;
this.injectableRegistry = injectableRegistry;
this.perf = perf;
this.includeClassMetadata = includeClassMetadata;
this.compilationMode = compilationMode;
this.errorOnDuplicateProv = errorOnDuplicateProv;
}
precedence = checker.HandlerPrecedence.SHARED;
name = 'InjectableDecoratorHandler';
detect(node, decorators) {
if (!decorators) {
return undefined;
}
const decorator = checker.findAngularDecorator(decorators, 'Injectable', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,
decorator: decorator,
metadata: decorator,
};
}
else {
return undefined;
}
}
analyze(node, decorator) {
this.perf.eventCount(checker.PerfEvent.AnalyzeInjectable);
const meta = extractInjectableMetadata(node, decorator, this.reflector);
const decorators = this.reflector.getDecoratorsOfDeclaration(node);
return {
analysis: {
meta,
ctorDeps: extractInjectableCtorDeps(node, meta, decorator, this.reflector, this.isCore, this.strictCtorDeps),
classMetadata: this.includeClassMetadata
? extractClassMetadata(node, this.reflector, this.isCore)
: null,
// Avoid generating multiple factories if a class has
// more Angular decorators, apart from Injectable.
needsFactory: !decorators ||
decorators.every((current) => !checker.isAngularCore(current) || current.name === 'Injectable'),
},
};
}
symbol() {
return null;
}
register(node, analysis) {
if (this.compilationMode === checker.CompilationMode.LOCAL) {
return;
}
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.ctorDeps,
});
}
resolve(node, analysis) {
if (this.compilationMode === checker.CompilationMode.LOCAL) {
return {};
}
if (requiresValidCtor(analysis.meta)) {
const diagnostic = checkInheritanceOfInjectable(node, this.injectableRegistry, this.reflector, this.evaluator, this.strictCtorDeps, 'Injectable');
if (diagnostic !== null) {
return {
diagnostics: [diagnostic],
};
}
}
return {};
}
compileFull(node, analysis) {
return this.compile(compileNgFactoryDefField, (meta) => checker.compileInjectable(meta, false), compileClassMetadata, node, analysis);
}
compilePartial(node, analysis) {
return this.compile(compileDeclareFactory, compileDeclareInjectableFromMetadata, compileDeclareClassMetadata, node, analysis);
}
compileLocal(node, analysis) {
return this.compile(compileNgFactoryDefField, (meta) => checker.compileInjectable(meta, false), compileClassMetadata, node, analysis);
}
compile(compileFactoryFn, compileInjectableFn, compileClassMetadataFn, node, analysis) {
const results = [];
if (analysis.needsFactory) {
const meta = analysis.meta;
const factoryRes = compileFactoryFn(checker.toFactoryMetadata({ ...meta, deps: analysis.ctorDeps }, checker.FactoryTarget.Injectable));
if (analysis.classMetadata !== null) {
factoryRes.statements.push(compileClassMetadataFn(analysis.classMetadata).toStmt());
}
results.push(factoryRes);
}
const ɵprov = this.reflector.getMembersOfClass(node).find((member) => member.name === 'ɵprov');
if (ɵprov !== undefined && this.errorOnDuplicateProv) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.INJECTABLE_DUPLICATE_PROV, ɵprov.nameNode || ɵprov.node || node, 'Injectables cannot contain a static ɵprov property, because the compiler is going to generate one.');
}
if (ɵprov === undefined) {
// Only add a new ɵprov if there is not one already
const res = compileInjectableFn(analysis.meta);
results.push({
name: 'ɵprov',
initializer: res.expression,
statements: res.statements,
type: res.type,
deferrableImports: null,
});
}
return results;
}
}
/**
* Read metadata from the `@Injectable` decorator and produce the `IvyInjectableMetadata`, the
* input metadata needed to run `compileInjectable`.
*
* A `null` return value indicates this is @Injectable has invalid data.
*/
function extractInjectableMetadata(clazz, decorator, reflector) {
const name = clazz.name.text;
const type = checker.wrapTypeReference(reflector, clazz);
const typeArgumentCount = reflector.getGenericArityOfClass(clazz) || 0;
if (decorator.args === null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called');
}
if (decorator.args.length === 0) {
return {
name,
type,
typeArgumentCount,
providedIn: checker.createMayBeForwardRefExpression(new checker.LiteralExpr(null), 0 /* ForwardRefHandling.None */),
};
}
else if (decorator.args.length === 1) {
const metaNode = decorator.args[0];
// Firstly make sure the decorator argument is an inline literal - if not, it's illegal to
// transport references from one location to another. This is the problem that lowering
// used to solve - if this restriction proves too undesirable we can re-implement lowering.
if (!ts__default["default"].isObjectLiteralExpression(metaNode)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARG_NOT_LITERAL, metaNode, `@Injectable argument must be an object literal`);
}
// Resolve the fields of the literal into a map of field name to expression.
const meta = checker.reflectObjectLiteral(metaNode);
const providedIn = meta.has('providedIn')
? getProviderExpression(meta.get('providedIn'), reflector)
: checker.createMayBeForwardRefExpression(new checker.LiteralExpr(null), 0 /* ForwardRefHandling.None */);
let deps = undefined;
if ((meta.has('useClass') || meta.has('useFactory')) && meta.has('deps')) {
const depsExpr = meta.get('deps');
if (!ts__default["default"].isArrayLiteralExpression(depsExpr)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.VALUE_NOT_LITERAL, depsExpr, `@Injectable deps metadata must be an inline array`);
}
deps = depsExpr.elements.map((dep) => getDep(dep, reflector));
}
const result = { name, type, typeArgumentCount, providedIn };
if (meta.has('useValue')) {
result.useValue = getProviderExpression(meta.get('useValue'), reflector);
}
else if (meta.has('useExisting')) {
result.useExisting = getProviderExpression(meta.get('useExisting'), reflector);
}
else if (meta.has('useClass')) {
result.useClass = getProviderExpression(meta.get('useClass'), reflector);
result.deps = deps;
}
else if (meta.has('useFactory')) {
result.useFactory = new checker.WrappedNodeExpr(meta.get('useFactory'));
result.deps = deps;
}
return result;
}
else {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], 'Too many arguments to @Injectable');
}
}
/**
* Get the `R3ProviderExpression` for this `expression`.
*
* The `useValue`, `useExisting` and `useClass` properties might be wrapped in a `ForwardRef`, which
* needs to be unwrapped. This function will do that unwrapping and set a flag on the returned
* object to indicate whether the value needed unwrapping.
*/
function getProviderExpression(expression, reflector) {
const forwardRefValue = checker.tryUnwrapForwardRef(expression, reflector);
return checker.createMayBeForwardRefExpression(new checker.WrappedNodeExpr(forwardRefValue ?? expression), forwardRefValue !== null ? 2 /* ForwardRefHandling.Unwrapped */ : 0 /* ForwardRefHandling.None */);
}
function extractInjectableCtorDeps(clazz, meta, decorator, reflector, isCore, strictCtorDeps) {
if (decorator.args === null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called');
}
let ctorDeps = null;
if (decorator.args.length === 0) {
// Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be
// subject to the same validation. However, existing Angular code abuses @Injectable, applying
// it to things like abstract classes with constructors that were never meant for use with
// Angular's DI.
//
// To deal with this, @Injectable() without an argument is more lenient, and if the
// constructor signature does not work for DI then a factory definition (ɵfac) that throws is
// generated.
if (strictCtorDeps && !checker.isAbstractClassDeclaration(clazz)) {
ctorDeps = getValidConstructorDependencies(clazz, reflector, isCore);
}
else {
ctorDeps = unwrapConstructorDependencies(getConstructorDependencies(clazz, reflector, isCore));
}
return ctorDeps;
}
else if (decorator.args.length === 1) {
const rawCtorDeps = getConstructorDependencies(clazz, reflector, isCore);
if (strictCtorDeps && !checker.isAbstractClassDeclaration(clazz) && requiresValidCtor(meta)) {
// Since use* was not provided for a concrete class, validate the deps according to
// strictCtorDeps.
ctorDeps = validateConstructorDependencies(clazz, rawCtorDeps);
}
else {
ctorDeps = unwrapConstructorDependencies(rawCtorDeps);
}
}
return ctorDeps;
}
function requiresValidCtor(meta) {
return (meta.useValue === undefined &&
meta.useExisting === undefined &&
meta.useClass === undefined &&
meta.useFactory === undefined);
}
function getDep(dep, reflector) {
const meta = {
token: new checker.WrappedNodeExpr(dep),
attributeNameType: null,
host: false,
optional: false,
self: false,
skipSelf: false,
};
function maybeUpdateDecorator(dec, reflector, token) {
const source = reflector.getImportOfIdentifier(dec);
if (source === null || source.from !== '@angular/core') {
return false;
}
switch (source.name) {
case 'Inject':
if (token !== undefined) {
meta.token = new checker.WrappedNodeExpr(token);
}
break;
case 'Optional':
meta.optional = true;
break;
case 'SkipSelf':
meta.skipSelf = true;
break;
case 'Self':
meta.self = true;
break;
default:
return false;
}
return true;
}
if (ts__default["default"].isArrayLiteralExpression(dep)) {
dep.elements.forEach((el) => {
let isDecorator = false;
if (ts__default["default"].isIdentifier(el)) {
isDecorator = maybeUpdateDecorator(el, reflector);
}
else if (ts__default["default"].isNewExpression(el) && ts__default["default"].isIdentifier(el.expression)) {
const token = (el.arguments && el.arguments.length > 0 && el.arguments[0]) || undefined;
isDecorator = maybeUpdateDecorator(el.expression, reflector, token);
}
if (!isDecorator) {
meta.token = new checker.WrappedNodeExpr(el);
}
});
}
return meta;
}
/**
* Represents an Angular pipe.
*/
class PipeSymbol extends SemanticSymbol {
name;
constructor(decl, name) {
super(decl);
this.name = name;
}
isPublicApiAffected(previousSymbol) {
if (!(previousSymbol instanceof PipeSymbol)) {
return true;
}
return this.name !== previousSymbol.name;
}
isTypeCheckApiAffected(previousSymbol) {
return this.isPublicApiAffected(previousSymbol);
}
}
class PipeDecoratorHandler {
reflector;
evaluator;
metaRegistry;
scopeRegistry;
injectableRegistry;
isCore;
perf;
includeClassMetadata;
compilationMode;
generateExtraImportsInLocalMode;
strictStandalone;
implicitStandaloneValue;
constructor(reflector, evaluator, metaRegistry, scopeRegistry, injectableRegistry, isCore, perf, includeClassMetadata, compilationMode, generateExtraImportsInLocalMode, strictStandalone, implicitStandaloneValue) {
this.reflector = reflector;
this.evaluator = evaluator;
this.metaRegistry = metaRegistry;
this.scopeRegistry = scopeRegistry;
this.injectableRegistry = injectableRegistry;
this.isCore = isCore;
this.perf = perf;
this.includeClassMetadata = includeClassMetadata;
this.compilationMode = compilationMode;
this.generateExtraImportsInLocalMode = generateExtraImportsInLocalMode;
this.strictStandalone = strictStandalone;
this.implicitStandaloneValue = implicitStandaloneValue;
}
precedence = checker.HandlerPrecedence.PRIMARY;
name = 'PipeDecoratorHandler';
detect(node, decorators) {
if (!decorators) {
return undefined;
}
const decorator = checker.findAngularDecorator(decorators, 'Pipe', this.isCore);
if (decorator !== undefined) {
return {
trigger: decorator.node,
decorator: decorator,
metadata: decorator,
};
}
else {
return undefined;
}
}
analyze(clazz, decorator) {
this.perf.eventCount(checker.PerfEvent.AnalyzePipe);
const name = clazz.name.text;
const type = checker.wrapTypeReference(this.reflector, clazz);
if (decorator.args === null) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_NOT_CALLED, decorator.node, `@Pipe must be called`);
}
if (decorator.args.length !== 1) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, '@Pipe must have exactly one argument');
}
const meta = checker.unwrapExpression(decorator.args[0]);
if (!ts__default["default"].isObjectLiteralExpression(meta)) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, '@Pipe must have a literal argument');
}
const pipe = checker.reflectObjectLiteral(meta);
if (!pipe.has('name')) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.PIPE_MISSING_NAME, meta, `@Pipe decorator is missing name field`);
}
const pipeNameExpr = pipe.get('name');
const pipeName = this.evaluator.evaluate(pipeNameExpr);
if (typeof pipeName !== 'string') {
throw createValueHasWrongTypeError(pipeNameExpr, pipeName, `@Pipe.name must be a string`);
}
let pure = true;
if (pipe.has('pure')) {
const expr = pipe.get('pure');
const pureValue = this.evaluator.evaluate(expr);
if (typeof pureValue !== 'boolean') {
throw createValueHasWrongTypeError(expr, pureValue, `@Pipe.pure must be a boolean`);
}
pure = pureValue;
}
let isStandalone = this.implicitStandaloneValue;
if (pipe.has('standalone')) {
const expr = pipe.get('standalone');
const resolved = this.evaluator.evaluate(expr);
if (typeof resolved !== 'boolean') {
throw createValueHasWrongTypeError(expr, resolved, `standalone flag must be a boolean`);
}
isStandalone = resolved;
if (!isStandalone && this.strictStandalone) {
throw new checker.FatalDiagnosticError(checker.ErrorCode.NON_STANDALONE_NOT_ALLOWED, expr, `Only standalone pipes are allowed when 'strictStandalone' is enabled.`);
}
}
return {
analysis: {
meta: {
name,
type,
typeArgumentCount: this.reflector.getGenericArityOfClass(clazz) || 0,
pipeName,
deps: getValidConstructorDependencies(clazz, this.reflector, this.isCore),
pure,
isStandalone,
},
classMetadata: this.includeClassMetadata
? extractClassMetadata(clazz, this.reflector, this.isCore)
: null,
pipeNameExpr,
decorator: decorator?.node ?? null,
},
};
}
symbol(node, analysis) {
return new PipeSymbol(node, analysis.meta.pipeName);
}
register(node, analysis) {
const ref = new checker.Reference(node);
this.metaRegistry.registerPipeMetadata({
kind: checker.MetaKind.Pipe,
ref,
name: analysis.meta.pipeName,
nameExpr: analysis.pipeNameExpr,
isStandalone: analysis.meta.isStandalone,
decorator: analysis.decorator,
isExplicitlyDeferred: false,
});
this.injectableRegistry.registerInjectable(node, {
ctorDeps: analysis.meta.deps,
});
}
resolve(node) {
if (this.compilationMode === checker.CompilationMode.LOCAL) {
return {};
}
const duplicateDeclData = this.scopeRegistry.getDuplicateDeclarations(node);
if (duplicateDeclData !== null) {
// This pipe was declared twice (or more).
return {
diagnostics: [makeDuplicateDeclarationError(node, duplicateDeclData, 'Pipe')],
};
}
return {};
}
compileFull(node, analysis) {
const fac = compileNgFactoryDefField(checker.toFactoryMetadata(analysis.meta, checker.FactoryTarget.Pipe));
const def = checker.compilePipeFromMetadata(analysis.meta);
const classMetadata = analysis.classMetadata !== null
? compileClassMetadata(analysis.classMetadata).toStmt()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */);
}
compilePartial(node, analysis) {
const fac = compileDeclareFactory(checker.toFactoryMetadata(analysis.meta, checker.FactoryTarget.Pipe));
const def = compileDeclarePipeFromMetadata(analysis.meta);
const classMetadata = analysis.classMetadata !== null
? compileDeclareClassMetadata(analysis.classMetadata).toStmt()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */);
}
compileLocal(node, analysis) {
const fac = compileNgFactoryDefField(checker.toFactoryMetadata(analysis.meta, checker.FactoryTarget.Pipe));
const def = checker.compilePipeFromMetadata(analysis.meta);
const classMetadata = analysis.classMetadata !== null
? compileClassMetadata(analysis.classMetadata).toStmt()
: null;
return checker.compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */);
}
}
/**
* Whether a given decorator should be treated as an Angular decorator.
* Either it's used in @angular/core, or it's imported from there.
*/
function isAngularDecorator(decorator, isCore) {
return isCore || (decorator.import !== null && decorator.import.from === '@angular/core');
}
/*
#####################################################################
Code below has been extracted from the tsickle decorator downlevel transformer
and a few local modifications have been applied:
1. Tsickle by default processed all decorators that had the `@Annotation` JSDoc.
We modified the transform to only be concerned with known Angular decorators.
2. Tsickle by default added `@nocollapse` to all generated `ctorParameters` properties.
We only do this when `annotateForClosureCompiler` is enabled.
3. Tsickle does not handle union types for dependency injection. i.e. if a injected type
is denoted with `@Optional`, the actual type could be set to `T | null`.
See: https://github.com/angular/angular-cli/commit/826803d0736b807867caff9f8903e508970ad5e4.
4. Tsickle relied on `emitDecoratorMetadata` to be set to `true`. This is due to a limitation
in TypeScript transformers that never has been fixed. We were able to work around this
limitation so that `emitDecoratorMetadata` doesn't need to be specified.
See: `patchAliasReferenceResolution` for more details.
Here is a link to the tsickle revision on which this transformer is based:
https://github.com/angular/tsickle/blob/fae06becb1570f491806060d83f29f2d50c43cdd/src/decorator_downlevel_transformer.ts
#####################################################################
*/
const DECORATOR_INVOCATION_JSDOC_TYPE = '!Array<{type: !Function, args: (undefined|!Array>)}>';
/**
* Extracts the type of the decorator (the function or expression invoked), as well as all the
* arguments passed to the decorator. Returns an AST with the form:
*
* // For @decorator(arg1, arg2)
* { type: decorator, args: [arg1, arg2] }
*/
function extractMetadataFromSingleDecorator(decorator, diagnostics) {
const metadataProperties = [];
const expr = decorator.expression;
switch (expr.kind) {
case ts__default["default"].SyntaxKind.Identifier:
// The decorator was a plain @Foo.
metadataProperties.push(ts__default["default"].factory.createPropertyAssignment('type', expr));
break;
case ts__default["default"].SyntaxKind.CallExpression:
// The decorator was a call, like @Foo(bar).
const call = expr;
metadataProperties.push(ts__default["default"].factory.createPropertyAssignment('type', call.expression));
if (call.arguments.length) {
const args = [];
for (const arg of call.arguments) {
args.push(arg);
}
const argsArrayLiteral = ts__default["default"].factory.createArrayLiteralExpression(ts__default["default"].factory.createNodeArray(args, true));
metadataProperties.push(ts__default["default"].factory.createPropertyAssignment('args', argsArrayLiteral));
}
break;
default:
diagnostics.push({
file: decorator.getSourceFile(),
start: decorator.getStart(),
length: decorator.getEnd() - decorator.getStart(),
messageText: `${ts__default["default"].SyntaxKind[decorator.kind]} not implemented in gathering decorator metadata.`,
category: ts__default["default"].DiagnosticCategory.Error,
code: 0,
});
break;
}
return ts__default["default"].factory.createObjectLiteralExpression(metadataProperties);
}
/**
* createCtorParametersClassProperty creates a static 'ctorParameters' property containing
* downleveled decorator information.
*
* The property contains an arrow function that returns an array of object literals of the shape:
* static ctorParameters = () => [{
* type: SomeClass|undefined, // the type of the param that's decorated, if it's a value.
* decorators: [{
* type: DecoratorFn, // the type of the decorator that's invoked.
* args: [ARGS], // the arguments passed to the decorator.
* }]
* }];
*/
function createCtorParametersClassProperty(diagnostics, entityNameToExpression, ctorParameters, isClosureCompilerEnabled) {
const params = [];
for (const ctorParam of ctorParameters) {
if (!ctorParam.type && ctorParam.decorators.length === 0) {
params.push(ts__default["default"].factory.createNull());
continue;
}
const paramType = ctorParam.type
? typeReferenceToExpression(entityNameToExpression, ctorParam.type)
: undefined;
const members = [
ts__default["default"].factory.createPropertyAssignment('type', paramType || ts__default["default"].factory.createIdentifier('undefined')),
];
const decorators = [];
for (const deco of ctorParam.decorators) {
decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics));
}
if (decorators.length) {
members.push(ts__default["default"].factory.createPropertyAssignment('decorators', ts__default["default"].factory.createArrayLiteralExpression(decorators)));
}
params.push(ts__default["default"].factory.createObjectLiteralExpression(members));
}
const initializer = ts__default["default"].factory.createArrowFunction(undefined, undefined, [], undefined, ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.EqualsGreaterThanToken), ts__default["default"].factory.createArrayLiteralExpression(params, true));
const ctorProp = ts__default["default"].factory.createPropertyDeclaration([ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.StaticKeyword)], 'ctorParameters', undefined, undefined, initializer);
if (isClosureCompilerEnabled) {
ts__default["default"].setSyntheticLeadingComments(ctorProp, [
{
kind: ts__default["default"].SyntaxKind.MultiLineCommentTrivia,
text: [
`*`,
` * @type {function(): !Array<(null|{`,
` * type: ?,`,
` * decorators: (undefined|${DECORATOR_INVOCATION_JSDOC_TYPE}),`,
` * })>}`,
` * @nocollapse`,
` `,
].join('\n'),
pos: -1,
end: -1,
hasTrailingNewLine: true,
},
]);
}
return ctorProp;
}
/**
* Returns an expression representing the (potentially) value part for the given node.
*
* This is a partial re-implementation of TypeScript's serializeTypeReferenceNode. This is a
* workaround for https://github.com/Microsoft/TypeScript/issues/17516 (serializeTypeReferenceNode
* not being exposed). In practice this implementation is sufficient for Angular's use of type
* metadata.
*/
function typeReferenceToExpression(entityNameToExpression, node) {
let kind = node.kind;
if (ts__default["default"].isLiteralTypeNode(node)) {
// Treat literal types like their base type (boolean, string, number).
kind = node.literal.kind;
}
switch (kind) {
case ts__default["default"].SyntaxKind.FunctionType:
case ts__default["default"].SyntaxKind.ConstructorType:
return ts__default["default"].factory.createIdentifier('Function');
case ts__default["default"].SyntaxKind.ArrayType:
case ts__default["default"].SyntaxKind.TupleType:
return ts__default["default"].factory.createIdentifier('Array');
case ts__default["default"].SyntaxKind.TypePredicate:
case ts__default["default"].SyntaxKind.TrueKeyword:
case ts__default["default"].SyntaxKind.FalseKeyword:
case ts__default["default"].SyntaxKind.BooleanKeyword:
return ts__default["default"].factory.createIdentifier('Boolean');
case ts__default["default"].SyntaxKind.StringLiteral:
case ts__default["default"].SyntaxKind.StringKeyword:
return ts__default["default"].factory.createIdentifier('String');
case ts__default["default"].SyntaxKind.ObjectKeyword:
return ts__default["default"].factory.createIdentifier('Object');
case ts__default["default"].SyntaxKind.NumberKeyword:
case ts__default["default"].SyntaxKind.NumericLiteral:
return ts__default["default"].factory.createIdentifier('Number');
case ts__default["default"].SyntaxKind.TypeReference:
const typeRef = node;
// Ignore any generic types, just return the base type.
return entityNameToExpression(typeRef.typeName);
case ts__default["default"].SyntaxKind.UnionType:
const childTypeNodes = node.types.filter((t) => !(ts__default["default"].isLiteralTypeNode(t) && t.literal.kind === ts__default["default"].SyntaxKind.NullKeyword));
return childTypeNodes.length === 1
? typeReferenceToExpression(entityNameToExpression, childTypeNodes[0])
: undefined;
default:
return undefined;
}
}
/**
* Checks whether a given symbol refers to a value that exists at runtime (as distinct from a type).
*
* Expands aliases, which is important for the case where
* import * as x from 'some-module';
* and x is now a value (the module object).
*/
function symbolIsRuntimeValue(typeChecker, symbol) {
if (symbol.flags & ts__default["default"].SymbolFlags.Alias) {
symbol = typeChecker.getAliasedSymbol(symbol);
}
// Note that const enums are a special case, because
// while they have a value, they don't exist at runtime.
return (symbol.flags & ts__default["default"].SymbolFlags.Value & ts__default["default"].SymbolFlags.ConstEnumExcludes) !== 0;
}
/**
* Gets a transformer for downleveling Angular constructor parameter and property decorators.
*
* Note that Angular class decorators are never processed as those rely on side effects that
* would otherwise no longer be executed. i.e. the creation of a component definition.
*
* @param typeChecker Reference to the program's type checker.
* @param host Reflection host that is used for determining decorators.
* @param diagnostics List which will be populated with diagnostics if any.
* @param isCore Whether the current TypeScript program is for the `@angular/core` package.
* @param isClosureCompilerEnabled Whether closure annotations need to be added where needed.
* @param shouldTransformClass Optional function to check if a given class should be transformed.
*/
function getDownlevelDecoratorsTransform(typeChecker, host, diagnostics, isCore, isClosureCompilerEnabled, shouldTransformClass) {
function addJSDocTypeAnnotation(node, jsdocType) {
if (!isClosureCompilerEnabled) {
return;
}
ts__default["default"].setSyntheticLeadingComments(node, [
{
kind: ts__default["default"].SyntaxKind.MultiLineCommentTrivia,
text: `* @type {${jsdocType}} `,
pos: -1,
end: -1,
hasTrailingNewLine: true,
},
]);
}
/**
* createPropDecoratorsClassProperty creates a static 'propDecorators'
* property containing type information for every property that has a
* decorator applied.
*
* static propDecorators: {[key: string]: {type: Function, args?:
* any[]}[]} = { propA: [{type: MyDecorator, args: [1, 2]}, ...],
* ...
* };
*/
function createPropDecoratorsClassProperty(diagnostics, properties) {
// `static propDecorators: {[key: string]: ` + {type: Function, args?:
// any[]}[] + `} = {\n`);
const entries = [];
for (const [name, decorators] of properties.entries()) {
entries.push(ts__default["default"].factory.createPropertyAssignment(name, ts__default["default"].factory.createArrayLiteralExpression(decorators.map((deco) => extractMetadataFromSingleDecorator(deco, diagnostics)))));
}
const initializer = ts__default["default"].factory.createObjectLiteralExpression(entries, true);
const prop = ts__default["default"].factory.createPropertyDeclaration([ts__default["default"].factory.createToken(ts__default["default"].SyntaxKind.StaticKeyword)], 'propDecorators', undefined, undefined, initializer);
addJSDocTypeAnnotation(prop, `!Object`);
return prop;
}
return (context) => {
// Ensure that referenced type symbols are not elided by TypeScript. Imports for
// such parameter type symbols previously could be type-only, but now might be also
// used in the `ctorParameters` static property as a value. We want to make sure
// that TypeScript does not elide imports for such type references. Read more
// about this in the description for `loadIsReferencedAliasDeclarationPatch`.
const referencedParameterTypes = checker.loadIsReferencedAliasDeclarationPatch(context);
/**
* Converts an EntityName (from a type annotation) to an expression (accessing a value).
*
* For a given qualified name, this walks depth first to find the leftmost identifier,
* and then converts the path into a property access that can be used as expression.
*/
function entityNameToExpression(name) {
const symbol = typeChecker.getSymbolAtLocation(name);
// Check if the entity name references a symbol that is an actual value. If it is not, it
// cannot be referenced by an expression, so return undefined.
if (!symbol ||
!symbolIsRuntimeValue(typeChecker, symbol) ||
!symbol.declarations ||
symbol.declarations.length === 0) {
return undefined;
}
// If we deal with a qualified name, build up a property access expression
// that could be used in the JavaScript output.
if (ts__default["default"].isQualifiedName(name)) {
const containerExpr = entityNameToExpression(name.left);
if (containerExpr === undefined) {
return undefined;
}
return ts__default["default"].factory.createPropertyAccessExpression(containerExpr, name.right);
}
const decl = symbol.declarations[0];
// If the given entity name has been resolved to an alias import declaration,
// ensure that the alias declaration is not elided by TypeScript, and use its
// name identifier to reference it at runtime.
if (checker.isAliasImportDeclaration(decl)) {
referencedParameterTypes.add(decl);
// If the entity name resolves to an alias import declaration, we reference the
// entity based on the alias import name. This ensures that TypeScript properly
// resolves the link to the import. Cloning the original entity name identifier
// could lead to an incorrect resolution at local scope. e.g. Consider the following
// snippet: `constructor(Dep: Dep) {}`. In such a case, the local `Dep` identifier
// would resolve to the actual parameter name, and not to the desired import.
// This happens because the entity name identifier symbol is internally considered
// as type-only and therefore TypeScript tries to resolve it as value manually.
// We can help TypeScript and avoid this non-reliable resolution by using an identifier
// that is not type-only and is directly linked to the import alias declaration.
if (decl.name !== undefined) {
return ts__default["default"].setOriginalNode(ts__default["default"].factory.createIdentifier(decl.name.text), decl.name);
}
}
// Clone the original entity name identifier so that it can be used to reference
// its value at runtime. This is used when the identifier is resolving to a file
// local declaration (otherwise it would resolve to an alias import declaration).
return ts__default["default"].setOriginalNode(ts__default["default"].factory.createIdentifier(name.text), name);
}
/**
* Transforms a class element. Returns a three tuple of name, transformed element, and
* decorators found. Returns an undefined name if there are no decorators to lower on the
* element, or the element has an exotic name.
*/
function transformClassElement(element) {
element = ts__default["default"].visitEachChild(element, decoratorDownlevelVisitor, context);
const decoratorsToKeep = [];
const toLower = [];
const decorators = host.getDecoratorsOfDeclaration(element) || [];
for (const decorator of decorators) {
// We only deal with concrete nodes in TypeScript sources, so we don't
// need to handle synthetically created decorators.
const decoratorNode = decorator.node;
if (!isAngularDecorator(decorator, isCore)) {
decoratorsToKeep.push(decoratorNode);
continue;
}
toLower.push(decoratorNode);
}
if (!toLower.length)
return [undefined, element, []];
if (!element.name || !ts__default["default"].isIdentifier(element.name)) {
// Method has a weird name, e.g.
// [Symbol.foo]() {...}
diagnostics.push({
file: element.getSourceFile(),
start: element.getStart(),
length: element.getEnd() - element.getStart(),
messageText: `Cannot process decorators for class element with non-analyzable name.`,
category: ts__default["default"].DiagnosticCategory.Error,
code: 0,
});
return [undefined, element, []];
}
const elementModifiers = ts__default["default"].canHaveModifiers(element) ? ts__default["default"].getModifiers(element) : undefined;
let modifiers;
if (decoratorsToKeep.length || elementModifiers?.length) {
modifiers = ts__default["default"].setTextRange(ts__default["default"].factory.createNodeArray([...decoratorsToKeep, ...(elementModifiers || [])]), element.modifiers);
}
return [element.name.text, cloneClassElementWithModifiers(element, modifiers), toLower];
}
/**
* Transforms a constructor. Returns the transformed constructor and the list of parameter
* information collected, consisting of decorators and optional type.
*/
function transformConstructor(ctor) {
ctor = ts__default["default"].visitEachChild(ctor, decoratorDownlevelVisitor, context);
const newParameters = [];
const oldParameters = ctor.parameters;
const parametersInfo = [];
for (const param of oldParameters) {
const decoratorsToKeep = [];
const paramInfo = { decorators: [], type: null };
const decorators = host.getDecoratorsOfDeclaration(param) || [];
for (const decorator of decorators) {
// We only deal with concrete nodes in TypeScript sources, so we don't
// need to handle synthetically created decorators.
const decoratorNode = decorator.node;
if (!isAngularDecorator(decorator, isCore)) {
decoratorsToKeep.push(decoratorNode);
continue;
}
paramInfo.decorators.push(decoratorNode);
}
if (param.type) {
// param has a type provided, e.g. "foo: Bar".
// The type will be emitted as a value expression in entityNameToExpression, which takes
// care not to emit anything for types that cannot be expressed as a value (e.g.
// interfaces).
paramInfo.type = param.type;
}
parametersInfo.push(paramInfo);
// Must pass 'undefined' to avoid emitting decorator metadata.
let modifiers;
const paramModifiers = ts__default["default"].getModifiers(param);
if (decoratorsToKeep.length || paramModifiers?.length) {
modifiers = [...decoratorsToKeep, ...(paramModifiers || [])];
}
const newParam = ts__default["default"].factory.updateParameterDeclaration(param, modifiers, param.dotDotDotToken, param.name, param.questionToken, param.type, param.initializer);
newParameters.push(newParam);
}
const updated = ts__default["default"].factory.updateConstructorDeclaration(ctor, ts__default["default"].getModifiers(ctor), newParameters, ctor.body);
return [updated, parametersInfo];
}
/**
* Transforms a single class declaration:
* - dispatches to strip decorators on members
* - converts decorators on the class to annotations
* - creates a ctorParameters property
* - creates a propDecorators property
*/
function transformClassDeclaration(classDecl) {
const newMembers = [];
const decoratedProperties = new Map();
let classParameters = null;
for (const member of classDecl.members) {
switch (member.kind) {
case ts__default["default"].SyntaxKind.PropertyDeclaration:
case ts__default["default"].SyntaxKind.GetAccessor:
case ts__default["default"].SyntaxKind.SetAccessor:
case ts__default["default"].SyntaxKind.MethodDeclaration: {
const [name, newMember, decorators] = transformClassElement(member);
newMembers.push(newMember);
if (name)
decoratedProperties.set(name, decorators);
continue;
}
case ts__default["default"].SyntaxKind.Constructor: {
const ctor = member;
if (!ctor.body)
break;
const [newMember, parametersInfo] = transformConstructor(member);
classParameters = parametersInfo;
newMembers.push(newMember);
continue;
}
}
newMembers.push(ts__default["default"].visitEachChild(member, decoratorDownlevelVisitor, context));
}
// Note: The `ReflectionHost.getDecoratorsOfDeclaration()` method will not
// return all decorators, but only ones that could be possible Angular decorators.
const possibleAngularDecorators = host.getDecoratorsOfDeclaration(classDecl) || [];
// Keep track if we come across an Angular class decorator. This is used
// to determine whether constructor parameters should be captured or not.
const hasAngularDecorator = possibleAngularDecorators.some((d) => isAngularDecorator(d, isCore));
if (classParameters) {
if (hasAngularDecorator || classParameters.some((p) => !!p.decorators.length)) {
// Capture constructor parameters if the class has Angular decorator applied,
// or if any of the parameters has decorators applied directly.
newMembers.push(createCtorParametersClassProperty(diagnostics, entityNameToExpression, classParameters, isClosureCompilerEnabled));
}
}
if (decoratedProperties.size) {
newMembers.push(createPropDecoratorsClassProperty(diagnostics, decoratedProperties));
}
const members = ts__default["default"].setTextRange(ts__default["default"].factory.createNodeArray(newMembers, classDecl.members.hasTrailingComma), classDecl.members);
return ts__default["default"].factory.updateClassDeclaration(classDecl, classDecl.modifiers, classDecl.name, classDecl.typeParameters, classDecl.heritageClauses, members);
}
/**
* Transformer visitor that looks for Angular decorators and replaces them with
* downleveled static properties. Also collects constructor type metadata for
* class declaration that are decorated with an Angular decorator.
*/
function decoratorDownlevelVisitor(node) {
if (ts__default["default"].isClassDeclaration(node) &&
(shouldTransformClass === undefined || shouldTransformClass(node))) {
return transformClassDeclaration(node);
}
return ts__default["default"].visitEachChild(node, decoratorDownlevelVisitor, context);
}
return (sf) => {
// Downlevel decorators and constructor parameter types. We will keep track of all
// referenced constructor parameter types so that we can instruct TypeScript to
// not elide their imports if they previously were only type-only.
return ts__default["default"].visitEachChild(sf, decoratorDownlevelVisitor, context);
};
};
}
function cloneClassElementWithModifiers(node, modifiers) {
let clone;
if (ts__default["default"].isMethodDeclaration(node)) {
clone = ts__default["default"].factory.createMethodDeclaration(modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, node.body);
}
else if (ts__default["default"].isPropertyDeclaration(node)) {
clone = ts__default["default"].factory.createPropertyDeclaration(modifiers, node.name, node.questionToken, node.type, node.initializer);
}
else if (ts__default["default"].isGetAccessor(node)) {
clone = ts__default["default"].factory.createGetAccessorDeclaration(modifiers, node.name, node.parameters, node.type, node.body);
}
else if (ts__default["default"].isSetAccessor(node)) {
clone = ts__default["default"].factory.createSetAccessorDeclaration(modifiers, node.name, node.parameters, node.body);
}
else {
throw new Error(`Unsupported decorated member with kind ${ts__default["default"].SyntaxKind[node.kind]}`);
}
return ts__default["default"].setOriginalNode(clone, node);
}
/**
* Creates an import and access for a given Angular core import while
* ensuring the decorator symbol access can be traced back to an Angular core
* import in order to make the synthetic decorator compatible with the JIT
* decorator downlevel transform.
*/
function createSyntheticAngularCoreDecoratorAccess(factory, importManager, ngClassDecorator, sourceFile, decoratorName) {
const classDecoratorIdentifier = ts__default["default"].isIdentifier(ngClassDecorator.identifier)
? ngClassDecorator.identifier
: ngClassDecorator.identifier.expression;
return factory.createPropertyAccessExpression(importManager.addImport({
exportModuleSpecifier: '@angular/core',
exportSymbolName: null,
requestedFile: sourceFile,
}),
// The synthetic identifier may be checked later by the downlevel decorators
// transform to resolve to an Angular import using `getSymbolAtLocation`. We trick
// the transform to think it's not synthetic and comes from Angular core.
ts__default["default"].setOriginalNode(factory.createIdentifier(decoratorName), classDecoratorIdentifier));
}
/** Casts the given expression as `any`. */
function castAsAny(factory, expr) {
return factory.createAsExpression(expr, factory.createKeywordTypeNode(ts__default["default"].SyntaxKind.AnyKeyword));
}
/**
* Transform that will automatically add an `@Input` decorator for all signal
* inputs in Angular classes. The decorator will capture metadata of the signal
* input, derived from the `input()/input.required()` initializer.
*
* This transform is useful for JIT environments where signal inputs would like to be
* used. e.g. for Angular CLI unit testing. In such environments, signal inputs are not
* statically retrievable at runtime. JIT compilation needs to know about all possible inputs
* before instantiating directives. A decorator exposes this information to the class without
* the class needing to be instantiated.
*/
const signalInputsTransform = (member, sourceFile, host, factory, importTracker, importManager, classDecorator, isCore) => {
// If the field already is decorated, we handle this gracefully and skip it.
if (host
.getDecoratorsOfDeclaration(member.node)
?.some((d) => checker.isAngularDecorator(d, 'Input', isCore))) {
return member.node;
}
const inputMapping = checker.tryParseSignalInputMapping(member, host, importTracker);
if (inputMapping === null) {
return member.node;
}
const fields = {
'isSignal': factory.createTrue(),
'alias': factory.createStringLiteral(inputMapping.bindingPropertyName),
'required': inputMapping.required ? factory.createTrue() : factory.createFalse(),
// For signal inputs, transforms are captured by the input signal. The runtime will
// determine whether a transform needs to be run via the input signal, so the `transform`
// option is always `undefined`.
'transform': factory.createIdentifier('undefined'),
};
const newDecorator = factory.createDecorator(factory.createCallExpression(createSyntheticAngularCoreDecoratorAccess(factory, importManager, classDecorator, sourceFile, 'Input'), undefined, [
// Cast to `any` because `isSignal` will be private, and in case this
// transform is used directly as a pre-compilation step, the decorator should
// not fail. It is already validated now due to us parsing the input metadata.
castAsAny(factory, factory.createObjectLiteralExpression(Object.entries(fields).map(([name, value]) => factory.createPropertyAssignment(name, value)))),
]));
return factory.updatePropertyDeclaration(member.node, [newDecorator, ...(member.node.modifiers ?? [])], member.name, member.node.questionToken, member.node.type, member.node.initializer);
};
/**
* Transform that automatically adds `@Input` and `@Output` to members initialized as `model()`.
* It is useful for JIT environments where models can't be recognized based on the initializer.
*/
const signalModelTransform = (member, sourceFile, host, factory, importTracker, importManager, classDecorator, isCore) => {
if (host.getDecoratorsOfDeclaration(member.node)?.some((d) => {
return checker.isAngularDecorator(d, 'Input', isCore) || checker.isAngularDecorator(d, 'Output', isCore);
})) {
return member.node;
}
const modelMapping = checker.tryParseSignalModelMapping(member, host, importTracker);
if (modelMapping === null) {
return member.node;
}
const inputConfig = factory.createObjectLiteralExpression([
factory.createPropertyAssignment('isSignal', modelMapping.input.isSignal ? factory.createTrue() : factory.createFalse()),
factory.createPropertyAssignment('alias', factory.createStringLiteral(modelMapping.input.bindingPropertyName)),
factory.createPropertyAssignment('required', modelMapping.input.required ? factory.createTrue() : factory.createFalse()),
]);
const inputDecorator = createDecorator('Input',
// Config is cast to `any` because `isSignal` will be private, and in case this
// transform is used directly as a pre-compilation step, the decorator should
// not fail. It is already validated now due to us parsing the input metadata.
factory.createAsExpression(inputConfig, factory.createKeywordTypeNode(ts__default["default"].SyntaxKind.AnyKeyword)), classDecorator, factory, sourceFile, importManager);
const outputDecorator = createDecorator('Output', factory.createStringLiteral(modelMapping.output.bindingPropertyName), classDecorator, factory, sourceFile, importManager);
return factory.updatePropertyDeclaration(member.node, [inputDecorator, outputDecorator, ...(member.node.modifiers ?? [])], member.node.name, member.node.questionToken, member.node.type, member.node.initializer);
};
function createDecorator(name, config, classDecorator, factory, sourceFile, importManager) {
const callTarget = createSyntheticAngularCoreDecoratorAccess(factory, importManager, classDecorator, sourceFile, name);
return factory.createDecorator(factory.createCallExpression(callTarget, undefined, [config]));
}
/**
* Transform that will automatically add an `@Output` decorator for all initializer API
* outputs in Angular classes. The decorator will capture metadata of the output, such
* as the alias.
*
* This transform is useful for JIT environments. In such environments, such outputs are not
* statically retrievable at runtime. JIT compilation needs to know about all possible outputs
* before instantiating directives. A decorator exposes this information to the class without
* the class needing to be instantiated.
*/
const initializerApiOutputTransform = (member, sourceFile, host, factory, importTracker, importManager, classDecorator, isCore) => {
// If the field already is decorated, we handle this gracefully and skip it.
if (host
.getDecoratorsOfDeclaration(member.node)
?.some((d) => checker.isAngularDecorator(d, 'Output', isCore))) {
return member.node;
}
const output = checker.tryParseInitializerBasedOutput(member, host, importTracker);
if (output === null) {
return member.node;
}
const newDecorator = factory.createDecorator(factory.createCallExpression(createSyntheticAngularCoreDecoratorAccess(factory, importManager, classDecorator, sourceFile, 'Output'), undefined, [factory.createStringLiteral(output.metadata.bindingPropertyName)]));
return factory.updatePropertyDeclaration(member.node, [newDecorator, ...(member.node.modifiers ?? [])], member.node.name, member.node.questionToken, member.node.type, member.node.initializer);
};
/** Maps a query function to its decorator. */
const queryFunctionToDecorator = {
viewChild: 'ViewChild',
viewChildren: 'ViewChildren',
contentChild: 'ContentChild',
contentChildren: 'ContentChildren',
};
/**
* Transform that will automatically add query decorators for all signal-based
* queries in Angular classes. The decorator will capture metadata of the signal
* query, derived from the initializer-based API call.
*
* This transform is useful for JIT environments where signal queries would like to be
* used. e.g. for Angular CLI unit testing. In such environments, signal queries are not
* statically retrievable at runtime. JIT compilation needs to know about all possible queries
* before instantiating directives to construct the definition. A decorator exposes this
* information to the class without the class needing to be instantiated.
*/
const queryFunctionsTransforms = (member, sourceFile, host, factory, importTracker, importManager, classDecorator, isCore) => {
const decorators = host.getDecoratorsOfDeclaration(member.node);
// If the field already is decorated, we handle this gracefully and skip it.
const queryDecorators = decorators && checker.getAngularDecorators(decorators, queryDecoratorNames, isCore);
if (queryDecorators !== null && queryDecorators.length > 0) {
return member.node;
}
const queryDefinition = checker.tryParseSignalQueryFromInitializer(member, host, importTracker);
if (queryDefinition === null) {
return member.node;
}
const callArgs = queryDefinition.call.arguments;
const newDecorator = factory.createDecorator(factory.createCallExpression(createSyntheticAngularCoreDecoratorAccess(factory, importManager, classDecorator, sourceFile, queryFunctionToDecorator[queryDefinition.name]), undefined,
// All positional arguments of the query functions can be mostly re-used as is
// for the decorator. i.e. predicate is always first argument. Options are second.
[
queryDefinition.call.arguments[0],
// Note: Casting as `any` because `isSignal` is not publicly exposed and this
// transform might pre-transform TS sources.
castAsAny(factory, factory.createObjectLiteralExpression([
...(callArgs.length > 1 ? [factory.createSpreadAssignment(callArgs[1])] : []),
factory.createPropertyAssignment('isSignal', factory.createTrue()),
])),
]));
return factory.updatePropertyDeclaration(member.node, [newDecorator, ...(member.node.modifiers ?? [])], member.node.name, member.node.questionToken, member.node.type, member.node.initializer);
};
/** Decorators for classes that should be transformed. */
const decoratorsWithInputs = ['Directive', 'Component'];
/**
* List of possible property transforms.
* The first one matched on a class member will apply.
*/
const propertyTransforms = [
signalInputsTransform,
initializerApiOutputTransform,
queryFunctionsTransforms,
signalModelTransform,
];
/**
* Creates an AST transform that looks for Angular classes and transforms
* initializer-based declared members to work with JIT compilation.
*
* For example, an `input()` member may be transformed to add an `@Input`
* decorator for JIT.
*
* @param host Reflection host
* @param importTracker Import tracker for efficient import checking.
* @param isCore Whether this transforms runs against `@angular/core`.
* @param shouldTransformClass Optional function to check if a given class should be transformed.
*/
function getInitializerApiJitTransform(host, importTracker, isCore, shouldTransformClass) {
return (ctx) => {
return (sourceFile) => {
const importManager = new checker.ImportManager();
sourceFile = ts__default["default"].visitNode(sourceFile, createTransformVisitor(ctx, host, importManager, importTracker, isCore, shouldTransformClass), ts__default["default"].isSourceFile);
return importManager.transformTsFile(ctx, sourceFile);
};
};
}
function createTransformVisitor(ctx, host, importManager, importTracker, isCore, shouldTransformClass) {
const visitor = (node) => {
if (ts__default["default"].isClassDeclaration(node) && node.name !== undefined) {
const originalNode = ts__default["default"].getOriginalNode(node, ts__default["default"].isClassDeclaration);
// Note: Attempt to detect the `angularDecorator` on the original node of the class.
// That is because e.g. Tsickle or other transforms might have transformed the node
// already to transform decorators.
const angularDecorator = host
.getDecoratorsOfDeclaration(originalNode)
?.find((d) => decoratorsWithInputs.some((name) => checker.isAngularDecorator(d, name, isCore)));
if (angularDecorator !== undefined &&
(shouldTransformClass === undefined || shouldTransformClass(node))) {
let hasChanged = false;
const sourceFile = originalNode.getSourceFile();
const members = node.members.map((memberNode) => {
if (!ts__default["default"].isPropertyDeclaration(memberNode)) {
return memberNode;
}
const member = checker.reflectClassMember(memberNode);
if (member === null) {
return memberNode;
}
// Find the first matching transform and update the class member.
for (const transform of propertyTransforms) {
const newNode = transform({ ...member, node: memberNode }, sourceFile, host, ctx.factory, importTracker, importManager, angularDecorator, isCore);
if (newNode !== member.node) {
hasChanged = true;
return newNode;
}
}
return memberNode;
});
if (hasChanged) {
return ctx.factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, members);
}
}
}
return ts__default["default"].visitEachChild(node, visitor, ctx);
};
return visitor;
}
/**
* JIT transform for Angular applications. Used by the Angular CLI for unit tests and
* explicit JIT applications.
*
* The transforms include:
*
* - A transform for downleveling Angular decorators and Angular-decorated class constructor
* parameters for dependency injection. This transform can be used by the CLI for JIT-mode
* compilation where constructor parameters and associated Angular decorators should be
* downleveled so that apps are not exposed to the ES2015 temporal dead zone limitation
* in TypeScript. See https://github.com/angular/angular-cli/pull/14473 for more details.
*
* - A transform for adding `@Input` to signal inputs. Signal inputs cannot be recognized
* at runtime using reflection. That is because the class would need to be instantiated-
* but is not possible before creation. To fix this for JIT, a decorator is automatically
* added that will declare the input as a signal input while also capturing the necessary
* metadata
*/
function angularJitApplicationTransform(program, isCore = false, shouldTransformClass) {
const typeChecker = program.getTypeChecker();
const reflectionHost = new checker.TypeScriptReflectionHost(typeChecker);
const importTracker = new ImportedSymbolsTracker();
const downlevelDecoratorTransform = getDownlevelDecoratorsTransform(typeChecker, reflectionHost, [], isCore,
/* enableClosureCompiler */ false, shouldTransformClass);
const initializerApisJitTransform = getInitializerApiJitTransform(reflectionHost, importTracker, isCore, shouldTransformClass);
return (ctx) => {
return (sourceFile) => {
sourceFile = initializerApisJitTransform(ctx)(sourceFile);
sourceFile = downlevelDecoratorTransform(ctx)(sourceFile);
return sourceFile;
};
};
}
const UNKNOWN_ERROR_CODE = 500;
exports.EmitFlags = void 0;
(function (EmitFlags) {
EmitFlags[EmitFlags["DTS"] = 1] = "DTS";
EmitFlags[EmitFlags["JS"] = 2] = "JS";
EmitFlags[EmitFlags["Metadata"] = 4] = "Metadata";
EmitFlags[EmitFlags["I18nBundle"] = 8] = "I18nBundle";
EmitFlags[EmitFlags["Codegen"] = 16] = "Codegen";
EmitFlags[EmitFlags["Default"] = 19] = "Default";
EmitFlags[EmitFlags["All"] = 31] = "All";
})(exports.EmitFlags || (exports.EmitFlags = {}));
function i18nGetExtension(formatName) {
const format = formatName.toLowerCase();
switch (format) {
case 'xmb':
return 'xmb';
case 'xlf':
case 'xlif':
case 'xliff':
case 'xlf2':
case 'xliff2':
return 'xlf';
}
throw new Error(`Unsupported format "${formatName}"`);
}
function i18nExtract(formatName, outFile, host, options, bundle, pathResolve = p__namespace.resolve) {
formatName = formatName || 'xlf';
// Checks the format and returns the extension
const ext = i18nGetExtension(formatName);
const content = i18nSerialize(bundle, formatName, options);
const dstFile = outFile || `messages.${ext}`;
const dstPath = pathResolve(options.outDir || options.basePath, dstFile);
host.writeFile(dstPath, content, false, undefined, []);
return [dstPath];
}
function i18nSerialize(bundle, formatName, options) {
const format = formatName.toLowerCase();
let serializer;
switch (format) {
case 'xmb':
serializer = new checker.Xmb();
break;
case 'xliff2':
case 'xlf2':
serializer = new Xliff2();
break;
case 'xlf':
case 'xliff':
default:
serializer = new Xliff();
}
return bundle.write(serializer, getPathNormalizer(options.basePath));
}
function getPathNormalizer(basePath) {
// normalize source paths by removing the base path and always using "/" as a separator
return (sourcePath) => {
sourcePath = basePath ? p__namespace.relative(basePath, sourcePath) : sourcePath;
return sourcePath.split(p__namespace.sep).join('/');
};
}
/**
* Converts a `string` version into an array of numbers
* @example
* toNumbers('2.0.1'); // returns [2, 0, 1]
*/
function toNumbers(value) {
// Drop any suffixes starting with `-` so that versions like `1.2.3-rc.5` are treated as `1.2.3`.
const suffixIndex = value.lastIndexOf('-');
return value
.slice(0, suffixIndex === -1 ? value.length : suffixIndex)
.split('.')
.map((segment) => {
const parsed = parseInt(segment, 10);
if (isNaN(parsed)) {
throw Error(`Unable to parse version string ${value}.`);
}
return parsed;
});
}
/**
* Compares two arrays of positive numbers with lexicographical order in mind.
*
* However - unlike lexicographical order - for arrays of different length we consider:
* [1, 2, 3] = [1, 2, 3, 0] instead of [1, 2, 3] < [1, 2, 3, 0]
*
* @param a The 'left hand' array in the comparison test
* @param b The 'right hand' in the comparison test
* @returns {-1|0|1} The comparison result: 1 if a is greater, -1 if b is greater, 0 is the two
* arrays are equals
*/
function compareNumbers(a, b) {
const max = Math.max(a.length, b.length);
const min = Math.min(a.length, b.length);
for (let i = 0; i < min; i++) {
if (a[i] > b[i])
return 1;
if (a[i] < b[i])
return -1;
}
if (min !== max) {
const longestArray = a.length === max ? a : b;
// The result to return in case the to arrays are considered different (1 if a is greater,
// -1 if b is greater)
const comparisonResult = a.length === max ? 1 : -1;
// Check that at least one of the remaining elements is greater than 0 to consider that the two
// arrays are different (e.g. [1, 0] and [1] are considered the same but not [1, 0, 1] and [1])
for (let i = min; i < max; i++) {
if (longestArray[i] > 0) {
return comparisonResult;
}
}
}
return 0;
}
/**
* Compares two versions
*
* @param v1 The 'left hand' version in the comparison test
* @param v2 The 'right hand' version in the comparison test
* @returns {-1|0|1} The comparison result: 1 if v1 is greater, -1 if v2 is greater, 0 is the two
* versions are equals
*/
function compareVersions(v1, v2) {
return compareNumbers(toNumbers(v1), toNumbers(v2));
}
/**
* Minimum supported TypeScript version
* ∀ supported typescript version v, v >= MIN_TS_VERSION
*
* Note: this check is disabled in g3, search for
* `angularCompilerOptions.disableTypeScriptVersionCheck` config param value in g3.
*/
const MIN_TS_VERSION = '5.5.0';
/**
* Supremum of supported TypeScript versions
* ∀ supported typescript version v, v < MAX_TS_VERSION
* MAX_TS_VERSION is not considered as a supported TypeScript version
*
* Note: this check is disabled in g3, search for
* `angularCompilerOptions.disableTypeScriptVersionCheck` config param value in g3.
*/
const MAX_TS_VERSION = '5.7.0';
/**
* The currently used version of TypeScript, which can be adjusted for testing purposes using
* `setTypeScriptVersionForTesting` and `restoreTypeScriptVersionForTesting` below.
*/
let tsVersion = ts__default["default"].version;
/**
* Checks whether a given version ∈ [minVersion, maxVersion[.
* An error will be thrown when the given version ∉ [minVersion, maxVersion[.
*
* @param version The version on which the check will be performed
* @param minVersion The lower bound version. A valid version needs to be greater than minVersion
* @param maxVersion The upper bound version. A valid version needs to be strictly less than
* maxVersion
*
* @throws Will throw an error if the given version ∉ [minVersion, maxVersion[
*/
function checkVersion(version, minVersion, maxVersion) {
if (compareVersions(version, minVersion) < 0 || compareVersions(version, maxVersion) >= 0) {
throw new Error(`The Angular Compiler requires TypeScript >=${minVersion} and <${maxVersion} but ${version} was found instead.`);
}
}
function verifySupportedTypeScriptVersion() {
checkVersion(tsVersion, MIN_TS_VERSION, MAX_TS_VERSION);
}
/**
* Analyzes a `ts.Program` for cycles.
*/
class CycleAnalyzer {
importGraph;
/**
* Cycle detection is requested with the same `from` source file for all used directives and pipes
* within a component, which makes it beneficial to cache the results as long as the `from` source
* file has not changed. This avoids visiting the import graph that is reachable from multiple
* directives/pipes more than once.
*/
cachedResults = null;
constructor(importGraph) {
this.importGraph = importGraph;
}
/**
* Check for a cycle to be created in the `ts.Program` by adding an import between `from` and
* `to`.
*
* @returns a `Cycle` object if an import between `from` and `to` would create a cycle; `null`
* otherwise.
*/
wouldCreateCycle(from, to) {
// Try to reuse the cached results as long as the `from` source file is the same.
if (this.cachedResults === null || this.cachedResults.from !== from) {
this.cachedResults = new CycleResults(from, this.importGraph);
}
// Import of 'from' -> 'to' is illegal if an edge 'to' -> 'from' already exists.
return this.cachedResults.wouldBeCyclic(to) ? new Cycle(this.importGraph, from, to) : null;
}
/**
* Record a synthetic import from `from` to `to`.
*
* This is an import that doesn't exist in the `ts.Program` but will be considered as part of the
* import graph for cycle creation.
*/
recordSyntheticImport(from, to) {
this.cachedResults = null;
this.importGraph.addSyntheticImport(from, to);
}
}
const NgCyclicResult = Symbol('NgCyclicResult');
/**
* Stores the results of cycle detection in a memory efficient manner. A symbol is attached to
* source files that indicate what the cyclic analysis result is, as indicated by two markers that
* are unique to this instance. This alleviates memory pressure in large import graphs, as each
* execution is able to store its results in the same memory location (i.e. in the symbol
* on the source file) as earlier executions.
*/
class CycleResults {
from;
importGraph;
cyclic = {};
acyclic = {};
constructor(from, importGraph) {
this.from = from;
this.importGraph = importGraph;
}
wouldBeCyclic(sf) {
const cached = this.getCachedResult(sf);
if (cached !== null) {
// The result for this source file has already been computed, so return its result.
return cached;
}
if (sf === this.from) {
// We have reached the source file that we want to create an import from, which means that
// doing so would create a cycle.
return true;
}
// Assume for now that the file will be acyclic; this prevents infinite recursion in the case
// that `sf` is visited again as part of an existing cycle in the graph.
this.markAcyclic(sf);
const imports = this.importGraph.importsOf(sf);
for (const imported of imports) {
if (this.wouldBeCyclic(imported)) {
this.markCyclic(sf);
return true;
}
}
return false;
}
/**
* Returns whether the source file is already known to be cyclic, or `null` if the result is not
* yet known.
*/
getCachedResult(sf) {
const result = sf[NgCyclicResult];
if (result === this.cyclic) {
return true;
}
else if (result === this.acyclic) {
return false;
}
else {
// Either the symbol is missing or its value does not correspond with one of the current
// result markers. As such, the result is unknown.
return null;
}
}
markCyclic(sf) {
sf[NgCyclicResult] = this.cyclic;
}
markAcyclic(sf) {
sf[NgCyclicResult] = this.acyclic;
}
}
/**
* Represents an import cycle between `from` and `to` in the program.
*
* This class allows us to do the work to compute the cyclic path between `from` and `to` only if
* needed.
*/
class Cycle {
importGraph;
from;
to;
constructor(importGraph, from, to) {
this.importGraph = importGraph;
this.from = from;
this.to = to;
}
/**
* Compute an array of source-files that illustrates the cyclic path between `from` and `to`.
*
* Note that a `Cycle` will not be created unless a path is available between `to` and `from`,
* so `findPath()` will never return `null`.
*/
getPath() {
return [this.from, ...this.importGraph.findPath(this.to, this.from)];
}
}
/**
* A cached graph of imports in the `ts.Program`.
*
* The `ImportGraph` keeps track of dependencies (imports) of individual `ts.SourceFile`s. Only
* dependencies within the same program are tracked; imports into packages on NPM are not.
*/
class ImportGraph {
checker;
perf;
imports = new Map();
constructor(checker, perf) {
this.checker = checker;
this.perf = perf;
}
/**
* List the direct (not transitive) imports of a given `ts.SourceFile`.
*
* This operation is cached.
*/
importsOf(sf) {
if (!this.imports.has(sf)) {
this.imports.set(sf, this.scanImports(sf));
}
return this.imports.get(sf);
}
/**
* Find an import path from the `start` SourceFile to the `end` SourceFile.
*
* This function implements a breadth first search that results in finding the
* shortest path between the `start` and `end` points.
*
* @param start the starting point of the path.
* @param end the ending point of the path.
* @returns an array of source files that connect the `start` and `end` source files, or `null` if
* no path could be found.
*/
findPath(start, end) {
if (start === end) {
// Escape early for the case where `start` and `end` are the same.
return [start];
}
const found = new Set([start]);
const queue = [new Found(start, null)];
while (queue.length > 0) {
const current = queue.shift();
const imports = this.importsOf(current.sourceFile);
for (const importedFile of imports) {
if (!found.has(importedFile)) {
const next = new Found(importedFile, current);
if (next.sourceFile === end) {
// We have hit the target `end` path so we can stop here.
return next.toPath();
}
found.add(importedFile);
queue.push(next);
}
}
}
return null;
}
/**
* Add a record of an import from `sf` to `imported`, that's not present in the original
* `ts.Program` but will be remembered by the `ImportGraph`.
*/
addSyntheticImport(sf, imported) {
if (isLocalFile(imported)) {
this.importsOf(sf).add(imported);
}
}
scanImports(sf) {
return this.perf.inPhase(checker.PerfPhase.CycleDetection, () => {
const imports = new Set();
// Look through the source file for import and export statements.
for (const stmt of sf.statements) {
if ((!ts__default["default"].isImportDeclaration(stmt) && !ts__default["default"].isExportDeclaration(stmt)) ||
stmt.moduleSpecifier === undefined) {
continue;
}
if (ts__default["default"].isImportDeclaration(stmt) &&
stmt.importClause !== undefined &&
isTypeOnlyImportClause(stmt.importClause)) {
// Exclude type-only imports as they are always elided, so they don't contribute to
// cycles.
continue;
}
const symbol = this.checker.getSymbolAtLocation(stmt.moduleSpecifier);
if (symbol === undefined || symbol.valueDeclaration === undefined) {
// No symbol could be found to skip over this import/export.
continue;
}
const moduleFile = symbol.valueDeclaration;
if (ts__default["default"].isSourceFile(moduleFile) && isLocalFile(moduleFile)) {
// Record this local import.
imports.add(moduleFile);
}
}
return imports;
});
}
}
function isLocalFile(sf) {
return !sf.isDeclarationFile;
}
function isTypeOnlyImportClause(node) {
// The clause itself is type-only (e.g. `import type {foo} from '...'`).
if (node.isTypeOnly) {
return true;
}
// All the specifiers in the cause are type-only (e.g. `import {type a, type b} from '...'`).
if (node.namedBindings !== undefined &&
ts__default["default"].isNamedImports(node.namedBindings) &&
node.namedBindings.elements.every((specifier) => specifier.isTypeOnly)) {
return true;
}
return false;
}
/**
* A helper class to track which SourceFiles are being processed when searching for a path in
* `getPath()` above.
*/
class Found {
sourceFile;
parent;
constructor(sourceFile, parent) {
this.sourceFile = sourceFile;
this.parent = parent;
}
/**
* Back track through this found SourceFile and its ancestors to generate an array of
* SourceFiles that form am import path between two SourceFiles.
*/
toPath() {
const array = [];
let current = this;
while (current !== null) {
array.push(current.sourceFile);
current = current.parent;
}
// Pushing and then reversing, O(n), rather than unshifting repeatedly, O(n^2), avoids
// manipulating the array on every iteration: https://stackoverflow.com/a/26370620
return array.reverse();
}
}
/** Type of top-level documentation entry. */
var EntryType;
(function (EntryType) {
EntryType["Block"] = "block";
EntryType["Component"] = "component";
EntryType["Constant"] = "constant";
EntryType["Decorator"] = "decorator";
EntryType["Directive"] = "directive";
EntryType["Element"] = "element";
EntryType["Enum"] = "enum";
EntryType["Function"] = "function";
EntryType["Interface"] = "interface";
EntryType["NgModule"] = "ng_module";
EntryType["Pipe"] = "pipe";
EntryType["TypeAlias"] = "type_alias";
EntryType["UndecoratedClass"] = "undecorated_class";
EntryType["InitializerApiFunction"] = "initializer_api_function";
})(EntryType || (EntryType = {}));
/** Types of class members */
var MemberType;
(function (MemberType) {
MemberType["Property"] = "property";
MemberType["Method"] = "method";
MemberType["Getter"] = "getter";
MemberType["Setter"] = "setter";
MemberType["EnumItem"] = "enum_item";
})(MemberType || (MemberType = {}));
var DecoratorType;
(function (DecoratorType) {
DecoratorType["Class"] = "class";
DecoratorType["Member"] = "member";
DecoratorType["Parameter"] = "parameter";
})(DecoratorType || (DecoratorType = {}));
/** Informational tags applicable to class members. */
var MemberTags;
(function (MemberTags) {
MemberTags["Abstract"] = "abstract";
MemberTags["Static"] = "static";
MemberTags["Readonly"] = "readonly";
MemberTags["Protected"] = "protected";
MemberTags["Optional"] = "optional";
MemberTags["Input"] = "input";
MemberTags["Output"] = "output";
MemberTags["Inherited"] = "override";
})(MemberTags || (MemberTags = {}));
/** Gets whether a symbol's name indicates it is an Angular-private API. */
function isAngularPrivateName(name) {
const firstChar = name[0] ?? '';
return firstChar === 'ɵ' || firstChar === '_';
}
/** Gets a list of all the generic type parameters for a declaration. */
function extractGenerics(declaration) {
return (declaration.typeParameters?.map((typeParam) => ({
name: typeParam.name.getText(),
constraint: typeParam.constraint?.getText(),
default: typeParam.default?.getText(),
})) ?? []);
}
/**
* RegExp to match the `@` character follow by any Angular decorator, used to escape Angular
* decorators in JsDoc blocks so that they're not parsed as JsDoc tags.
*/
const decoratorExpression = /@(?=(Injectable|Component|Directive|Pipe|NgModule|Input|Output|HostBinding|HostListener|Inject|Optional|Self|Host|SkipSelf|ViewChild|ViewChildren|ContentChild|ContentChildren))/g;
/** Gets the set of JsDoc tags applied to a node. */
function extractJsDocTags(node) {
const escapedNode = getEscapedNode(node);
return ts__default["default"].getJSDocTags(escapedNode).map((t) => {
return {
name: t.tagName.getText(),
comment: unescapeAngularDecorators(ts__default["default"].getTextOfJSDocComment(t.comment) ?? ''),
};
});
}
/**
* Gets the JsDoc description for a node. If the node does not have
* a description, returns the empty string.
*/
function extractJsDocDescription(node) {
const escapedNode = getEscapedNode(node);
// If the node is a top-level statement (const, class, function, etc.), we will get
// a `ts.JSDoc` here. If the node is a `ts.ParameterDeclaration`, we will get
// a `ts.JSDocParameterTag`.
const commentOrTag = ts__default["default"].getJSDocCommentsAndTags(escapedNode).find((d) => {
return ts__default["default"].isJSDoc(d) || ts__default["default"].isJSDocParameterTag(d);
});
const comment = commentOrTag?.comment ?? '';
const description = typeof comment === 'string' ? comment : (ts__default["default"].getTextOfJSDocComment(comment) ?? '');
return unescapeAngularDecorators(description);
}
/**
* Gets the raw JsDoc applied to a node.
* If the node does not have a JsDoc block, returns the empty string.
*/
function extractRawJsDoc(node) {
// Assume that any node has at most one JsDoc block.
const comment = ts__default["default"].getJSDocCommentsAndTags(node).find(ts__default["default"].isJSDoc)?.getFullText() ?? '';
return unescapeAngularDecorators(comment);
}
/**
* Gets an "escaped" version of the node by copying its raw JsDoc into a new source file
* on top of a dummy class declaration. For the purposes of JsDoc extraction, we don't actually
* care about the node itself, only its JsDoc block.
*/
function getEscapedNode(node) {
// TODO(jelbourn): It's unclear whether we need to escape @param JsDoc, since they're unlikely
// to have an Angular decorator on the beginning of a line. If we do need to escape them,
// it will require some more complicated copying below.
if (ts__default["default"].isParameter(node)) {
return node;
}
const rawComment = extractRawJsDoc(node);
const escaped = escapeAngularDecorators(rawComment);
const file = ts__default["default"].createSourceFile('x.ts', `${escaped}class X {}`, ts__default["default"].ScriptTarget.ES2020, true);
return file.statements.find((s) => ts__default["default"].isClassDeclaration(s));
}
/** Escape the `@` character for Angular decorators. */
function escapeAngularDecorators(comment) {
return comment.replace(decoratorExpression, '_NG_AT_');
}
/** Unescapes the `@` character for Angular decorators. */
function unescapeAngularDecorators(comment) {
return comment.replace(/_NG_AT_/g, '@');
}
/** Gets the string representation of a node's resolved type. */
function extractResolvedTypeString(node, checker) {
return checker.typeToString(checker.getTypeAtLocation(node));
}
class FunctionExtractor {
name;
exportDeclaration;
typeChecker;
constructor(name, exportDeclaration, typeChecker) {
this.name = name;
this.exportDeclaration = exportDeclaration;
this.typeChecker = typeChecker;
}
extract() {
// TODO: is there any real situation in which the signature would not be available here?
// Is void a better type?
const signature = this.typeChecker.getSignatureFromDeclaration(this.exportDeclaration);
const returnType = signature
? this.typeChecker.typeToString(this.typeChecker.getReturnTypeOfSignature(signature), undefined,
// This ensures that e.g. `T | undefined` is not reduced to `T`.
ts__default["default"].TypeFormatFlags.NoTypeReduction | ts__default["default"].TypeFormatFlags.NoTruncation)
: 'unknown';
const implementation = findImplementationOfFunction(this.exportDeclaration, this.typeChecker) ??
this.exportDeclaration;
const type = this.typeChecker.getTypeAtLocation(this.exportDeclaration);
const overloads = extractCallSignatures(this.name, this.typeChecker, type);
const jsdocsTags = extractJsDocTags(implementation);
const description = extractJsDocDescription(implementation);
return {
name: this.name,
signatures: overloads,
implementation: {
params: extractAllParams(implementation.parameters, this.typeChecker),
isNewType: ts__default["default"].isConstructSignatureDeclaration(implementation),
returnType,
returnDescription: jsdocsTags.find((tag) => tag.name === 'returns')?.comment,
generics: extractGenerics(implementation),
name: this.name,
description,
entryType: EntryType.Function,
jsdocTags: jsdocsTags,
rawComment: extractRawJsDoc(implementation),
},
entryType: EntryType.Function,
description,
jsdocTags: jsdocsTags,
rawComment: extractRawJsDoc(implementation),
};
}
}
/** Extracts parameters of the given parameter declaration AST nodes. */
function extractAllParams(params, typeChecker) {
return params.map((param) => ({
name: param.name.getText(),
description: extractJsDocDescription(param),
type: extractResolvedTypeString(param, typeChecker),
isOptional: !!(param.questionToken || param.initializer),
isRestParam: !!param.dotDotDotToken,
}));
}
/** Filters the list signatures to valid function and initializer API signatures. */
function filterSignatureDeclarations(signatures) {
const result = [];
for (const signature of signatures) {
const decl = signature.getDeclaration();
if (ts__default["default"].isFunctionDeclaration(decl) ||
ts__default["default"].isCallSignatureDeclaration(decl) ||
ts__default["default"].isMethodDeclaration(decl)) {
result.push({ signature, decl });
}
}
return result;
}
function extractCallSignatures(name, typeChecker, type) {
return filterSignatureDeclarations(type.getCallSignatures()).map(({ decl, signature }) => ({
name,
entryType: EntryType.Function,
description: extractJsDocDescription(decl),
generics: extractGenerics(decl),
isNewType: false,
jsdocTags: extractJsDocTags(decl),
params: extractAllParams(decl.parameters, typeChecker),
rawComment: extractRawJsDoc(decl),
returnType: typeChecker.typeToString(typeChecker.getReturnTypeOfSignature(signature), undefined,
// This ensures that e.g. `T | undefined` is not reduced to `T`.
ts__default["default"].TypeFormatFlags.NoTypeReduction | ts__default["default"].TypeFormatFlags.NoTruncation),
}));
}
/** Finds the implementation of the given function declaration overload signature. */
function findImplementationOfFunction(node, typeChecker) {
if (node.body !== undefined || node.name === undefined) {
return node;
}
const symbol = typeChecker.getSymbolAtLocation(node.name);
const implementation = symbol?.declarations?.find((s) => ts__default["default"].isFunctionDeclaration(s) && s.body !== undefined);
return implementation;
}
/**
* Check if the member has a JSDoc @internal or a @internal is a normal comment
*/
function isInternal(member) {
return (extractJsDocTags(member).some((tag) => tag.name === 'internal') ||
hasLeadingInternalComment(member));
}
/*
* Check if the member has a comment block with @internal
*/
function hasLeadingInternalComment(member) {
const memberText = member.getSourceFile().text;
return (ts__default["default"].reduceEachLeadingCommentRange(memberText, member.getFullStart(), (pos, end, kind, hasTrailingNewLine, containsInternal) => {
return containsInternal || memberText.slice(pos, end).includes('@internal');
},
/* state */ false,
/* initial */ false) ?? false);
}
/** Extractor to pull info for API reference documentation for a TypeScript class or interface. */
class ClassExtractor {
declaration;
typeChecker;
constructor(declaration, typeChecker) {
this.declaration = declaration;
this.typeChecker = typeChecker;
}
/** Extract docs info specific to classes. */
extract() {
return {
name: this.declaration.name.text,
isAbstract: this.isAbstract(),
entryType: ts__default["default"].isInterfaceDeclaration(this.declaration)
? EntryType.Interface
: EntryType.UndecoratedClass,
members: this.extractSignatures().concat(this.extractAllClassMembers()),
generics: extractGenerics(this.declaration),
description: extractJsDocDescription(this.declaration),
jsdocTags: extractJsDocTags(this.declaration),
rawComment: extractRawJsDoc(this.declaration),
extends: this.extractInheritance(this.declaration),
implements: this.extractInterfaceConformance(this.declaration),
};
}
/** Extracts doc info for a class's members. */
extractAllClassMembers() {
const members = [];
for (const member of this.getMemberDeclarations()) {
if (this.isMemberExcluded(member))
continue;
const memberEntry = this.extractClassMember(member);
if (memberEntry) {
members.push(memberEntry);
}
}
return members;
}
/** Extract docs for a class's members (methods and properties). */
extractClassMember(memberDeclaration) {
if (this.isMethod(memberDeclaration)) {
return this.extractMethod(memberDeclaration);
}
else if (this.isProperty(memberDeclaration) &&
!this.hasPrivateComputedProperty(memberDeclaration)) {
return this.extractClassProperty(memberDeclaration);
}
else if (ts__default["default"].isAccessor(memberDeclaration)) {
return this.extractGetterSetter(memberDeclaration);
}
// We only expect methods, properties, and accessors. If we encounter something else,
// return undefined and let the rest of the program filter it out.
return undefined;
}
/** Extract docs for all call signatures in the current class/interface. */
extractSignatures() {
return this.computeAllSignatureDeclarations().map((s) => this.extractSignature(s));
}
/** Extracts docs for a class method. */
extractMethod(methodDeclaration) {
const functionExtractor = new FunctionExtractor(methodDeclaration.name.getText(), methodDeclaration, this.typeChecker);
return {
...functionExtractor.extract(),
memberType: MemberType.Method,
memberTags: this.getMemberTags(methodDeclaration),
};
}
/** Extracts docs for a signature element (usually inside an interface). */
extractSignature(signature) {
// No name for the function if we are dealing with call signatures.
// For construct signatures we are using `new` as the name of the function for now.
// TODO: Consider exposing a new entry type for signature types.
const functionExtractor = new FunctionExtractor(ts__default["default"].isConstructSignatureDeclaration(signature) ? 'new' : '', signature, this.typeChecker);
return {
...functionExtractor.extract(),
memberType: MemberType.Method,
memberTags: [],
};
}
/** Extracts doc info for a property declaration. */
extractClassProperty(propertyDeclaration) {
return {
name: propertyDeclaration.name.getText(),
type: extractResolvedTypeString(propertyDeclaration, this.typeChecker),
memberType: MemberType.Property,
memberTags: this.getMemberTags(propertyDeclaration),
description: extractJsDocDescription(propertyDeclaration),
jsdocTags: extractJsDocTags(propertyDeclaration),
};
}
/** Extracts doc info for an accessor member (getter/setter). */
extractGetterSetter(accessor) {
return {
...this.extractClassProperty(accessor),
memberType: ts__default["default"].isGetAccessor(accessor) ? MemberType.Getter : MemberType.Setter,
};
}
extractInheritance(declaration) {
if (!declaration.heritageClauses) {
return undefined;
}
for (const clause of declaration.heritageClauses) {
if (clause.token === ts__default["default"].SyntaxKind.ExtendsKeyword) {
// We are assuming a single class can only extend one class.
const types = clause.types;
if (types.length > 0) {
const baseClass = types[0];
return baseClass.getText();
}
}
}
return undefined;
}
extractInterfaceConformance(declaration) {
const implementClause = declaration.heritageClauses?.find((clause) => clause.token === ts__default["default"].SyntaxKind.ImplementsKeyword);
return implementClause?.types.map((m) => m.getText()) ?? [];
}
/** Gets the tags for a member (protected, readonly, static, etc.) */
getMemberTags(member) {
const tags = this.getMemberTagsFromModifiers(member.modifiers ?? []);
if (member.questionToken) {
tags.push(MemberTags.Optional);
}
if (member.parent !== this.declaration) {
tags.push(MemberTags.Inherited);
}
return tags;
}
/** Computes all signature declarations of the class/interface. */
computeAllSignatureDeclarations() {
const type = this.typeChecker.getTypeAtLocation(this.declaration);
const signatures = [...type.getCallSignatures(), ...type.getConstructSignatures()];
const result = [];
for (const signature of signatures) {
const decl = signature.getDeclaration();
if (this.isDocumentableSignature(decl) && this.isDocumentableMember(decl)) {
result.push(decl);
}
}
return result;
}
/** Gets all member declarations, including inherited members. */
getMemberDeclarations() {
// We rely on TypeScript to resolve all the inherited members to their
// ultimate form via `getProperties`. This is important because child
// classes may narrow types or add method overloads.
const type = this.typeChecker.getTypeAtLocation(this.declaration);
const members = type.getProperties();
// While the properties of the declaration type represent the properties that exist
// on a class *instance*, static members are properties on the class symbol itself.
const typeOfConstructor = this.typeChecker.getTypeOfSymbol(type.symbol);
const staticMembers = typeOfConstructor.getProperties();
const result = [];
for (const member of [...members, ...staticMembers]) {
// A member may have multiple declarations in the case of function overloads.
const memberDeclarations = this.filterMethodOverloads(member.getDeclarations() ?? []);
for (const memberDeclaration of memberDeclarations) {
if (this.isDocumentableMember(memberDeclaration)) {
result.push(memberDeclaration);
}
}
}
return result;
}
/** The result only contains properties, method implementations and abstracts */
filterMethodOverloads(declarations) {
return declarations.filter((declaration, index) => {
// Check if the declaration is a function or method
if (ts__default["default"].isFunctionDeclaration(declaration) || ts__default["default"].isMethodDeclaration(declaration)) {
// TypeScript ensures that all declarations for a given abstract method appear consecutively.
const nextDeclaration = declarations[index + 1];
const isNextAbstractMethodWithSameName = nextDeclaration &&
ts__default["default"].isMethodDeclaration(nextDeclaration) &&
nextDeclaration.name.getText() === declaration.name?.getText();
// Return only the last occurrence of an abstract method to avoid overload duplication.
// Subsequent overloads or implementations are handled separately by the function extractor.
return !isNextAbstractMethodWithSameName;
}
// Include non-method declarations, such as properties, without filtering.
return true;
});
}
/** Get the tags for a member that come from the declaration modifiers. */
getMemberTagsFromModifiers(mods) {
const tags = [];
for (const mod of mods) {
const tag = this.getTagForMemberModifier(mod);
if (tag)
tags.push(tag);
}
return tags;
}
/** Gets the doc tag corresponding to a class member modifier (readonly, protected, etc.). */
getTagForMemberModifier(mod) {
switch (mod.kind) {
case ts__default["default"].SyntaxKind.StaticKeyword:
return MemberTags.Static;
case ts__default["default"].SyntaxKind.ReadonlyKeyword:
return MemberTags.Readonly;
case ts__default["default"].SyntaxKind.ProtectedKeyword:
return MemberTags.Protected;
case ts__default["default"].SyntaxKind.AbstractKeyword:
return MemberTags.Abstract;
default:
return undefined;
}
}
/**
* Gets whether a given class member should be excluded from public API docs.
* This is the case if:
* - The member does not have a name
* - The member is neither a method nor property
* - The member is private
* - The member has a name that marks it as Angular-internal.
* - The member is marked as internal via JSDoc.
*/
isMemberExcluded(member) {
return (!member.name ||
!this.isDocumentableMember(member) ||
(!ts__default["default"].isCallSignatureDeclaration(member) &&
member.modifiers?.some((mod) => mod.kind === ts__default["default"].SyntaxKind.PrivateKeyword)) ||
member.name.getText() === 'prototype' ||
isAngularPrivateName(member.name.getText()) ||
isInternal(member));
}
/** Gets whether a class member is a method, property, or accessor. */
isDocumentableMember(member) {
return (this.isMethod(member) ||
this.isProperty(member) ||
ts__default["default"].isAccessor(member) ||
// Signatures are documentable if they are part of an interface.
ts__default["default"].isCallSignatureDeclaration(member));
}
/** Check if the parameter is a constructor parameter with a public modifier */
isPublicConstructorParameterProperty(node) {
if (ts__default["default"].isParameterPropertyDeclaration(node, node.parent) && node.modifiers) {
return node.modifiers.some((modifier) => modifier.kind === ts__default["default"].SyntaxKind.PublicKeyword);
}
return false;
}
/** Gets whether a member is a property. */
isProperty(member) {
// Classes have declarations, interface have signatures
return (ts__default["default"].isPropertyDeclaration(member) ||
ts__default["default"].isPropertySignature(member) ||
this.isPublicConstructorParameterProperty(member));
}
/** Gets whether a member is a method. */
isMethod(member) {
// Classes have declarations, interface have signatures
return ts__default["default"].isMethodDeclaration(member) || ts__default["default"].isMethodSignature(member);
}
/** Gets whether the given signature declaration is documentable. */
isDocumentableSignature(signature) {
return (ts__default["default"].isConstructSignatureDeclaration(signature) || ts__default["default"].isCallSignatureDeclaration(signature));
}
/** Gets whether the declaration for this extractor is abstract. */
isAbstract() {
const modifiers = this.declaration.modifiers ?? [];
return modifiers.some((mod) => mod.kind === ts__default["default"].SyntaxKind.AbstractKeyword);
}
/**
* Check wether a member has a private computed property name like [ɵWRITABLE_SIGNAL]
*
* This will prevent exposing private computed properties in the docs.
*/
hasPrivateComputedProperty(property) {
return (ts__default["default"].isComputedPropertyName(property.name) && property.name.expression.getText().startsWith('ɵ'));
}
}
/** Extractor to pull info for API reference documentation for an Angular directive. */
class DirectiveExtractor extends ClassExtractor {
reference;
metadata;
constructor(declaration, reference, metadata, checker) {
super(declaration, checker);
this.reference = reference;
this.metadata = metadata;
}
/** Extract docs info for directives and components (including underlying class info). */
extract() {
return {
...super.extract(),
isStandalone: this.metadata.isStandalone,
selector: this.metadata.selector ?? '',
exportAs: this.metadata.exportAs ?? [],
entryType: this.metadata.isComponent ? EntryType.Component : EntryType.Directive,
};
}
/** Extracts docs info for a directive property, including input/output metadata. */
extractClassProperty(propertyDeclaration) {
const entry = super.extractClassProperty(propertyDeclaration);
const inputMetadata = this.getInputMetadata(propertyDeclaration);
if (inputMetadata) {
entry.memberTags.push(MemberTags.Input);
entry.inputAlias = inputMetadata.bindingPropertyName;
entry.isRequiredInput = inputMetadata.required;
}
const outputMetadata = this.getOutputMetadata(propertyDeclaration);
if (outputMetadata) {
entry.memberTags.push(MemberTags.Output);
entry.outputAlias = outputMetadata.bindingPropertyName;
}
return entry;
}
/** Gets the input metadata for a directive property. */
getInputMetadata(prop) {
const propName = prop.name.getText();
return this.metadata.inputs?.getByClassPropertyName(propName) ?? undefined;
}
/** Gets the output metadata for a directive property. */
getOutputMetadata(prop) {
const propName = prop.name.getText();
return this.metadata?.outputs?.getByClassPropertyName(propName) ?? undefined;
}
}
/** Extractor to pull info for API reference documentation for an Angular pipe. */
class PipeExtractor extends ClassExtractor {
reference;
metadata;
constructor(declaration, reference, metadata, typeChecker) {
super(declaration, typeChecker);
this.reference = reference;
this.metadata = metadata;
}
extract() {
return {
...super.extract(),
pipeName: this.metadata.name,
entryType: EntryType.Pipe,
isStandalone: this.metadata.isStandalone,
};
}
}
/** Extractor to pull info for API reference documentation for an Angular pipe. */
class NgModuleExtractor extends ClassExtractor {
reference;
metadata;
constructor(declaration, reference, metadata, typeChecker) {
super(declaration, typeChecker);
this.reference = reference;
this.metadata = metadata;
}
extract() {
return {
...super.extract(),
entryType: EntryType.NgModule,
};
}
}
/** Extracts documentation info for a class, potentially including Angular-specific info. */
function extractClass(classDeclaration, metadataReader, typeChecker) {
const ref = new checker.Reference(classDeclaration);
let extractor;
let directiveMetadata = metadataReader.getDirectiveMetadata(ref);
let pipeMetadata = metadataReader.getPipeMetadata(ref);
let ngModuleMetadata = metadataReader.getNgModuleMetadata(ref);
if (directiveMetadata) {
extractor = new DirectiveExtractor(classDeclaration, ref, directiveMetadata, typeChecker);
}
else if (pipeMetadata) {
extractor = new PipeExtractor(classDeclaration, ref, pipeMetadata, typeChecker);
}
else if (ngModuleMetadata) {
extractor = new NgModuleExtractor(classDeclaration, ref, ngModuleMetadata, typeChecker);
}
else {
extractor = new ClassExtractor(classDeclaration, typeChecker);
}
return extractor.extract();
}
/** Extracts documentation info for an interface. */
function extractInterface(declaration, typeChecker) {
const extractor = new ClassExtractor(declaration, typeChecker);
return extractor.extract();
}
/** Name of the tag indicating that an object literal should be shown as an enum in docs. */
const LITERAL_AS_ENUM_TAG = 'object-literal-as-enum';
/** Extracts documentation entry for a constant. */
function extractConstant(declaration, typeChecker) {
// For constants specifically, we want to get the base type for any literal types.
// For example, TypeScript by default extracts `const PI = 3.14` as PI having a type of the
// literal `3.14`. We don't want this behavior for constants, since generally one wants the
// _value_ of the constant to be able to change between releases without changing the type.
// `VERSION` is a good example here; the version is always a `string`, but the actual value of
// the version string shouldn't matter to the type system.
const resolvedType = typeChecker.getBaseTypeOfLiteralType(typeChecker.getTypeAtLocation(declaration));
// In the TS AST, the leading comment for a variable declaration is actually
// on the ancestor `ts.VariableStatement` (since a single variable statement may
// contain multiple variable declarations).
const rawComment = extractRawJsDoc(declaration.parent.parent);
const jsdocTags = extractJsDocTags(declaration);
const description = extractJsDocDescription(declaration);
const name = declaration.name.getText();
// Some constants have to be treated as enums for documentation purposes.
if (jsdocTags.some((tag) => tag.name === LITERAL_AS_ENUM_TAG)) {
return {
name,
entryType: EntryType.Enum,
members: extractLiteralPropertiesAsEnumMembers(declaration),
rawComment,
description,
jsdocTags: jsdocTags.filter((tag) => tag.name !== LITERAL_AS_ENUM_TAG),
};
}
return {
name: name,
type: typeChecker.typeToString(resolvedType),
entryType: EntryType.Constant,
rawComment,
description,
jsdocTags,
};
}
/** Gets whether a given constant is an Angular-added const that should be ignored for docs. */
function isSyntheticAngularConstant(declaration) {
return declaration.name.getText() === 'USED_FOR_NG_TYPE_CHECKING';
}
/**
* Extracts the properties of a variable initialized as an object literal as if they were enum
* members. Will throw for any variables that can't be statically analyzed easily.
*/
function extractLiteralPropertiesAsEnumMembers(declaration) {
let initializer = declaration.initializer;
// Unwrap `as` and parenthesized expressions.
while (initializer &&
(ts__default["default"].isAsExpression(initializer) || ts__default["default"].isParenthesizedExpression(initializer))) {
initializer = initializer.expression;
}
if (initializer === undefined || !ts__default["default"].isObjectLiteralExpression(initializer)) {
throw new Error(`Declaration tagged with "${LITERAL_AS_ENUM_TAG}" must be initialized to an object literal, but received ${initializer ? ts__default["default"].SyntaxKind[initializer.kind] : 'undefined'}`);
}
return initializer.properties.map((prop) => {
if (!ts__default["default"].isPropertyAssignment(prop) || !ts__default["default"].isIdentifier(prop.name)) {
throw new Error(`Property in declaration tagged with "${LITERAL_AS_ENUM_TAG}" must be a property assignment with a static name`);
}
if (!ts__default["default"].isNumericLiteral(prop.initializer) && !ts__default["default"].isStringLiteralLike(prop.initializer)) {
throw new Error(`Property in declaration tagged with "${LITERAL_AS_ENUM_TAG}" must be initialized to a number or string literal`);
}
return {
name: prop.name.text,
type: `${declaration.name.getText()}.${prop.name.text}`,
value: prop.initializer.getText(),
memberType: MemberType.EnumItem,
jsdocTags: extractJsDocTags(prop),
description: extractJsDocDescription(prop),
memberTags: [],
};
});
}
/** Extracts an API documentation entry for an Angular decorator. */
function extractorDecorator(declaration, typeChecker) {
const documentedNode = getDecoratorJsDocNode(declaration);
const decoratorType = getDecoratorType(declaration);
if (!decoratorType) {
throw new Error(`"${declaration.name.getText()} is not a decorator."`);
}
return {
name: declaration.name.getText(),
decoratorType: decoratorType,
entryType: EntryType.Decorator,
rawComment: extractRawJsDoc(documentedNode),
description: extractJsDocDescription(documentedNode),
jsdocTags: extractJsDocTags(documentedNode),
members: getDecoratorOptions(declaration, typeChecker),
};
}
/** Gets whether the given variable declaration is an Angular decorator declaration. */
function isDecoratorDeclaration(declaration) {
return !!getDecoratorType(declaration);
}
/** Gets whether an interface is the options interface for a decorator in the same file. */
function isDecoratorOptionsInterface(declaration) {
return declaration
.getSourceFile()
.statements.some((s) => ts__default["default"].isVariableStatement(s) &&
s.declarationList.declarations.some((d) => isDecoratorDeclaration(d) && d.name.getText() === declaration.name.getText()));
}
/** Gets the type of decorator, or undefined if the declaration is not a decorator. */
function getDecoratorType(declaration) {
// All Angular decorators are initialized with one of `makeDecorator`, `makePropDecorator`,
// or `makeParamDecorator`.
const initializer = declaration.initializer?.getFullText() ?? '';
if (initializer.includes('makeDecorator'))
return DecoratorType.Class;
if (initializer.includes('makePropDecorator'))
return DecoratorType.Member;
if (initializer.includes('makeParamDecorator'))
return DecoratorType.Parameter;
return undefined;
}
/** Gets the doc entry for the options object for an Angular decorator */
function getDecoratorOptions(declaration, typeChecker) {
const name = declaration.name.getText();
// Every decorator has an interface with its options in the same SourceFile.
// Queries, however, are defined as a type alias pointing to an interface.
const optionsDeclaration = declaration.getSourceFile().statements.find((node) => {
return ((ts__default["default"].isInterfaceDeclaration(node) || ts__default["default"].isTypeAliasDeclaration(node)) &&
node.name.getText() === name);
});
if (!optionsDeclaration) {
throw new Error(`Decorator "${name}" has no corresponding options interface.`);
}
let optionsInterface;
if (ts__default["default"].isTypeAliasDeclaration(optionsDeclaration)) {
// We hard-code the assumption that if the decorator's option type is a type alias,
// it resolves to a single interface (this is true for all query decorators at time of
// this writing).
const aliasedType = typeChecker.getTypeAtLocation(optionsDeclaration.type);
optionsInterface = (aliasedType.getSymbol()?.getDeclarations() ?? []).find((d) => ts__default["default"].isInterfaceDeclaration(d));
}
else {
optionsInterface = optionsDeclaration;
}
if (!optionsInterface || !ts__default["default"].isInterfaceDeclaration(optionsInterface)) {
throw new Error(`Options for decorator "${name}" is not an interface.`);
}
// Take advantage of the interface extractor to pull the appropriate member info.
// Hard code the knowledge that decorator options only have properties, never methods.
return extractInterface(optionsInterface, typeChecker).members;
}
/**
* Gets the call signature node that has the decorator's public JsDoc block.
*
* Every decorator has three parts:
* - A const that has the actual decorator.
* - An interface with the same name as the const that documents the decorator's options.
* - An interface suffixed with "Decorator" that has the decorator's call signature and JsDoc block.
*
* For the description and JsDoc tags, we need the interface suffixed with "Decorator".
*/
function getDecoratorJsDocNode(declaration) {
const name = declaration.name.getText();
// Assume the existence of an interface in the same file with the same name
// suffixed with "Decorator".
const decoratorInterface = declaration.getSourceFile().statements.find((s) => {
return ts__default["default"].isInterfaceDeclaration(s) && s.name.getText() === `${name}Decorator`;
});
if (!decoratorInterface || !ts__default["default"].isInterfaceDeclaration(decoratorInterface)) {
throw new Error(`No interface "${name}Decorator" found.`);
}
// The public-facing JsDoc for each decorator is on one of its interface's call signatures.
const callSignature = decoratorInterface.members.find((node) => {
// The description block lives on one of the call signatures for this interface.
return ts__default["default"].isCallSignatureDeclaration(node) && extractRawJsDoc(node);
});
if (!callSignature || !ts__default["default"].isCallSignatureDeclaration(callSignature)) {
throw new Error(`No call signature with JsDoc on "${name}Decorator"`);
}
return callSignature;
}
/** Extracts documentation entry for an enum. */
function extractEnum(declaration, typeChecker) {
return {
name: declaration.name.getText(),
entryType: EntryType.Enum,
members: extractEnumMembers(declaration, typeChecker),
rawComment: extractRawJsDoc(declaration),
description: extractJsDocDescription(declaration),
jsdocTags: extractJsDocTags(declaration),
};
}
/** Extracts doc info for an enum's members. */
function extractEnumMembers(declaration, checker) {
return declaration.members.map((member) => ({
name: member.name.getText(),
type: extractResolvedTypeString(member, checker),
value: getEnumMemberValue(member),
memberType: MemberType.EnumItem,
jsdocTags: extractJsDocTags(member),
description: extractJsDocDescription(member),
memberTags: [],
}));
}
/** Gets the explicitly assigned value for an enum member, or an empty string if there is none. */
function getEnumMemberValue(memberNode) {
// If the enum member has a child number literal or string literal,
// we use that literal as the "value" of the member.
const literal = memberNode.getChildren().find((n) => {
return (ts__default["default"].isNumericLiteral(n) ||
ts__default["default"].isStringLiteral(n) ||
(ts__default["default"].isPrefixUnaryExpression(n) &&
n.operator === ts__default["default"].SyntaxKind.MinusToken &&
ts__default["default"].isNumericLiteral(n.operand)));
});
return literal?.getText() ?? '';
}
/** JSDoc used to recognize an initializer API function. */
const initializerApiTag = 'initializerApiFunction';
/**
* Checks whether the given node corresponds to an initializer API function.
*
* An initializer API function is a function declaration or variable declaration
* that is explicitly annotated with `@initializerApiFunction`.
*
* Note: The node may be a function overload signature that is automatically
* resolved to its implementation to detect the JSDoc tag.
*/
function isInitializerApiFunction(node, typeChecker) {
// If this is matching an overload signature, resolve to the implementation
// as it would hold the `@initializerApiFunction` tag.
if (ts__default["default"].isFunctionDeclaration(node) && node.name !== undefined && node.body === undefined) {
const implementation = findImplementationOfFunction(node, typeChecker);
if (implementation !== undefined) {
node = implementation;
}
}
if (!ts__default["default"].isFunctionDeclaration(node) && !ts__default["default"].isVariableDeclaration(node)) {
return false;
}
let tagContainer = ts__default["default"].isFunctionDeclaration(node) ? node : getContainerVariableStatement(node);
if (tagContainer === null) {
return false;
}
const tags = ts__default["default"].getJSDocTags(tagContainer);
return tags.some((t) => t.tagName.text === initializerApiTag);
}
/**
* Extracts the given node as initializer API function and returns
* a docs entry that can be rendered to represent the API function.
*/
function extractInitializerApiFunction(node, typeChecker) {
if (node.name === undefined || !ts__default["default"].isIdentifier(node.name)) {
throw new Error(`Initializer API: Expected literal variable name.`);
}
const container = ts__default["default"].isFunctionDeclaration(node) ? node : getContainerVariableStatement(node);
if (container === null) {
throw new Error('Initializer API: Could not find container AST node of variable.');
}
const name = node.name.text;
const type = typeChecker.getTypeAtLocation(node);
// Top-level call signatures. E.g. `input()`, `input(initialValue: ReadT)`. etc.
const callFunction = extractFunctionWithOverloads(name, type, typeChecker);
// Sub-functions like `input.required()`.
const subFunctions = [];
for (const property of type.getProperties()) {
const subName = property.getName();
const subDecl = property.getDeclarations()?.[0];
if (subDecl === undefined || !ts__default["default"].isPropertySignature(subDecl)) {
throw new Error(`Initializer API: Could not resolve declaration of sub-property: ${name}.${subName}`);
}
const subType = typeChecker.getTypeAtLocation(subDecl);
subFunctions.push(extractFunctionWithOverloads(subName, subType, typeChecker));
}
let jsdocTags;
let description;
let rawComment;
// Extract container API documentation.
// The container description describes the overall function, while
// we allow the individual top-level call signatures to represent
// their individual overloads.
if (ts__default["default"].isFunctionDeclaration(node)) {
const implementation = findImplementationOfFunction(node, typeChecker);
if (implementation === undefined) {
throw new Error(`Initializer API: Could not find implementation of function: ${name}`);
}
callFunction.implementation = {
name,
entryType: EntryType.Function,
isNewType: false,
description: extractJsDocDescription(implementation),
generics: extractGenerics(implementation),
jsdocTags: extractJsDocTags(implementation),
params: extractAllParams(implementation.parameters, typeChecker),
rawComment: extractRawJsDoc(implementation),
returnType: typeChecker.typeToString(typeChecker.getReturnTypeOfSignature(typeChecker.getSignatureFromDeclaration(implementation))),
};
jsdocTags = callFunction.implementation.jsdocTags;
description = callFunction.implementation.description;
rawComment = callFunction.implementation.description;
}
else {
jsdocTags = extractJsDocTags(container);
description = extractJsDocDescription(container);
rawComment = extractRawJsDoc(container);
}
// Extract additional docs metadata from the initializer API JSDoc tag.
const metadataTag = jsdocTags.find((t) => t.name === initializerApiTag);
if (metadataTag === undefined) {
throw new Error('Initializer API: Detected initializer API function does ' +
`not have "@initializerApiFunction" tag: ${name}`);
}
let parsedMetadata = undefined;
if (metadataTag.comment.trim() !== '') {
try {
parsedMetadata = JSON.parse(metadataTag.comment);
}
catch (e) {
throw new Error(`Could not parse initializer API function metadata: ${e}`);
}
}
return {
entryType: EntryType.InitializerApiFunction,
name,
description,
jsdocTags,
rawComment,
callFunction,
subFunctions,
__docsMetadata__: parsedMetadata,
};
}
/**
* Gets the container node of the given variable declaration.
*
* A variable declaration may be annotated with e.g. `@initializerApiFunction`,
* but the JSDoc tag is not attached to the node, but to the containing variable
* statement.
*/
function getContainerVariableStatement(node) {
if (!ts__default["default"].isVariableDeclarationList(node.parent)) {
return null;
}
if (!ts__default["default"].isVariableStatement(node.parent.parent)) {
return null;
}
return node.parent.parent;
}
/**
* Extracts all given signatures and returns them as a function with
* overloads.
*
* The implementation of the function may be attached later, or may
* be non-existent. E.g. initializer APIs declared using an interface
* with call signatures do not have an associated implementation function
* that is statically retrievable. The constant holds the overall API description.
*/
function extractFunctionWithOverloads(name, type, typeChecker) {
return {
name,
signatures: extractCallSignatures(name, typeChecker, type),
// Implementation may be populated later.
implementation: null,
};
}
/** Extract the documentation entry for a type alias. */
function extractTypeAlias(declaration) {
// TODO: this does not yet resolve type queries (`typeof`). We may want to
// fix this eventually, but for now it does not appear that any type aliases in
// Angular's public API rely on this.
return {
name: declaration.name.getText(),
type: declaration.type.getText(),
entryType: EntryType.TypeAlias,
generics: extractGenerics(declaration),
rawComment: extractRawJsDoc(declaration),
description: extractJsDocDescription(declaration),
jsdocTags: extractJsDocTags(declaration),
};
}
/**
* For a given SourceFile, it extracts all imported symbols from other Angular packages.
*
* @returns a map Symbol => Package, eg: ApplicationRef => @angular/core
*/
function getImportedSymbols(sourceFile) {
const importSpecifiers = new Map();
function visit(node) {
if (ts__default["default"].isImportDeclaration(node)) {
let moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, '');
if (moduleSpecifier.startsWith('@angular/')) {
const namedBindings = node.importClause?.namedBindings;
if (namedBindings && ts__default["default"].isNamedImports(namedBindings)) {
namedBindings.elements.forEach((importSpecifier) => {
const importName = importSpecifier.name.text;
const importAlias = importSpecifier.propertyName
? importSpecifier.propertyName.text
: undefined;
importSpecifiers.set(importAlias ?? importName, moduleSpecifier);
});
}
}
}
ts__default["default"].forEachChild(node, visit);
}
visit(sourceFile);
return importSpecifiers;
}
/**
* Extracts all information from a source file that may be relevant for generating
* public API documentation.
*/
class DocsExtractor {
typeChecker;
metadataReader;
constructor(typeChecker, metadataReader) {
this.typeChecker = typeChecker;
this.metadataReader = metadataReader;
}
/**
* Gets the set of all documentable entries from a source file, including
* declarations that are re-exported from this file as an entry-point.
*
* @param sourceFile The file from which to extract documentable entries.
*/
extractAll(sourceFile, rootDir, privateModules) {
const entries = [];
const symbols = new Map();
const exportedDeclarations = this.getExportedDeclarations(sourceFile);
for (const [exportName, node] of exportedDeclarations) {
// Skip any symbols with an Angular-internal name.
if (isAngularPrivateName(exportName)) {
continue;
}
const entry = this.extractDeclaration(node);
if (entry && !isIgnoredDocEntry(entry)) {
// The source file parameter is the package entry: the index.ts
// We want the real source file of the declaration.
const realSourceFile = node.getSourceFile();
/**
* The `sourceFile` from `extractAll` is the main entry-point file of a package.
* Usually following a format like `export * from './public_api';`, simply re-exporting.
* It is necessary to pick-up every import from the actual source files
* where declarations are living, so that we can determine what symbols
* are actually referenced in the context of that particular declaration
* By doing this, the generation remains independent from other packages
*/
const importedSymbols = getImportedSymbols(realSourceFile);
importedSymbols.forEach((moduleName, symbolName) => {
if (symbolName.startsWith('ɵ') || privateModules.has(moduleName)) {
return;
}
if (symbols.has(symbolName) && symbols.get(symbolName) !== moduleName) {
// If this ever throws, we need to improve the symbol extraction strategy
throw new Error(`Ambigous symbol \`${symbolName}\` exported by both ${symbols.get(symbolName)} & ${moduleName}`);
}
symbols.set(symbolName, moduleName);
});
// Set the source code references for the extracted entry.
entry.source = {
filePath: getRelativeFilePath(realSourceFile, rootDir),
// Start & End are off by 1
startLine: ts__default["default"].getLineAndCharacterOfPosition(realSourceFile, node.getStart()).line + 1,
endLine: ts__default["default"].getLineAndCharacterOfPosition(realSourceFile, node.getEnd()).line + 1,
};
// The exported name of an API may be different from its declaration name, so
// use the declaration name.
entries.push({ ...entry, name: exportName });
}
}
return { entries, symbols };
}
/** Extract the doc entry for a single declaration. */
extractDeclaration(node) {
// Ignore anonymous classes.
if (checker.isNamedClassDeclaration(node)) {
return extractClass(node, this.metadataReader, this.typeChecker);
}
if (isInitializerApiFunction(node, this.typeChecker)) {
return extractInitializerApiFunction(node, this.typeChecker);
}
if (ts__default["default"].isInterfaceDeclaration(node) && !isIgnoredInterface(node)) {
return extractInterface(node, this.typeChecker);
}
if (ts__default["default"].isFunctionDeclaration(node)) {
// Name is guaranteed to be set, because it's exported directly.
const functionExtractor = new FunctionExtractor(node.name.getText(), node, this.typeChecker);
return functionExtractor.extract();
}
if (ts__default["default"].isVariableDeclaration(node) && !isSyntheticAngularConstant(node)) {
return isDecoratorDeclaration(node)
? extractorDecorator(node, this.typeChecker)
: extractConstant(node, this.typeChecker);
}
if (ts__default["default"].isTypeAliasDeclaration(node)) {
return extractTypeAlias(node);
}
if (ts__default["default"].isEnumDeclaration(node)) {
return extractEnum(node, this.typeChecker);
}
return null;
}
/** Gets the list of exported declarations for doc extraction. */
getExportedDeclarations(sourceFile) {
// Use the reflection host to get all the exported declarations from this
// source file entry point.
const reflector = new checker.TypeScriptReflectionHost(this.typeChecker);
const exportedDeclarationMap = reflector.getExportsOfModule(sourceFile);
// Augment each declaration with the exported name in the public API.
let exportedDeclarations = Array.from(exportedDeclarationMap?.entries() ?? []).map(([exportName, declaration]) => [exportName, declaration.node]);
// Sort the declaration nodes into declaration position because their order is lost in
// reading from the export map. This is primarily useful for testing and debugging.
return exportedDeclarations.sort(([a, declarationA], [b, declarationB]) => declarationA.pos - declarationB.pos);
}
}
/** Gets whether an interface should be ignored for docs extraction. */
function isIgnoredInterface(node) {
// We filter out all interfaces that end with "Decorator" because we capture their
// types as part of the main decorator entry (which are declared as constants).
// This approach to dealing with decorators is admittedly fuzzy, but this aspect of
// the framework's source code is unlikely to change. We also filter out the interfaces
// that contain the decorator options.
return node.name.getText().endsWith('Decorator') || isDecoratorOptionsInterface(node);
}
/**
* Whether the doc entry should be ignored.
*
* Note: We cannot check whether a node is marked as docs private
* before extraction because the extractor may find the attached
* JSDoc tags on different AST nodes. For example, a variable declaration
* never has JSDoc tags attached, but rather the parent variable statement.
*/
function isIgnoredDocEntry(entry) {
const isDocsPrivate = entry.jsdocTags.find((e) => e.name === 'docsPrivate');
if (isDocsPrivate !== undefined && isDocsPrivate.comment === '') {
throw new Error(`Docs extraction: Entry "${entry.name}" is marked as ` +
`"@docsPrivate" but without reasoning.`);
}
return isDocsPrivate !== undefined;
}
function getRelativeFilePath(sourceFile, rootDir) {
const fullPath = sourceFile.fileName;
const relativePath = fullPath.replace(rootDir, '');
return relativePath;
}
///
class FlatIndexGenerator {
entryPoint;
moduleName;
flatIndexPath;
shouldEmit = true;
constructor(entryPoint, relativeFlatIndexPath, moduleName) {
this.entryPoint = entryPoint;
this.moduleName = moduleName;
this.flatIndexPath =
checker.join(checker.dirname(entryPoint), relativeFlatIndexPath).replace(/\.js$/, '') + '.ts';
}
makeTopLevelShim() {
const relativeEntryPoint = relativePathBetween(this.flatIndexPath, this.entryPoint);
const contents = `/**
* Generated bundle index. Do not edit.
*/
export * from '${relativeEntryPoint}';
`;
const genFile = ts__default["default"].createSourceFile(this.flatIndexPath, contents, ts__default["default"].ScriptTarget.ES2015, true, ts__default["default"].ScriptKind.TS);
if (this.moduleName !== null) {
genFile.moduleName = this.moduleName;
}
return genFile;
}
}
function findFlatIndexEntryPoint(rootFiles) {
// There are two ways for a file to be recognized as the flat module index:
// 1) if it's the only file!!!!!!
// 2) (deprecated) if it's named 'index.ts' and has the shortest path of all such files.
const tsFiles = rootFiles.filter((file) => checker.isNonDeclarationTsPath(file));
let resolvedEntryPoint = null;
if (tsFiles.length === 1) {
// There's only one file - this is the flat module index.
resolvedEntryPoint = tsFiles[0];
}
else {
// In the event there's more than one TS file, one of them can still be selected as the
// flat module index if it's named 'index.ts'. If there's more than one 'index.ts', the one
// with the shortest path wins.
//
// This behavior is DEPRECATED and only exists to support existing usages.
for (const tsFile of tsFiles) {
if (checker.getFileSystem().basename(tsFile) === 'index.ts' &&
(resolvedEntryPoint === null || tsFile.length <= resolvedEntryPoint.length)) {
resolvedEntryPoint = tsFile;
}
}
}
return resolvedEntryPoint;
}
/**
* Produce `ts.Diagnostic`s for classes that are visible from exported types (e.g. directives
* exposed by exported `NgModule`s) that are not themselves exported.
*
* This function reconciles two concepts:
*
* A class is Exported if it's exported from the main library `entryPoint` file.
* A class is Visible if, via Angular semantics, a downstream consumer can import an Exported class
* and be affected by the class in question. For example, an Exported NgModule may expose a
* directive class to its consumers. Consumers that import the NgModule may have the directive
* applied to elements in their templates. In this case, the directive is considered Visible.
*
* `checkForPrivateExports` attempts to verify that all Visible classes are Exported, and report
* `ts.Diagnostic`s for those that aren't.
*
* @param entryPoint `ts.SourceFile` of the library's entrypoint, which should export the library's
* public API.
* @param checker `ts.TypeChecker` for the current program.
* @param refGraph `ReferenceGraph` tracking the visibility of Angular types.
* @returns an array of `ts.Diagnostic`s representing errors when visible classes are not exported
* properly.
*/
function checkForPrivateExports(entryPoint, checker$1, refGraph) {
const diagnostics = [];
// Firstly, compute the exports of the entry point. These are all the Exported classes.
const topLevelExports = new Set();
// Do this via `ts.TypeChecker.getExportsOfModule`.
const moduleSymbol = checker$1.getSymbolAtLocation(entryPoint);
if (moduleSymbol === undefined) {
throw new Error(`Internal error: failed to get symbol for entrypoint`);
}
const exportedSymbols = checker$1.getExportsOfModule(moduleSymbol);
// Loop through the exported symbols, de-alias if needed, and add them to `topLevelExports`.
// TODO(alxhub): use proper iteration when build.sh is removed. (#27762)
exportedSymbols.forEach((symbol) => {
if (symbol.flags & ts__default["default"].SymbolFlags.Alias) {
symbol = checker$1.getAliasedSymbol(symbol);
}
const decl = symbol.valueDeclaration;
if (decl !== undefined) {
topLevelExports.add(decl);
}
});
// Next, go through each exported class and expand it to the set of classes it makes Visible,
// using the `ReferenceGraph`. For each Visible class, verify that it's also Exported, and queue
// an error if it isn't. `checkedSet` ensures only one error is queued per class.
const checkedSet = new Set();
// Loop through each Exported class.
// TODO(alxhub): use proper iteration when the legacy build is removed. (#27762)
topLevelExports.forEach((mainExport) => {
// Loop through each class made Visible by the Exported class.
refGraph.transitiveReferencesOf(mainExport).forEach((transitiveReference) => {
// Skip classes which have already been checked.
if (checkedSet.has(transitiveReference)) {
return;
}
checkedSet.add(transitiveReference);
// Verify that the Visible class is also Exported.
if (!topLevelExports.has(transitiveReference)) {
// This is an error, `mainExport` makes `transitiveReference` Visible, but
// `transitiveReference` is not Exported from the entrypoint. Construct a diagnostic to
// give to the user explaining the situation.
const descriptor = getDescriptorOfDeclaration(transitiveReference);
const name = getNameOfDeclaration(transitiveReference);
// Construct the path of visibility, from `mainExport` to `transitiveReference`.
let visibleVia = 'NgModule exports';
const transitivePath = refGraph.pathFrom(mainExport, transitiveReference);
if (transitivePath !== null) {
visibleVia = transitivePath.map((seg) => getNameOfDeclaration(seg)).join(' -> ');
}
const diagnostic = {
category: ts__default["default"].DiagnosticCategory.Error,
code: checker.ngErrorCode(checker.ErrorCode.SYMBOL_NOT_EXPORTED),
file: transitiveReference.getSourceFile(),
...getPosOfDeclaration(transitiveReference),
messageText: `Unsupported private ${descriptor} ${name}. This ${descriptor} is visible to consumers via ${visibleVia}, but is not exported from the top-level library entrypoint.`,
};
diagnostics.push(diagnostic);
}
});
});
return diagnostics;
}
function getPosOfDeclaration(decl) {
const node = getIdentifierOfDeclaration(decl) || decl;
return {
start: node.getStart(),
length: node.getEnd() + 1 - node.getStart(),
};
}
function getIdentifierOfDeclaration(decl) {
if ((ts__default["default"].isClassDeclaration(decl) ||
ts__default["default"].isVariableDeclaration(decl) ||
ts__default["default"].isFunctionDeclaration(decl)) &&
decl.name !== undefined &&
ts__default["default"].isIdentifier(decl.name)) {
return decl.name;
}
else {
return null;
}
}
function getNameOfDeclaration(decl) {
const id = getIdentifierOfDeclaration(decl);
return id !== null ? id.text : '(unnamed)';
}
function getDescriptorOfDeclaration(decl) {
switch (decl.kind) {
case ts__default["default"].SyntaxKind.ClassDeclaration:
return 'class';
case ts__default["default"].SyntaxKind.FunctionDeclaration:
return 'function';
case ts__default["default"].SyntaxKind.VariableDeclaration:
return 'variable';
case ts__default["default"].SyntaxKind.EnumDeclaration:
return 'enum';
default:
return 'declaration';
}
}
class ReferenceGraph {
references = new Map();
add(from, to) {
if (!this.references.has(from)) {
this.references.set(from, new Set());
}
this.references.get(from).add(to);
}
transitiveReferencesOf(target) {
const set = new Set();
this.collectTransitiveReferences(set, target);
return set;
}
pathFrom(source, target) {
return this.collectPathFrom(source, target, new Set());
}
collectPathFrom(source, target, seen) {
if (source === target) {
// Looking for a path from the target to itself - that path is just the target. This is the
// "base case" of the search.
return [target];
}
else if (seen.has(source)) {
// The search has already looked through this source before.
return null;
}
// Consider outgoing edges from `source`.
seen.add(source);
if (!this.references.has(source)) {
// There are no outgoing edges from `source`.
return null;
}
else {
// Look through the outgoing edges of `source`.
// TODO(alxhub): use proper iteration when the legacy build is removed. (#27762)
let candidatePath = null;
this.references.get(source).forEach((edge) => {
// Early exit if a path has already been found.
if (candidatePath !== null) {
return;
}
// Look for a path from this outgoing edge to `target`.
const partialPath = this.collectPathFrom(edge, target, seen);
if (partialPath !== null) {
// A path exists from `edge` to `target`. Insert `source` at the beginning.
candidatePath = [source, ...partialPath];
}
});
return candidatePath;
}
}
collectTransitiveReferences(set, decl) {
if (this.references.has(decl)) {
// TODO(alxhub): use proper iteration when the legacy build is removed. (#27762)
this.references.get(decl).forEach((ref) => {
if (!set.has(ref)) {
set.add(ref);
this.collectTransitiveReferences(set, ref);
}
});
}
}
}
/**
* An implementation of the `DependencyTracker` dependency graph API.
*
* The `FileDependencyGraph`'s primary job is to determine whether a given file has "logically"
* changed, given the set of physical changes (direct changes to files on disk).
*
* A file is logically changed if at least one of three conditions is met:
*
* 1. The file itself has physically changed.
* 2. One of its dependencies has physically changed.
* 3. One of its resource dependencies has physically changed.
*/
class FileDependencyGraph {
nodes = new Map();
addDependency(from, on) {
this.nodeFor(from).dependsOn.add(checker.absoluteFromSourceFile(on));
}
addResourceDependency(from, resource) {
this.nodeFor(from).usesResources.add(resource);
}
recordDependencyAnalysisFailure(file) {
this.nodeFor(file).failedAnalysis = true;
}
getResourceDependencies(from) {
const node = this.nodes.get(from);
return node ? [...node.usesResources] : [];
}
/**
* Update the current dependency graph from a previous one, incorporating a set of physical
* changes.
*
* This method performs two tasks:
*
* 1. For files which have not logically changed, their dependencies from `previous` are added to
* `this` graph.
* 2. For files which have logically changed, they're added to a set of logically changed files
* which is eventually returned.
*
* In essence, for build `n`, this method performs:
*
* G(n) + L(n) = G(n - 1) + P(n)
*
* where:
*
* G(n) = the dependency graph of build `n`
* L(n) = the logically changed files from build n - 1 to build n.
* P(n) = the physically changed files from build n - 1 to build n.
*/
updateWithPhysicalChanges(previous, changedTsPaths, deletedTsPaths, changedResources) {
const logicallyChanged = new Set();
for (const sf of previous.nodes.keys()) {
const sfPath = checker.absoluteFromSourceFile(sf);
const node = previous.nodeFor(sf);
if (isLogicallyChanged(sf, node, changedTsPaths, deletedTsPaths, changedResources)) {
logicallyChanged.add(sfPath);
}
else if (!deletedTsPaths.has(sfPath)) {
this.nodes.set(sf, {
dependsOn: new Set(node.dependsOn),
usesResources: new Set(node.usesResources),
failedAnalysis: false,
});
}
}
return logicallyChanged;
}
nodeFor(sf) {
if (!this.nodes.has(sf)) {
this.nodes.set(sf, {
dependsOn: new Set(),
usesResources: new Set(),
failedAnalysis: false,
});
}
return this.nodes.get(sf);
}
}
/**
* Determine whether `sf` has logically changed, given its dependencies and the set of physically
* changed files and resources.
*/
function isLogicallyChanged(sf, node, changedTsPaths, deletedTsPaths, changedResources) {
// A file is assumed to have logically changed if its dependencies could not be determined
// accurately.
if (node.failedAnalysis) {
return true;
}
const sfPath = checker.absoluteFromSourceFile(sf);
// A file is logically changed if it has physically changed itself (including being deleted).
if (changedTsPaths.has(sfPath) || deletedTsPaths.has(sfPath)) {
return true;
}
// A file is logically changed if one of its dependencies has physically changed.
for (const dep of node.dependsOn) {
if (changedTsPaths.has(dep) || deletedTsPaths.has(dep)) {
return true;
}
}
// A file is logically changed if one of its resources has physically changed.
for (const dep of node.usesResources) {
if (changedResources.has(dep)) {
return true;
}
}
return false;
}
/**
* Discriminant of the `IncrementalState` union.
*/
var IncrementalStateKind;
(function (IncrementalStateKind) {
IncrementalStateKind[IncrementalStateKind["Fresh"] = 0] = "Fresh";
IncrementalStateKind[IncrementalStateKind["Delta"] = 1] = "Delta";
IncrementalStateKind[IncrementalStateKind["Analyzed"] = 2] = "Analyzed";
})(IncrementalStateKind || (IncrementalStateKind = {}));
/**
* Discriminant of the `Phase` type union.
*/
var PhaseKind;
(function (PhaseKind) {
PhaseKind[PhaseKind["Analysis"] = 0] = "Analysis";
PhaseKind[PhaseKind["TypeCheckAndEmit"] = 1] = "TypeCheckAndEmit";
})(PhaseKind || (PhaseKind = {}));
/**
* Manages the incremental portion of an Angular compilation, allowing for reuse of a prior
* compilation if available, and producing an output state for reuse of the current compilation in a
* future one.
*/
class IncrementalCompilation {
depGraph;
versions;
step;
phase;
/**
* `IncrementalState` of this compilation if it were to be reused in a subsequent incremental
* compilation at the current moment.
*
* Exposed via the `state` read-only getter.
*/
_state;
constructor(state, depGraph, versions, step) {
this.depGraph = depGraph;
this.versions = versions;
this.step = step;
this._state = state;
// The compilation begins in analysis phase.
this.phase = {
kind: PhaseKind.Analysis,
semanticDepGraphUpdater: new SemanticDepGraphUpdater(step !== null ? step.priorState.semanticDepGraph : null),
};
}
/**
* Begin a fresh `IncrementalCompilation`.
*/
static fresh(program, versions) {
const state = {
kind: IncrementalStateKind.Fresh,
};
return new IncrementalCompilation(state, new FileDependencyGraph(), versions, /* reuse */ null);
}
static incremental(program, newVersions, oldProgram, oldState, modifiedResourceFiles, perf) {
return perf.inPhase(checker.PerfPhase.Reconciliation, () => {
const physicallyChangedTsFiles = new Set();
const changedResourceFiles = new Set(modifiedResourceFiles ?? []);
let priorAnalysis;
switch (oldState.kind) {
case IncrementalStateKind.Fresh:
// Since this line of program has never been successfully analyzed to begin with, treat
// this as a fresh compilation.
return IncrementalCompilation.fresh(program, newVersions);
case IncrementalStateKind.Analyzed:
// The most recent program was analyzed successfully, so we can use that as our prior
// state and don't need to consider any other deltas except changes in the most recent
// program.
priorAnalysis = oldState;
break;
case IncrementalStateKind.Delta:
// There is an ancestor program which was analyzed successfully and can be used as a
// starting point, but we need to determine what's changed since that program.
priorAnalysis = oldState.lastAnalyzedState;
for (const sfPath of oldState.physicallyChangedTsFiles) {
physicallyChangedTsFiles.add(sfPath);
}
for (const resourcePath of oldState.changedResourceFiles) {
changedResourceFiles.add(resourcePath);
}
break;
}
const oldVersions = priorAnalysis.versions;
const oldFilesArray = oldProgram.getSourceFiles().map(toOriginalSourceFile);
const oldFiles = new Set(oldFilesArray);
const deletedTsFiles = new Set(oldFilesArray.map((sf) => checker.absoluteFromSourceFile(sf)));
for (const possiblyRedirectedNewFile of program.getSourceFiles()) {
const sf = toOriginalSourceFile(possiblyRedirectedNewFile);
const sfPath = checker.absoluteFromSourceFile(sf);
// Since we're seeing a file in the incoming program with this name, it can't have been
// deleted.
deletedTsFiles.delete(sfPath);
if (oldFiles.has(sf)) {
// This source file has the same object identity as in the previous program. We need to
// determine if it's really the same file, or if it might have changed versions since the
// last program without changing its identity.
// If there's no version information available, then this is the same file, and we can
// skip it.
if (oldVersions === null || newVersions === null) {
continue;
}
// If a version is available for the file from both the prior and the current program, and
// that version is the same, then this is the same file, and we can skip it.
if (oldVersions.has(sfPath) &&
newVersions.has(sfPath) &&
oldVersions.get(sfPath) === newVersions.get(sfPath)) {
continue;
}
// Otherwise, assume that the file has changed. Either its versions didn't match, or we
// were missing version information about it on one side for some reason.
}
// Bail out if a .d.ts file changes - the semantic dep graph is not able to process such
// changes correctly yet.
if (sf.isDeclarationFile) {
return IncrementalCompilation.fresh(program, newVersions);
}
// The file has changed physically, so record it.
physicallyChangedTsFiles.add(sfPath);
}
// Remove any files that have been deleted from the list of physical changes.
for (const deletedFileName of deletedTsFiles) {
physicallyChangedTsFiles.delete(checker.resolve(deletedFileName));
}
// Use the prior dependency graph to project physical changes into a set of logically changed
// files.
const depGraph = new FileDependencyGraph();
const logicallyChangedTsFiles = depGraph.updateWithPhysicalChanges(priorAnalysis.depGraph, physicallyChangedTsFiles, deletedTsFiles, changedResourceFiles);
// Physically changed files aren't necessarily counted as logically changed by the dependency
// graph (files do not have edges to themselves), so add them to the logical changes
// explicitly.
for (const sfPath of physicallyChangedTsFiles) {
logicallyChangedTsFiles.add(sfPath);
}
// Start off in a `DeltaIncrementalState` as a delta against the previous successful analysis,
// until this compilation completes its own analysis.
const state = {
kind: IncrementalStateKind.Delta,
physicallyChangedTsFiles,
changedResourceFiles,
lastAnalyzedState: priorAnalysis,
};
return new IncrementalCompilation(state, depGraph, newVersions, {
priorState: priorAnalysis,
logicallyChangedTsFiles,
});
});
}
get state() {
return this._state;
}
get semanticDepGraphUpdater() {
if (this.phase.kind !== PhaseKind.Analysis) {
throw new Error(`AssertionError: Cannot update the SemanticDepGraph after analysis completes`);
}
return this.phase.semanticDepGraphUpdater;
}
recordSuccessfulAnalysis(traitCompiler) {
if (this.phase.kind !== PhaseKind.Analysis) {
throw new Error(`AssertionError: Incremental compilation in phase ${PhaseKind[this.phase.kind]}, expected Analysis`);
}
const { needsEmit, needsTypeCheckEmit, newGraph } = this.phase.semanticDepGraphUpdater.finalize();
// Determine the set of files which have already been emitted.
let emitted;
if (this.step === null) {
// Since there is no prior compilation, no files have yet been emitted.
emitted = new Set();
}
else {
// Begin with the files emitted by the prior successful compilation, but remove those which we
// know need to bee re-emitted.
emitted = new Set(this.step.priorState.emitted);
// Files need re-emitted if they've logically changed.
for (const sfPath of this.step.logicallyChangedTsFiles) {
emitted.delete(sfPath);
}
// Files need re-emitted if they've semantically changed.
for (const sfPath of needsEmit) {
emitted.delete(sfPath);
}
}
// Transition to a successfully analyzed compilation. At this point, a subsequent compilation
// could use this state as a starting point.
this._state = {
kind: IncrementalStateKind.Analyzed,
versions: this.versions,
depGraph: this.depGraph,
semanticDepGraph: newGraph,
priorAnalysis: traitCompiler.getAnalyzedRecords(),
typeCheckResults: null,
emitted,
};
// We now enter the type-check and emit phase of compilation.
this.phase = {
kind: PhaseKind.TypeCheckAndEmit,
needsEmit,
needsTypeCheckEmit,
};
}
recordSuccessfulTypeCheck(results) {
if (this._state.kind !== IncrementalStateKind.Analyzed) {
throw new Error(`AssertionError: Expected successfully analyzed compilation.`);
}
else if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) {
throw new Error(`AssertionError: Incremental compilation in phase ${PhaseKind[this.phase.kind]}, expected TypeCheck`);
}
this._state.typeCheckResults = results;
}
recordSuccessfulEmit(sf) {
if (this._state.kind !== IncrementalStateKind.Analyzed) {
throw new Error(`AssertionError: Expected successfully analyzed compilation.`);
}
this._state.emitted.add(checker.absoluteFromSourceFile(sf));
}
priorAnalysisFor(sf) {
if (this.step === null) {
return null;
}
const sfPath = checker.absoluteFromSourceFile(sf);
// If the file has logically changed, its previous analysis cannot be reused.
if (this.step.logicallyChangedTsFiles.has(sfPath)) {
return null;
}
const priorAnalysis = this.step.priorState.priorAnalysis;
if (!priorAnalysis.has(sf)) {
return null;
}
return priorAnalysis.get(sf);
}
priorTypeCheckingResultsFor(sf) {
if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) {
throw new Error(`AssertionError: Expected successfully analyzed compilation.`);
}
if (this.step === null) {
return null;
}
const sfPath = checker.absoluteFromSourceFile(sf);
// If the file has logically changed, or its template type-checking results have semantically
// changed, then past type-checking results cannot be reused.
if (this.step.logicallyChangedTsFiles.has(sfPath) ||
this.phase.needsTypeCheckEmit.has(sfPath)) {
return null;
}
// Past results also cannot be reused if they're not available.
if (this.step.priorState.typeCheckResults === null ||
!this.step.priorState.typeCheckResults.has(sfPath)) {
return null;
}
const priorResults = this.step.priorState.typeCheckResults.get(sfPath);
// If the past results relied on inlining, they're not safe for reuse.
if (priorResults.hasInlines) {
return null;
}
return priorResults;
}
safeToSkipEmit(sf) {
// If this is a fresh compilation, it's never safe to skip an emit.
if (this.step === null) {
return false;
}
const sfPath = checker.absoluteFromSourceFile(sf);
// If the file has itself logically changed, it must be emitted.
if (this.step.logicallyChangedTsFiles.has(sfPath)) {
return false;
}
if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) {
throw new Error(`AssertionError: Expected successful analysis before attempting to emit files`);
}
// If during analysis it was determined that this file has semantically changed, it must be
// emitted.
if (this.phase.needsEmit.has(sfPath)) {
return false;
}
// Generally it should be safe to assume here that the file was previously emitted by the last
// successful compilation. However, as a defense-in-depth against incorrectness, we explicitly
// check that the last emit included this file, and re-emit it otherwise.
return this.step.priorState.emitted.has(sfPath);
}
}
/**
* To accurately detect whether a source file was affected during an incremental rebuild, the
* "original" source file needs to be consistently used.
*
* First, TypeScript may have created source file redirects when declaration files of the same
* version of a library are included multiple times. The non-redirected source file should be used
* to detect changes, as otherwise the redirected source files cause a mismatch when compared to
* a prior program.
*
* Second, the program that is used for template type checking may contain mutated source files, if
* inline type constructors or inline template type-check blocks had to be used. Such source files
* store their original, non-mutated source file from the original program in a symbol. For
* computing the affected files in an incremental build this original source file should be used, as
* the mutated source file would always be considered affected.
*/
function toOriginalSourceFile(sf) {
const unredirectedSf = checker.toUnredirectedSourceFile(sf);
const originalFile = unredirectedSf[checker.NgOriginalFile];
if (originalFile !== undefined) {
return originalFile;
}
else {
return unredirectedSf;
}
}
/**
* A noop implementation of `IncrementalBuildStrategy` which neither returns nor tracks any
* incremental data.
*/
/**
* Tracks an `IncrementalState` within the strategy itself.
*/
class TrackedIncrementalBuildStrategy {
state = null;
isSet = false;
getIncrementalState() {
return this.state;
}
setIncrementalState(state) {
this.state = state;
this.isSet = true;
}
toNextBuildStrategy() {
const strategy = new TrackedIncrementalBuildStrategy();
// Only reuse state that was explicitly set via `setIncrementalState`.
strategy.state = this.isSet ? this.state : null;
return strategy;
}
}
/**
* Describes the kind of identifier found in a template.
*/
var IdentifierKind;
(function (IdentifierKind) {
IdentifierKind[IdentifierKind["Property"] = 0] = "Property";
IdentifierKind[IdentifierKind["Method"] = 1] = "Method";
IdentifierKind[IdentifierKind["Element"] = 2] = "Element";
IdentifierKind[IdentifierKind["Template"] = 3] = "Template";
IdentifierKind[IdentifierKind["Attribute"] = 4] = "Attribute";
IdentifierKind[IdentifierKind["Reference"] = 5] = "Reference";
IdentifierKind[IdentifierKind["Variable"] = 6] = "Variable";
IdentifierKind[IdentifierKind["LetDeclaration"] = 7] = "LetDeclaration";
})(IdentifierKind || (IdentifierKind = {}));
/**
* Describes the absolute byte offsets of a text anchor in a source code.
*/
class AbsoluteSourceSpan {
start;
end;
constructor(start, end) {
this.start = start;
this.end = end;
}
}
/**
* A context for storing indexing information about components of a program.
*
* An `IndexingContext` collects component and template analysis information from
* `DecoratorHandler`s and exposes them to be indexed.
*/
class IndexingContext {
components = new Set();
/**
* Adds a component to the context.
*/
addComponent(info) {
this.components.add(info);
}
}
/**
* Visits the AST of an Angular template syntax expression, finding interesting
* entities (variable references, etc.). Creates an array of Entities found in
* the expression, with the location of the Entities being relative to the
* expression.
*
* Visiting `text {{prop}}` will return
* `[TopLevelIdentifier {name: 'prop', span: {start: 7, end: 11}}]`.
*/
class ExpressionVisitor extends checker.RecursiveAstVisitor$1 {
expressionStr;
absoluteOffset;
boundTemplate;
targetToIdentifier;
identifiers = [];
errors = [];
constructor(expressionStr, absoluteOffset, boundTemplate, targetToIdentifier) {
super();
this.expressionStr = expressionStr;
this.absoluteOffset = absoluteOffset;
this.boundTemplate = boundTemplate;
this.targetToIdentifier = targetToIdentifier;
}
/**
* Returns identifiers discovered in an expression.
*
* @param ast expression AST to visit
* @param source expression AST source code
* @param absoluteOffset absolute byte offset from start of the file to the start of the AST
* source code.
* @param boundTemplate bound target of the entire template, which can be used to query for the
* entities expressions target.
* @param targetToIdentifier closure converting a template target node to its identifier.
*/
static getIdentifiers(ast, source, absoluteOffset, boundTemplate, targetToIdentifier) {
const visitor = new ExpressionVisitor(source, absoluteOffset, boundTemplate, targetToIdentifier);
visitor.visit(ast);
return { identifiers: visitor.identifiers, errors: visitor.errors };
}
visit(ast) {
ast.visit(this);
}
visitPropertyRead(ast, context) {
this.visitIdentifier(ast, IdentifierKind.Property);
super.visitPropertyRead(ast, context);
}
visitPropertyWrite(ast, context) {
this.visitIdentifier(ast, IdentifierKind.Property);
super.visitPropertyWrite(ast, context);
}
/**
* Visits an identifier, adding it to the identifier store if it is useful for indexing.
*
* @param ast expression AST the identifier is in
* @param kind identifier kind
*/
visitIdentifier(ast, kind) {
// The definition of a non-top-level property such as `bar` in `{{foo.bar}}` is currently
// impossible to determine by an indexer and unsupported by the indexing module.
// The indexing module also does not currently support references to identifiers declared in the
// template itself, which have a non-null expression target.
if (!(ast.receiver instanceof checker.ImplicitReceiver)) {
return;
}
// The source span of the requested AST starts at a location that is offset from the expression.
let identifierStart = ast.sourceSpan.start - this.absoluteOffset;
if (ast instanceof checker.PropertyRead || ast instanceof checker.PropertyWrite) {
// For `PropertyRead` and `PropertyWrite`, the identifier starts at the `nameSpan`, not
// necessarily the `sourceSpan`.
identifierStart = ast.nameSpan.start - this.absoluteOffset;
}
if (!this.expressionStr.substring(identifierStart).startsWith(ast.name)) {
this.errors.push(new Error(`Impossible state: "${ast.name}" not found in "${this.expressionStr}" at location ${identifierStart}`));
return;
}
// Join the relative position of the expression within a node with the absolute position
// of the node to get the absolute position of the expression in the source code.
const absoluteStart = this.absoluteOffset + identifierStart;
const span = new AbsoluteSourceSpan(absoluteStart, absoluteStart + ast.name.length);
const targetAst = this.boundTemplate.getExpressionTarget(ast);
const target = targetAst ? this.targetToIdentifier(targetAst) : null;
const identifier = {
name: ast.name,
span,
kind,
target,
};
this.identifiers.push(identifier);
}
}
/**
* Visits the AST of a parsed Angular template. Discovers and stores
* identifiers of interest, deferring to an `ExpressionVisitor` as needed.
*/
class TemplateVisitor$1 extends checker.RecursiveVisitor$1 {
boundTemplate;
// Identifiers of interest found in the template.
identifiers = new Set();
errors = [];
// Map of targets in a template to their identifiers.
targetIdentifierCache = new Map();
// Map of elements and templates to their identifiers.
elementAndTemplateIdentifierCache = new Map();
/**
* Creates a template visitor for a bound template target. The bound target can be used when
* deferred to the expression visitor to get information about the target of an expression.
*
* @param boundTemplate bound template target
*/
constructor(boundTemplate) {
super();
this.boundTemplate = boundTemplate;
}
/**
* Visits a node in the template.
*
* @param node node to visit
*/
visit(node) {
node.visit(this);
}
visitAll(nodes) {
nodes.forEach((node) => this.visit(node));
}
/**
* Add an identifier for an HTML element and visit its children recursively.
*
* @param element
*/
visitElement(element) {
const elementIdentifier = this.elementOrTemplateToIdentifier(element);
if (elementIdentifier !== null) {
this.identifiers.add(elementIdentifier);
}
this.visitAll(element.references);
this.visitAll(element.inputs);
this.visitAll(element.attributes);
this.visitAll(element.children);
this.visitAll(element.outputs);
}
visitTemplate(template) {
const templateIdentifier = this.elementOrTemplateToIdentifier(template);
if (templateIdentifier !== null) {
this.identifiers.add(templateIdentifier);
}
this.visitAll(template.variables);
this.visitAll(template.attributes);
this.visitAll(template.templateAttrs);
this.visitAll(template.children);
this.visitAll(template.references);
}
visitBoundAttribute(attribute) {
// If the bound attribute has no value, it cannot have any identifiers in the value expression.
if (attribute.valueSpan === undefined) {
return;
}
const { identifiers, errors } = ExpressionVisitor.getIdentifiers(attribute.value, attribute.valueSpan.toString(), attribute.valueSpan.start.offset, this.boundTemplate, this.targetToIdentifier.bind(this));
identifiers.forEach((id) => this.identifiers.add(id));
this.errors.push(...errors);
}
visitBoundEvent(attribute) {
this.visitExpression(attribute.handler);
}
visitBoundText(text) {
this.visitExpression(text.value);
}
visitReference(reference) {
const referenceIdentifier = this.targetToIdentifier(reference);
if (referenceIdentifier === null) {
return;
}
this.identifiers.add(referenceIdentifier);
}
visitVariable(variable) {
const variableIdentifier = this.targetToIdentifier(variable);
if (variableIdentifier === null) {
return;
}
this.identifiers.add(variableIdentifier);
}
visitDeferredBlock(deferred) {
deferred.visitAll(this);
}
visitDeferredBlockPlaceholder(block) {
this.visitAll(block.children);
}
visitDeferredBlockError(block) {
this.visitAll(block.children);
}
visitDeferredBlockLoading(block) {
this.visitAll(block.children);
}
visitDeferredTrigger(trigger) {
if (trigger instanceof checker.BoundDeferredTrigger) {
this.visitExpression(trigger.value);
}
}
visitSwitchBlock(block) {
this.visitExpression(block.expression);
this.visitAll(block.cases);
}
visitSwitchBlockCase(block) {
block.expression && this.visitExpression(block.expression);
this.visitAll(block.children);
}
visitForLoopBlock(block) {
block.item.visit(this);
this.visitAll(block.contextVariables);
this.visitExpression(block.expression);
this.visitAll(block.children);
block.empty?.visit(this);
}
visitForLoopBlockEmpty(block) {
this.visitAll(block.children);
}
visitIfBlock(block) {
this.visitAll(block.branches);
}
visitIfBlockBranch(block) {
block.expression && this.visitExpression(block.expression);
block.expressionAlias?.visit(this);
this.visitAll(block.children);
}
visitLetDeclaration(decl) {
const identifier = this.targetToIdentifier(decl);
if (identifier !== null) {
this.identifiers.add(identifier);
}
this.visitExpression(decl.value);
}
/** Creates an identifier for a template element or template node. */
elementOrTemplateToIdentifier(node) {
// If this node has already been seen, return the cached result.
if (this.elementAndTemplateIdentifierCache.has(node)) {
return this.elementAndTemplateIdentifierCache.get(node);
}
let name;
let kind;
if (node instanceof checker.Template) {
name = node.tagName ?? 'ng-template';
kind = IdentifierKind.Template;
}
else {
name = node.name;
kind = IdentifierKind.Element;
}
// Namespaced elements have a particular format for `node.name` that needs to be handled.
// For example, an `
© 2015 - 2025 Weber Informatics LLC | Privacy Policy