Skip to content

Commit

Permalink
WIP: better custom element support
Browse files Browse the repository at this point in the history
  • Loading branch information
sijakret committed Sep 28, 2021
1 parent 9d290bd commit a5cc45e
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -388,4 +388,12 @@ export interface MiscOptions {
* Disable TypeScript Version Check.
*/
disableTypeScriptVersionCheck?: boolean;

/**
* String (RegExp) that will be used to detect custom elements
* when CUSTOM_ELEMENTS_SCHEMA is used. Very usefull to whitelist
* specific custom elements without entirely opting out of validations in templates
* defaults to '-' which means all tags with dashes are assumed to be custom elements
*/
webComponentPattern?: string;
}
4 changes: 4 additions & 0 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,8 @@ export class NgCompiler {
const strictTemplates = !!this.options.strictTemplates;

const useInlineTypeConstructors = this.programDriver.supportsInlineOperations;
// by default anything with a dash could be a web component
const webComponentPattern = this.options.webComponentPattern || '-';

// First select a type-checking configuration, based on whether full template type-checking is
// requested.
Expand Down Expand Up @@ -806,6 +808,7 @@ export class NgCompiler {
// (providing the full TemplateTypeChecker API) and if strict mode is not enabled. In strict
// mode, the user is in full control of type inference.
suggestionsForSuboptimalTypeInference: this.enableTemplateTypeChecker && !strictTemplates,
webComponentPattern,
};
} else {
typeCheckingConfig = {
Expand Down Expand Up @@ -834,6 +837,7 @@ export class NgCompiler {
// In "basic" template type-checking mode, no warnings are produced since most things are
// not checked anyways.
suggestionsForSuboptimalTypeInference: false,
webComponentPattern,
};
}

Expand Down
8 changes: 8 additions & 0 deletions packages/compiler-cli/src/ngtsc/typecheck/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,14 @@ export interface TypeCheckingConfig {
* opportunities to improve their own developer experience.
*/
suggestionsForSuboptimalTypeInference: boolean;

/**
* String (RegExp) that will be used to detect custom elements
* when CUSTOM_ELEMENTS_SCHEMA is used. Very usefull to whitelist
* specific custom elements without entirely opting out of validations in templates
* defaults to '-' which means all tags with dashes are assumed to be custom elements
*/
webComponentPattern: string;
}


Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-cli/src/ngtsc/typecheck/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,9 @@ export class TypeCheckContextImpl implements TypeCheckContext {
const shimPath = TypeCheckShimGenerator.shimFor(absoluteFromSourceFile(node.getSourceFile()));
if (!fileData.shimData.has(shimPath)) {
fileData.shimData.set(shimPath, {
domSchemaChecker: new RegistryDomSchemaChecker(fileData.sourceManager),
// by passing in this.config we allow access to webComponentsPattern so
// RegistryDomSchemaChecker can be aware of it
domSchemaChecker: new RegistryDomSchemaChecker(fileData.sourceManager, this.config),
oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager),
file: new TypeCheckFile(
shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost),
Expand Down
21 changes: 15 additions & 6 deletions packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/

import {DomElementSchemaRegistry, ParseSourceSpan, SchemaMetadata, TmplAstElement} from '@angular/compiler';
import {CompilerConfig, DomElementSchemaRegistry, ParseSourceSpan, SchemaMetadata, TmplAstElement} from '@angular/compiler';
import * as ts from 'typescript';

import {ErrorCode, ngErrorCode} from '../../diagnostics';
import {TemplateDiagnostic, TemplateId} from '../api';
import {TemplateDiagnostic, TemplateId, TypeCheckingConfig} from '../api';
import {makeTemplateDiagnostic} from '../diagnostics';

import {TemplateSourceResolver} from './tcb_util';

const REGISTRY = new DomElementSchemaRegistry();
const REMOVE_XHTML_REGEX = /^:xhtml:/;



/**
* Checks every non-Angular element/property processed in a template and potentially produces
* `ts.Diagnostic`s related to improper usage.
Expand Down Expand Up @@ -66,20 +67,28 @@ export interface DomSchemaChecker {
*/
export class RegistryDomSchemaChecker implements DomSchemaChecker {
private _diagnostics: TemplateDiagnostic[] = [];
private _registry: DomElementSchemaRegistry

get diagnostics(): ReadonlyArray<TemplateDiagnostic> {
return this._diagnostics;
}

constructor(private resolver: TemplateSourceResolver) {}
constructor(
private resolver: TemplateSourceResolver,
typeCheckingConfig: TypeCheckingConfig,
) {
// Allow registry to be initialized late (once config is ready)
this._registry =
new DomElementSchemaRegistry(new RegExp(typeCheckingConfig.webComponentPattern, 'i'))
}

checkElement(id: TemplateId, element: TmplAstElement, schemas: SchemaMetadata[]): void {
// HTML elements inside an SVG `foreignObject` are declared in the `xhtml` namespace.
// We need to strip it before handing it over to the registry because all HTML tag names
// in the registry are without a namespace.
const name = element.name.replace(REMOVE_XHTML_REGEX, '');

if (!REGISTRY.hasElement(name, schemas)) {
if (!this._registry.hasElement(name, schemas)) {
const mapping = this.resolver.getSourceMapping(id);

let errorMsg = `'${name}' is not a known element:\n`;
Expand All @@ -103,7 +112,7 @@ export class RegistryDomSchemaChecker implements DomSchemaChecker {
checkProperty(
id: TemplateId, element: TmplAstElement, name: string, span: ParseSourceSpan,
schemas: SchemaMetadata[]): void {
if (!REGISTRY.hasProperty(element.name, name, schemas)) {
if (!this._registry.hasProperty(element.name, name, schemas)) {
const mapping = this.resolver.getSourceMapping(id);

let errorMsg =
Expand Down
21 changes: 16 additions & 5 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2121,6 +2121,12 @@ export interface ParseTemplateOptions {
* rules on a case by case basis, instead of for their whole project within a configuration file.
*/
collectCommentNodes?: boolean;


/**
* jan:TODO
*/
webComponentPattern?: string;
}

/**
Expand All @@ -2129,11 +2135,13 @@ export interface ParseTemplateOptions {
* @param template text of the template to parse
* @param templateUrl URL to use for source mapping of the parsed template
* @param options options to modify how the template is parsed
* @param elementRegistry DomElementSchemaRegistry that can potentially have settings from config
*/
export function parseTemplate(
template: string, templateUrl: string, options: ParseTemplateOptions = {}): ParsedTemplate {
template: string, templateUrl: string, options: ParseTemplateOptions = {},
elementRegistry: DomElementSchemaRegistry = defaultElementRegistry): ParsedTemplate {
const {interpolationConfig, preserveWhitespaces, enableI18nLegacyMessageIdFormat} = options;
const bindingParser = makeBindingParser(interpolationConfig);
const bindingParser = makeBindingParser(interpolationConfig, elementRegistry);
const htmlParser = new HtmlParser();
const parseResult = htmlParser.parse(
template, templateUrl,
Expand Down Expand Up @@ -2219,13 +2227,15 @@ export function parseTemplate(
return parsedTemplate;
}

const elementRegistry = new DomElementSchemaRegistry();

const defaultElementRegistry = new DomElementSchemaRegistry();

/**
* Construct a `BindingParser` with a default configuration.
*/
export function makeBindingParser(
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): BindingParser {
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG,
elementRegistry = defaultElementRegistry): BindingParser {
return new BindingParser(
new IvyParser(new Lexer()), interpolationConfig, elementRegistry, null, []);
}
Expand All @@ -2250,10 +2260,11 @@ export function resolveSanitizationFn(context: core.SecurityContext, isAttribute
}
}


function trustedConstAttribute(tagName: string, attr: t.TextAttribute): o.Expression {
const value = asLiteral(attr.value);
if (isTrustedTypesSink(tagName, attr.name)) {
switch (elementRegistry.securityContext(tagName, attr.name, /* isAttribute */ true)) {
switch (defaultElementRegistry.securityContext(tagName, attr.name, /* isAttribute */ true)) {
case core.SecurityContext.HTML:
return o.taggedTemplate(
o.importExpr(R3.trustConstantHtml),
Expand Down
16 changes: 13 additions & 3 deletions packages/compiler/src/schema/dom_element_schema_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {CompilerConfig} from '../config';
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '../core';

import {isNgContainer, isNgContent} from '../ml_parser/tags';
Expand Down Expand Up @@ -250,7 +251,10 @@ const _PROP_TO_ATTR: {[name: string]: string} =
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
private _schema: {[element: string]: {[property: string]: string}} = {};

constructor() {
// this regexp is used to narrow down which tags are custom elements and which are
// angular components, only effective in conjunction with CUSTOM_ELEMENTS_SCHEMA
// defaults to '-' which was the behavior up until this point
constructor(private _webComponentsRegExp: RegExp = new RegExp('-')) {
super();
SCHEMA.forEach(encodedType => {
const type: {[property: string]: string} = {};
Expand Down Expand Up @@ -296,7 +300,10 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
return true;
}

if (tagName.indexOf('-') > -1) {
// this is probably a hot code path but a regexp just seems like the best trade-off here
// if this._webComponentsRegExp does not match we assume the component is NOT a WC
// and we run it down the angular component path
if (tagName.match(this._webComponentsRegExp)) {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return false;
}
Expand All @@ -317,7 +324,10 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
return true;
}

if (tagName.indexOf('-') > -1) {
// this is probably a hot code path but a regexp just seems like the best trade-off here
// if this._webComponentsRegExp does not match we assume the component is NOT a WC
// and we run it down the angular component path
if (tagName.match(this._webComponentsRegExp)) {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return true;
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/linker/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export type CompilerOptions = {
providers?: StaticProvider[],
missingTranslation?: MissingTranslationStrategy,
preserveWhitespaces?: boolean,
webComponentPattern?: string
};

/**
Expand Down

0 comments on commit a5cc45e

Please sign in to comment.