@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.
@@ -5,14 +5,14 @@ import type { ComponentInputProperty, OutputType } from './types';
5
5
  *
6
6
  * @param tagName The tag name of the component.
7
7
  * @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
8
- * @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']).
9
8
  * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
10
9
  * @param includeImportCustomElements Whether to define the component as a custom element.
11
10
  * @param standalone Whether to define the component as a standalone component.
12
11
  * @param inlineComponentProps List of properties that should be inlined into the component definition.
12
+ * @param events The events of the Stencil component for generating outputs.
13
13
  * @returns The component declaration as a string.
14
14
  */
15
- export declare const createAngularComponentDefinition: (tagName: string, inputs: readonly ComponentInputProperty[], outputs: readonly string[], methods: readonly string[], includeImportCustomElements?: boolean, standalone?: boolean, inlineComponentProps?: readonly ComponentCompilerProperty[]) => string;
15
+ export declare const createAngularComponentDefinition: (tagName: string, inputs: readonly ComponentInputProperty[], methods: readonly string[], includeImportCustomElements?: boolean, standalone?: boolean, inlineComponentProps?: readonly ComponentCompilerProperty[], events?: readonly ComponentCompilerEvent[]) => string;
16
16
  /**
17
17
  * Creates the component interface type definition.
18
18
  * @param outputType The output type.
@@ -48,15 +48,16 @@ function formatInputs(inputs) {
48
48
  *
49
49
  * @param tagName The tag name of the component.
50
50
  * @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
51
- * @param outputs The outputs/events of the Stencil component. (e.g. ['myOutput']).
52
51
  * @param methods The methods of the Stencil component. (e.g. ['myMethod']).
53
52
  * @param includeImportCustomElements Whether to define the component as a custom element.
54
53
  * @param standalone Whether to define the component as a standalone component.
55
54
  * @param inlineComponentProps List of properties that should be inlined into the component definition.
55
+ * @param events The events of the Stencil component for generating outputs.
56
56
  * @returns The component declaration as a string.
57
57
  */
58
- export const createAngularComponentDefinition = (tagName, inputs, outputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = []) => {
58
+ export const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = []) => {
59
59
  const tagNameAsPascal = dashToPascalCase(tagName);
60
+ const outputs = events.filter((event) => !event.internal).map((event) => event.name);
60
61
  const hasInputs = inputs.length > 0;
61
62
  const hasOutputs = outputs.length > 0;
62
63
  const hasMethods = methods.length > 0;
@@ -85,7 +86,18 @@ export const createAngularComponentDefinition = (tagName, inputs, outputs, metho
85
86
  standaloneOption = `\n standalone: false`;
86
87
  }
87
88
  const propertyDeclarations = inlineComponentProps.map((m) => createPropertyDeclaration(m, `Components.${tagNameAsPascal}['${m.name}']`, true));
88
- const propertiesDeclarationText = [`protected el: HTML${tagNameAsPascal}Element;`, ...propertyDeclarations].join('\n ');
89
+ const outputDeclarations = events
90
+ .filter((event) => !event.internal)
91
+ .map((event) => {
92
+ const camelCaseOutput = event.name.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
93
+ const outputType = `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`;
94
+ return `@Output() ${camelCaseOutput} = new ${outputType}();`;
95
+ });
96
+ const propertiesDeclarationText = [
97
+ `protected el: HTML${tagNameAsPascal}Element;`,
98
+ ...propertyDeclarations,
99
+ ...outputDeclarations,
100
+ ].join('\n ');
89
101
  /**
90
102
  * Notes on the generated output:
91
103
  * - We disable @angular-eslint/no-inputs-metadata-property, so that
@@ -99,16 +111,13 @@ export const createAngularComponentDefinition = (tagName, inputs, outputs, metho
99
111
  changeDetection: ChangeDetectionStrategy.OnPush,
100
112
  template: '<ng-content></ng-content>',
101
113
  // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
102
- inputs: [${formattedInputs}],${standaloneOption}
114
+ inputs: [${formattedInputs}],${hasOutputs ? `\n outputs: [${formattedOutputs}],` : ''}${standaloneOption}
103
115
  })
104
116
  export class ${tagNameAsPascal} {
105
117
  ${propertiesDeclarationText}
106
118
  constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
107
119
  c.detach();
108
- this.el = r.nativeElement;${hasOutputs
109
- ? `
110
- proxyOutputs(this, this.el, [${formattedOutputs}]);`
111
- : ''}
120
+ this.el = r.nativeElement;
112
121
  }
113
122
  }`;
114
123
  return output;
@@ -0,0 +1,8 @@
1
+ import type { CompilerCtx, ComponentCompilerMeta } from '@stencil/core/internal';
2
+ import type { OutputTargetAngular } from './types';
3
+ /**
4
+ * Generates the patch-transform-selectors.mjs script for Angular transformTag support.
5
+ * This script patches component selectors in the built Angular library to use the
6
+ * transformed tag names (e.g., 'my-component' -> 'v1-my-component').
7
+ */
8
+ export declare function generateTransformTagScript(compilerCtx: CompilerCtx, components: ComponentCompilerMeta[], outputTarget: OutputTargetAngular, packageName: string): Promise<void>;
@@ -0,0 +1,307 @@
1
+ import path from 'path';
2
+ import { dashToPascalCase } from './utils';
3
+ /**
4
+ * Generates the patch-transform-selectors.mjs script for Angular transformTag support.
5
+ * This script patches component selectors in the built Angular library to use the
6
+ * transformed tag names (e.g., 'my-component' -> 'v1-my-component').
7
+ */
8
+ export async function generateTransformTagScript(compilerCtx, components, outputTarget, packageName) {
9
+ const scriptsDirectory = path.join(path.dirname(outputTarget.directivesProxyFile), '../../scripts');
10
+ const customElementsDir = outputTarget.customElementsDir || 'dist/components';
11
+ const stencilImportPath = `${outputTarget.componentCorePackage}/${customElementsDir}/index.js`;
12
+ // Generate the mappings object
13
+ const mappings = components
14
+ .map((component) => {
15
+ const tagName = component.tagName;
16
+ const pascalName = dashToPascalCase(tagName);
17
+ return ` '${tagName}': '${pascalName}'`;
18
+ })
19
+ .join(',\n');
20
+ // Generate selector patcher script
21
+ const patchSelectorsContent = `#!/usr/bin/env node
22
+ /* eslint-disable */
23
+ /* tslint:disable */
24
+ /**
25
+ * Selector Patcher for transformTag support
26
+ *
27
+ * AUTO-GENERATED - DO NOT EDIT
28
+ *
29
+ * This script patches @Component selectors in the installed Angular component library
30
+ * to match your runtime tag transformer. Run this as a postinstall script in your app.
31
+ *
32
+ * Usage Option 1 - Config file (recommended for complex transformers):
33
+ * Create tag-transformer.config.mjs in your app root:
34
+ * export default (tag) => {
35
+ * if (tag.startsWith('my-transform-')) return \`v1-\${tag}\`;
36
+ * // ... complex logic
37
+ * return tag;
38
+ * };
39
+ *
40
+ * Then in package.json:
41
+ * "scripts": {
42
+ * "postinstall": "patch-transform-selectors"
43
+ * }
44
+ *
45
+ * Usage Option 2 - CLI argument (for simple transformers):
46
+ * "scripts": {
47
+ * "postinstall": "patch-transform-selectors \\"(tag) => tag.startsWith('my-transform-') ? \\\\\`v1-\\\${tag}\\\\\` : tag\\""
48
+ * }
49
+ */
50
+
51
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
52
+ import { join, dirname } from 'path';
53
+ import { fileURLToPath, pathToFileURL } from 'url';
54
+
55
+ const __filename = fileURLToPath(import.meta.url);
56
+ const __dirname = dirname(__filename);
57
+
58
+ // Try to load transformer from config file or CLI argument
59
+ let TAG_TRANSFORMER;
60
+ let transformerArg;
61
+
62
+ // Option 1: Look for tag-transformer.config.mjs in the consuming app
63
+ const configPath = join(process.cwd(), 'tag-transformer.config.mjs');
64
+ if (existsSync(configPath)) {
65
+ console.log('[TransformTag] Loading transformer from tag-transformer.config.mjs');
66
+ try {
67
+ const configUrl = pathToFileURL(configPath).href;
68
+ const config = await import(configUrl);
69
+ TAG_TRANSFORMER = config.default;
70
+
71
+ if (typeof TAG_TRANSFORMER !== 'function') {
72
+ throw new Error('Config file must export a default function');
73
+ }
74
+
75
+ // Store as string for injection later
76
+ transformerArg = TAG_TRANSFORMER.toString();
77
+ console.log('[TransformTag] Loaded transformer from config file');
78
+ } catch (error) {
79
+ console.error('[TransformTag] Error loading tag-transformer.config.mjs:', error.message);
80
+ console.error('Make sure the file exports a default function.');
81
+ process.exit(1);
82
+ }
83
+ } else {
84
+ // Option 2: Fall back to CLI argument
85
+ transformerArg = process.argv[2];
86
+
87
+ if (!transformerArg) {
88
+ console.error('[TransformTag] Error: No transformer provided.');
89
+ console.error('');
90
+ console.error('Option 1 - Create tag-transformer.config.mjs in your app root:');
91
+ console.error(' export default (tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag;');
92
+ console.error('');
93
+ console.error('Option 2 - Pass transformer as CLI argument:');
94
+ console.error(' patch-transform-selectors "(tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag"');
95
+ process.exit(1);
96
+ }
97
+
98
+ // Evaluate the transformer string to get the function
99
+ try {
100
+ TAG_TRANSFORMER = eval(transformerArg);
101
+ if (typeof TAG_TRANSFORMER !== 'function') {
102
+ throw new Error('Transformer must be a function');
103
+ }
104
+ console.log('[TransformTag] Using transformer from CLI argument');
105
+ } catch (error) {
106
+ console.error('[TransformTag] Error: Invalid transformer function:', error.message);
107
+ console.error('The transformer must be a valid JavaScript function expression.');
108
+ console.error('Example: "(tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag"');
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ const TAG_MAPPINGS = {
114
+ ${mappings}
115
+ };
116
+
117
+ console.log('[TransformTag] Patching component selectors...');
118
+
119
+ try {
120
+ // Find the bundled JavaScript file (could be fesm2022, fesm2015, fesm5, etc.)
121
+ const parentDir = join(__dirname, '..');
122
+
123
+ // Find all .js/.mjs files in fesm* directories AND fesm*.js/mjs files at root
124
+ let bundlePaths = [];
125
+
126
+ try {
127
+ const entries = readdirSync(parentDir);
128
+ for (const entry of entries) {
129
+ const entryPath = join(parentDir, entry);
130
+ let stat;
131
+ try {
132
+ stat = statSync(entryPath);
133
+ } catch (e) {
134
+ continue;
135
+ }
136
+
137
+ // Check for fesm* directories
138
+ if (stat.isDirectory() && /^fesm/.test(entry)) {
139
+ try {
140
+ const fesmFiles = readdirSync(entryPath);
141
+ for (const file of fesmFiles) {
142
+ if (/\\.m?js$/.test(file)) {
143
+ bundlePaths.push(join(entryPath, file));
144
+ }
145
+ }
146
+ } catch (e) {
147
+ // Skip if can't read fesm directory
148
+ }
149
+ }
150
+ // Check for fesm*.js or fesm*.mjs files at root
151
+ else if (stat.isFile() && /^fesm.*\\.m?js$/.test(entry)) {
152
+ bundlePaths.push(entryPath);
153
+ }
154
+ }
155
+ } catch (e) {
156
+ console.error('[TransformTag] Could not read parent directory:', parentDir);
157
+ process.exit(1);
158
+ }
159
+
160
+ if (bundlePaths.length === 0) {
161
+ console.error('[TransformTag] Could not find any fesm* directories or files to patch.');
162
+ process.exit(1);
163
+ }
164
+
165
+ console.log('[TransformTag] Found bundles:', bundlePaths);
166
+
167
+ // Patch all bundled JavaScript files
168
+ let totalPatchedCount = 0;
169
+
170
+ for (const bundlePath of bundlePaths) {
171
+ let bundleContent;
172
+ try {
173
+ bundleContent = readFileSync(bundlePath, 'utf8');
174
+ } catch (e) {
175
+ console.error('[TransformTag] Could not read bundle:', bundlePath);
176
+ continue;
177
+ }
178
+
179
+ let patchedCount = 0;
180
+
181
+ for (const [originalTag, pascalName] of Object.entries(TAG_MAPPINGS)) {
182
+ const transformedTag = TAG_TRANSFORMER(originalTag);
183
+
184
+ // Only patch if the tag is actually transformed
185
+ if (transformedTag !== originalTag) {
186
+ // Update selector from original tag name to transformed tag name
187
+ // e.g., selector: 'my-transform-test' becomes selector: 'v1-my-transform-test'
188
+ const selectorRegex = new RegExp(
189
+ \`(selector:\\\\s*)(['"\\\`])\${originalTag}\\\\2\`,
190
+ 'g'
191
+ );
192
+
193
+ const newContent = bundleContent.replace(
194
+ selectorRegex,
195
+ \`$1'\${transformedTag}'\`
196
+ );
197
+
198
+ if (newContent !== bundleContent) {
199
+ bundleContent = newContent;
200
+ patchedCount++;
201
+ console.log(\`[TransformTag] Patched selector for \${originalTag} -> \${transformedTag}\`);
202
+ }
203
+ }
204
+ }
205
+
206
+ // Inject setTagTransformer call with the user's transformer
207
+ // Find the export statement and add the call before it
208
+ const exportMatch = bundleContent.match(/export \\{ setTagTransformer/);
209
+ if (exportMatch && patchedCount > 0) {
210
+ const transformerCode = \`
211
+ // Auto-injected by patch-transform-selectors
212
+ // Call setTagTransformer with the user-provided transformer
213
+ import { setTagTransformer as stencilSetTagTransformer } from '${stencilImportPath}';
214
+ stencilSetTagTransformer(\${transformerArg});
215
+ \`;
216
+ bundleContent = transformerCode + bundleContent;
217
+ console.log('[TransformTag] Injected setTagTransformer call into bundle');
218
+ }
219
+
220
+ // Write the patched bundle
221
+ if (patchedCount > 0) {
222
+ writeFileSync(bundlePath, bundleContent);
223
+ totalPatchedCount += patchedCount;
224
+ console.log(\`[TransformTag] Successfully patched \${patchedCount} component selectors in \${bundlePath}\`);
225
+ }
226
+ }
227
+
228
+ // Find and patch all .d.ts files
229
+ let totalTypePatchedCount = 0;
230
+
231
+ function patchTypeDefsInDir(dir) {
232
+ let files;
233
+ try {
234
+ files = readdirSync(dir);
235
+ } catch (e) {
236
+ return;
237
+ }
238
+
239
+ for (const file of files) {
240
+ const filePath = join(dir, file);
241
+ let stat;
242
+ try {
243
+ stat = statSync(filePath);
244
+ } catch (e) {
245
+ continue;
246
+ }
247
+
248
+ if (stat.isDirectory()) {
249
+ patchTypeDefsInDir(filePath);
250
+ } else if (file.endsWith('.d.ts')) {
251
+ let typeDefsContent;
252
+ try {
253
+ typeDefsContent = readFileSync(filePath, 'utf8');
254
+ } catch (e) {
255
+ continue;
256
+ }
257
+
258
+ let modified = false;
259
+
260
+ for (const [originalTag, pascalName] of Object.entries(TAG_MAPPINGS)) {
261
+ const transformedTag = TAG_TRANSFORMER(originalTag);
262
+
263
+ if (transformedTag !== originalTag) {
264
+ // Update selector in type definitions - format: ɵɵComponentDeclaration<ClassName, "tag-name", ...>
265
+ const typeDefRegex = new RegExp(
266
+ \`(ɵɵComponentDeclaration<\${pascalName},\\\\s*)"(\${originalTag})"\`,
267
+ 'g'
268
+ );
269
+
270
+ const newTypeContent = typeDefsContent.replace(
271
+ typeDefRegex,
272
+ \`$1"\${transformedTag}"\`
273
+ );
274
+
275
+ if (newTypeContent !== typeDefsContent) {
276
+ typeDefsContent = newTypeContent;
277
+ modified = true;
278
+ }
279
+ }
280
+ }
281
+
282
+ if (modified) {
283
+ writeFileSync(filePath, typeDefsContent);
284
+ totalTypePatchedCount++;
285
+ console.log(\`[TransformTag] Patched type definitions in: \${filePath}\`);
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ patchTypeDefsInDir(parentDir);
292
+
293
+ if (totalTypePatchedCount > 0) {
294
+ console.log(\`[TransformTag] Successfully patched selectors in \${totalTypePatchedCount} type definition files.\`);
295
+ }
296
+
297
+ if (totalPatchedCount === 0 && totalTypePatchedCount === 0) {
298
+ console.log('[TransformTag] No selectors needed patching.');
299
+ }
300
+ } catch (error) {
301
+ console.error('[TransformTag] Error patching selectors:', error.message);
302
+ console.error('Stack:', error.stack);
303
+ process.exit(1);
304
+ }
305
+ `;
306
+ await compilerCtx.fs.writeFile(path.join(scriptsDirectory, 'patch-transform-selectors.mjs'), patchSelectorsContent);
307
+ }