@stencil/angular-output-target
Advanced tools
Comparing version 0.6.1-dev.11657573317.16e0205c to 0.6.1-dev.11662151255.17ea4066
@@ -7,3 +7,3 @@ /* eslint-disable */ | ||
const Prototype = Cmp.prototype; | ||
inputs.forEach(item => { | ||
inputs.forEach((item) => { | ||
Object.defineProperty(Prototype, item, { | ||
@@ -15,3 +15,3 @@ get() { | ||
this.z.runOutsideAngular(() => (this.el[item] = val)); | ||
} | ||
}, | ||
}); | ||
@@ -23,8 +23,6 @@ }); | ||
const Prototype = Cmp.prototype; | ||
methods.forEach(methodName => { | ||
methods.forEach((methodName) => { | ||
Prototype[methodName] = function () { | ||
const args = arguments; | ||
return this.z.runOutsideAngular(() => | ||
this.el[methodName].apply(this.el, args) | ||
); | ||
return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args)); | ||
}; | ||
@@ -35,17 +33,13 @@ }); | ||
export const proxyOutputs = (instance: any, el: any, events: string[]) => { | ||
events.forEach(eventName => instance[eventName] = fromEvent(el, eventName)); | ||
} | ||
events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName))); | ||
}; | ||
export const defineCustomElement = (tagName: string, customElement: any) => { | ||
if ( | ||
customElement !== undefined && | ||
typeof customElements !== 'undefined' && | ||
!customElements.get(tagName) | ||
) { | ||
if (customElement !== undefined && typeof customElements !== 'undefined' && !customElements.get(tagName)) { | ||
customElements.define(tagName, customElement); | ||
} | ||
} | ||
}; | ||
// tslint:disable-next-line: only-arrow-functions | ||
export function ProxyCmp(opts: { defineCustomElementFn?: () => void, inputs?: any; methods?: any }) { | ||
export function ProxyCmp(opts: { defineCustomElementFn?: () => void; inputs?: any; methods?: any }) { | ||
const decorator = function (cls: any) { | ||
@@ -52,0 +46,0 @@ const { defineCustomElementFn, inputs, methods } = opts; |
@@ -1,2 +0,22 @@ | ||
import type { ComponentCompilerMeta } from '@stencil/core/internal'; | ||
export declare const createComponentDefinition: (componentCorePackage: string, distTypesDir: string, rootDir: string, includeImportCustomElements?: boolean, customElementsDir?: string) => (cmpMeta: ComponentCompilerMeta) => string; | ||
import type { ComponentCompilerEvent } from '@stencil/core/internal'; | ||
/** | ||
* 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. | ||
* @returns The component declaration as a string. | ||
*/ | ||
export declare const createAngularComponentDefinition: (tagName: string, inputs: readonly string[], outputs: readonly string[], methods: readonly string[], includeImportCustomElements?: boolean) => string; | ||
/** | ||
* Creates the component interface type definition. | ||
* @param tagNameAsPascal The tag name as PascalCase. | ||
* @param events The events to generate the interface properties for. | ||
* @param componentCorePackage The component core package. | ||
* @param includeImportCustomElements Whether to include the import for the custom element definition. | ||
* @param customElementsDir The custom elements directory. | ||
* @returns The component interface type definition as a string. | ||
*/ | ||
export declare const createComponentTypeDefinition: (tagNameAsPascal: string, events: readonly ComponentCompilerEvent[], componentCorePackage: string, includeImportCustomElements?: boolean, customElementsDir?: string | undefined) => string; |
@@ -1,101 +0,124 @@ | ||
import { dashToPascalCase, normalizePath } from './utils'; | ||
export const createComponentDefinition = (componentCorePackage, distTypesDir, rootDir, includeImportCustomElements = false, customElementsDir = 'components') => (cmpMeta) => { | ||
// Collect component meta | ||
const inputs = [ | ||
...cmpMeta.properties.filter((prop) => !prop.internal).map((prop) => prop.name), | ||
...cmpMeta.virtualProperties.map((prop) => prop.name), | ||
].sort(); | ||
const outputs = cmpMeta.events.filter((ev) => !ev.internal).map((prop) => prop); | ||
const methods = cmpMeta.methods.filter((method) => !method.internal).map((prop) => prop.name); | ||
// Process meta | ||
import { createComponentEventTypeImports, dashToPascalCase, formatToQuotedList } from './utils'; | ||
/** | ||
* 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. | ||
* @returns The component declaration as a string. | ||
*/ | ||
export const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false) => { | ||
const tagNameAsPascal = dashToPascalCase(tagName); | ||
const hasInputs = inputs.length > 0; | ||
const hasOutputs = outputs.length > 0; | ||
// Generate Angular @Directive | ||
const directiveOpts = [ | ||
`selector: \'${cmpMeta.tagName}\'`, | ||
`changeDetection: ChangeDetectionStrategy.OnPush`, | ||
`template: '<ng-content></ng-content>'`, | ||
]; | ||
if (inputs.length > 0) { | ||
directiveOpts.push(`inputs: ['${inputs.join(`', '`)}']`); | ||
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}`); | ||
} | ||
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); | ||
const outputsInterface = new Set(); | ||
const outputReferenceRemap = {}; | ||
outputs.forEach((output) => { | ||
Object.entries(output.complexType.references).forEach(([reference, refObject]) => { | ||
// Add import line for each local/import reference, and add new mapping name. | ||
// `outputReferenceRemap` should be updated only if the import interface is set in outputsInterface, | ||
// this will prevent global types to be remapped. | ||
const remappedReference = `I${cmpMeta.componentClassName}${reference}`; | ||
if (refObject.location === 'local' || refObject.location === 'import') { | ||
outputReferenceRemap[reference] = remappedReference; | ||
let importLocation = componentCorePackage; | ||
if (componentCorePackage !== undefined) { | ||
const dirPath = includeImportCustomElements ? `/${customElementsDir || 'components'}` : ''; | ||
importLocation = `${normalizePath(componentCorePackage)}${dirPath}`; | ||
} | ||
outputsInterface.add(`import type { ${reference} as ${remappedReference} } from '${importLocation}';`); | ||
} | ||
}); | ||
}); | ||
const componentEvents = [ | ||
'' // Empty first line | ||
]; | ||
// Generate outputs | ||
outputs.forEach((output, index) => { | ||
componentEvents.push(` /** | ||
* ${output.docs.text} ${output.docs.tags.map((tag) => `@${tag.name} ${tag.text}`)} | ||
*/`); | ||
/** | ||
* 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. | ||
**/ | ||
const outputTypeRemapped = Object.entries(outputReferenceRemap).reduce((type, [src, dst]) => { | ||
return type | ||
.replace(new RegExp(`^${src}$`, 'g'), `${dst}`) | ||
.replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].join('')); | ||
}, output.complexType.original | ||
.replace(/\n/g, ' ') | ||
.replace(/\s{2,}/g, ' ') | ||
.replace(/,\s*/g, ', ')); | ||
componentEvents.push(` ${output.name}: EventEmitter<CustomEvent<${outputTypeRemapped.trim()}>>;`); | ||
if (index === outputs.length - 1) { | ||
// Empty line to push end `}` to new line | ||
componentEvents.push('\n'); | ||
} | ||
}); | ||
const lines = [ | ||
'', | ||
`${[...outputsInterface].join('\n')} | ||
export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {${componentEvents.length > 1 ? componentEvents.join('\n') : ''}} | ||
${getProxyCmp(cmpMeta.tagName, includeImportCustomElements, inputs, methods)} | ||
if (hasInputs) { | ||
proxyCmpOptions.push(`\n inputs: [${formattedInputs}]`); | ||
} | ||
if (hasMethods) { | ||
proxyCmpOptions.push(`\n methods: [${formattedMethods}]`); | ||
} | ||
/** | ||
* 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({ | ||
${directiveOpts.join(',\n ')} | ||
selector: '${tagName}', | ||
template: '<ng-content></ng-content>', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property | ||
inputs: [${formattedInputs}], | ||
}) | ||
export class ${tagNameAsPascal} {`, | ||
]; | ||
lines.push(' protected el: HTMLElement;'); | ||
lines.push(` constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { | ||
export class ${tagNameAsPascal} { | ||
protected el: HTMLElement; | ||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { | ||
c.detach(); | ||
this.el = r.nativeElement;`); | ||
if (hasOutputs) { | ||
lines.push(` proxyOutputs(this, this.el, ['${outputs.map((output) => output.name).join(`', '`)}']);`); | ||
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) => { | ||
/** | ||
* 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]) => { | ||
const renamedType = `I${componentClassName}${type}`; | ||
return renamedType | ||
.replace(new RegExp(`^${src}$`, 'g'), `${dst}`) | ||
.replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].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 ''; | ||
} | ||
lines.push(` }`); | ||
lines.push(`}`); | ||
return lines.join('\n'); | ||
return `/** | ||
* ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)} | ||
*/`; | ||
}; | ||
function getProxyCmp(tagName, includeCustomElement, inputs, methods) { | ||
const hasInputs = inputs.length > 0; | ||
const hasMethods = methods.length > 0; | ||
const proxMeta = [ | ||
`defineCustomElementFn: ${includeCustomElement ? 'define' + dashToPascalCase(tagName) : 'undefined'}` | ||
]; | ||
if (hasInputs) | ||
proxMeta.push(`inputs: ['${inputs.join(`', '`)}']`); | ||
if (hasMethods) | ||
proxMeta.push(`methods: ['${methods.join(`', '`)}']`); | ||
return `@ProxyCmp({\n ${proxMeta.join(',\n ')}\n})`; | ||
} | ||
/** | ||
* Creates the component interface type definition. | ||
* @param tagNameAsPascal The tag name as PascalCase. | ||
* @param events The events to generate the interface properties for. | ||
* @param componentCorePackage The component core package. | ||
* @param includeImportCustomElements Whether to include the import for the custom element definition. | ||
* @param customElementsDir The custom elements directory. | ||
* @returns The component interface type definition as a string. | ||
*/ | ||
export const createComponentTypeDefinition = (tagNameAsPascal, events, componentCorePackage, includeImportCustomElements = false, customElementsDir) => { | ||
const typeDefinition = `${createComponentEventTypeImports(tagNameAsPascal, events, { | ||
componentCorePackage, | ||
includeImportCustomElements, | ||
customElementsDir, | ||
})} | ||
export interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} { | ||
${events | ||
.map((event) => { | ||
const comment = createDocComment(event.docs); | ||
return `${comment.length > 0 ? ` ${comment}` : ''} | ||
${event.name}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`; | ||
}) | ||
.join('\n')} | ||
}`; | ||
return typeDefinition; | ||
}; |
@@ -7,3 +7,3 @@ import { dashToPascalCase, relativeImport } from './utils'; | ||
} | ||
const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.directivesProxyFile, '.ts'); | ||
const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.proxyDeclarationFile, '.ts'); | ||
const directives = components | ||
@@ -10,0 +10,0 @@ .map((cmpMeta) => dashToPascalCase(cmpMeta.tagName)) |
import { EOL } from 'os'; | ||
import path from 'path'; | ||
export default async function generateValueAccessors(compilerCtx, components, outputTarget, config) { | ||
if (!Array.isArray(outputTarget.valueAccessorConfigs) || | ||
outputTarget.valueAccessorConfigs.length === 0) { | ||
if (!Array.isArray(outputTarget.valueAccessorConfigs) || outputTarget.valueAccessorConfigs.length === 0) { | ||
return; | ||
} | ||
const targetDir = path.dirname(outputTarget.directivesProxyFile); | ||
const targetDir = path.dirname(outputTarget.proxyDeclarationFile); | ||
const normalizedValueAccessors = outputTarget.valueAccessorConfigs.reduce((allAccessors, va) => { | ||
const elementSelectors = Array.isArray(va.elementSelectors) | ||
? va.elementSelectors | ||
: [va.elementSelectors]; | ||
const elementSelectors = Array.isArray(va.elementSelectors) ? va.elementSelectors : [va.elementSelectors]; | ||
const type = va.type; | ||
@@ -14,0 +11,0 @@ let allElementSelectors = []; |
@@ -84,107 +84,175 @@ 'use strict'; | ||
} | ||
const EXTENDED_PATH_REGEX = /^\\\\\?\\/; | ||
const NON_ASCII_REGEX = /[^\x00-\x80]+/; | ||
const SLASH_REGEX = /\\/g; | ||
const createComponentDefinition = (componentCorePackage, distTypesDir, rootDir, includeImportCustomElements = false, customElementsDir = 'components') => (cmpMeta) => { | ||
// Collect component meta | ||
const inputs = [ | ||
...cmpMeta.properties.filter((prop) => !prop.internal).map((prop) => prop.name), | ||
...cmpMeta.virtualProperties.map((prop) => prop.name), | ||
].sort(); | ||
const outputs = cmpMeta.events.filter((ev) => !ev.internal).map((prop) => prop); | ||
const methods = cmpMeta.methods.filter((method) => !method.internal).map((prop) => prop.name); | ||
// Process meta | ||
const hasOutputs = outputs.length > 0; | ||
// Generate Angular @Directive | ||
const directiveOpts = [ | ||
`selector: \'${cmpMeta.tagName}\'`, | ||
`changeDetection: ChangeDetectionStrategy.OnPush`, | ||
`template: '<ng-content></ng-content>'`, | ||
]; | ||
if (inputs.length > 0) { | ||
directiveOpts.push(`inputs: ['${inputs.join(`', '`)}']`); | ||
/** | ||
* Formats an array of strings to a string of quoted, comma separated values. | ||
* @param list The list of unformatted strings to format | ||
* @returns The formatted array of strings. (e.g. ['foo', 'bar']) => `'foo', 'bar'` | ||
*/ | ||
const formatToQuotedList = (list) => list.map((item) => `'${item}'`).join(', '); | ||
/** | ||
* Creates an import statement for a list of named imports from a module. | ||
* @param imports The list of named imports. | ||
* @param module The module to import from. | ||
* | ||
* @returns The import statement as a string. | ||
*/ | ||
const createImportStatement = (imports, module) => { | ||
if (imports.length === 0) { | ||
return ''; | ||
} | ||
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); | ||
const outputsInterface = new Set(); | ||
const outputReferenceRemap = {}; | ||
outputs.forEach((output) => { | ||
Object.entries(output.complexType.references).forEach(([reference, refObject]) => { | ||
// Add import line for each local/import reference, and add new mapping name. | ||
// `outputReferenceRemap` should be updated only if the import interface is set in outputsInterface, | ||
// this will prevent global types to be remapped. | ||
const remappedReference = `I${cmpMeta.componentClassName}${reference}`; | ||
return `import { ${imports.join(', ')} } from '${module}';`; | ||
}; | ||
/** | ||
* Creates the collection of import statements for a component based on the component's events type dependencies. | ||
* @param componentTagName The tag name of the component (pascal case). | ||
* @param events The events compiler metadata. | ||
* @param options The options for generating the import statements (e.g. whether to import from the custom elements directory). | ||
* @returns The import statements as an array of strings. | ||
*/ | ||
const createComponentEventTypeImports = (componentTagName, events, options) => { | ||
const { componentCorePackage, includeImportCustomElements, customElementsDir } = options; | ||
const imports = []; | ||
const namedImports = new Set(); | ||
const importPathName = normalizePath(componentCorePackage) + (includeImportCustomElements ? `/${customElementsDir || 'components'}` : ''); | ||
events.forEach((event) => { | ||
Object.entries(event.complexType.references).forEach(([typeName, refObject]) => { | ||
if (refObject.location === 'local' || refObject.location === 'import') { | ||
outputReferenceRemap[reference] = remappedReference; | ||
let importLocation = componentCorePackage; | ||
if (componentCorePackage !== undefined) { | ||
const dirPath = includeImportCustomElements ? `/${customElementsDir || 'components'}` : ''; | ||
importLocation = `${normalizePath(componentCorePackage)}${dirPath}`; | ||
const newTypeName = `I${componentTagName}${typeName}`; | ||
// Prevents duplicate imports for the same type. | ||
if (!namedImports.has(newTypeName)) { | ||
imports.push(`import type { ${typeName} as ${newTypeName} } from '${importPathName}';`); | ||
namedImports.add(newTypeName); | ||
} | ||
outputsInterface.add(`import type { ${reference} as ${remappedReference} } from '${importLocation}';`); | ||
} | ||
}); | ||
}); | ||
const componentEvents = [ | ||
'' // Empty first line | ||
]; | ||
// Generate outputs | ||
outputs.forEach((output, index) => { | ||
componentEvents.push(` /** | ||
* ${output.docs.text} ${output.docs.tags.map((tag) => `@${tag.name} ${tag.text}`)} | ||
*/`); | ||
/** | ||
* 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. | ||
**/ | ||
const outputTypeRemapped = Object.entries(outputReferenceRemap).reduce((type, [src, dst]) => { | ||
return type | ||
.replace(new RegExp(`^${src}$`, 'g'), `${dst}`) | ||
.replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].join('')); | ||
}, output.complexType.original | ||
.replace(/\n/g, ' ') | ||
.replace(/\s{2,}/g, ' ') | ||
.replace(/,\s*/g, ', ')); | ||
componentEvents.push(` ${output.name}: EventEmitter<CustomEvent<${outputTypeRemapped.trim()}>>;`); | ||
if (index === outputs.length - 1) { | ||
// Empty line to push end `}` to new line | ||
componentEvents.push('\n'); | ||
} | ||
}); | ||
const lines = [ | ||
'', | ||
`${[...outputsInterface].join('\n')} | ||
export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {${componentEvents.length > 1 ? componentEvents.join('\n') : ''}} | ||
return imports.join('\n'); | ||
}; | ||
const EXTENDED_PATH_REGEX = /^\\\\\?\\/; | ||
const NON_ASCII_REGEX = /[^\x00-\x80]+/; | ||
const SLASH_REGEX = /\\/g; | ||
${getProxyCmp(cmpMeta.tagName, includeImportCustomElements, inputs, methods)} | ||
/** | ||
* 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. | ||
* @returns The component declaration as a string. | ||
*/ | ||
const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false) => { | ||
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}]`); | ||
} | ||
/** | ||
* 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({ | ||
${directiveOpts.join(',\n ')} | ||
selector: '${tagName}', | ||
template: '<ng-content></ng-content>', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property | ||
inputs: [${formattedInputs}], | ||
}) | ||
export class ${tagNameAsPascal} {`, | ||
]; | ||
lines.push(' protected el: HTMLElement;'); | ||
lines.push(` constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { | ||
export class ${tagNameAsPascal} { | ||
protected el: HTMLElement; | ||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { | ||
c.detach(); | ||
this.el = r.nativeElement;`); | ||
if (hasOutputs) { | ||
lines.push(` proxyOutputs(this, this.el, ['${outputs.map((output) => output.name).join(`', '`)}']);`); | ||
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) => { | ||
/** | ||
* 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]) => { | ||
const renamedType = `I${componentClassName}${type}`; | ||
return renamedType | ||
.replace(new RegExp(`^${src}$`, 'g'), `${dst}`) | ||
.replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].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 ''; | ||
} | ||
lines.push(` }`); | ||
lines.push(`}`); | ||
return lines.join('\n'); | ||
return `/** | ||
* ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)} | ||
*/`; | ||
}; | ||
function getProxyCmp(tagName, includeCustomElement, inputs, methods) { | ||
const hasInputs = inputs.length > 0; | ||
const hasMethods = methods.length > 0; | ||
const proxMeta = [ | ||
`defineCustomElementFn: ${includeCustomElement ? 'define' + dashToPascalCase(tagName) : 'undefined'}` | ||
]; | ||
if (hasInputs) | ||
proxMeta.push(`inputs: ['${inputs.join(`', '`)}']`); | ||
if (hasMethods) | ||
proxMeta.push(`methods: ['${methods.join(`', '`)}']`); | ||
return `@ProxyCmp({\n ${proxMeta.join(',\n ')}\n})`; | ||
} | ||
/** | ||
* Creates the component interface type definition. | ||
* @param tagNameAsPascal The tag name as PascalCase. | ||
* @param events The events to generate the interface properties for. | ||
* @param componentCorePackage The component core package. | ||
* @param includeImportCustomElements Whether to include the import for the custom element definition. | ||
* @param customElementsDir The custom elements directory. | ||
* @returns The component interface type definition as a string. | ||
*/ | ||
const createComponentTypeDefinition = (tagNameAsPascal, events, componentCorePackage, includeImportCustomElements = false, customElementsDir) => { | ||
const typeDefinition = `${createComponentEventTypeImports(tagNameAsPascal, events, { | ||
componentCorePackage, | ||
includeImportCustomElements, | ||
customElementsDir, | ||
})} | ||
export interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} { | ||
${events | ||
.map((event) => { | ||
const comment = createDocComment(event.docs); | ||
return `${comment.length > 0 ? ` ${comment}` : ''} | ||
${event.name}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`; | ||
}) | ||
.join('\n')} | ||
}`; | ||
return typeDefinition; | ||
}; | ||
function generateAngularDirectivesFile(compilerCtx, components, outputTarget) { | ||
@@ -195,3 +263,3 @@ // Only create the file if it is defined in the stencil configuration | ||
} | ||
const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.directivesProxyFile, '.ts'); | ||
const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.proxyDeclarationFile, '.ts'); | ||
const directives = components | ||
@@ -212,11 +280,8 @@ .map((cmpMeta) => dashToPascalCase(cmpMeta.tagName)) | ||
async function generateValueAccessors(compilerCtx, components, outputTarget, config) { | ||
if (!Array.isArray(outputTarget.valueAccessorConfigs) || | ||
outputTarget.valueAccessorConfigs.length === 0) { | ||
if (!Array.isArray(outputTarget.valueAccessorConfigs) || outputTarget.valueAccessorConfigs.length === 0) { | ||
return; | ||
} | ||
const targetDir = path__default['default'].dirname(outputTarget.directivesProxyFile); | ||
const targetDir = path__default['default'].dirname(outputTarget.proxyDeclarationFile); | ||
const normalizedValueAccessors = outputTarget.valueAccessorConfigs.reduce((allAccessors, va) => { | ||
const elementSelectors = Array.isArray(va.elementSelectors) | ||
? va.elementSelectors | ||
: [va.elementSelectors]; | ||
const elementSelectors = Array.isArray(va.elementSelectors) ? va.elementSelectors : [va.elementSelectors]; | ||
const type = va.type; | ||
@@ -270,2 +335,19 @@ let allElementSelectors = []; | ||
/** | ||
* Creates an Angular module declaration for a component wrapper. | ||
* @param componentTagName The tag name of the Stencil component. | ||
* @returns The Angular module declaration as a string. | ||
*/ | ||
const generateAngularModuleForComponent = (componentTagName) => { | ||
const tagNameAsPascal = dashToPascalCase(componentTagName); | ||
const componentClassName = `${tagNameAsPascal}`; | ||
const moduleClassName = `${tagNameAsPascal}Module`; | ||
const moduleDefinition = `@NgModule({ | ||
declarations: [${componentClassName}], | ||
exports: [${componentClassName}] | ||
}) | ||
export class ${moduleClassName} { }`; | ||
return moduleDefinition; | ||
}; | ||
async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) { | ||
@@ -277,3 +359,3 @@ const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components); | ||
await Promise.all([ | ||
compilerCtx.fs.writeFile(outputTarget.directivesProxyFile, finalText), | ||
compilerCtx.fs.writeFile(outputTarget.proxyDeclarationFile, finalText), | ||
copyResources$1(config, outputTarget), | ||
@@ -292,3 +374,3 @@ generateAngularDirectivesFile(compilerCtx, filteredComponents, outputTarget), | ||
const srcDirectory = path__default['default'].join(__dirname, '..', 'angular-component-lib'); | ||
const destDirectory = path__default['default'].join(path__default['default'].dirname(outputTarget.directivesProxyFile), 'angular-component-lib'); | ||
const destDirectory = path__default['default'].join(path__default['default'].dirname(outputTarget.proxyDeclarationFile), 'angular-component-lib'); | ||
return config.sys.copy([ | ||
@@ -304,9 +386,30 @@ { | ||
function generateProxies(components, pkgData, outputTarget, rootDir) { | ||
var _a; | ||
const distTypesDir = path__default['default'].dirname(pkgData.types); | ||
const dtsFilePath = path__default['default'].join(rootDir, distTypesDir, GENERATED_DTS); | ||
const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts'); | ||
const componentsTypeFile = relativeImport(outputTarget.proxyDeclarationFile, dtsFilePath, '.d.ts'); | ||
const createSingleComponentAngularModules = (_a = outputTarget.createSingleComponentAngularModules) !== null && _a !== void 0 ? _a : false; | ||
/** | ||
* The collection of named imports from @angular/core. | ||
*/ | ||
const angularCoreImports = [ | ||
'ChangeDetectionStrategy', | ||
'ChangeDetectorRef', | ||
'Component', | ||
'ElementRef', | ||
'EventEmitter', | ||
'NgZone', | ||
]; | ||
/** | ||
* The collection of named imports from the angular-component-lib/utils. | ||
*/ | ||
const componentLibImports = ['ProxyCmp', 'proxyOutputs']; | ||
if (createSingleComponentAngularModules) { | ||
angularCoreImports.push('NgModule'); | ||
} | ||
const imports = `/* tslint:disable */ | ||
/* auto-generated angular directive proxies */ | ||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, NgZone } from '@angular/core'; | ||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`; | ||
${createImportStatement(angularCoreImports, '@angular/core')} | ||
${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`; | ||
/** | ||
@@ -319,4 +422,8 @@ * Generate JSX import type from correct location. | ||
const generateTypeImports = () => { | ||
let importLocation = outputTarget.componentCorePackage ? normalizePath(outputTarget.componentCorePackage) : normalizePath(componentsTypeFile); | ||
importLocation += outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : ''; | ||
let importLocation = outputTarget.componentCorePackage | ||
? normalizePath(outputTarget.componentCorePackage) | ||
: normalizePath(componentsTypeFile); | ||
importLocation += outputTarget.includeImportCustomElements | ||
? `/${outputTarget.customElementsDir || 'components'}` | ||
: ''; | ||
return `import ${outputTarget.includeImportCustomElements ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`; | ||
@@ -333,17 +440,48 @@ }; | ||
if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) { | ||
const cmpImports = components.map(component => { | ||
const cmpImports = components.map((component) => { | ||
const pascalImport = dashToPascalCase(component.tagName); | ||
return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || | ||
'components'}/${component.tagName}.js';`; | ||
return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || 'components'}/${component.tagName}.js';`; | ||
}); | ||
sourceImports = cmpImports.join('\n'); | ||
} | ||
const final = [ | ||
imports, | ||
typeImports, | ||
sourceImports, | ||
components | ||
.map(createComponentDefinition(outputTarget.componentCorePackage, distTypesDir, rootDir, outputTarget.includeImportCustomElements, outputTarget.customElementsDir)) | ||
.join('\n'), | ||
]; | ||
if (createSingleComponentAngularModules) { | ||
// Generating Angular modules is only supported in the dist-custom-elements build | ||
if (!outputTarget.includeImportCustomElements) { | ||
throw new Error('Generating single component Angular modules requires the "includeImportCustomElements" option to be set to true.'); | ||
} | ||
} | ||
const proxyFileOutput = []; | ||
const filterInternalProps = (prop) => !prop.internal; | ||
const mapPropName = (prop) => prop.name; | ||
const { includeImportCustomElements, componentCorePackage, customElementsDir } = outputTarget; | ||
for (let cmpMeta of components) { | ||
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); | ||
const inputs = []; | ||
if (cmpMeta.properties) { | ||
inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
if (cmpMeta.virtualProperties) { | ||
inputs.push(...cmpMeta.virtualProperties.map(mapPropName)); | ||
} | ||
inputs.sort(); | ||
const outputs = []; | ||
if (cmpMeta.events) { | ||
outputs.push(...cmpMeta.events.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
const methods = []; | ||
if (cmpMeta.methods) { | ||
methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
/** | ||
* For each component, we need to generate: | ||
* 1. The @Component decorated class | ||
* 2. Optionally the @NgModule decorated class (if createSingleComponentAngularModules is true) | ||
* 3. The component interface (using declaration merging for types). | ||
*/ | ||
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, includeImportCustomElements); | ||
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName); | ||
const componentTypeDefinition = createComponentTypeDefinition(tagNameAsPascal, cmpMeta.events, componentCorePackage, includeImportCustomElements, customElementsDir); | ||
proxyFileOutput.push(componentDefinition, '\n', createSingleComponentAngularModules ? moduleDefinition : '', '\n', componentTypeDefinition, '\n'); | ||
} | ||
const final = [imports, typeImports, sourceImports, ...proxyFileOutput]; | ||
return final.join('\n') + '\n'; | ||
@@ -367,12 +505,15 @@ } | ||
function normalizeOutputTarget(config, outputTarget) { | ||
const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfig: outputTarget.valueAccessorConfig || [] }); | ||
const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfigs: outputTarget.valueAccessorConfigs || [] }); | ||
if (config.rootDir == null) { | ||
throw new Error('rootDir is not set and it should be set by stencil itself'); | ||
} | ||
if (outputTarget.directivesProxyFile == null) { | ||
throw new Error('directivesProxyFile is required'); | ||
if (outputTarget.directivesProxyFile !== undefined) { | ||
throw new Error('directivesProxyFile has been removed. Use proxyDeclarationFile instead.'); | ||
} | ||
if (outputTarget.directivesProxyFile && !path__default['default'].isAbsolute(outputTarget.directivesProxyFile)) { | ||
results.directivesProxyFile = normalizePath(path__default['default'].join(config.rootDir, outputTarget.directivesProxyFile)); | ||
if (outputTarget.proxyDeclarationFile == null) { | ||
throw new Error('proxyDeclarationFile is required. Please set it in the Stencil config.'); | ||
} | ||
if (outputTarget.proxyDeclarationFile && !path__default['default'].isAbsolute(outputTarget.proxyDeclarationFile)) { | ||
results.proxyDeclarationFile = normalizePath(path__default['default'].join(config.rootDir, outputTarget.proxyDeclarationFile)); | ||
} | ||
if (outputTarget.directivesArrayFile && !path__default['default'].isAbsolute(outputTarget.directivesArrayFile)) { | ||
@@ -379,0 +520,0 @@ results.directivesArrayFile = normalizePath(path__default['default'].join(config.rootDir, outputTarget.directivesArrayFile)); |
@@ -76,107 +76,175 @@ import path from 'path'; | ||
} | ||
const EXTENDED_PATH_REGEX = /^\\\\\?\\/; | ||
const NON_ASCII_REGEX = /[^\x00-\x80]+/; | ||
const SLASH_REGEX = /\\/g; | ||
const createComponentDefinition = (componentCorePackage, distTypesDir, rootDir, includeImportCustomElements = false, customElementsDir = 'components') => (cmpMeta) => { | ||
// Collect component meta | ||
const inputs = [ | ||
...cmpMeta.properties.filter((prop) => !prop.internal).map((prop) => prop.name), | ||
...cmpMeta.virtualProperties.map((prop) => prop.name), | ||
].sort(); | ||
const outputs = cmpMeta.events.filter((ev) => !ev.internal).map((prop) => prop); | ||
const methods = cmpMeta.methods.filter((method) => !method.internal).map((prop) => prop.name); | ||
// Process meta | ||
const hasOutputs = outputs.length > 0; | ||
// Generate Angular @Directive | ||
const directiveOpts = [ | ||
`selector: \'${cmpMeta.tagName}\'`, | ||
`changeDetection: ChangeDetectionStrategy.OnPush`, | ||
`template: '<ng-content></ng-content>'`, | ||
]; | ||
if (inputs.length > 0) { | ||
directiveOpts.push(`inputs: ['${inputs.join(`', '`)}']`); | ||
/** | ||
* Formats an array of strings to a string of quoted, comma separated values. | ||
* @param list The list of unformatted strings to format | ||
* @returns The formatted array of strings. (e.g. ['foo', 'bar']) => `'foo', 'bar'` | ||
*/ | ||
const formatToQuotedList = (list) => list.map((item) => `'${item}'`).join(', '); | ||
/** | ||
* Creates an import statement for a list of named imports from a module. | ||
* @param imports The list of named imports. | ||
* @param module The module to import from. | ||
* | ||
* @returns The import statement as a string. | ||
*/ | ||
const createImportStatement = (imports, module) => { | ||
if (imports.length === 0) { | ||
return ''; | ||
} | ||
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); | ||
const outputsInterface = new Set(); | ||
const outputReferenceRemap = {}; | ||
outputs.forEach((output) => { | ||
Object.entries(output.complexType.references).forEach(([reference, refObject]) => { | ||
// Add import line for each local/import reference, and add new mapping name. | ||
// `outputReferenceRemap` should be updated only if the import interface is set in outputsInterface, | ||
// this will prevent global types to be remapped. | ||
const remappedReference = `I${cmpMeta.componentClassName}${reference}`; | ||
return `import { ${imports.join(', ')} } from '${module}';`; | ||
}; | ||
/** | ||
* Creates the collection of import statements for a component based on the component's events type dependencies. | ||
* @param componentTagName The tag name of the component (pascal case). | ||
* @param events The events compiler metadata. | ||
* @param options The options for generating the import statements (e.g. whether to import from the custom elements directory). | ||
* @returns The import statements as an array of strings. | ||
*/ | ||
const createComponentEventTypeImports = (componentTagName, events, options) => { | ||
const { componentCorePackage, includeImportCustomElements, customElementsDir } = options; | ||
const imports = []; | ||
const namedImports = new Set(); | ||
const importPathName = normalizePath(componentCorePackage) + (includeImportCustomElements ? `/${customElementsDir || 'components'}` : ''); | ||
events.forEach((event) => { | ||
Object.entries(event.complexType.references).forEach(([typeName, refObject]) => { | ||
if (refObject.location === 'local' || refObject.location === 'import') { | ||
outputReferenceRemap[reference] = remappedReference; | ||
let importLocation = componentCorePackage; | ||
if (componentCorePackage !== undefined) { | ||
const dirPath = includeImportCustomElements ? `/${customElementsDir || 'components'}` : ''; | ||
importLocation = `${normalizePath(componentCorePackage)}${dirPath}`; | ||
const newTypeName = `I${componentTagName}${typeName}`; | ||
// Prevents duplicate imports for the same type. | ||
if (!namedImports.has(newTypeName)) { | ||
imports.push(`import type { ${typeName} as ${newTypeName} } from '${importPathName}';`); | ||
namedImports.add(newTypeName); | ||
} | ||
outputsInterface.add(`import type { ${reference} as ${remappedReference} } from '${importLocation}';`); | ||
} | ||
}); | ||
}); | ||
const componentEvents = [ | ||
'' // Empty first line | ||
]; | ||
// Generate outputs | ||
outputs.forEach((output, index) => { | ||
componentEvents.push(` /** | ||
* ${output.docs.text} ${output.docs.tags.map((tag) => `@${tag.name} ${tag.text}`)} | ||
*/`); | ||
/** | ||
* 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. | ||
**/ | ||
const outputTypeRemapped = Object.entries(outputReferenceRemap).reduce((type, [src, dst]) => { | ||
return type | ||
.replace(new RegExp(`^${src}$`, 'g'), `${dst}`) | ||
.replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].join('')); | ||
}, output.complexType.original | ||
.replace(/\n/g, ' ') | ||
.replace(/\s{2,}/g, ' ') | ||
.replace(/,\s*/g, ', ')); | ||
componentEvents.push(` ${output.name}: EventEmitter<CustomEvent<${outputTypeRemapped.trim()}>>;`); | ||
if (index === outputs.length - 1) { | ||
// Empty line to push end `}` to new line | ||
componentEvents.push('\n'); | ||
} | ||
}); | ||
const lines = [ | ||
'', | ||
`${[...outputsInterface].join('\n')} | ||
export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {${componentEvents.length > 1 ? componentEvents.join('\n') : ''}} | ||
return imports.join('\n'); | ||
}; | ||
const EXTENDED_PATH_REGEX = /^\\\\\?\\/; | ||
const NON_ASCII_REGEX = /[^\x00-\x80]+/; | ||
const SLASH_REGEX = /\\/g; | ||
${getProxyCmp(cmpMeta.tagName, includeImportCustomElements, inputs, methods)} | ||
/** | ||
* 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. | ||
* @returns The component declaration as a string. | ||
*/ | ||
const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false) => { | ||
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}]`); | ||
} | ||
/** | ||
* 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({ | ||
${directiveOpts.join(',\n ')} | ||
selector: '${tagName}', | ||
template: '<ng-content></ng-content>', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property | ||
inputs: [${formattedInputs}], | ||
}) | ||
export class ${tagNameAsPascal} {`, | ||
]; | ||
lines.push(' protected el: HTMLElement;'); | ||
lines.push(` constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { | ||
export class ${tagNameAsPascal} { | ||
protected el: HTMLElement; | ||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { | ||
c.detach(); | ||
this.el = r.nativeElement;`); | ||
if (hasOutputs) { | ||
lines.push(` proxyOutputs(this, this.el, ['${outputs.map((output) => output.name).join(`', '`)}']);`); | ||
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) => { | ||
/** | ||
* 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]) => { | ||
const renamedType = `I${componentClassName}${type}`; | ||
return renamedType | ||
.replace(new RegExp(`^${src}$`, 'g'), `${dst}`) | ||
.replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].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 ''; | ||
} | ||
lines.push(` }`); | ||
lines.push(`}`); | ||
return lines.join('\n'); | ||
return `/** | ||
* ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)} | ||
*/`; | ||
}; | ||
function getProxyCmp(tagName, includeCustomElement, inputs, methods) { | ||
const hasInputs = inputs.length > 0; | ||
const hasMethods = methods.length > 0; | ||
const proxMeta = [ | ||
`defineCustomElementFn: ${includeCustomElement ? 'define' + dashToPascalCase(tagName) : 'undefined'}` | ||
]; | ||
if (hasInputs) | ||
proxMeta.push(`inputs: ['${inputs.join(`', '`)}']`); | ||
if (hasMethods) | ||
proxMeta.push(`methods: ['${methods.join(`', '`)}']`); | ||
return `@ProxyCmp({\n ${proxMeta.join(',\n ')}\n})`; | ||
} | ||
/** | ||
* Creates the component interface type definition. | ||
* @param tagNameAsPascal The tag name as PascalCase. | ||
* @param events The events to generate the interface properties for. | ||
* @param componentCorePackage The component core package. | ||
* @param includeImportCustomElements Whether to include the import for the custom element definition. | ||
* @param customElementsDir The custom elements directory. | ||
* @returns The component interface type definition as a string. | ||
*/ | ||
const createComponentTypeDefinition = (tagNameAsPascal, events, componentCorePackage, includeImportCustomElements = false, customElementsDir) => { | ||
const typeDefinition = `${createComponentEventTypeImports(tagNameAsPascal, events, { | ||
componentCorePackage, | ||
includeImportCustomElements, | ||
customElementsDir, | ||
})} | ||
export interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} { | ||
${events | ||
.map((event) => { | ||
const comment = createDocComment(event.docs); | ||
return `${comment.length > 0 ? ` ${comment}` : ''} | ||
${event.name}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`; | ||
}) | ||
.join('\n')} | ||
}`; | ||
return typeDefinition; | ||
}; | ||
function generateAngularDirectivesFile(compilerCtx, components, outputTarget) { | ||
@@ -187,3 +255,3 @@ // Only create the file if it is defined in the stencil configuration | ||
} | ||
const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.directivesProxyFile, '.ts'); | ||
const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.proxyDeclarationFile, '.ts'); | ||
const directives = components | ||
@@ -204,11 +272,8 @@ .map((cmpMeta) => dashToPascalCase(cmpMeta.tagName)) | ||
async function generateValueAccessors(compilerCtx, components, outputTarget, config) { | ||
if (!Array.isArray(outputTarget.valueAccessorConfigs) || | ||
outputTarget.valueAccessorConfigs.length === 0) { | ||
if (!Array.isArray(outputTarget.valueAccessorConfigs) || outputTarget.valueAccessorConfigs.length === 0) { | ||
return; | ||
} | ||
const targetDir = path.dirname(outputTarget.directivesProxyFile); | ||
const targetDir = path.dirname(outputTarget.proxyDeclarationFile); | ||
const normalizedValueAccessors = outputTarget.valueAccessorConfigs.reduce((allAccessors, va) => { | ||
const elementSelectors = Array.isArray(va.elementSelectors) | ||
? va.elementSelectors | ||
: [va.elementSelectors]; | ||
const elementSelectors = Array.isArray(va.elementSelectors) ? va.elementSelectors : [va.elementSelectors]; | ||
const type = va.type; | ||
@@ -262,2 +327,19 @@ let allElementSelectors = []; | ||
/** | ||
* Creates an Angular module declaration for a component wrapper. | ||
* @param componentTagName The tag name of the Stencil component. | ||
* @returns The Angular module declaration as a string. | ||
*/ | ||
const generateAngularModuleForComponent = (componentTagName) => { | ||
const tagNameAsPascal = dashToPascalCase(componentTagName); | ||
const componentClassName = `${tagNameAsPascal}`; | ||
const moduleClassName = `${tagNameAsPascal}Module`; | ||
const moduleDefinition = `@NgModule({ | ||
declarations: [${componentClassName}], | ||
exports: [${componentClassName}] | ||
}) | ||
export class ${moduleClassName} { }`; | ||
return moduleDefinition; | ||
}; | ||
async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) { | ||
@@ -269,3 +351,3 @@ const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components); | ||
await Promise.all([ | ||
compilerCtx.fs.writeFile(outputTarget.directivesProxyFile, finalText), | ||
compilerCtx.fs.writeFile(outputTarget.proxyDeclarationFile, finalText), | ||
copyResources$1(config, outputTarget), | ||
@@ -284,3 +366,3 @@ generateAngularDirectivesFile(compilerCtx, filteredComponents, outputTarget), | ||
const srcDirectory = path.join(__dirname, '..', 'angular-component-lib'); | ||
const destDirectory = path.join(path.dirname(outputTarget.directivesProxyFile), 'angular-component-lib'); | ||
const destDirectory = path.join(path.dirname(outputTarget.proxyDeclarationFile), 'angular-component-lib'); | ||
return config.sys.copy([ | ||
@@ -296,9 +378,30 @@ { | ||
function generateProxies(components, pkgData, outputTarget, rootDir) { | ||
var _a; | ||
const distTypesDir = path.dirname(pkgData.types); | ||
const dtsFilePath = path.join(rootDir, distTypesDir, GENERATED_DTS); | ||
const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts'); | ||
const componentsTypeFile = relativeImport(outputTarget.proxyDeclarationFile, dtsFilePath, '.d.ts'); | ||
const createSingleComponentAngularModules = (_a = outputTarget.createSingleComponentAngularModules) !== null && _a !== void 0 ? _a : false; | ||
/** | ||
* The collection of named imports from @angular/core. | ||
*/ | ||
const angularCoreImports = [ | ||
'ChangeDetectionStrategy', | ||
'ChangeDetectorRef', | ||
'Component', | ||
'ElementRef', | ||
'EventEmitter', | ||
'NgZone', | ||
]; | ||
/** | ||
* The collection of named imports from the angular-component-lib/utils. | ||
*/ | ||
const componentLibImports = ['ProxyCmp', 'proxyOutputs']; | ||
if (createSingleComponentAngularModules) { | ||
angularCoreImports.push('NgModule'); | ||
} | ||
const imports = `/* tslint:disable */ | ||
/* auto-generated angular directive proxies */ | ||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, NgZone } from '@angular/core'; | ||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`; | ||
${createImportStatement(angularCoreImports, '@angular/core')} | ||
${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`; | ||
/** | ||
@@ -311,4 +414,8 @@ * Generate JSX import type from correct location. | ||
const generateTypeImports = () => { | ||
let importLocation = outputTarget.componentCorePackage ? normalizePath(outputTarget.componentCorePackage) : normalizePath(componentsTypeFile); | ||
importLocation += outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : ''; | ||
let importLocation = outputTarget.componentCorePackage | ||
? normalizePath(outputTarget.componentCorePackage) | ||
: normalizePath(componentsTypeFile); | ||
importLocation += outputTarget.includeImportCustomElements | ||
? `/${outputTarget.customElementsDir || 'components'}` | ||
: ''; | ||
return `import ${outputTarget.includeImportCustomElements ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`; | ||
@@ -325,17 +432,48 @@ }; | ||
if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) { | ||
const cmpImports = components.map(component => { | ||
const cmpImports = components.map((component) => { | ||
const pascalImport = dashToPascalCase(component.tagName); | ||
return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || | ||
'components'}/${component.tagName}.js';`; | ||
return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || 'components'}/${component.tagName}.js';`; | ||
}); | ||
sourceImports = cmpImports.join('\n'); | ||
} | ||
const final = [ | ||
imports, | ||
typeImports, | ||
sourceImports, | ||
components | ||
.map(createComponentDefinition(outputTarget.componentCorePackage, distTypesDir, rootDir, outputTarget.includeImportCustomElements, outputTarget.customElementsDir)) | ||
.join('\n'), | ||
]; | ||
if (createSingleComponentAngularModules) { | ||
// Generating Angular modules is only supported in the dist-custom-elements build | ||
if (!outputTarget.includeImportCustomElements) { | ||
throw new Error('Generating single component Angular modules requires the "includeImportCustomElements" option to be set to true.'); | ||
} | ||
} | ||
const proxyFileOutput = []; | ||
const filterInternalProps = (prop) => !prop.internal; | ||
const mapPropName = (prop) => prop.name; | ||
const { includeImportCustomElements, componentCorePackage, customElementsDir } = outputTarget; | ||
for (let cmpMeta of components) { | ||
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); | ||
const inputs = []; | ||
if (cmpMeta.properties) { | ||
inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
if (cmpMeta.virtualProperties) { | ||
inputs.push(...cmpMeta.virtualProperties.map(mapPropName)); | ||
} | ||
inputs.sort(); | ||
const outputs = []; | ||
if (cmpMeta.events) { | ||
outputs.push(...cmpMeta.events.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
const methods = []; | ||
if (cmpMeta.methods) { | ||
methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
/** | ||
* For each component, we need to generate: | ||
* 1. The @Component decorated class | ||
* 2. Optionally the @NgModule decorated class (if createSingleComponentAngularModules is true) | ||
* 3. The component interface (using declaration merging for types). | ||
*/ | ||
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, includeImportCustomElements); | ||
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName); | ||
const componentTypeDefinition = createComponentTypeDefinition(tagNameAsPascal, cmpMeta.events, componentCorePackage, includeImportCustomElements, customElementsDir); | ||
proxyFileOutput.push(componentDefinition, '\n', createSingleComponentAngularModules ? moduleDefinition : '', '\n', componentTypeDefinition, '\n'); | ||
} | ||
const final = [imports, typeImports, sourceImports, ...proxyFileOutput]; | ||
return final.join('\n') + '\n'; | ||
@@ -359,12 +497,15 @@ } | ||
function normalizeOutputTarget(config, outputTarget) { | ||
const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfig: outputTarget.valueAccessorConfig || [] }); | ||
const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfigs: outputTarget.valueAccessorConfigs || [] }); | ||
if (config.rootDir == null) { | ||
throw new Error('rootDir is not set and it should be set by stencil itself'); | ||
} | ||
if (outputTarget.directivesProxyFile == null) { | ||
throw new Error('directivesProxyFile is required'); | ||
if (outputTarget.directivesProxyFile !== undefined) { | ||
throw new Error('directivesProxyFile has been removed. Use proxyDeclarationFile instead.'); | ||
} | ||
if (outputTarget.directivesProxyFile && !path.isAbsolute(outputTarget.directivesProxyFile)) { | ||
results.directivesProxyFile = normalizePath(path.join(config.rootDir, outputTarget.directivesProxyFile)); | ||
if (outputTarget.proxyDeclarationFile == null) { | ||
throw new Error('proxyDeclarationFile is required. Please set it in the Stencil config.'); | ||
} | ||
if (outputTarget.proxyDeclarationFile && !path.isAbsolute(outputTarget.proxyDeclarationFile)) { | ||
results.proxyDeclarationFile = normalizePath(path.join(config.rootDir, outputTarget.proxyDeclarationFile)); | ||
} | ||
if (outputTarget.directivesArrayFile && !path.isAbsolute(outputTarget.directivesArrayFile)) { | ||
@@ -371,0 +512,0 @@ results.directivesArrayFile = normalizePath(path.join(config.rootDir, outputTarget.directivesArrayFile)); |
import path from 'path'; | ||
import { relativeImport, normalizePath, sortBy, readPackageJson, dashToPascalCase } from './utils'; | ||
import { createComponentDefinition } from './generate-angular-component'; | ||
import { relativeImport, normalizePath, sortBy, readPackageJson, dashToPascalCase, createImportStatement, } from './utils'; | ||
import { createAngularComponentDefinition, createComponentTypeDefinition } from './generate-angular-component'; | ||
import { generateAngularDirectivesFile } from './generate-angular-directives-file'; | ||
import generateValueAccessors from './generate-value-accessors'; | ||
import { generateAngularModuleForComponent } from './generate-angular-modules'; | ||
export async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) { | ||
@@ -12,3 +13,3 @@ const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components); | ||
await Promise.all([ | ||
compilerCtx.fs.writeFile(outputTarget.directivesProxyFile, finalText), | ||
compilerCtx.fs.writeFile(outputTarget.proxyDeclarationFile, finalText), | ||
copyResources(config, outputTarget), | ||
@@ -27,3 +28,3 @@ generateAngularDirectivesFile(compilerCtx, filteredComponents, outputTarget), | ||
const srcDirectory = path.join(__dirname, '..', 'angular-component-lib'); | ||
const destDirectory = path.join(path.dirname(outputTarget.directivesProxyFile), 'angular-component-lib'); | ||
const destDirectory = path.join(path.dirname(outputTarget.proxyDeclarationFile), 'angular-component-lib'); | ||
return config.sys.copy([ | ||
@@ -39,9 +40,30 @@ { | ||
export function generateProxies(components, pkgData, outputTarget, rootDir) { | ||
var _a; | ||
const distTypesDir = path.dirname(pkgData.types); | ||
const dtsFilePath = path.join(rootDir, distTypesDir, GENERATED_DTS); | ||
const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts'); | ||
const componentsTypeFile = relativeImport(outputTarget.proxyDeclarationFile, dtsFilePath, '.d.ts'); | ||
const createSingleComponentAngularModules = (_a = outputTarget.createSingleComponentAngularModules) !== null && _a !== void 0 ? _a : false; | ||
/** | ||
* The collection of named imports from @angular/core. | ||
*/ | ||
const angularCoreImports = [ | ||
'ChangeDetectionStrategy', | ||
'ChangeDetectorRef', | ||
'Component', | ||
'ElementRef', | ||
'EventEmitter', | ||
'NgZone', | ||
]; | ||
/** | ||
* The collection of named imports from the angular-component-lib/utils. | ||
*/ | ||
const componentLibImports = ['ProxyCmp', 'proxyOutputs']; | ||
if (createSingleComponentAngularModules) { | ||
angularCoreImports.push('NgModule'); | ||
} | ||
const imports = `/* tslint:disable */ | ||
/* auto-generated angular directive proxies */ | ||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, NgZone } from '@angular/core'; | ||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`; | ||
${createImportStatement(angularCoreImports, '@angular/core')} | ||
${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`; | ||
/** | ||
@@ -54,4 +76,8 @@ * Generate JSX import type from correct location. | ||
const generateTypeImports = () => { | ||
let importLocation = outputTarget.componentCorePackage ? normalizePath(outputTarget.componentCorePackage) : normalizePath(componentsTypeFile); | ||
importLocation += outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : ''; | ||
let importLocation = outputTarget.componentCorePackage | ||
? normalizePath(outputTarget.componentCorePackage) | ||
: normalizePath(componentsTypeFile); | ||
importLocation += outputTarget.includeImportCustomElements | ||
? `/${outputTarget.customElementsDir || 'components'}` | ||
: ''; | ||
return `import ${outputTarget.includeImportCustomElements ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`; | ||
@@ -68,17 +94,48 @@ }; | ||
if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) { | ||
const cmpImports = components.map(component => { | ||
const cmpImports = components.map((component) => { | ||
const pascalImport = dashToPascalCase(component.tagName); | ||
return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || | ||
'components'}/${component.tagName}.js';`; | ||
return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || 'components'}/${component.tagName}.js';`; | ||
}); | ||
sourceImports = cmpImports.join('\n'); | ||
} | ||
const final = [ | ||
imports, | ||
typeImports, | ||
sourceImports, | ||
components | ||
.map(createComponentDefinition(outputTarget.componentCorePackage, distTypesDir, rootDir, outputTarget.includeImportCustomElements, outputTarget.customElementsDir)) | ||
.join('\n'), | ||
]; | ||
if (createSingleComponentAngularModules) { | ||
// Generating Angular modules is only supported in the dist-custom-elements build | ||
if (!outputTarget.includeImportCustomElements) { | ||
throw new Error('Generating single component Angular modules requires the "includeImportCustomElements" option to be set to true.'); | ||
} | ||
} | ||
const proxyFileOutput = []; | ||
const filterInternalProps = (prop) => !prop.internal; | ||
const mapPropName = (prop) => prop.name; | ||
const { includeImportCustomElements, componentCorePackage, customElementsDir } = outputTarget; | ||
for (let cmpMeta of components) { | ||
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); | ||
const inputs = []; | ||
if (cmpMeta.properties) { | ||
inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
if (cmpMeta.virtualProperties) { | ||
inputs.push(...cmpMeta.virtualProperties.map(mapPropName)); | ||
} | ||
inputs.sort(); | ||
const outputs = []; | ||
if (cmpMeta.events) { | ||
outputs.push(...cmpMeta.events.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
const methods = []; | ||
if (cmpMeta.methods) { | ||
methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName)); | ||
} | ||
/** | ||
* For each component, we need to generate: | ||
* 1. The @Component decorated class | ||
* 2. Optionally the @NgModule decorated class (if createSingleComponentAngularModules is true) | ||
* 3. The component interface (using declaration merging for types). | ||
*/ | ||
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, includeImportCustomElements); | ||
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName); | ||
const componentTypeDefinition = createComponentTypeDefinition(tagNameAsPascal, cmpMeta.events, componentCorePackage, includeImportCustomElements, customElementsDir); | ||
proxyFileOutput.push(componentDefinition, '\n', createSingleComponentAngularModules ? moduleDefinition : '', '\n', componentTypeDefinition, '\n'); | ||
} | ||
const final = [imports, typeImports, sourceImports, ...proxyFileOutput]; | ||
return final.join('\n') + '\n'; | ||
@@ -85,0 +142,0 @@ } |
import type { Config, OutputTargetCustom } from '@stencil/core/internal'; | ||
import type { OutputTargetAngular } from './types'; | ||
export declare const angularOutputTarget: (outputTarget: OutputTargetAngular) => OutputTargetCustom; | ||
export declare function normalizeOutputTarget(config: Config, outputTarget: any): OutputTargetAngular; | ||
export declare function normalizeOutputTarget(config: Config, outputTarget: OutputTargetAngular): OutputTargetAngular; |
@@ -17,12 +17,15 @@ import { normalizePath } from './utils'; | ||
export function normalizeOutputTarget(config, outputTarget) { | ||
const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfig: outputTarget.valueAccessorConfig || [] }); | ||
const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfigs: outputTarget.valueAccessorConfigs || [] }); | ||
if (config.rootDir == null) { | ||
throw new Error('rootDir is not set and it should be set by stencil itself'); | ||
} | ||
if (outputTarget.directivesProxyFile == null) { | ||
throw new Error('directivesProxyFile is required'); | ||
if (outputTarget.directivesProxyFile !== undefined) { | ||
throw new Error('directivesProxyFile has been removed. Use proxyDeclarationFile instead.'); | ||
} | ||
if (outputTarget.directivesProxyFile && !path.isAbsolute(outputTarget.directivesProxyFile)) { | ||
results.directivesProxyFile = normalizePath(path.join(config.rootDir, outputTarget.directivesProxyFile)); | ||
if (outputTarget.proxyDeclarationFile == null) { | ||
throw new Error('proxyDeclarationFile is required. Please set it in the Stencil config.'); | ||
} | ||
if (outputTarget.proxyDeclarationFile && !path.isAbsolute(outputTarget.proxyDeclarationFile)) { | ||
results.proxyDeclarationFile = normalizePath(path.join(config.rootDir, outputTarget.proxyDeclarationFile)); | ||
} | ||
if (outputTarget.directivesArrayFile && !path.isAbsolute(outputTarget.directivesArrayFile)) { | ||
@@ -29,0 +32,0 @@ results.directivesArrayFile = normalizePath(path.join(config.rootDir, outputTarget.directivesArrayFile)); |
export interface OutputTargetAngular { | ||
componentCorePackage?: string; | ||
directivesProxyFile: string; | ||
/** | ||
* The package name of the component library. | ||
* This is used to generate the import statements. | ||
*/ | ||
componentCorePackage: string; | ||
/** | ||
* @deprecated Use `proxyDeclarationFile` instead. This property has been replaced. | ||
*/ | ||
directivesProxyFile?: string; | ||
directivesArrayFile?: string; | ||
directivesUtilsFile?: string; | ||
/** | ||
* The path to the proxy file that will be generated. This can be an absolute path | ||
* or a relative path from the root directory of the Stencil library. | ||
*/ | ||
proxyDeclarationFile: string; | ||
valueAccessorConfigs?: ValueAccessorConfig[]; | ||
@@ -10,2 +22,6 @@ excludeComponents?: string[]; | ||
customElementsDir?: string; | ||
/** | ||
* `true` to generate a single component Angular module for each component. | ||
*/ | ||
createSingleComponentAngularModules?: boolean; | ||
} | ||
@@ -12,0 +28,0 @@ export declare type ValueAccessorTypes = 'text' | 'radio' | 'select' | 'number' | 'boolean'; |
@@ -1,2 +0,2 @@ | ||
import { Config } from '@stencil/core/internal'; | ||
import { ComponentCompilerEvent, Config } from '@stencil/core/internal'; | ||
import type { PackageJSON } from './types'; | ||
@@ -10,1 +10,27 @@ export declare const toLowerCase: (str: string) => string; | ||
export declare function readPackageJson(config: Config, rootDir: string): Promise<PackageJSON>; | ||
/** | ||
* Formats an array of strings to a string of quoted, comma separated values. | ||
* @param list The list of unformatted strings to format | ||
* @returns The formatted array of strings. (e.g. ['foo', 'bar']) => `'foo', 'bar'` | ||
*/ | ||
export declare const formatToQuotedList: (list: readonly string[]) => string; | ||
/** | ||
* Creates an import statement for a list of named imports from a module. | ||
* @param imports The list of named imports. | ||
* @param module The module to import from. | ||
* | ||
* @returns The import statement as a string. | ||
*/ | ||
export declare const createImportStatement: (imports: string[], module: string) => string; | ||
/** | ||
* Creates the collection of import statements for a component based on the component's events type dependencies. | ||
* @param componentTagName The tag name of the component (pascal case). | ||
* @param events The events compiler metadata. | ||
* @param options The options for generating the import statements (e.g. whether to import from the custom elements directory). | ||
* @returns The import statements as an array of strings. | ||
*/ | ||
export declare const createComponentEventTypeImports: (componentTagName: string, events: readonly ComponentCompilerEvent[], options: { | ||
componentCorePackage: string; | ||
includeImportCustomElements?: boolean; | ||
customElementsDir?: string; | ||
}) => string; |
@@ -77,4 +77,49 @@ import path from 'path'; | ||
} | ||
/** | ||
* Formats an array of strings to a string of quoted, comma separated values. | ||
* @param list The list of unformatted strings to format | ||
* @returns The formatted array of strings. (e.g. ['foo', 'bar']) => `'foo', 'bar'` | ||
*/ | ||
export const formatToQuotedList = (list) => list.map((item) => `'${item}'`).join(', '); | ||
/** | ||
* Creates an import statement for a list of named imports from a module. | ||
* @param imports The list of named imports. | ||
* @param module The module to import from. | ||
* | ||
* @returns The import statement as a string. | ||
*/ | ||
export const createImportStatement = (imports, module) => { | ||
if (imports.length === 0) { | ||
return ''; | ||
} | ||
return `import { ${imports.join(', ')} } from '${module}';`; | ||
}; | ||
/** | ||
* Creates the collection of import statements for a component based on the component's events type dependencies. | ||
* @param componentTagName The tag name of the component (pascal case). | ||
* @param events The events compiler metadata. | ||
* @param options The options for generating the import statements (e.g. whether to import from the custom elements directory). | ||
* @returns The import statements as an array of strings. | ||
*/ | ||
export const createComponentEventTypeImports = (componentTagName, events, options) => { | ||
const { componentCorePackage, includeImportCustomElements, customElementsDir } = options; | ||
const imports = []; | ||
const namedImports = new Set(); | ||
const importPathName = normalizePath(componentCorePackage) + (includeImportCustomElements ? `/${customElementsDir || 'components'}` : ''); | ||
events.forEach((event) => { | ||
Object.entries(event.complexType.references).forEach(([typeName, refObject]) => { | ||
if (refObject.location === 'local' || refObject.location === 'import') { | ||
const newTypeName = `I${componentTagName}${typeName}`; | ||
// Prevents duplicate imports for the same type. | ||
if (!namedImports.has(newTypeName)) { | ||
imports.push(`import type { ${typeName} as ${newTypeName} } from '${importPathName}';`); | ||
namedImports.add(newTypeName); | ||
} | ||
} | ||
}); | ||
}); | ||
return imports.join('\n'); | ||
}; | ||
const EXTENDED_PATH_REGEX = /^\\\\\?\\/; | ||
const NON_ASCII_REGEX = /[^\x00-\x80]+/; | ||
const SLASH_REGEX = /\\/g; |
{ | ||
"name": "@stencil/angular-output-target", | ||
"version": "0.6.1-dev.11657573317.16e0205c", | ||
"version": "0.6.1-dev.11662151255.17ea4066", | ||
"description": "Angular output target for @stencil/core components.", | ||
@@ -23,2 +23,5 @@ "main": "dist/index.cjs.js", | ||
"version": "npm run build", | ||
"prettier": "npm run prettier.base -- --write", | ||
"prettier.base": "prettier \"./({angular-component-lib,src,test,__tests__}/**/*.{ts,tsx,js,jsx})|*.{ts,tsx,js,jsx}\"", | ||
"prettier.dry-run": "npm run prettier.base -- --list-different", | ||
"release": "np", | ||
@@ -59,3 +62,3 @@ "test": "jest --passWithNoTests", | ||
}, | ||
"gitHead": "6e0205c1e021addd4683bd5fc976cc6c9a126c3a" | ||
"gitHead": "7ea40666153180bc16a62602feba11b767d8e396" | ||
} |
@@ -30,3 +30,3 @@ # @stencil/angular-output-target | ||
componentCorePackage: 'component-library', | ||
directivesProxyFile: '../component-library-angular/src/directives/proxies.ts', | ||
proxyDeclarationFile: '../component-library-angular/src/directives/proxies.ts', | ||
directivesArrayFile: '../component-library-angular/src/directives/index.ts', | ||
@@ -47,3 +47,3 @@ }), | ||
| `componentCorePackage` | The NPM package name of your Stencil component library. This package is used as a dependency for your Angular wrappers. | | ||
| `directivesProxyFile` | The output file of all the component wrappers generated by the output target. This file path should point to a location within your Angular library/project. | | ||
| `proxyDeclarationFile` | The output file of all the component wrappers generated by the output target. This file path should point to a location within your Angular library/project. | | ||
| `directivesArrayFile` | The output file of a constant of all the generated component wrapper classes. Used for easily declaring and exporting the generated components from an `NgModule`. This file path should point to a location within your Angular library/project. | | ||
@@ -50,0 +50,0 @@ | `valueAccessorConfigs` | The configuration object for how individual web components behave with Angular control value accessors. | |
91351
29
1835