UNPKG

6.22 kBJavaScriptView Raw
1import { createComponentEventTypeImports, dashToPascalCase, formatToQuotedList } from './utils';
2/**
3 * Creates an Angular component declaration from formatted Stencil compiler metadata.
4 *
5 * @param tagName The tag name of the component.
6 * @param inputs The inputs of the Stencil component (e.g. ['myInput']).
7 * @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']).
8 * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
9 * @param includeImportCustomElements Whether to define the component as a custom element.
10 * @returns The component declaration as a string.
11 */
12export const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false) => {
13 const tagNameAsPascal = dashToPascalCase(tagName);
14 const hasInputs = inputs.length > 0;
15 const hasOutputs = outputs.length > 0;
16 const hasMethods = methods.length > 0;
17 // Formats the input strings into comma separated, single quoted values.
18 const formattedInputs = formatToQuotedList(inputs);
19 // Formats the output strings into comma separated, single quoted values.
20 const formattedOutputs = formatToQuotedList(outputs);
21 // Formats the method strings into comma separated, single quoted values.
22 const formattedMethods = formatToQuotedList(methods);
23 const proxyCmpOptions = [];
24 if (includeImportCustomElements) {
25 const defineCustomElementFn = `define${tagNameAsPascal}`;
26 proxyCmpOptions.push(`\n defineCustomElementFn: ${defineCustomElementFn}`);
27 }
28 if (hasInputs) {
29 proxyCmpOptions.push(`\n inputs: [${formattedInputs}]`);
30 }
31 if (hasMethods) {
32 proxyCmpOptions.push(`\n methods: [${formattedMethods}]`);
33 }
34 /**
35 * Notes on the generated output:
36 * - We disable @angular-eslint/no-inputs-metadata-property, so that
37 * Angular does not complain about the inputs property. The output target
38 * uses the inputs property to define the inputs of the component instead of
39 * having to use the @Input decorator (and manually define the type and default value).
40 */
41 const output = `@ProxyCmp({${proxyCmpOptions.join(',')}\n})
42@Component({
43 selector: '${tagName}',
44 changeDetection: ChangeDetectionStrategy.OnPush,
45 template: '<ng-content></ng-content>',
46 // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
47 inputs: [${formattedInputs}],
48})
49export class ${tagNameAsPascal} {
50 protected el: HTMLElement;
51 constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
52 c.detach();
53 this.el = r.nativeElement;${hasOutputs
54 ? `
55 proxyOutputs(this, this.el, [${formattedOutputs}]);`
56 : ''}
57 }
58}`;
59 return output;
60};
61/**
62 * Sanitizes and formats the component event type.
63 * @param componentClassName The class name of the component (e.g. 'MyComponent')
64 * @param event The Stencil component event.
65 * @returns The sanitized event type as a string.
66 */
67const formatOutputType = (componentClassName, event) => {
68 /**
69 * The original attribute contains the original type defined by the devs.
70 * This regexp normalizes the reference, by removing linebreaks,
71 * replacing consecutive spaces with a single space, and adding a single space after commas.
72 */
73 return Object.entries(event.complexType.references)
74 .filter(([_, refObject]) => refObject.location === 'local' || refObject.location === 'import')
75 .reduce((type, [src, dst]) => {
76 const renamedType = `I${componentClassName}${type}`;
77 return (renamedType
78 .replace(new RegExp(`^${src}$`, 'g'), `${dst}`)
79 // Capture all instances of the `src` field surrounded by non-word characters on each side and join them.
80 .replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].join('')));
81 }, event.complexType.original
82 .replace(/\n/g, ' ')
83 .replace(/\s{2,}/g, ' ')
84 .replace(/,\s*/g, ', '));
85};
86/**
87 * Creates a formatted comment block based on the JS doc comment.
88 * @param doc The compiler jsdoc.
89 * @returns The formatted comment block as a string.
90 */
91const createDocComment = (doc) => {
92 if (doc.text.trim().length === 0 && doc.tags.length === 0) {
93 return '';
94 }
95 return `/**
96 * ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)}
97 */`;
98};
99/**
100 * Creates the component interface type definition.
101 * @param tagNameAsPascal The tag name as PascalCase.
102 * @param events The events to generate the interface properties for.
103 * @param componentCorePackage The component core package.
104 * @param includeImportCustomElements Whether to include the import for the custom element definition.
105 * @param customElementsDir The custom elements directory.
106 * @returns The component interface type definition as a string.
107 */
108export const createComponentTypeDefinition = (tagNameAsPascal, events, componentCorePackage, includeImportCustomElements = false, customElementsDir) => {
109 const publicEvents = events.filter((ev) => !ev.internal);
110 const eventTypeImports = createComponentEventTypeImports(tagNameAsPascal, publicEvents, {
111 componentCorePackage,
112 includeImportCustomElements,
113 customElementsDir,
114 });
115 const eventTypes = publicEvents.map((event) => {
116 const comment = createDocComment(event.docs);
117 let eventName = event.name;
118 if (event.name.includes('-')) {
119 // If an event name includes a dash, we need to wrap it in quotes.
120 // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
121 eventName = `'${event.name}'`;
122 }
123 return `${comment.length > 0 ? ` ${comment}` : ''}
124 ${eventName}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`;
125 });
126 const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
127 const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') +
128 `${interfaceDeclaration}${eventTypes.length === 0
129 ? '}'
130 : `
131${eventTypes.join('\n')}
132}`}`;
133 return typeDefinition;
134};