@stencil/angular-output-target 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -73,110 +73,188 @@ async function readPackageJson(config, rootDir) {
73
73
  }
74
74
  return pkgData;
75
75
  }
76
- const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
77
- const NON_ASCII_REGEX = /[^\x00-\x80]+/;
78
- const SLASH_REGEX = /\\/g;
79
-
80
- const createComponentDefinition = (componentCorePackage, distTypesDir, rootDir, includeImportCustomElements = false, customElementsDir = 'components') => (cmpMeta) => {
81
- // Collect component meta
82
- const inputs = [
83
- ...cmpMeta.properties.filter((prop) => !prop.internal).map((prop) => prop.name),
84
- ...cmpMeta.virtualProperties.map((prop) => prop.name),
85
- ].sort();
86
- const outputs = cmpMeta.events.filter((ev) => !ev.internal).map((prop) => prop);
87
- const methods = cmpMeta.methods.filter((method) => !method.internal).map((prop) => prop.name);
88
- // Process meta
89
- const hasOutputs = outputs.length > 0;
90
- // Generate Angular @Directive
91
- const directiveOpts = [
92
- `selector: \'${cmpMeta.tagName}\'`,
93
- `changeDetection: ChangeDetectionStrategy.OnPush`,
94
- `template: '<ng-content></ng-content>'`,
95
- ];
96
- if (inputs.length > 0) {
97
- directiveOpts.push(`inputs: ['${inputs.join(`', '`)}']`);
76
+ /**
77
+ * Formats an array of strings to a string of quoted, comma separated values.
78
+ * @param list The list of unformatted strings to format
79
+ * @returns The formatted array of strings. (e.g. ['foo', 'bar']) => `'foo', 'bar'`
80
+ */
81
+ const formatToQuotedList = (list) => list.map((item) => `'${item}'`).join(', ');
82
+ /**
83
+ * Creates an import statement for a list of named imports from a module.
84
+ * @param imports The list of named imports.
85
+ * @param module The module to import from.
86
+ *
87
+ * @returns The import statement as a string.
88
+ */
89
+ const createImportStatement = (imports, module) => {
90
+ if (imports.length === 0) {
91
+ return '';
98
92
  }
99
- const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
100
- const outputsInterface = new Set();
101
- const outputReferenceRemap = {};
102
- outputs.forEach((output) => {
103
- Object.entries(output.complexType.references).forEach(([reference, refObject]) => {
104
- // Add import line for each local/import reference, and add new mapping name.
105
- // `outputReferenceRemap` should be updated only if the import interface is set in outputsInterface,
106
- // this will prevent global types to be remapped.
107
- const remappedReference = `I${cmpMeta.componentClassName}${reference}`;
93
+ return `import { ${imports.join(', ')} } from '${module}';`;
94
+ };
95
+ /**
96
+ * Creates the collection of import statements for a component based on the component's events type dependencies.
97
+ * @param componentTagName The tag name of the component (pascal case).
98
+ * @param events The events compiler metadata.
99
+ * @param options The options for generating the import statements (e.g. whether to import from the custom elements directory).
100
+ * @returns The import statements as an array of strings.
101
+ */
102
+ const createComponentEventTypeImports = (componentTagName, events, options) => {
103
+ const { componentCorePackage, includeImportCustomElements, customElementsDir } = options;
104
+ const imports = [];
105
+ const namedImports = new Set();
106
+ const importPathName = normalizePath(componentCorePackage) + (includeImportCustomElements ? `/${customElementsDir || 'components'}` : '');
107
+ events.forEach((event) => {
108
+ Object.entries(event.complexType.references).forEach(([typeName, refObject]) => {
108
109
  if (refObject.location === 'local' || refObject.location === 'import') {
109
- outputReferenceRemap[reference] = remappedReference;
110
- let importLocation = componentCorePackage;
111
- if (componentCorePackage !== undefined) {
112
- const dirPath = includeImportCustomElements ? `/${customElementsDir || 'components'}` : '';
113
- importLocation = `${normalizePath(componentCorePackage)}${dirPath}`;
110
+ const newTypeName = `I${componentTagName}${typeName}`;
111
+ // Prevents duplicate imports for the same type.
112
+ if (!namedImports.has(newTypeName)) {
113
+ imports.push(`import type { ${typeName} as ${newTypeName} } from '${importPathName}';`);
114
+ namedImports.add(newTypeName);
114
115
  }
115
- outputsInterface.add(`import type { ${reference} as ${remappedReference} } from '${importLocation}';`);
116
116
  }
117
117
  });
118
118
  });
119
- const componentEvents = [
120
- '' // Empty first line
121
- ];
122
- // Generate outputs
123
- outputs.forEach((output, index) => {
124
- componentEvents.push(` /**
125
- * ${output.docs.text} ${output.docs.tags.map((tag) => `@${tag.name} ${tag.text}`)}
126
- */`);
127
- /**
128
- * The original attribute contains the original type defined by the devs.
129
- * This regexp normalizes the reference, by removing linebreaks,
130
- * replacing consecutive spaces with a single space, and adding a single space after commas.
131
- **/
132
- const outputTypeRemapped = Object.entries(outputReferenceRemap).reduce((type, [src, dst]) => {
133
- return type
134
- .replace(new RegExp(`^${src}$`, 'g'), `${dst}`)
135
- .replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].join(''));
136
- }, output.complexType.original
137
- .replace(/\n/g, ' ')
138
- .replace(/\s{2,}/g, ' ')
139
- .replace(/,\s*/g, ', '));
140
- componentEvents.push(` ${output.name}: EventEmitter<CustomEvent<${outputTypeRemapped.trim()}>>;`);
141
- if (index === outputs.length - 1) {
142
- // Empty line to push end `}` to new line
143
- componentEvents.push('\n');
144
- }
145
- });
146
- const lines = [
147
- '',
148
- `${[...outputsInterface].join('\n')}
149
- export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {${componentEvents.length > 1 ? componentEvents.join('\n') : ''}}
119
+ return imports.join('\n');
120
+ };
121
+ const EXTENDED_PATH_REGEX = /^\\\\\?\\/;
122
+ const NON_ASCII_REGEX = /[^\x00-\x80]+/;
123
+ const SLASH_REGEX = /\\/g;
150
124
 
151
- ${getProxyCmp(cmpMeta.tagName, includeImportCustomElements, inputs, methods)}
125
+ /**
126
+ * Creates an Angular component declaration from formatted Stencil compiler metadata.
127
+ *
128
+ * @param tagName The tag name of the component.
129
+ * @param inputs The inputs of the Stencil component (e.g. ['myInput']).
130
+ * @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']).
131
+ * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
132
+ * @param includeImportCustomElements Whether to define the component as a custom element.
133
+ * @returns The component declaration as a string.
134
+ */
135
+ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false) => {
136
+ const tagNameAsPascal = dashToPascalCase(tagName);
137
+ const hasInputs = inputs.length > 0;
138
+ const hasOutputs = outputs.length > 0;
139
+ const hasMethods = methods.length > 0;
140
+ // Formats the input strings into comma separated, single quoted values.
141
+ const formattedInputs = formatToQuotedList(inputs);
142
+ // Formats the output strings into comma separated, single quoted values.
143
+ const formattedOutputs = formatToQuotedList(outputs);
144
+ // Formats the method strings into comma separated, single quoted values.
145
+ const formattedMethods = formatToQuotedList(methods);
146
+ const proxyCmpOptions = [];
147
+ if (includeImportCustomElements) {
148
+ const defineCustomElementFn = `define${tagNameAsPascal}`;
149
+ proxyCmpOptions.push(`\n defineCustomElementFn: ${defineCustomElementFn}`);
150
+ }
151
+ if (hasInputs) {
152
+ proxyCmpOptions.push(`\n inputs: [${formattedInputs}]`);
153
+ }
154
+ if (hasMethods) {
155
+ proxyCmpOptions.push(`\n methods: [${formattedMethods}]`);
156
+ }
157
+ /**
158
+ * Notes on the generated output:
159
+ * - We disable @angular-eslint/no-inputs-metadata-property, so that
160
+ * Angular does not complain about the inputs property. The output target
161
+ * uses the inputs property to define the inputs of the component instead of
162
+ * having to use the @Input decorator (and manually define the type and default value).
163
+ */
164
+ const output = `@ProxyCmp({${proxyCmpOptions.join(',')}\n})
152
165
  @Component({
153
- ${directiveOpts.join(',\n ')}
166
+ selector: '${tagName}',
167
+ changeDetection: ChangeDetectionStrategy.OnPush,
168
+ template: '<ng-content></ng-content>',
169
+ // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
170
+ inputs: [${formattedInputs}],
154
171
  })
155
- export class ${tagNameAsPascal} {`,
156
- ];
157
- lines.push(' protected el: HTMLElement;');
158
- lines.push(` constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
172
+ export class ${tagNameAsPascal} {
173
+ protected el: HTMLElement;
174
+ constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
159
175
  c.detach();
160
- this.el = r.nativeElement;`);
161
- if (hasOutputs) {
162
- lines.push(` proxyOutputs(this, this.el, ['${outputs.map((output) => output.name).join(`', '`)}']);`);
176
+ this.el = r.nativeElement;${hasOutputs
177
+ ? `
178
+ proxyOutputs(this, this.el, [${formattedOutputs}]);`
179
+ : ''}
180
+ }
181
+ }`;
182
+ return output;
183
+ };
184
+ /**
185
+ * Sanitizes and formats the component event type.
186
+ * @param componentClassName The class name of the component (e.g. 'MyComponent')
187
+ * @param event The Stencil component event.
188
+ * @returns The sanitized event type as a string.
189
+ */
190
+ const formatOutputType = (componentClassName, event) => {
191
+ /**
192
+ * The original attribute contains the original type defined by the devs.
193
+ * This regexp normalizes the reference, by removing linebreaks,
194
+ * replacing consecutive spaces with a single space, and adding a single space after commas.
195
+ */
196
+ return Object.entries(event.complexType.references)
197
+ .filter(([_, refObject]) => refObject.location === 'local' || refObject.location === 'import')
198
+ .reduce((type, [src, dst]) => {
199
+ const renamedType = `I${componentClassName}${type}`;
200
+ return (renamedType
201
+ .replace(new RegExp(`^${src}$`, 'g'), `${dst}`)
202
+ // Capture all instances of the `src` field surrounded by non-word characters on each side and join them.
203
+ .replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (v, p1, p2) => [p1, dst, p2].join('')));
204
+ }, event.complexType.original
205
+ .replace(/\n/g, ' ')
206
+ .replace(/\s{2,}/g, ' ')
207
+ .replace(/,\s*/g, ', '));
208
+ };
209
+ /**
210
+ * Creates a formatted comment block based on the JS doc comment.
211
+ * @param doc The compiler jsdoc.
212
+ * @returns The formatted comment block as a string.
213
+ */
214
+ const createDocComment = (doc) => {
215
+ if (doc.text.trim().length === 0 && doc.tags.length === 0) {
216
+ return '';
163
217
  }
164
- lines.push(` }`);
165
- lines.push(`}`);
166
- return lines.join('\n');
218
+ return `/**
219
+ * ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)}
220
+ */`;
221
+ };
222
+ /**
223
+ * Creates the component interface type definition.
224
+ * @param tagNameAsPascal The tag name as PascalCase.
225
+ * @param events The events to generate the interface properties for.
226
+ * @param componentCorePackage The component core package.
227
+ * @param includeImportCustomElements Whether to include the import for the custom element definition.
228
+ * @param customElementsDir The custom elements directory.
229
+ * @returns The component interface type definition as a string.
230
+ */
231
+ const createComponentTypeDefinition = (tagNameAsPascal, events, componentCorePackage, includeImportCustomElements = false, customElementsDir) => {
232
+ const publicEvents = events.filter((ev) => !ev.internal);
233
+ const eventTypeImports = createComponentEventTypeImports(tagNameAsPascal, publicEvents, {
234
+ componentCorePackage,
235
+ includeImportCustomElements,
236
+ customElementsDir,
237
+ });
238
+ const eventTypes = publicEvents.map((event) => {
239
+ const comment = createDocComment(event.docs);
240
+ let eventName = event.name;
241
+ if (event.name.includes('-')) {
242
+ // If an event name includes a dash, we need to wrap it in quotes.
243
+ // https://github.com/ionic-team/stencil-ds-output-targets/issues/212
244
+ eventName = `'${event.name}'`;
245
+ }
246
+ return `${comment.length > 0 ? ` ${comment}` : ''}
247
+ ${eventName}: EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>;`;
248
+ });
249
+ const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
250
+ const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') +
251
+ `${interfaceDeclaration}${eventTypes.length === 0
252
+ ? '}'
253
+ : `
254
+ ${eventTypes.join('\n')}
255
+ }`}`;
256
+ return typeDefinition;
167
257
  };
168
- function getProxyCmp(tagName, includeCustomElement, inputs, methods) {
169
- const hasInputs = inputs.length > 0;
170
- const hasMethods = methods.length > 0;
171
- const proxMeta = [
172
- `defineCustomElementFn: ${includeCustomElement ? 'define' + dashToPascalCase(tagName) : 'undefined'}`
173
- ];
174
- if (hasInputs)
175
- proxMeta.push(`inputs: ['${inputs.join(`', '`)}']`);
176
- if (hasMethods)
177
- proxMeta.push(`methods: ['${methods.join(`', '`)}']`);
178
- return `@ProxyCmp({\n ${proxMeta.join(',\n ')}\n})`;
179
- }
180
258
 
181
259
  function generateAngularDirectivesFile(compilerCtx, components, outputTarget) {
182
260
  // Only create the file if it is defined in the stencil configuration
@@ -199,15 +277,12 @@ export const DIRECTIVES = [
199
277
  }
200
278
 
201
279
  async function generateValueAccessors(compilerCtx, components, outputTarget, config) {
202
- if (!Array.isArray(outputTarget.valueAccessorConfigs) ||
203
- outputTarget.valueAccessorConfigs.length === 0) {
280
+ if (!Array.isArray(outputTarget.valueAccessorConfigs) || outputTarget.valueAccessorConfigs.length === 0) {
204
281
  return;
205
282
  }
206
283
  const targetDir = path.dirname(outputTarget.directivesProxyFile);
207
284
  const normalizedValueAccessors = outputTarget.valueAccessorConfigs.reduce((allAccessors, va) => {
208
- const elementSelectors = Array.isArray(va.elementSelectors)
209
- ? va.elementSelectors
210
- : [va.elementSelectors];
285
+ const elementSelectors = Array.isArray(va.elementSelectors) ? va.elementSelectors : [va.elementSelectors];
211
286
  const type = va.type;
212
287
  let allElementSelectors = [];
213
288
  let allEventTargets = [];
@@ -256,6 +331,23 @@ const VALUE_ACCESSOR_EVENT = `<VALUE_ACCESSOR_EVENT>`;
256
331
  const VALUE_ACCESSOR_TARGETATTR = '<VALUE_ACCESSOR_TARGETATTR>';
257
332
  const VALUE_ACCESSOR_EVENTTARGETS = ` '(<VALUE_ACCESSOR_EVENT>)': 'handleChangeEvent($event.target.<VALUE_ACCESSOR_TARGETATTR>)'`;
258
333
 
334
+ /**
335
+ * Creates an Angular module declaration for a component wrapper.
336
+ * @param componentTagName The tag name of the Stencil component.
337
+ * @returns The Angular module declaration as a string.
338
+ */
339
+ const generateAngularModuleForComponent = (componentTagName) => {
340
+ const tagNameAsPascal = dashToPascalCase(componentTagName);
341
+ const componentClassName = `${tagNameAsPascal}`;
342
+ const moduleClassName = `${tagNameAsPascal}Module`;
343
+ const moduleDefinition = `@NgModule({
344
+ declarations: [${componentClassName}],
345
+ exports: [${componentClassName}]
346
+ })
347
+ export class ${moduleClassName} { }`;
348
+ return moduleDefinition;
349
+ };
350
+
259
351
  async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
260
352
  const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
261
353
  const rootDir = config.rootDir;
@@ -287,13 +379,34 @@ async function copyResources$1(config, outputTarget) {
287
379
  ], srcDirectory);
288
380
  }
289
381
  function generateProxies(components, pkgData, outputTarget, rootDir) {
382
+ var _a;
290
383
  const distTypesDir = path.dirname(pkgData.types);
291
384
  const dtsFilePath = path.join(rootDir, distTypesDir, GENERATED_DTS);
292
385
  const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts');
386
+ const includeSingleComponentAngularModules = (_a = outputTarget.includeSingleComponentAngularModules) !== null && _a !== void 0 ? _a : false;
387
+ /**
388
+ * The collection of named imports from @angular/core.
389
+ */
390
+ const angularCoreImports = [
391
+ 'ChangeDetectionStrategy',
392
+ 'ChangeDetectorRef',
393
+ 'Component',
394
+ 'ElementRef',
395
+ 'EventEmitter',
396
+ 'NgZone',
397
+ ];
398
+ /**
399
+ * The collection of named imports from the angular-component-lib/utils.
400
+ */
401
+ const componentLibImports = ['ProxyCmp', 'proxyOutputs'];
402
+ if (includeSingleComponentAngularModules) {
403
+ angularCoreImports.push('NgModule');
404
+ }
293
405
  const imports = `/* tslint:disable */
294
406
  /* auto-generated angular directive proxies */
295
- import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, NgZone } from '@angular/core';
296
- import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`;
407
+ ${createImportStatement(angularCoreImports, '@angular/core')}
408
+
409
+ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`;
297
410
  /**
298
411
  * Generate JSX import type from correct location.
299
412
  * When using custom elements build, we need to import from
@@ -301,8 +414,12 @@ import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`;
301
414
  * otherwise we risk bundlers pulling in lazy loaded imports.
302
415
  */
303
416
  const generateTypeImports = () => {
304
- let importLocation = outputTarget.componentCorePackage ? normalizePath(outputTarget.componentCorePackage) : normalizePath(componentsTypeFile);
305
- importLocation += outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : '';
417
+ let importLocation = outputTarget.componentCorePackage
418
+ ? normalizePath(outputTarget.componentCorePackage)
419
+ : normalizePath(componentsTypeFile);
420
+ importLocation += outputTarget.includeImportCustomElements
421
+ ? `/${outputTarget.customElementsDir || 'components'}`
422
+ : '';
306
423
  return `import ${outputTarget.includeImportCustomElements ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`;
307
424
  };
308
425
  const typeImports = generateTypeImports();
@@ -314,21 +431,56 @@ import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`;
314
431
  * IonButton React Component that takes in the Web Component as a parameter.
315
432
  */
316
433
  if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) {
317
- const cmpImports = components.map(component => {
434
+ const cmpImports = components.map((component) => {
318
435
  const pascalImport = dashToPascalCase(component.tagName);
319
- return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir ||
320
- 'components'}/${component.tagName}.js';`;
436
+ return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || 'components'}/${component.tagName}.js';`;
321
437
  });
322
438
  sourceImports = cmpImports.join('\n');
323
439
  }
324
- const final = [
325
- imports,
326
- typeImports,
327
- sourceImports,
328
- components
329
- .map(createComponentDefinition(outputTarget.componentCorePackage, distTypesDir, rootDir, outputTarget.includeImportCustomElements, outputTarget.customElementsDir))
330
- .join('\n'),
331
- ];
440
+ if (includeSingleComponentAngularModules) {
441
+ // Generating Angular modules is only supported in the dist-custom-elements build
442
+ if (!outputTarget.includeImportCustomElements) {
443
+ throw new Error('Generating single component Angular modules requires the "includeImportCustomElements" option to be set to true.');
444
+ }
445
+ }
446
+ const proxyFileOutput = [];
447
+ const filterInternalProps = (prop) => !prop.internal;
448
+ const mapPropName = (prop) => prop.name;
449
+ const { includeImportCustomElements, componentCorePackage, customElementsDir } = outputTarget;
450
+ for (let cmpMeta of components) {
451
+ const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
452
+ const inputs = [];
453
+ if (cmpMeta.properties) {
454
+ inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName));
455
+ }
456
+ if (cmpMeta.virtualProperties) {
457
+ inputs.push(...cmpMeta.virtualProperties.map(mapPropName));
458
+ }
459
+ inputs.sort();
460
+ const outputs = [];
461
+ if (cmpMeta.events) {
462
+ outputs.push(...cmpMeta.events.filter(filterInternalProps).map(mapPropName));
463
+ }
464
+ const methods = [];
465
+ if (cmpMeta.methods) {
466
+ methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
467
+ }
468
+ /**
469
+ * For each component, we need to generate:
470
+ * 1. The @Component decorated class
471
+ * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
472
+ * 3. The component interface (using declaration merging for types).
473
+ */
474
+ const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, includeImportCustomElements);
475
+ const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
476
+ const componentTypeDefinition = createComponentTypeDefinition(tagNameAsPascal, cmpMeta.events, componentCorePackage, includeImportCustomElements, customElementsDir);
477
+ proxyFileOutput.push(componentDefinition, '\n');
478
+ if (includeSingleComponentAngularModules) {
479
+ proxyFileOutput.push(moduleDefinition, '\n');
480
+ }
481
+ proxyFileOutput.push(componentTypeDefinition, '\n');
482
+ }
483
+ const final = [imports, typeImports, sourceImports, ...proxyFileOutput];
332
484
  return final.join('\n') + '\n';
333
485
  }
334
486
  const GENERATED_DTS = 'components.d.ts';
@@ -347,12 +499,12 @@ const angularOutputTarget = (outputTarget) => ({
347
499
  },
348
500
  });
349
501
  function normalizeOutputTarget(config, outputTarget) {
350
- const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfig: outputTarget.valueAccessorConfig || [] });
502
+ const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfigs: outputTarget.valueAccessorConfigs || [] });
351
503
  if (config.rootDir == null) {
352
504
  throw new Error('rootDir is not set and it should be set by stencil itself');
353
505
  }
354
506
  if (outputTarget.directivesProxyFile == null) {
355
- throw new Error('directivesProxyFile is required');
507
+ throw new Error('directivesProxyFile is required. Please set it in the Stencil config.');
356
508
  }
357
509
  if (outputTarget.directivesProxyFile && !path.isAbsolute(outputTarget.directivesProxyFile)) {
358
510
  results.directivesProxyFile = normalizePath(path.join(config.rootDir, outputTarget.directivesProxyFile));
@@ -360,8 +512,8 @@ function normalizeOutputTarget(config, outputTarget) {
360
512
  if (outputTarget.directivesArrayFile && !path.isAbsolute(outputTarget.directivesArrayFile)) {
361
513
  results.directivesArrayFile = normalizePath(path.join(config.rootDir, outputTarget.directivesArrayFile));
362
514
  }
363
- if (outputTarget.directivesUtilsFile && !path.isAbsolute(outputTarget.directivesUtilsFile)) {
364
- results.directivesUtilsFile = normalizePath(path.join(config.rootDir, outputTarget.directivesUtilsFile));
515
+ if (outputTarget.includeSingleComponentAngularModules !== undefined) {
516
+ console.warn('**Experimental**: includeSingleComponentAngularModules is a developer preview feature and may change or be removed in the future.');
365
517
  }
366
518
  return results;
367
519
  }
@@ -1,8 +1,9 @@
1
1
  import path from 'path';
2
- import { relativeImport, normalizePath, sortBy, readPackageJson, dashToPascalCase } from './utils';
3
- import { createComponentDefinition } from './generate-angular-component';
2
+ import { relativeImport, normalizePath, sortBy, readPackageJson, dashToPascalCase, createImportStatement, } from './utils';
3
+ import { createAngularComponentDefinition, createComponentTypeDefinition } from './generate-angular-component';
4
4
  import { generateAngularDirectivesFile } from './generate-angular-directives-file';
5
5
  import generateValueAccessors from './generate-value-accessors';
6
+ import { generateAngularModuleForComponent } from './generate-angular-modules';
6
7
  export async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
7
8
  const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
8
9
  const rootDir = config.rootDir;
@@ -34,13 +35,34 @@ async function copyResources(config, outputTarget) {
34
35
  ], srcDirectory);
35
36
  }
36
37
  export function generateProxies(components, pkgData, outputTarget, rootDir) {
38
+ var _a;
37
39
  const distTypesDir = path.dirname(pkgData.types);
38
40
  const dtsFilePath = path.join(rootDir, distTypesDir, GENERATED_DTS);
39
41
  const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts');
42
+ const includeSingleComponentAngularModules = (_a = outputTarget.includeSingleComponentAngularModules) !== null && _a !== void 0 ? _a : false;
43
+ /**
44
+ * The collection of named imports from @angular/core.
45
+ */
46
+ const angularCoreImports = [
47
+ 'ChangeDetectionStrategy',
48
+ 'ChangeDetectorRef',
49
+ 'Component',
50
+ 'ElementRef',
51
+ 'EventEmitter',
52
+ 'NgZone',
53
+ ];
54
+ /**
55
+ * The collection of named imports from the angular-component-lib/utils.
56
+ */
57
+ const componentLibImports = ['ProxyCmp', 'proxyOutputs'];
58
+ if (includeSingleComponentAngularModules) {
59
+ angularCoreImports.push('NgModule');
60
+ }
40
61
  const imports = `/* tslint:disable */
41
62
  /* auto-generated angular directive proxies */
42
- import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, NgZone } from '@angular/core';
43
- import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`;
63
+ ${createImportStatement(angularCoreImports, '@angular/core')}
64
+
65
+ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`;
44
66
  /**
45
67
  * Generate JSX import type from correct location.
46
68
  * When using custom elements build, we need to import from
@@ -48,8 +70,12 @@ import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`;
48
70
  * otherwise we risk bundlers pulling in lazy loaded imports.
49
71
  */
50
72
  const generateTypeImports = () => {
51
- let importLocation = outputTarget.componentCorePackage ? normalizePath(outputTarget.componentCorePackage) : normalizePath(componentsTypeFile);
52
- importLocation += outputTarget.includeImportCustomElements ? `/${outputTarget.customElementsDir || 'components'}` : '';
73
+ let importLocation = outputTarget.componentCorePackage
74
+ ? normalizePath(outputTarget.componentCorePackage)
75
+ : normalizePath(componentsTypeFile);
76
+ importLocation += outputTarget.includeImportCustomElements
77
+ ? `/${outputTarget.customElementsDir || 'components'}`
78
+ : '';
53
79
  return `import ${outputTarget.includeImportCustomElements ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`;
54
80
  };
55
81
  const typeImports = generateTypeImports();
@@ -61,21 +87,56 @@ import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';\n`;
61
87
  * IonButton React Component that takes in the Web Component as a parameter.
62
88
  */
63
89
  if (outputTarget.includeImportCustomElements && outputTarget.componentCorePackage !== undefined) {
64
- const cmpImports = components.map(component => {
90
+ const cmpImports = components.map((component) => {
65
91
  const pascalImport = dashToPascalCase(component.tagName);
66
- return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir ||
67
- 'components'}/${component.tagName}.js';`;
92
+ return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir || 'components'}/${component.tagName}.js';`;
68
93
  });
69
94
  sourceImports = cmpImports.join('\n');
70
95
  }
71
- const final = [
72
- imports,
73
- typeImports,
74
- sourceImports,
75
- components
76
- .map(createComponentDefinition(outputTarget.componentCorePackage, distTypesDir, rootDir, outputTarget.includeImportCustomElements, outputTarget.customElementsDir))
77
- .join('\n'),
78
- ];
96
+ if (includeSingleComponentAngularModules) {
97
+ // Generating Angular modules is only supported in the dist-custom-elements build
98
+ if (!outputTarget.includeImportCustomElements) {
99
+ throw new Error('Generating single component Angular modules requires the "includeImportCustomElements" option to be set to true.');
100
+ }
101
+ }
102
+ const proxyFileOutput = [];
103
+ const filterInternalProps = (prop) => !prop.internal;
104
+ const mapPropName = (prop) => prop.name;
105
+ const { includeImportCustomElements, componentCorePackage, customElementsDir } = outputTarget;
106
+ for (let cmpMeta of components) {
107
+ const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
108
+ const inputs = [];
109
+ if (cmpMeta.properties) {
110
+ inputs.push(...cmpMeta.properties.filter(filterInternalProps).map(mapPropName));
111
+ }
112
+ if (cmpMeta.virtualProperties) {
113
+ inputs.push(...cmpMeta.virtualProperties.map(mapPropName));
114
+ }
115
+ inputs.sort();
116
+ const outputs = [];
117
+ if (cmpMeta.events) {
118
+ outputs.push(...cmpMeta.events.filter(filterInternalProps).map(mapPropName));
119
+ }
120
+ const methods = [];
121
+ if (cmpMeta.methods) {
122
+ methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
123
+ }
124
+ /**
125
+ * For each component, we need to generate:
126
+ * 1. The @Component decorated class
127
+ * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
128
+ * 3. The component interface (using declaration merging for types).
129
+ */
130
+ const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, inputs, outputs, methods, includeImportCustomElements);
131
+ const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
132
+ const componentTypeDefinition = createComponentTypeDefinition(tagNameAsPascal, cmpMeta.events, componentCorePackage, includeImportCustomElements, customElementsDir);
133
+ proxyFileOutput.push(componentDefinition, '\n');
134
+ if (includeSingleComponentAngularModules) {
135
+ proxyFileOutput.push(moduleDefinition, '\n');
136
+ }
137
+ proxyFileOutput.push(componentTypeDefinition, '\n');
138
+ }
139
+ const final = [imports, typeImports, sourceImports, ...proxyFileOutput];
79
140
  return final.join('\n') + '\n';
80
141
  }
81
142
  const GENERATED_DTS = 'components.d.ts';
package/dist/plugin.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { Config, OutputTargetCustom } from '@stencil/core/internal';
2
2
  import type { OutputTargetAngular } from './types';
3
3
  export declare const angularOutputTarget: (outputTarget: OutputTargetAngular) => OutputTargetCustom;
4
- export declare function normalizeOutputTarget(config: Config, outputTarget: any): OutputTargetAngular;
4
+ export declare function normalizeOutputTarget(config: Config, outputTarget: OutputTargetAngular): OutputTargetAngular;
package/dist/plugin.js CHANGED
@@ -14,12 +14,12 @@ export const angularOutputTarget = (outputTarget) => ({
14
14
  },
15
15
  });
16
16
  export function normalizeOutputTarget(config, outputTarget) {
17
- const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfig: outputTarget.valueAccessorConfig || [] });
17
+ const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfigs: outputTarget.valueAccessorConfigs || [] });
18
18
  if (config.rootDir == null) {
19
19
  throw new Error('rootDir is not set and it should be set by stencil itself');
20
20
  }
21
21
  if (outputTarget.directivesProxyFile == null) {
22
- throw new Error('directivesProxyFile is required');
22
+ throw new Error('directivesProxyFile is required. Please set it in the Stencil config.');
23
23
  }
24
24
  if (outputTarget.directivesProxyFile && !path.isAbsolute(outputTarget.directivesProxyFile)) {
25
25
  results.directivesProxyFile = normalizePath(path.join(config.rootDir, outputTarget.directivesProxyFile));
@@ -27,8 +27,8 @@ export function normalizeOutputTarget(config, outputTarget) {
27
27
  if (outputTarget.directivesArrayFile && !path.isAbsolute(outputTarget.directivesArrayFile)) {
28
28
  results.directivesArrayFile = normalizePath(path.join(config.rootDir, outputTarget.directivesArrayFile));
29
29
  }
30
- if (outputTarget.directivesUtilsFile && !path.isAbsolute(outputTarget.directivesUtilsFile)) {
31
- results.directivesUtilsFile = normalizePath(path.join(config.rootDir, outputTarget.directivesUtilsFile));
30
+ if (outputTarget.includeSingleComponentAngularModules !== undefined) {
31
+ console.warn('**Experimental**: includeSingleComponentAngularModules is a developer preview feature and may change or be removed in the future.');
32
32
  }
33
33
  return results;
34
34
  }