diff --git a/src/compiler/transformers/decorators-to-static/attach-internals.ts b/src/compiler/transformers/decorators-to-static/attach-internals.ts index dc95562ac47..b7937b88984 100644 --- a/src/compiler/transformers/decorators-to-static/attach-internals.ts +++ b/src/compiler/transformers/decorators-to-static/attach-internals.ts @@ -29,15 +29,17 @@ import { isDecoratorNamed } from './decorator-utils'; * @param newMembers an out param for new class members * @param typeChecker a TypeScript typechecker, needed for resolving the prop * declaration name + * @param decoratorName the name of the decorator to look for */ export const attachInternalsDecoratorsToStatic = ( diagnostics: d.Diagnostic[], decoratedMembers: ts.ClassElement[], newMembers: ts.ClassElement[], typeChecker: ts.TypeChecker, + decoratorName: string, ) => { const attachInternalsMembers = decoratedMembers.filter(ts.isPropertyDeclaration).filter((prop) => { - return !!retrieveTsDecorators(prop)?.find(isDecoratorNamed('AttachInternals')); + return !!retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName)); }); // no decorated fields, return! diff --git a/src/compiler/transformers/decorators-to-static/convert-decorators.ts b/src/compiler/transformers/decorators-to-static/convert-decorators.ts index fa5c0d1bd58..49930c46bb5 100644 --- a/src/compiler/transformers/decorators-to-static/convert-decorators.ts +++ b/src/compiler/transformers/decorators-to-static/convert-decorators.ts @@ -18,6 +18,7 @@ import { } from './decorators-constants'; import { elementDecoratorsToStatic } from './element-decorator'; import { eventDecoratorsToStatic } from './event-decorator'; +import { ImportAliasMap } from './import-alias-map'; import { listenDecoratorsToStatic } from './listen-decorator'; import { methodDecoratorsToStatic, validateMethods } from './method-decorator'; import { propDecoratorsToStatic } from './prop-decorator'; @@ -48,14 +49,16 @@ export const convertDecoratorsToStatic = ( program: ts.Program, ): ts.TransformerFactory => { return (transformCtx) => { + let sourceFile: ts.SourceFile; const visit = (node: ts.Node): ts.VisitResult => { if (ts.isClassDeclaration(node)) { - return visitClassDeclaration(config, diagnostics, typeChecker, program, node); + return visitClassDeclaration(config, diagnostics, typeChecker, program, node, sourceFile); } return ts.visitEachChild(node, visit, transformCtx); }; return (tsSourceFile) => { + sourceFile = tsSourceFile; return ts.visitEachChild(tsSourceFile, visit, transformCtx); }; }; @@ -83,6 +86,7 @@ export const convertDecoratorsToStatic = ( * @param typeChecker a TypeScript typechecker instance * @param program a {@link ts.Program} object * @param classNode the node currently being visited + * @param sourceFile the source file containing the class node * @returns a class node, possibly updated with new static values */ const visitClassDeclaration = ( @@ -91,8 +95,11 @@ const visitClassDeclaration = ( typeChecker: ts.TypeChecker, program: ts.Program, classNode: ts.ClassDeclaration, + sourceFile: ts.SourceFile, ): ts.ClassDeclaration => { - const componentDecorator = retrieveTsDecorators(classNode)?.find(isDecoratorNamed('Component')); + const importAliasMap = new ImportAliasMap(sourceFile); + + const componentDecorator = retrieveTsDecorators(classNode)?.find(isDecoratorNamed(importAliasMap.get('Component'))); if (!componentDecorator) { return classNode; } @@ -103,17 +110,31 @@ const visitClassDeclaration = ( // create an array of all class members which are _not_ methods decorated // with a Stencil decorator. We do this here because we'll implement the // behavior specified for those decorated methods later on. - const filteredMethodsAndFields = removeStencilMethodDecorators(Array.from(classMembers), diagnostics); + const filteredMethodsAndFields = removeStencilMethodDecorators(Array.from(classMembers), diagnostics, importAliasMap); - // parser component decorator (Component) + // parse component decorator componentDecoratorToStatic(config, typeChecker, diagnostics, classNode, filteredMethodsAndFields, componentDecorator); // stores a reference to fields that should be watched for changes // parse member decorators (Prop, State, Listen, Event, Method, Element and Watch) if (decoratedMembers.length > 0) { - propDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, program, filteredMethodsAndFields); - stateDecoratorsToStatic(decoratedMembers, filteredMethodsAndFields, typeChecker); - eventDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, program, filteredMethodsAndFields); + propDecoratorsToStatic( + diagnostics, + decoratedMembers, + typeChecker, + program, + filteredMethodsAndFields, + importAliasMap.get('Prop'), + ); + stateDecoratorsToStatic(decoratedMembers, filteredMethodsAndFields, typeChecker, importAliasMap.get('State')); + eventDecoratorsToStatic( + diagnostics, + decoratedMembers, + typeChecker, + program, + filteredMethodsAndFields, + importAliasMap.get('Event'), + ); methodDecoratorsToStatic( config, diagnostics, @@ -122,17 +143,30 @@ const visitClassDeclaration = ( typeChecker, program, filteredMethodsAndFields, + importAliasMap.get('Method'), + ); + elementDecoratorsToStatic(diagnostics, decoratedMembers, filteredMethodsAndFields, importAliasMap.get('Element')); + watchDecoratorsToStatic(typeChecker, decoratedMembers, filteredMethodsAndFields, importAliasMap.get('Watch')); + listenDecoratorsToStatic( + diagnostics, + typeChecker, + decoratedMembers, + filteredMethodsAndFields, + importAliasMap.get('Listen'), + ); + attachInternalsDecoratorsToStatic( + diagnostics, + decoratedMembers, + filteredMethodsAndFields, + typeChecker, + importAliasMap.get('AttachInternals'), ); - elementDecoratorsToStatic(diagnostics, decoratedMembers, typeChecker, filteredMethodsAndFields); - watchDecoratorsToStatic(typeChecker, decoratedMembers, filteredMethodsAndFields); - listenDecoratorsToStatic(diagnostics, typeChecker, decoratedMembers, filteredMethodsAndFields); - attachInternalsDecoratorsToStatic(diagnostics, decoratedMembers, filteredMethodsAndFields, typeChecker); } // We call the `handleClassFields` method which handles transforming any // class fields, removing them from the class and adding statements to the // class' constructor which instantiate them there instead. - const updatedClassFields = handleClassFields(classNode, filteredMethodsAndFields, typeChecker); + const updatedClassFields = handleClassFields(classNode, filteredMethodsAndFields, typeChecker, importAliasMap); validateMethods(diagnostics, classMembers); @@ -140,7 +174,10 @@ const visitClassDeclaration = ( return ts.factory.updateClassDeclaration( classNode, [ - ...(filterDecorators(currentDecorators, CLASS_DECORATORS_TO_REMOVE) ?? []), + ...(filterDecorators( + currentDecorators, + CLASS_DECORATORS_TO_REMOVE.map((decorator) => importAliasMap.get(decorator)), + ) ?? []), ...(retrieveTsModifiers(classNode) ?? []), ], classNode.name, @@ -163,16 +200,21 @@ const visitClassDeclaration = ( * @param classMembers a list of ClassElement AST nodes * @param diagnostics a collection of compiler diagnostics, to which an error * may be added + * @param importAliasMap a map of Stencil decorator names to their import names * @returns a new list of the same ClassElement nodes, with any nodes which have * Stencil-specific decorators modified to remove them */ const removeStencilMethodDecorators = ( classMembers: ts.ClassElement[], diagnostics: d.Diagnostic[], + importAliasMap: ImportAliasMap, ): ts.ClassElement[] => { return classMembers.map((member) => { const currentDecorators = retrieveTsDecorators(member); - const newDecorators = filterDecorators(currentDecorators, MEMBER_DECORATORS_TO_REMOVE); + const newDecorators = filterDecorators( + currentDecorators, + MEMBER_DECORATORS_TO_REMOVE.map((decorator) => importAliasMap.get(decorator)), + ); if (currentDecorators !== newDecorators) { if (ts.isMethodDeclaration(member)) { @@ -188,7 +230,7 @@ const removeStencilMethodDecorators = ( member.body, ); } else if (ts.isPropertyDeclaration(member)) { - if (shouldInitializeInConstructor(member)) { + if (shouldInitializeInConstructor(member, importAliasMap)) { // if the current class member is decorated with either 'State' or // 'Prop' we need to modify the property declaration to transform it // from a class field but we handle this in the `handleClassFields` @@ -348,18 +390,20 @@ export const filterDecorators = ( * @param classNode a TypeScript AST node for a Stencil component class * @param classMembers the class members that we need to update * @param typeChecker a reference to the {@link ts.TypeChecker} + * @param importAliasMap a map of Stencil decorator names to their import names * @returns a list of updated class elements which can be inserted into the class */ function handleClassFields( classNode: ts.ClassDeclaration, classMembers: ts.ClassElement[], typeChecker: ts.TypeChecker, + importAliasMap: ImportAliasMap, ): ts.ClassElement[] { const statements: ts.ExpressionStatement[] = []; const updatedClassMembers: ts.ClassElement[] = []; for (const member of classMembers) { - if (shouldInitializeInConstructor(member) && ts.isPropertyDeclaration(member)) { + if (shouldInitializeInConstructor(member, importAliasMap) && ts.isPropertyDeclaration(member)) { const memberName = tsPropDeclNameAsString(member, typeChecker); // this is a class field that we'll need to handle, so lets push a statement for @@ -401,15 +445,19 @@ function handleClassFields( * details. * * @param member the member to check + * @param importAliasMap a map of Stencil decorator names to their import names * @returns whether this should be rewritten or not */ -const shouldInitializeInConstructor = (member: ts.ClassElement): boolean => { +const shouldInitializeInConstructor = (member: ts.ClassElement, importAliasMap: ImportAliasMap): boolean => { const currentDecorators = retrieveTsDecorators(member); if (currentDecorators === undefined) { // decorators have already been removed from this element, indicating that // we don't need to do anything return false; } - const filteredDecorators = filterDecorators(currentDecorators, CONSTRUCTOR_DEFINED_MEMBER_DECORATORS); + const filteredDecorators = filterDecorators( + currentDecorators, + CONSTRUCTOR_DEFINED_MEMBER_DECORATORS.map((decorator) => importAliasMap.get(decorator)), + ); return currentDecorators !== filteredDecorators; }; diff --git a/src/compiler/transformers/decorators-to-static/decorator-utils.ts b/src/compiler/transformers/decorators-to-static/decorator-utils.ts index fe6cca75d68..78b63d1ad6d 100644 --- a/src/compiler/transformers/decorators-to-static/decorator-utils.ts +++ b/src/compiler/transformers/decorators-to-static/decorator-utils.ts @@ -1,7 +1,6 @@ import ts from 'typescript'; import { objectLiteralToObjectMap } from '../transform-utils'; -import type { StencilDecorator } from './decorators-constants'; export const getDecoratorParameters: GetDecoratorParameters = ( decorator: ts.Decorator, @@ -43,7 +42,7 @@ const getDecoratorParameter = (arg: ts.Expression, typeChecker: ts.TypeChecker): * @param propName the name of the decorator to match against * @returns true if the conditions above are both true, false otherwise */ -export const isDecoratorNamed = (propName: StencilDecorator) => { +export const isDecoratorNamed = (propName: string) => { return (dec: ts.Decorator): boolean => { return ts.isCallExpression(dec.expression) && dec.expression.expression.getText() === propName; }; diff --git a/src/compiler/transformers/decorators-to-static/element-decorator.ts b/src/compiler/transformers/decorators-to-static/element-decorator.ts index 8a297ff777c..5e54b872bba 100644 --- a/src/compiler/transformers/decorators-to-static/element-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/element-decorator.ts @@ -8,12 +8,12 @@ import { isDecoratorNamed } from './decorator-utils'; export const elementDecoratorsToStatic = ( diagnostics: d.Diagnostic[], decoratedMembers: ts.ClassElement[], - typeChecker: ts.TypeChecker, newMembers: ts.ClassElement[], + decoratorName: string, ) => { const elementRefs = decoratedMembers .filter(ts.isPropertyDeclaration) - .map((prop) => parseElementDecorator(diagnostics, typeChecker, prop)) + .map((prop) => parseElementDecorator(prop, decoratorName)) .filter((element) => !!element); if (elementRefs.length > 0) { @@ -25,12 +25,8 @@ export const elementDecoratorsToStatic = ( } }; -const parseElementDecorator = ( - _diagnostics: d.Diagnostic[], - _typeChecker: ts.TypeChecker, - prop: ts.PropertyDeclaration, -): string | null => { - const elementDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed('Element')); +const parseElementDecorator = (prop: ts.PropertyDeclaration, decoratorName: string): string | null => { + const elementDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName)); if (elementDecorator == null) { return null; diff --git a/src/compiler/transformers/decorators-to-static/event-decorator.ts b/src/compiler/transformers/decorators-to-static/event-decorator.ts index 5cd58ca5570..4e3ad523d2a 100644 --- a/src/compiler/transformers/decorators-to-static/event-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/event-decorator.ts @@ -19,10 +19,11 @@ export const eventDecoratorsToStatic = ( typeChecker: ts.TypeChecker, program: ts.Program, newMembers: ts.ClassElement[], + decoratorName: string, ) => { const events = decoratedProps .filter(ts.isPropertyDeclaration) - .map((prop) => parseEventDecorator(diagnostics, typeChecker, program, prop)) + .map((prop) => parseEventDecorator(diagnostics, typeChecker, program, prop, decoratorName)) .filter((ev) => !!ev); if (events.length > 0) { @@ -39,6 +40,7 @@ export const eventDecoratorsToStatic = ( * @param program a {@link ts.Program} object * its surrounding context in the AST * @param prop the property on the Stencil component class that is decorated with `@Event()` + * @param decoratorName the name of the decorator to look for * @returns generated metadata for the class member decorated by `@Event()`, or `null` if none could be derived */ const parseEventDecorator = ( @@ -46,8 +48,9 @@ const parseEventDecorator = ( typeChecker: ts.TypeChecker, program: ts.Program, prop: ts.PropertyDeclaration, + decoratorName: string, ): d.ComponentCompilerStaticEvent | null => { - const eventDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed('Event')); + const eventDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName)); if (eventDecorator == null) { return null; diff --git a/src/compiler/transformers/decorators-to-static/import-alias-map.ts b/src/compiler/transformers/decorators-to-static/import-alias-map.ts new file mode 100644 index 00000000000..30d2b8ca6d6 --- /dev/null +++ b/src/compiler/transformers/decorators-to-static/import-alias-map.ts @@ -0,0 +1,42 @@ +import ts from 'typescript'; + +import { STENCIL_DECORATORS, StencilDecorator } from './decorators-constants'; + +export class ImportAliasMap extends Map { + constructor(sourceFile: ts.SourceFile) { + super(); + this.generateImportAliasMap(sourceFile); + } + + /** + * Parses a {@link ts.SourceFile} and generates a map of all imported Stencil decorators + * to their aliases import name (if one exists). + * + * @param sourceFile The source file to parse + */ + private generateImportAliasMap(sourceFile: ts.SourceFile) { + const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration); + + for (const importDeclaration of importDeclarations) { + if (importDeclaration.moduleSpecifier.getText().includes('@stencil/core')) { + const namedBindings = importDeclaration.importClause?.namedBindings; + + if (namedBindings && ts.isNamedImports(namedBindings)) { + for (const element of namedBindings.elements) { + const importName = element.name.getText(); + const originalImportName = element.propertyName?.getText() ?? importName; + + // We only care to generate a map for the Stencil decorators + if (STENCIL_DECORATORS.includes(originalImportName as StencilDecorator)) { + this.set(originalImportName as StencilDecorator, importName); + } + } + } + } + } + } + + override get(key: StencilDecorator): string { + return super.get(key) ?? key; + } +} diff --git a/src/compiler/transformers/decorators-to-static/listen-decorator.ts b/src/compiler/transformers/decorators-to-static/listen-decorator.ts index 54bbb9d0183..0cee01837f6 100644 --- a/src/compiler/transformers/decorators-to-static/listen-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/listen-decorator.ts @@ -10,10 +10,11 @@ export const listenDecoratorsToStatic = ( typeChecker: ts.TypeChecker, decoratedMembers: ts.ClassElement[], newMembers: ts.ClassElement[], + decoratorName: string, ) => { const listeners = decoratedMembers .filter(ts.isMethodDeclaration) - .map((method) => parseListenDecorators(diagnostics, typeChecker, method)); + .map((method) => parseListenDecorators(diagnostics, typeChecker, method, decoratorName)); const flatListeners = flatOne(listeners); if (flatListeners.length > 0) { @@ -25,8 +26,9 @@ const parseListenDecorators = ( diagnostics: d.Diagnostic[], typeChecker: ts.TypeChecker, method: ts.MethodDeclaration, + decoratorName: string, ): d.ComponentCompilerListener[] => { - const listenDecorators = (retrieveTsDecorators(method) ?? []).filter(isDecoratorNamed('Listen')); + const listenDecorators = (retrieveTsDecorators(method) ?? []).filter(isDecoratorNamed(decoratorName)); if (listenDecorators.length === 0) { return []; } diff --git a/src/compiler/transformers/decorators-to-static/method-decorator.ts b/src/compiler/transformers/decorators-to-static/method-decorator.ts index 63d579222e7..5b5ae29b817 100644 --- a/src/compiler/transformers/decorators-to-static/method-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/method-decorator.ts @@ -25,11 +25,14 @@ export const methodDecoratorsToStatic = ( typeChecker: ts.TypeChecker, program: ts.Program, newMembers: ts.ClassElement[], + decoratorName: string, ) => { const tsSourceFile = cmpNode.getSourceFile(); const methods = decoratedProps .filter(ts.isMethodDeclaration) - .map((method) => parseMethodDecorator(config, diagnostics, tsSourceFile, typeChecker, program, method)) + .map((method) => + parseMethodDecorator(config, diagnostics, tsSourceFile, typeChecker, program, method, decoratorName), + ) .filter((method) => !!method); if (methods.length > 0) { @@ -44,8 +47,9 @@ const parseMethodDecorator = ( typeChecker: ts.TypeChecker, program: ts.Program, method: ts.MethodDeclaration, + decoratorName: string, ): ts.PropertyAssignment | null => { - const methodDecorator = retrieveTsDecorators(method)?.find(isDecoratorNamed('Method')); + const methodDecorator = retrieveTsDecorators(method)?.find(isDecoratorNamed(decoratorName)); if (methodDecorator == null) { return null; } diff --git a/src/compiler/transformers/decorators-to-static/prop-decorator.ts b/src/compiler/transformers/decorators-to-static/prop-decorator.ts index 2582aff593a..20bc7fda990 100644 --- a/src/compiler/transformers/decorators-to-static/prop-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/prop-decorator.ts @@ -27,8 +27,8 @@ import { getDecoratorParameters, isDecoratorNamed } from './decorator-utils'; * Only those decorated with `@Prop()` will be parsed. * @param typeChecker a reference to the TypeScript type checker * @param program a {@link ts.Program} object - * @param newMembers a collection that parsed `@Prop` annotated class members should be pushed to as a side effect of - * calling this function + * @param newMembers a collection that parsed `@Prop` annotated class members should be pushed to as a side effect of calling this function + * @param decoratorName the name of the decorator to look for */ export const propDecoratorsToStatic = ( diagnostics: d.Diagnostic[], @@ -36,10 +36,11 @@ export const propDecoratorsToStatic = ( typeChecker: ts.TypeChecker, program: ts.Program, newMembers: ts.ClassElement[], + decoratorName: string, ): void => { const properties = decoratedProps .filter(ts.isPropertyDeclaration) - .map((prop) => parsePropDecorator(diagnostics, typeChecker, program, prop)) + .map((prop) => parsePropDecorator(diagnostics, typeChecker, program, prop, decoratorName)) .filter((prop): prop is ts.PropertyAssignment => prop != null); if (properties.length > 0) { @@ -54,6 +55,7 @@ export const propDecoratorsToStatic = ( * @param typeChecker a reference to the TypeScript type checker * @param program a {@link ts.Program} object * @param prop the TypeScript `PropertyDeclaration` to parse + * @param decoratorName the name of the decorator to look for * @returns a property assignment expression to be added to the Stencil component's class */ const parsePropDecorator = ( @@ -61,8 +63,9 @@ const parsePropDecorator = ( typeChecker: ts.TypeChecker, program: ts.Program, prop: ts.PropertyDeclaration, + decoratorName: string, ): ts.PropertyAssignment | null => { - const propDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed('Prop')); + const propDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName)); if (propDecorator == null) { return null; } diff --git a/src/compiler/transformers/decorators-to-static/state-decorator.ts b/src/compiler/transformers/decorators-to-static/state-decorator.ts index f8ddb6afa6a..e21291628cf 100644 --- a/src/compiler/transformers/decorators-to-static/state-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/state-decorator.ts @@ -13,15 +13,17 @@ import { isDecoratorNamed } from './decorator-utils'; * @param decoratedProps TypeScript AST nodes representing class members * @param newMembers an out param containing new class members * @param typeChecker a reference to the TypeScript type checker + * @param decoratorName the name of the decorator to look for */ export const stateDecoratorsToStatic = ( decoratedProps: ts.ClassElement[], newMembers: ts.ClassElement[], typeChecker: ts.TypeChecker, + decoratorName: string, ) => { const states = decoratedProps .filter(ts.isPropertyDeclaration) - .map((prop) => stateDecoratorToStatic(prop, typeChecker)) + .map((prop) => stateDecoratorToStatic(prop, typeChecker, decoratorName)) .filter((state): state is ts.PropertyAssignment => !!state); if (states.length > 0) { @@ -39,14 +41,16 @@ export const stateDecoratorsToStatic = ( * * @param prop A TypeScript AST node representing a class property declaration * @param typeChecker a reference to the TypeScript type checker + * @param decoratorName the name of the decorator to look for * @returns a property assignment AST Node which maps the name of the state * prop to an empty object */ const stateDecoratorToStatic = ( prop: ts.PropertyDeclaration, typeChecker: ts.TypeChecker, + decoratorName: string, ): ts.PropertyAssignment | null => { - const stateDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed('State')); + const stateDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName)); if (stateDecorator == null) { return null; } diff --git a/src/compiler/transformers/decorators-to-static/watch-decorator.ts b/src/compiler/transformers/decorators-to-static/watch-decorator.ts index 5219bbf8810..82dd207f6f9 100644 --- a/src/compiler/transformers/decorators-to-static/watch-decorator.ts +++ b/src/compiler/transformers/decorators-to-static/watch-decorator.ts @@ -9,10 +9,11 @@ export const watchDecoratorsToStatic = ( typeChecker: ts.TypeChecker, decoratedProps: ts.ClassElement[], newMembers: ts.ClassElement[], + decoratorName: string, ) => { const watchers = decoratedProps .filter(ts.isMethodDeclaration) - .map((method) => parseWatchDecorator(typeChecker, method)); + .map((method) => parseWatchDecorator(typeChecker, method, decoratorName)); const flatWatchers = flatOne(watchers); @@ -21,10 +22,14 @@ export const watchDecoratorsToStatic = ( } }; -const parseWatchDecorator = (typeChecker: ts.TypeChecker, method: ts.MethodDeclaration): d.ComponentCompilerWatch[] => { +const parseWatchDecorator = ( + typeChecker: ts.TypeChecker, + method: ts.MethodDeclaration, + decoratorName: string, +): d.ComponentCompilerWatch[] => { const methodName = method.name.getText(); const decorators = retrieveTsDecorators(method) ?? []; - return decorators.filter(isDecoratorNamed('Watch')).map((decorator) => { + return decorators.filter(isDecoratorNamed(decoratorName)).map((decorator) => { const [propName] = getDecoratorParameters(decorator, typeChecker); return { diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index 198f5c0bf36..476494420f6 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -151,6 +151,10 @@ export namespace Components { } interface ImageImport { } + interface ImportAliasing { + "myMethod": () => Promise; + "user": string; + } interface InitCssRoot { } interface InputBasicRoot { @@ -418,6 +422,10 @@ export interface EventCustomTypeCustomEvent extends CustomEvent { detail: T; target: HTMLEventCustomTypeElement; } +export interface ImportAliasingCustomEvent extends CustomEvent { + detail: T; + target: HTMLImportAliasingElement; +} export interface LifecycleAsyncBCustomEvent extends CustomEvent { detail: T; target: HTMLLifecycleAsyncBElement; @@ -780,6 +788,23 @@ declare global { prototype: HTMLImageImportElement; new (): HTMLImageImportElement; }; + interface HTMLImportAliasingElementEventMap { + "myEvent": void; + } + interface HTMLImportAliasingElement extends Components.ImportAliasing, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLImportAliasingElement, ev: ImportAliasingCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLImportAliasingElement, ev: ImportAliasingCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLImportAliasingElement: { + prototype: HTMLImportAliasingElement; + new (): HTMLImportAliasingElement; + }; interface HTMLInitCssRootElement extends Components.InitCssRoot, HTMLStencilElement { } var HTMLInitCssRootElement: { @@ -1523,6 +1548,7 @@ declare global { "form-associated": HTMLFormAssociatedElement; "host-attr-override": HTMLHostAttrOverrideElement; "image-import": HTMLImageImportElement; + "import-aliasing": HTMLImportAliasingElement; "init-css-root": HTMLInitCssRootElement; "input-basic-root": HTMLInputBasicRootElement; "ion-child": HTMLIonChildElement; @@ -1772,6 +1798,10 @@ declare namespace LocalJSX { } interface ImageImport { } + interface ImportAliasing { + "onMyEvent"?: (event: ImportAliasingCustomEvent) => void; + "user"?: string; + } interface InitCssRoot { } interface InputBasicRoot { @@ -2087,6 +2117,7 @@ declare namespace LocalJSX { "form-associated": FormAssociated; "host-attr-override": HostAttrOverride; "image-import": ImageImport; + "import-aliasing": ImportAliasing; "init-css-root": InitCssRoot; "input-basic-root": InputBasicRoot; "ion-child": IonChild; @@ -2252,6 +2283,7 @@ declare module "@stencil/core" { "form-associated": LocalJSX.FormAssociated & JSXBase.HTMLAttributes; "host-attr-override": LocalJSX.HostAttrOverride & JSXBase.HTMLAttributes; "image-import": LocalJSX.ImageImport & JSXBase.HTMLAttributes; + "import-aliasing": LocalJSX.ImportAliasing & JSXBase.HTMLAttributes; "init-css-root": LocalJSX.InitCssRoot & JSXBase.HTMLAttributes; "input-basic-root": LocalJSX.InputBasicRoot & JSXBase.HTMLAttributes; "ion-child": LocalJSX.IonChild & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/import-aliasing/cmp.tsx b/test/karma/test-app/import-aliasing/cmp.tsx new file mode 100644 index 00000000000..a806fb28d92 --- /dev/null +++ b/test/karma/test-app/import-aliasing/cmp.tsx @@ -0,0 +1,63 @@ +import { + AttachInternals as ElInternals, + Component as Cmp, + Element as El, + Event as StencilEvent, + EventEmitter, + h, + Listen as StencilListen, + Method as StencilMethod, + Prop as Input, + State as StencilState, + Watch as StencilWatch, +} from '@stencil/core'; + +@Cmp({ + tag: 'import-aliasing', + formAssociated: true, +}) +export class FormAssociatedCmp { + @Input() user: string; + + @StencilEvent() myEvent: EventEmitter; + + @El() el!: HTMLElement; + + @ElInternals() + internals: ElementInternals; + + @StencilState() changeCount = 0; + @StencilState() methodCalledCount = 0; + @StencilState() eventCaughtCount = 0; + + @StencilListen('myEvent') + onMyEventTriggered() { + this.eventCaughtCount += 1; + } + + @StencilWatch('user') + onNameChange() { + this.changeCount += 1; + } + + @StencilMethod() + async myMethod() { + this.methodCalledCount += 1; + this.myEvent.emit(); + + return this.el; + } + + componentWillLoad() { + this.internals.setFormValue('my default value'); + } + + render() { + return [ +

My name is {this.user}

, +

Name changed {this.changeCount} time(s)

, +

Method called {this.methodCalledCount} time(s)

, +

Event triggered {this.eventCaughtCount} time(s)

, + ]; + } +} diff --git a/test/karma/test-app/import-aliasing/index.html b/test/karma/test-app/import-aliasing/index.html new file mode 100644 index 00000000000..a58bc382781 --- /dev/null +++ b/test/karma/test-app/import-aliasing/index.html @@ -0,0 +1,8 @@ + + + + + +
+ +
diff --git a/test/karma/test-app/import-aliasing/karma.spec.ts b/test/karma/test-app/import-aliasing/karma.spec.ts new file mode 100644 index 00000000000..9bc003ff810 --- /dev/null +++ b/test/karma/test-app/import-aliasing/karma.spec.ts @@ -0,0 +1,47 @@ +import { setupDomTests, waitForChanges } from '../util'; + +/** + * Test cases for using import aliases for Stencil decorators. Only + * tests a subset of all Stencil decorators. + */ +describe('import aliasing', function () { + const { setupDom, tearDownDom } = setupDomTests(document); + let app: HTMLElement; + + beforeEach(async () => { + app = await setupDom('/import-aliasing/index.html', 500); + }); + + afterEach(tearDownDom); + + it('should render correctly with alised imports', async () => { + const host = app.querySelector('import-aliasing'); + + expect(host.children[0].textContent).toBe('My name is John'); + expect(host.children[1].textContent).toBe('Name changed 0 time(s)'); + expect(host.children[2].textContent).toBe('Method called 0 time(s)'); + expect(host.children[3].textContent).toBe('Event triggered 0 time(s)'); + + host.setAttribute('user', 'Peter'); + await waitForChanges(); + + expect(host.children[0].textContent).toBe('My name is Peter'); + expect(host.children[1].textContent).toBe('Name changed 1 time(s)'); + expect(host.children[2].textContent).toBe('Method called 0 time(s)'); + expect(host.children[3].textContent).toBe('Event triggered 0 time(s)'); + + const el = await host.myMethod(); + await waitForChanges(); + + expect(el).toBe(host); + expect(host.children[0].textContent).toBe('My name is Peter'); + expect(host.children[1].textContent).toBe('Name changed 1 time(s)'); + expect(host.children[2].textContent).toBe('Method called 1 time(s)'); + expect(host.children[3].textContent).toBe('Event triggered 1 time(s)'); + }); + + it('should link up to the surrounding form', async () => { + const formEl = app.querySelector('form'); + expect(new FormData(formEl).get('test-input')).toBe('my default value'); + }); +});