UNPKG

@stencil/angular-output-target

Version:

Angular output target for @stencil/core components.

191 lines (190 loc) 9 kB
import { createComponentEventTypeImports, dashToPascalCase, formatToQuotedList } from './utils'; /** * Creates a property declaration. * * @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration. * @param type The name of the type (e.g. 'string') * @param inlinePropertyAsSetter Inlines the entire property as an empty Setter, to aid Angulars Compilerp * @returns The property declaration as a string. */ function createPropertyDeclaration(prop, type, inlinePropertyAsSetter = false) { const comment = createDocComment(prop.docs); let eventName = prop.name; if (/[-/]/.test(prop.name)) { // If a member name includes a dash or a forward slash, we need to wrap it in quotes. // https://github.com/ionic-team/stencil-ds-output-targets/issues/212 eventName = `'${prop.name}'`; } if (inlinePropertyAsSetter) { return `${comment.length > 0 ? ` ${comment}` : ''} set ${eventName}(_: ${type}) {};`; } else { return `${comment.length > 0 ? ` ${comment}` : ''} ${eventName}: ${type};`; } } /** * Creates an Angular component declaration from formatted Stencil compiler metadata. * * @param tagName The tag name of the component. * @param inputs The inputs of the Stencil component (e.g. ['myInput']). * @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']). * @param methods The methods of the Stencil component. (e.g. ['myMethod']). * @param includeImportCustomElements Whether to define the component as a custom element. * @param standalone Whether to define the component as a standalone component. * @param inlineComponentProps List of properties that should be inlined into the component definition. * @returns The component declaration as a string. */ export const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = []) => { const tagNameAsPascal = dashToPascalCase(tagName); const hasInputs = inputs.length > 0; const hasOutputs = outputs.length > 0; const hasMethods = methods.length > 0; // Formats the input strings into comma separated, single quoted values. const formattedInputs = formatToQuotedList(inputs); // Formats the output strings into comma separated, single quoted values. const formattedOutputs = formatToQuotedList(outputs); // Formats the method strings into comma separated, single quoted values. const formattedMethods = formatToQuotedList(methods); const proxyCmpOptions = []; if (includeImportCustomElements) { const defineCustomElementFn = `define${tagNameAsPascal}`; proxyCmpOptions.push(`\n defineCustomElementFn: ${defineCustomElementFn}`); } if (hasInputs) { proxyCmpOptions.push(`\n inputs: [${formattedInputs}]`); } if (hasMethods) { proxyCmpOptions.push(`\n methods: [${formattedMethods}]`); } let standaloneOption = ''; if (standalone && includeImportCustomElements) { standaloneOption = `\n standalone: true`; } const propertyDeclarations = inlineComponentProps.map((m) => createPropertyDeclaration(m, `Components.${tagNameAsPascal}['${m.name}']`, true)); const propertiesDeclarationText = [`protected el: HTML${tagNameAsPascal}Element;`, ...propertyDeclarations].join('\n '); /** * Notes on the generated output: * - We disable @angular-eslint/no-inputs-metadata-property, so that * Angular does not complain about the inputs property. The output target * uses the inputs property to define the inputs of the component instead of * having to use the @Input decorator (and manually define the type and default value). */ const output = `@ProxyCmp({${proxyCmpOptions.join(',')}\n}) @Component({ selector: '${tagName}', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: [${formattedInputs}],${standaloneOption} }) export class ${tagNameAsPascal} { ${propertiesDeclarationText} constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); this.el = r.nativeElement;${hasOutputs ? ` proxyOutputs(this, this.el, [${formattedOutputs}]);` : ''} } }`; return output; }; /** * Sanitizes and formats the component event type. * @param componentClassName The class name of the component (e.g. 'MyComponent') * @param event The Stencil component event. * @returns The sanitized event type as a string. */ const formatOutputType = (componentClassName, event) => { const prefix = `I${componentClassName}`; /** * The original attribute contains the original type defined by the devs. * This regexp normalizes the reference, by removing linebreaks, * replacing consecutive spaces with a single space, and adding a single space after commas. */ return Object.entries(event.complexType.references) .filter(([_, refObject]) => refObject.location === 'local' || refObject.location === 'import') .reduce((type, [src, dst]) => { let renamedType = type; if (!type.startsWith(prefix)) { if (type.startsWith('{') && type.endsWith('}')) { /** * If the type starts with { and ends with }, it is an inline type. * For example, `{ a: string }`. * We don't need to rename these types, so we return the original type. */ renamedType = type; } else { /** * If the type does not start with { and end with }, it is a reference type. * For example, `MyType`. * We need to rename these types, so we prepend the prefix. */ renamedType = `I${componentClassName}${type}`; } } return (renamedType .replace(new RegExp(`^${src}$`, 'g'), `${dst}`) // Capture all instances of the `src` field surrounded by non-word characters on each side and join them. .replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => { if ((dst === null || dst === void 0 ? void 0 : dst.location) === 'import') { /** * Replaces a complex type reference within a generic type. * For example, remapping a type like `EventEmitter<CustomEvent<MyEvent<T>>>` to * `EventEmitter<CustomEvent<IMyComponentMyEvent<IMyComponentT>>>`. */ return [p1, `I${componentClassName}${v.substring(1, v.length - 1)}`, p2].join(''); } return [p1, dst, p2].join(''); }) // Capture all instances that contain sub types, e.g. `IMyComponent.SomeMoreComplexType.SubType`. .replace(new RegExp(`^${src}(\.\\w+)+$`, 'g'), (type) => { return `I${componentClassName}${src}.${type.split('.').slice(1).join('.')}`; })); }, event.complexType.original .replace(/\n/g, ' ') .replace(/\s{2,}/g, ' ') .replace(/,\s*/g, ', ')); }; /** * Creates a formatted comment block based on the JS doc comment. * @param doc The compiler jsdoc. * @returns The formatted comment block as a string. */ const createDocComment = (doc) => { if (doc.text.trim().length === 0 && doc.tags.length === 0) { return ''; } return `/** * ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)} */`; }; /** * Creates the component interface type definition. * @param outputType The output type. * @param tagNameAsPascal The tag name as PascalCase. * @param events The events to generate the interface properties for. * @param componentCorePackage The component core package. * @param customElementsDir The custom elements directory. * @returns The component interface type definition as a string. */ export const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, componentCorePackage, customElementsDir) => { const publicEvents = events.filter((ev) => !ev.internal); const eventTypeImports = createComponentEventTypeImports(tagNameAsPascal, publicEvents, { componentCorePackage, customElementsDir, outputType, }); const eventTypes = publicEvents.map((event) => createPropertyDeclaration(event, `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`)); const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`; const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') + `${interfaceDeclaration}${eventTypes.length === 0 ? '}' : ` ${eventTypes.join('\n')} }`}`; return typeDefinition; };