@stencil/angular-output-target 1.1.0 → 1.2.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.cjs.js CHANGED
@@ -194,15 +194,16 @@ function formatInputs(inputs) {
194
194
  *
195
195
  * @param tagName The tag name of the component.
196
196
  * @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
197
- * @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']).
198
197
  * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
199
198
  * @param includeImportCustomElements Whether to define the component as a custom element.
200
199
  * @param standalone Whether to define the component as a standalone component.
201
200
  * @param inlineComponentProps List of properties that should be inlined into the component definition.
201
+ * @param events The events of the Stencil component for generating outputs.
202
202
  * @returns The component declaration as a string.
203
203
  */
204
- const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = []) => {
204
+ const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = []) => {
205
205
  const tagNameAsPascal = dashToPascalCase(tagName);
206
+ const outputs = events.filter((event) => !event.internal).map((event) => event.name);
206
207
  const hasInputs = inputs.length > 0;
207
208
  const hasOutputs = outputs.length > 0;
208
209
  const hasMethods = methods.length > 0;
@@ -231,7 +232,18 @@ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, inc
231
232
  standaloneOption = `\n standalone: false`;
232
233
  }
233
234
  const propertyDeclarations = inlineComponentProps.map((m) => createPropertyDeclaration(m, `Components.${tagNameAsPascal}['${m.name}']`, true));
234
- const propertiesDeclarationText = [`protected el: HTML${tagNameAsPascal}Element;`, ...propertyDeclarations].join('\n ');
235
+ const outputDeclarations = events
236
+ .filter((event) => !event.internal)
237
+ .map((event) => {
238
+ const camelCaseOutput = event.name.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
239
+ const outputType = `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`;
240
+ return `@Output() ${camelCaseOutput} = new ${outputType}();`;
241
+ });
242
+ const propertiesDeclarationText = [
243
+ `protected el: HTML${tagNameAsPascal}Element;`,
244
+ ...propertyDeclarations,
245
+ ...outputDeclarations,
246
+ ].join('\n ');
235
247
  /**
236
248
  * Notes on the generated output:
237
249
  * - We disable @angular-eslint/no-inputs-metadata-property, so that
@@ -245,16 +257,13 @@ const createAngularComponentDefinition = (tagName, inputs, outputs, methods, inc
245
257
  changeDetection: ChangeDetectionStrategy.OnPush,
246
258
  template: '<ng-content></ng-content>',
247
259
  // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
248
- inputs: [${formattedInputs}],${standaloneOption}
260
+ inputs: [${formattedInputs}],${hasOutputs ? `\n outputs: [${formattedOutputs}],` : ''}${standaloneOption}
249
261
  })
250
262
  export class ${tagNameAsPascal} {
251
263
  ${propertiesDeclarationText}
252
264
  constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
253
265
  c.detach();
254
- this.el = r.nativeElement;${hasOutputs
255
- ? `
256
- proxyOutputs(this, this.el, [${formattedOutputs}]);`
257
- : ''}
266
+ this.el = r.nativeElement;
258
267
  }
259
268
  }`;
260
269
  return output;
@@ -452,17 +461,348 @@ export class ${moduleClassName} { }`;
452
461
  return moduleDefinition;
453
462
  };
454
463
 
464
+ /**
465
+ * Generates the patch-transform-selectors.mjs script for Angular transformTag support.
466
+ * This script patches component selectors in the built Angular library to use the
467
+ * transformed tag names (e.g., 'my-component' -> 'v1-my-component').
468
+ */
469
+ async function generateTransformTagScript(compilerCtx, components, outputTarget, packageName) {
470
+ const scriptsDirectory = path__default["default"].join(path__default["default"].dirname(outputTarget.directivesProxyFile), '../../scripts');
471
+ const customElementsDir = outputTarget.customElementsDir || 'dist/components';
472
+ const stencilImportPath = `${outputTarget.componentCorePackage}/${customElementsDir}/index.js`;
473
+ // Generate the mappings object
474
+ const mappings = components
475
+ .map((component) => {
476
+ const tagName = component.tagName;
477
+ const pascalName = dashToPascalCase(tagName);
478
+ return ` '${tagName}': '${pascalName}'`;
479
+ })
480
+ .join(',\n');
481
+ // Generate selector patcher script
482
+ const patchSelectorsContent = `#!/usr/bin/env node
483
+ /* eslint-disable */
484
+ /* tslint:disable */
485
+ /**
486
+ * Selector Patcher for transformTag support
487
+ *
488
+ * AUTO-GENERATED - DO NOT EDIT
489
+ *
490
+ * This script patches @Component selectors in the installed Angular component library
491
+ * to match your runtime tag transformer. Run this as a postinstall script in your app.
492
+ *
493
+ * Usage Option 1 - Config file (recommended for complex transformers):
494
+ * Create tag-transformer.config.mjs in your app root:
495
+ * export default (tag) => {
496
+ * if (tag.startsWith('my-transform-')) return \`v1-\${tag}\`;
497
+ * // ... complex logic
498
+ * return tag;
499
+ * };
500
+ *
501
+ * Then in package.json:
502
+ * "scripts": {
503
+ * "postinstall": "patch-transform-selectors"
504
+ * }
505
+ *
506
+ * Usage Option 2 - CLI argument (for simple transformers):
507
+ * "scripts": {
508
+ * "postinstall": "patch-transform-selectors \\"(tag) => tag.startsWith('my-transform-') ? \\\\\`v1-\\\${tag}\\\\\` : tag\\""
509
+ * }
510
+ */
511
+
512
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
513
+ import { join, dirname } from 'path';
514
+ import { fileURLToPath, pathToFileURL } from 'url';
515
+
516
+ const __filename = fileURLToPath(import.meta.url);
517
+ const __dirname = dirname(__filename);
518
+
519
+ // Try to load transformer from config file or CLI argument
520
+ let TAG_TRANSFORMER;
521
+ let transformerArg;
522
+
523
+ // Option 1: Look for tag-transformer.config.mjs in the consuming app
524
+ const configPath = join(process.cwd(), 'tag-transformer.config.mjs');
525
+ if (existsSync(configPath)) {
526
+ console.log('[TransformTag] Loading transformer from tag-transformer.config.mjs');
527
+ try {
528
+ const configUrl = pathToFileURL(configPath).href;
529
+ const config = await import(configUrl);
530
+ TAG_TRANSFORMER = config.default;
531
+
532
+ if (typeof TAG_TRANSFORMER !== 'function') {
533
+ throw new Error('Config file must export a default function');
534
+ }
535
+
536
+ // Store as string for injection later
537
+ transformerArg = TAG_TRANSFORMER.toString();
538
+ console.log('[TransformTag] Loaded transformer from config file');
539
+ } catch (error) {
540
+ console.error('[TransformTag] Error loading tag-transformer.config.mjs:', error.message);
541
+ console.error('Make sure the file exports a default function.');
542
+ process.exit(1);
543
+ }
544
+ } else {
545
+ // Option 2: Fall back to CLI argument
546
+ transformerArg = process.argv[2];
547
+
548
+ if (!transformerArg) {
549
+ console.error('[TransformTag] Error: No transformer provided.');
550
+ console.error('');
551
+ console.error('Option 1 - Create tag-transformer.config.mjs in your app root:');
552
+ console.error(' export default (tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag;');
553
+ console.error('');
554
+ console.error('Option 2 - Pass transformer as CLI argument:');
555
+ console.error(' patch-transform-selectors "(tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag"');
556
+ process.exit(1);
557
+ }
558
+
559
+ // Evaluate the transformer string to get the function
560
+ try {
561
+ TAG_TRANSFORMER = eval(transformerArg);
562
+ if (typeof TAG_TRANSFORMER !== 'function') {
563
+ throw new Error('Transformer must be a function');
564
+ }
565
+ console.log('[TransformTag] Using transformer from CLI argument');
566
+ } catch (error) {
567
+ console.error('[TransformTag] Error: Invalid transformer function:', error.message);
568
+ console.error('The transformer must be a valid JavaScript function expression.');
569
+ console.error('Example: "(tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag"');
570
+ process.exit(1);
571
+ }
572
+ }
573
+
574
+ const TAG_MAPPINGS = {
575
+ ${mappings}
576
+ };
577
+
578
+ console.log('[TransformTag] Patching component selectors...');
579
+
580
+ try {
581
+ // Find the bundled JavaScript file (could be fesm2022, fesm2015, fesm5, etc.)
582
+ const parentDir = join(__dirname, '..');
583
+
584
+ // Find all .js/.mjs files in fesm* directories AND fesm*.js/mjs files at root
585
+ let bundlePaths = [];
586
+
587
+ try {
588
+ const entries = readdirSync(parentDir);
589
+ for (const entry of entries) {
590
+ const entryPath = join(parentDir, entry);
591
+ let stat;
592
+ try {
593
+ stat = statSync(entryPath);
594
+ } catch (e) {
595
+ continue;
596
+ }
597
+
598
+ // Check for fesm* directories
599
+ if (stat.isDirectory() && /^fesm/.test(entry)) {
600
+ try {
601
+ const fesmFiles = readdirSync(entryPath);
602
+ for (const file of fesmFiles) {
603
+ if (/\\.m?js$/.test(file)) {
604
+ bundlePaths.push(join(entryPath, file));
605
+ }
606
+ }
607
+ } catch (e) {
608
+ // Skip if can't read fesm directory
609
+ }
610
+ }
611
+ // Check for fesm*.js or fesm*.mjs files at root
612
+ else if (stat.isFile() && /^fesm.*\\.m?js$/.test(entry)) {
613
+ bundlePaths.push(entryPath);
614
+ }
615
+ }
616
+ } catch (e) {
617
+ console.error('[TransformTag] Could not read parent directory:', parentDir);
618
+ process.exit(1);
619
+ }
620
+
621
+ if (bundlePaths.length === 0) {
622
+ console.error('[TransformTag] Could not find any fesm* directories or files to patch.');
623
+ process.exit(1);
624
+ }
625
+
626
+ console.log('[TransformTag] Found bundles:', bundlePaths);
627
+
628
+ // Patch all bundled JavaScript files
629
+ let totalPatchedCount = 0;
630
+
631
+ for (const bundlePath of bundlePaths) {
632
+ let bundleContent;
633
+ try {
634
+ bundleContent = readFileSync(bundlePath, 'utf8');
635
+ } catch (e) {
636
+ console.error('[TransformTag] Could not read bundle:', bundlePath);
637
+ continue;
638
+ }
639
+
640
+ let patchedCount = 0;
641
+
642
+ for (const [originalTag, pascalName] of Object.entries(TAG_MAPPINGS)) {
643
+ const transformedTag = TAG_TRANSFORMER(originalTag);
644
+
645
+ // Only patch if the tag is actually transformed
646
+ if (transformedTag !== originalTag) {
647
+ // Update selector from original tag name to transformed tag name
648
+ // e.g., selector: 'my-transform-test' becomes selector: 'v1-my-transform-test'
649
+ const selectorRegex = new RegExp(
650
+ \`(selector:\\\\s*)(['"\\\`])\${originalTag}\\\\2\`,
651
+ 'g'
652
+ );
653
+
654
+ const newContent = bundleContent.replace(
655
+ selectorRegex,
656
+ \`$1'\${transformedTag}'\`
657
+ );
658
+
659
+ if (newContent !== bundleContent) {
660
+ bundleContent = newContent;
661
+ patchedCount++;
662
+ console.log(\`[TransformTag] Patched selector for \${originalTag} -> \${transformedTag}\`);
663
+ }
664
+ }
665
+ }
666
+
667
+ // Inject setTagTransformer call with the user's transformer
668
+ // Find the export statement and add the call before it
669
+ const exportMatch = bundleContent.match(/export \\{ setTagTransformer/);
670
+ if (exportMatch && patchedCount > 0) {
671
+ const transformerCode = \`
672
+ // Auto-injected by patch-transform-selectors
673
+ // Call setTagTransformer with the user-provided transformer
674
+ import { setTagTransformer as stencilSetTagTransformer } from '${stencilImportPath}';
675
+ stencilSetTagTransformer(\${transformerArg});
676
+ \`;
677
+ bundleContent = transformerCode + bundleContent;
678
+ console.log('[TransformTag] Injected setTagTransformer call into bundle');
679
+ }
680
+
681
+ // Write the patched bundle
682
+ if (patchedCount > 0) {
683
+ writeFileSync(bundlePath, bundleContent);
684
+ totalPatchedCount += patchedCount;
685
+ console.log(\`[TransformTag] Successfully patched \${patchedCount} component selectors in \${bundlePath}\`);
686
+ }
687
+ }
688
+
689
+ // Find and patch all .d.ts files
690
+ let totalTypePatchedCount = 0;
691
+
692
+ function patchTypeDefsInDir(dir) {
693
+ let files;
694
+ try {
695
+ files = readdirSync(dir);
696
+ } catch (e) {
697
+ return;
698
+ }
699
+
700
+ for (const file of files) {
701
+ const filePath = join(dir, file);
702
+ let stat;
703
+ try {
704
+ stat = statSync(filePath);
705
+ } catch (e) {
706
+ continue;
707
+ }
708
+
709
+ if (stat.isDirectory()) {
710
+ patchTypeDefsInDir(filePath);
711
+ } else if (file.endsWith('.d.ts')) {
712
+ let typeDefsContent;
713
+ try {
714
+ typeDefsContent = readFileSync(filePath, 'utf8');
715
+ } catch (e) {
716
+ continue;
717
+ }
718
+
719
+ let modified = false;
720
+
721
+ for (const [originalTag, pascalName] of Object.entries(TAG_MAPPINGS)) {
722
+ const transformedTag = TAG_TRANSFORMER(originalTag);
723
+
724
+ if (transformedTag !== originalTag) {
725
+ // Update selector in type definitions - format: ɵɵComponentDeclaration<ClassName, "tag-name", ...>
726
+ const typeDefRegex = new RegExp(
727
+ \`(ɵɵComponentDeclaration<\${pascalName},\\\\s*)"(\${originalTag})"\`,
728
+ 'g'
729
+ );
730
+
731
+ const newTypeContent = typeDefsContent.replace(
732
+ typeDefRegex,
733
+ \`$1"\${transformedTag}"\`
734
+ );
735
+
736
+ if (newTypeContent !== typeDefsContent) {
737
+ typeDefsContent = newTypeContent;
738
+ modified = true;
739
+ }
740
+ }
741
+ }
742
+
743
+ if (modified) {
744
+ writeFileSync(filePath, typeDefsContent);
745
+ totalTypePatchedCount++;
746
+ console.log(\`[TransformTag] Patched type definitions in: \${filePath}\`);
747
+ }
748
+ }
749
+ }
750
+ }
751
+
752
+ patchTypeDefsInDir(parentDir);
753
+
754
+ if (totalTypePatchedCount > 0) {
755
+ console.log(\`[TransformTag] Successfully patched selectors in \${totalTypePatchedCount} type definition files.\`);
756
+ }
757
+
758
+ if (totalPatchedCount === 0 && totalTypePatchedCount === 0) {
759
+ console.log('[TransformTag] No selectors needed patching.');
760
+ }
761
+ } catch (error) {
762
+ console.error('[TransformTag] Error patching selectors:', error.message);
763
+ console.error('Stack:', error.stack);
764
+ process.exit(1);
765
+ }
766
+ `;
767
+ await compilerCtx.fs.writeFile(path__default["default"].join(scriptsDirectory, 'patch-transform-selectors.mjs'), patchSelectorsContent);
768
+ }
769
+
455
770
  async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
456
771
  const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
457
772
  const rootDir = config.rootDir;
458
773
  const pkgData = await readPackageJson(config, rootDir);
459
774
  const finalText = generateProxies(filteredComponents, pkgData, outputTarget, config.rootDir);
460
- await Promise.all([
775
+ const tasks = [
461
776
  compilerCtx.fs.writeFile(outputTarget.directivesProxyFile, finalText),
462
777
  copyResources(config, outputTarget),
463
778
  generateAngularDirectivesFile(compilerCtx, filteredComponents, outputTarget),
464
779
  generateValueAccessors(compilerCtx, filteredComponents, outputTarget, config),
465
- ]);
780
+ ];
781
+ // Generate transformer script if transformTag is enabled
782
+ if (outputTarget.transformTag) {
783
+ // Read the Angular library's package.json to get its name
784
+ // directivesProxyFile is like: projects/library/src/directives/proxies.ts
785
+ // We need to go up to: projects/library/package.json
786
+ const angularLibraryDir = path__default["default"].dirname(path__default["default"].dirname(path__default["default"].dirname(outputTarget.directivesProxyFile)));
787
+ const angularPkgJsonPath = path__default["default"].join(angularLibraryDir, 'package.json');
788
+ let angularPackageName = '';
789
+ try {
790
+ const angularPkgJson = JSON.parse(await compilerCtx.fs.readFile(angularPkgJsonPath));
791
+ if (angularPkgJson.name) {
792
+ angularPackageName = angularPkgJson.name;
793
+ }
794
+ }
795
+ catch (e) {
796
+ throw new Error(`Could not read Angular library package.json at ${angularPkgJsonPath}. ` +
797
+ `The package name is required to generate the transformTag patch script.`);
798
+ }
799
+ if (!angularPackageName) {
800
+ throw new Error(`Angular library package.json at ${angularPkgJsonPath} does not have a "name" field. ` +
801
+ `The package name is required to generate the transformTag patch script.`);
802
+ }
803
+ tasks.push(generateTransformTagScript(compilerCtx, filteredComponents, outputTarget));
804
+ }
805
+ await Promise.all(tasks);
466
806
  }
467
807
  function getFilteredComponents(excludeComponents = [], cmps) {
468
808
  return sortBy(cmps, (cmp) => cmp.tagName).filter((c) => !excludeComponents.includes(c.tagName) && !c.internal);
@@ -497,16 +837,13 @@ function generateProxies(components, pkgData, outputTarget, rootDir) {
497
837
  */
498
838
  const angularCoreImports = ['ChangeDetectionStrategy', 'ChangeDetectorRef', 'Component', 'ElementRef'];
499
839
  if (includeOutputImports) {
500
- angularCoreImports.push('EventEmitter');
840
+ angularCoreImports.push('EventEmitter', 'Output');
501
841
  }
502
842
  angularCoreImports.push('NgZone');
503
843
  /**
504
844
  * The collection of named imports from the angular-component-lib/utils.
505
845
  */
506
846
  const componentLibImports = ['ProxyCmp'];
507
- if (includeOutputImports) {
508
- componentLibImports.push('proxyOutputs');
509
- }
510
847
  if (includeSingleComponentAngularModules) {
511
848
  angularCoreImports.push('NgModule');
512
849
  }
@@ -565,10 +902,6 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
565
902
  inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
566
903
  }
567
904
  const orderedInputs = sortBy(inputs, (cip) => cip.name);
568
- const outputs = [];
569
- if (cmpMeta.events) {
570
- outputs.push(...cmpMeta.events.filter(filterInternalProps).map(mapPropName));
571
- }
572
905
  const methods = [];
573
906
  if (cmpMeta.methods) {
574
907
  methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
@@ -580,7 +913,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
580
913
  * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
581
914
  * 3. The component interface (using declaration merging for types).
582
915
  */
583
- const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, outputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps);
916
+ const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
584
917
  const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
585
918
  const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir);
586
919
  proxyFileOutput.push(componentDefinition, '\n');