UNPKG

@stencil/angular-output-target

Version:

Angular output target for @stencil/core components.

1,078 lines (1,023 loc) 47.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var path = require('path'); var os = require('os'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var path__default = /*#__PURE__*/_interopDefaultLegacy(path); const OutputTypes = { Component: 'component', Scam: 'scam', Standalone: 'standalone', }; const toLowerCase = (str) => str.toLowerCase(); const mapPropName = (prop) => prop.name; const dashToPascalCase = (str) => toLowerCase(str) .split('-') .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(''); function sortBy(array, prop) { return array.slice().sort((a, b) => { const nameA = prop(a); const nameB = prop(b); if (nameA < nameB) return -1; if (nameA > nameB) return 1; return 0; }); } function normalizePath(str) { // Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar // https://github.com/sindresorhus/slash MIT // By Sindre Sorhus if (typeof str !== 'string') { throw new Error(`invalid path to normalize`); } str = str.trim(); if (EXTENDED_PATH_REGEX.test(str) || NON_ASCII_REGEX.test(str)) { return str; } str = str.replace(SLASH_REGEX, '/'); // always remove the trailing / // this makes our file cache look ups consistent if (str.charAt(str.length - 1) === '/') { const colonIndex = str.indexOf(':'); if (colonIndex > -1) { if (colonIndex < str.length - 2) { str = str.substring(0, str.length - 1); } } else if (str.length > 1) { str = str.substring(0, str.length - 1); } } return str; } function relativeImport(pathFrom, pathTo, ext) { let relativePath = path__default["default"].relative(path__default["default"].dirname(pathFrom), path__default["default"].dirname(pathTo)); if (relativePath === '') { relativePath = '.'; } else if (relativePath[0] !== '.') { relativePath = './' + relativePath; } return normalizePath(`${relativePath}/${path__default["default"].basename(pathTo, ext)}`); } async function readPackageJson(config, rootDir) { var _a; const pkgJsonPath = path__default["default"].join(rootDir, 'package.json'); let pkgJson; try { pkgJson = (await ((_a = config.sys) === null || _a === void 0 ? void 0 : _a.readFile(pkgJsonPath, 'utf8'))); } catch (e) { throw new Error(`Missing "package.json" file for distribution: ${pkgJsonPath}`); } let pkgData; try { pkgData = JSON.parse(pkgJson); } catch (e) { throw new Error(`Error parsing package.json: ${pkgJsonPath}, ${e}`); } return pkgData; } /** * 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 ''; } return `import { ${imports.join(', ')} } from '${module}';`; }; /** * Checks if the outputType is for the custom elements build. * @param outputType The output type. * @returns `true` if the output type is for the custom elements build. */ const isOutputTypeCustomElementsBuild = (outputType) => { return outputType === OutputTypes.Standalone || outputType === OutputTypes.Scam; }; /** * 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, customElementsDir } = options; const imports = []; const namedImports = new Set(); const isCustomElementsBuild = isOutputTypeCustomElementsBuild(options.outputType); const importPathName = normalizePath(componentCorePackage) + (isCustomElementsBuild ? `/${customElementsDir}` : ''); 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; /** * Creates a property declaration. * * @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration. * @param type The name of the type (e.g. 'string') * @param inlinePropertyAsSetter Inlines the entire property as an empty Setter, to aid Angulars Compilerp * @returns The property declaration as a string. */ function createPropertyDeclaration(prop, type, inlinePropertyAsSetter = false) { const comment = createDocComment(prop.docs); let eventName = prop.name; if (/[-/]/.test(prop.name)) { // If a member name includes a dash or a forward slash, we need to wrap it in quotes. // https://github.com/stenciljs/output-targets/issues/212 eventName = `'${prop.name}'`; } if (inlinePropertyAsSetter) { return `${comment.length > 0 ? ` ${comment}` : ''} set ${eventName}(_: ${type}) {};`; } else { return `${comment.length > 0 ? ` ${comment}` : ''} ${eventName}: ${type};`; } } /** * Creates a formatted inputs text with required declaration. * * @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration. * @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]). * @returns The inputs list declaration as a string. */ function formatInputs(inputs) { return inputs .map((item) => { if (item.required) { return `{ name: '${item.name}', required: true }`; } else { return `'${item.name}'`; } }) .join(', '); } /** * 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. [{name: 'myInput', required: true]). * @param methods The methods of the Stencil component. (e.g. ['myMethod']). * @param includeImportCustomElements Whether to define the component as a custom element. * @param standalone Whether to define the component as a standalone component. * @param inlineComponentProps List of properties that should be inlined into the component definition. * @param events The events of the Stencil component for generating outputs. * @returns The component declaration as a string. */ const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = []) => { const tagNameAsPascal = dashToPascalCase(tagName); const outputs = events.filter((event) => !event.internal).map((event) => event.name); 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 proxyCmpFormattedInputs = formatToQuotedList(inputs.map(mapPropName)); // Formats the input strings into comma separated, single quoted values if optional. // Formats the required input strings into comma separated {name, required} objects. const formattedInputs = formatInputs(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: [${proxyCmpFormattedInputs}]`); } if (hasMethods) { proxyCmpOptions.push(`\n methods: [${formattedMethods}]`); } let standaloneOption = ''; if (!standalone) { standaloneOption = `\n standalone: false`; } const propertyDeclarations = inlineComponentProps.map((m) => createPropertyDeclaration(m, `Components.${tagNameAsPascal}['${m.name}']`, true)); const outputDeclarations = events .filter((event) => !event.internal) .map((event) => { const camelCaseOutput = event.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); const outputType = `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`; return `@Output() ${camelCaseOutput} = new ${outputType}();`; }); const propertiesDeclarationText = [ `protected el: HTML${tagNameAsPascal}Element;`, ...propertyDeclarations, ...outputDeclarations, ].join('\n '); /** * 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({ selector: '${tagName}', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: [${formattedInputs}],${hasOutputs ? `\n outputs: [${formattedOutputs}],` : ''}${standaloneOption} }) export class ${tagNameAsPascal} { ${propertiesDeclarationText} constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); this.el = r.nativeElement; } }`; 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) => { const prefix = `I${componentClassName}`; /** * 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]) => { let renamedType = type; if (!type.startsWith(prefix)) { if (type.startsWith('{') && type.endsWith('}')) { /** * If the type starts with { and ends with }, it is an inline type. * For example, `{ a: string }`. * We don't need to rename these types, so we return the original type. */ renamedType = type; } else { /** * If the type does not start with { and end with }, it is a reference type. * For example, `MyType`. * We need to rename these types, so we prepend the prefix. */ renamedType = `I${componentClassName}${type}`; } } const prefixedTypeName = `${prefix}${src}`; return (renamedType .replace(new RegExp(`^${src}$`, 'g'), prefixedTypeName) // Capture all instances of the `src` field surrounded by non-word characters on each side and join them. .replace(new RegExp(`([^\\w])${src}([^\\w])`, 'g'), (_, p1, p2) => { /** * Replaces a complex type reference within a generic type. * For example, remapping a type like `EventEmitter<CustomEvent<MyEvent<T>>>` to * `EventEmitter<CustomEvent<IMyComponentMyEvent<IMyComponentT>>>`. */ return [p1, prefixedTypeName, p2].join(''); }) // Capture all instances that contain sub types, e.g. `IMyComponent.SomeMoreComplexType.SubType`. .replace(new RegExp(`^${src}(\.\\w+)+$`, 'g'), (type) => { return `${prefix}${src}.${type.split('.').slice(1).join('.')}`; })); }, event.complexType.original .replace(/\/\/.*$/gm, '') // Remove single-line comments before collapsing newlines .replace(/\n/g, ' ') .replace(/\s{2,}/g, ' ') .replace(/,\s*/g, ', ') .trim()); }; /** * 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 ''; } return `/** * ${doc.text}${doc.tags.length > 0 ? ' ' : ''}${doc.tags.map((tag) => `@${tag.name} ${tag.text}`)} */`; }; /** * Creates the component interface type definition. * @param outputType The output type. * @param tagNameAsPascal The tag name as PascalCase. * @param events The events to generate the interface properties for. * @param componentCorePackage The component core package. * @param customElementsDir The custom elements directory. * @returns The component interface type definition as a string. */ const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, componentCorePackage, customElementsDir) => { const publicEvents = events.filter((ev) => !ev.internal); const eventTypeImports = createComponentEventTypeImports(tagNameAsPascal, publicEvents, { componentCorePackage, customElementsDir, outputType, }); const eventTypes = publicEvents.map((event) => createPropertyDeclaration(event, `EventEmitter<CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`)); const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`; const typeDefinition = (eventTypeImports.length > 0 ? `${eventTypeImports + '\n\n'}` : '') + `${interfaceDeclaration}${eventTypes.length === 0 ? '}' : ` ${eventTypes.join('\n')} }`}`; return typeDefinition; }; function generateAngularDirectivesFile(compilerCtx, components, outputTarget) { // Only create the file if it is defined in the stencil configuration if (!outputTarget.directivesArrayFile) { return Promise.resolve(); } const proxyPath = relativeImport(outputTarget.directivesArrayFile, outputTarget.directivesProxyFile, '.ts'); const directives = components .map((cmpMeta) => dashToPascalCase(cmpMeta.tagName)) .map((className) => `d.${className}`) .join(',\n '); const c = ` import * as d from '${proxyPath}'; export const DIRECTIVES = [ ${directives} ]; `; return compilerCtx.fs.writeFile(outputTarget.directivesArrayFile, c); } async function generateValueAccessors(compilerCtx, components, outputTarget, config) { if (!Array.isArray(outputTarget.valueAccessorConfigs) || outputTarget.valueAccessorConfigs.length === 0) { return; } const targetDir = path__default["default"].dirname(outputTarget.directivesProxyFile); const normalizedValueAccessors = outputTarget.valueAccessorConfigs.reduce((allAccessors, va) => { const elementSelectors = Array.isArray(va.elementSelectors) ? va.elementSelectors : [va.elementSelectors]; const type = va.type; let allElementSelectors = []; let allEventTargets = []; if (allAccessors.hasOwnProperty(type)) { allElementSelectors = allAccessors[type].elementSelectors; allEventTargets = allAccessors[type].eventTargets; } return Object.assign(Object.assign({}, allAccessors), { [type]: { elementSelectors: allElementSelectors.concat(elementSelectors), eventTargets: allEventTargets.concat([[va.event, va.targetAttr]]), } }); }, {}); await Promise.all(Object.keys(normalizedValueAccessors).map(async (type) => { const valueAccessorType = type; // Object.keys converts to string const targetFileName = `${type}-value-accessor.ts`; const targetFilePath = path__default["default"].join(targetDir, targetFileName); const srcFilePath = path__default["default"].join(__dirname, '../resources/control-value-accessors/', targetFileName); const srcFileContents = await compilerCtx.fs.readFile(srcFilePath); const finalText = createValueAccessor(srcFileContents, normalizedValueAccessors[valueAccessorType], outputTarget.outputType); await compilerCtx.fs.writeFile(targetFilePath, finalText); })); await copyResources$1(config, ['value-accessor.ts'], targetDir); } function createValueAccessor(srcFileContents, valueAccessor, outputType) { const hostContents = valueAccessor.eventTargets.map((listItem) => VALUE_ACCESSOR_EVENTTARGETS.replace(VALUE_ACCESSOR_EVENT, listItem[0]).replace(VALUE_ACCESSOR_TARGETATTR, listItem[1])); return srcFileContents .replace(VALUE_ACCESSOR_SELECTORS, valueAccessor.elementSelectors.join(', ')) .replace(VALUE_ACCESSOR_EVENTTARGETS, hostContents.join(`,${os.EOL}`)) .replace(VALUE_ACCESSOR_STANDALONE, outputType && outputType !== OutputTypes.Standalone ? ',\nstandalone: false' : ''); } function copyResources$1(config, resourcesFilesToCopy, directory) { if (!config.sys || !config.sys.copy) { throw new Error('stencil is not properly initialized at this step. Notify the developer'); } const copyTasks = resourcesFilesToCopy.map((rf) => { return { src: path__default["default"].join(__dirname, '../resources/control-value-accessors/', rf), dest: path__default["default"].join(directory, rf), keepDirStructure: false, warn: false, ignore: [], }; }); return config.sys.copy(copyTasks, path__default["default"].join(directory)); } const VALUE_ACCESSOR_SELECTORS = `<VALUE_ACCESSOR_SELECTORS>`; const VALUE_ACCESSOR_EVENT = `<VALUE_ACCESSOR_EVENT>`; const VALUE_ACCESSOR_TARGETATTR = '<VALUE_ACCESSOR_TARGETATTR>'; const VALUE_ACCESSOR_STANDALONE = '<VALUE_ACCESSOR_STANDALONE>'; const VALUE_ACCESSOR_EVENTTARGETS = ` '(<VALUE_ACCESSOR_EVENT>)': 'handleChangeEvent($event.target?.["<VALUE_ACCESSOR_TARGETATTR>"])'`; /** * 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; }; /** * Generates the patch-transform-selectors.mjs script for Angular transformTag support. * This script patches component selectors in the built Angular library to use the * transformed tag names (e.g., 'my-component' -> 'v1-my-component'). */ async function generateTransformTagScript(compilerCtx, components, outputTarget, packageName) { const scriptsDirectory = path__default["default"].join(path__default["default"].dirname(outputTarget.directivesProxyFile), '../../scripts'); const customElementsDir = outputTarget.customElementsDir || 'dist/components'; const stencilImportPath = `${outputTarget.componentCorePackage}/${customElementsDir}/index.js`; // Generate the mappings object const mappings = components .map((component) => { const tagName = component.tagName; const pascalName = dashToPascalCase(tagName); return ` '${tagName}': '${pascalName}'`; }) .join(',\n'); // Generate selector patcher script const patchSelectorsContent = `#!/usr/bin/env node /* eslint-disable */ /* tslint:disable */ /** * Selector Patcher for transformTag support * * AUTO-GENERATED - DO NOT EDIT * * This script patches @Component selectors in the installed Angular component library * to match your runtime tag transformer. Run this as a postinstall script in your app. * * Usage Option 1 - Config file (recommended for complex transformers): * Create tag-transformer.config.mjs in your app root: * export default (tag) => { * if (tag.startsWith('my-transform-')) return \`v1-\${tag}\`; * // ... complex logic * return tag; * }; * * Then in package.json: * "scripts": { * "postinstall": "patch-transform-selectors" * } * * Usage Option 2 - CLI argument (for simple transformers): * "scripts": { * "postinstall": "patch-transform-selectors \\"(tag) => tag.startsWith('my-transform-') ? \\\\\`v1-\\\${tag}\\\\\` : tag\\"" * } */ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath, pathToFileURL } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Try to load transformer from config file or CLI argument let TAG_TRANSFORMER; let transformerArg; // Option 1: Look for tag-transformer.config.mjs in the consuming app const configPath = join(process.cwd(), 'tag-transformer.config.mjs'); if (existsSync(configPath)) { console.log('[TransformTag] Loading transformer from tag-transformer.config.mjs'); try { const configUrl = pathToFileURL(configPath).href; const config = await import(configUrl); TAG_TRANSFORMER = config.default; if (typeof TAG_TRANSFORMER !== 'function') { throw new Error('Config file must export a default function'); } // Store as string for injection later transformerArg = TAG_TRANSFORMER.toString(); console.log('[TransformTag] Loaded transformer from config file'); } catch (error) { console.error('[TransformTag] Error loading tag-transformer.config.mjs:', error.message); console.error('Make sure the file exports a default function.'); process.exit(1); } } else { // Option 2: Fall back to CLI argument transformerArg = process.argv[2]; if (!transformerArg) { console.error('[TransformTag] Error: No transformer provided.'); console.error(''); console.error('Option 1 - Create tag-transformer.config.mjs in your app root:'); console.error(' export default (tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag;'); console.error(''); console.error('Option 2 - Pass transformer as CLI argument:'); console.error(' patch-transform-selectors "(tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag"'); process.exit(1); } // Evaluate the transformer string to get the function try { TAG_TRANSFORMER = eval(transformerArg); if (typeof TAG_TRANSFORMER !== 'function') { throw new Error('Transformer must be a function'); } console.log('[TransformTag] Using transformer from CLI argument'); } catch (error) { console.error('[TransformTag] Error: Invalid transformer function:', error.message); console.error('The transformer must be a valid JavaScript function expression.'); console.error('Example: "(tag) => tag.startsWith(\\'my-\\') ? \`v1-\${tag}\` : tag"'); process.exit(1); } } const TAG_MAPPINGS = { ${mappings} }; console.log('[TransformTag] Patching component selectors...'); try { // Find the bundled JavaScript file (could be fesm2022, fesm2015, fesm5, etc.) const parentDir = join(__dirname, '..'); // Find all .js/.mjs files in fesm* directories AND fesm*.js/mjs files at root let bundlePaths = []; try { const entries = readdirSync(parentDir); for (const entry of entries) { const entryPath = join(parentDir, entry); let stat; try { stat = statSync(entryPath); } catch (e) { continue; } // Check for fesm* directories if (stat.isDirectory() && /^fesm/.test(entry)) { try { const fesmFiles = readdirSync(entryPath); for (const file of fesmFiles) { if (/\\.m?js$/.test(file)) { bundlePaths.push(join(entryPath, file)); } } } catch (e) { // Skip if can't read fesm directory } } // Check for fesm*.js or fesm*.mjs files at root else if (stat.isFile() && /^fesm.*\\.m?js$/.test(entry)) { bundlePaths.push(entryPath); } } } catch (e) { console.error('[TransformTag] Could not read parent directory:', parentDir); process.exit(1); } if (bundlePaths.length === 0) { console.error('[TransformTag] Could not find any fesm* directories or files to patch.'); process.exit(1); } console.log('[TransformTag] Found bundles:', bundlePaths); // Patch all bundled JavaScript files let totalPatchedCount = 0; for (const bundlePath of bundlePaths) { let bundleContent; try { bundleContent = readFileSync(bundlePath, 'utf8'); } catch (e) { console.error('[TransformTag] Could not read bundle:', bundlePath); continue; } let patchedCount = 0; for (const [originalTag, pascalName] of Object.entries(TAG_MAPPINGS)) { const transformedTag = TAG_TRANSFORMER(originalTag); // Only patch if the tag is actually transformed if (transformedTag !== originalTag) { // Update selector from original tag name to transformed tag name // e.g., selector: 'my-transform-test' becomes selector: 'v1-my-transform-test' const selectorRegex = new RegExp( \`(selector:\\\\s*)(['"\\\`])\${originalTag}\\\\2\`, 'g' ); const newContent = bundleContent.replace( selectorRegex, \`$1'\${transformedTag}'\` ); if (newContent !== bundleContent) { bundleContent = newContent; patchedCount++; console.log(\`[TransformTag] Patched selector for \${originalTag} -> \${transformedTag}\`); } } } // Inject setTagTransformer call with the user's transformer // Find the export statement and add the call before it const exportMatch = bundleContent.match(/export \\{ setTagTransformer/); if (exportMatch && patchedCount > 0) { const transformerCode = \` // Auto-injected by patch-transform-selectors // Call setTagTransformer with the user-provided transformer import { setTagTransformer as stencilSetTagTransformer } from '${stencilImportPath}'; stencilSetTagTransformer(\${transformerArg}); \`; bundleContent = transformerCode + bundleContent; console.log('[TransformTag] Injected setTagTransformer call into bundle'); } // Write the patched bundle if (patchedCount > 0) { writeFileSync(bundlePath, bundleContent); totalPatchedCount += patchedCount; console.log(\`[TransformTag] Successfully patched \${patchedCount} component selectors in \${bundlePath}\`); } } // Find and patch all .d.ts files let totalTypePatchedCount = 0; function patchTypeDefsInDir(dir) { let files; try { files = readdirSync(dir); } catch (e) { return; } for (const file of files) { const filePath = join(dir, file); let stat; try { stat = statSync(filePath); } catch (e) { continue; } if (stat.isDirectory()) { patchTypeDefsInDir(filePath); } else if (file.endsWith('.d.ts')) { let typeDefsContent; try { typeDefsContent = readFileSync(filePath, 'utf8'); } catch (e) { continue; } let modified = false; for (const [originalTag, pascalName] of Object.entries(TAG_MAPPINGS)) { const transformedTag = TAG_TRANSFORMER(originalTag); if (transformedTag !== originalTag) { // Update selector in type definitions - format: ɵɵComponentDeclaration<ClassName, "tag-name", ...> const typeDefRegex = new RegExp( \`(ɵɵComponentDeclaration<\${pascalName},\\\\s*)"(\${originalTag})"\`, 'g' ); const newTypeContent = typeDefsContent.replace( typeDefRegex, \`$1"\${transformedTag}"\` ); if (newTypeContent !== typeDefsContent) { typeDefsContent = newTypeContent; modified = true; } } } if (modified) { writeFileSync(filePath, typeDefsContent); totalTypePatchedCount++; console.log(\`[TransformTag] Patched type definitions in: \${filePath}\`); } } } } patchTypeDefsInDir(parentDir); if (totalTypePatchedCount > 0) { console.log(\`[TransformTag] Successfully patched selectors in \${totalTypePatchedCount} type definition files.\`); } if (totalPatchedCount === 0 && totalTypePatchedCount === 0) { console.log('[TransformTag] No selectors needed patching.'); } } catch (error) { console.error('[TransformTag] Error patching selectors:', error.message); console.error('Stack:', error.stack); process.exit(1); } `; await compilerCtx.fs.writeFile(path__default["default"].join(scriptsDirectory, 'patch-transform-selectors.mjs'), patchSelectorsContent); } async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) { const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components); const rootDir = config.rootDir; const pkgData = await readPackageJson(config, rootDir); // esModules defaults to true, but only applies when outputType is 'scam' or 'standalone' const isCustomElementsBuild = isOutputTypeCustomElementsBuild(outputTarget.outputType); const useEsModules = isCustomElementsBuild && outputTarget.esModules === true; const tasks = [ copyResources(config, outputTarget), generateValueAccessors(compilerCtx, filteredComponents, outputTarget, config), ]; if (useEsModules) { // Generate separate files for each component const proxiesDir = path__default["default"].dirname(outputTarget.directivesProxyFile); for (const component of filteredComponents) { const componentFile = path__default["default"].join(proxiesDir, `${component.tagName}.ts`); const componentText = generateComponentProxy(component, pkgData, outputTarget, rootDir); tasks.push(compilerCtx.fs.writeFile(componentFile, componentText)); } // Generate barrel file that re-exports all components const barrelText = generateBarrelFile(filteredComponents, outputTarget); tasks.push(compilerCtx.fs.writeFile(outputTarget.directivesProxyFile, barrelText)); // Generate DIRECTIVES file (imports from barrel) tasks.push(generateAngularDirectivesFile(compilerCtx, filteredComponents, outputTarget)); } else { // Generate single file with all components (original behavior) const finalText = generateProxies(filteredComponents, pkgData, outputTarget, rootDir); tasks.push(compilerCtx.fs.writeFile(outputTarget.directivesProxyFile, finalText)); tasks.push(generateAngularDirectivesFile(compilerCtx, filteredComponents, outputTarget)); } // Generate transformer script if transformTag is enabled if (outputTarget.transformTag) { // Read the Angular library's package.json to get its name // directivesProxyFile is like: projects/library/src/directives/proxies.ts // We need to go up to: projects/library/package.json const angularLibraryDir = path__default["default"].dirname(path__default["default"].dirname(path__default["default"].dirname(outputTarget.directivesProxyFile))); const angularPkgJsonPath = path__default["default"].join(angularLibraryDir, 'package.json'); let angularPackageName = ''; try { const angularPkgJson = JSON.parse(await compilerCtx.fs.readFile(angularPkgJsonPath)); if (angularPkgJson.name) { angularPackageName = angularPkgJson.name; } } catch (e) { throw new Error(`Could not read Angular library package.json at ${angularPkgJsonPath}. ` + `The package name is required to generate the transformTag patch script.`); } if (!angularPackageName) { throw new Error(`Angular library package.json at ${angularPkgJsonPath} does not have a "name" field. ` + `The package name is required to generate the transformTag patch script.`); } tasks.push(generateTransformTagScript(compilerCtx, filteredComponents, outputTarget)); } await Promise.all(tasks); } function getFilteredComponents(excludeComponents = [], cmps) { return sortBy(cmps, (cmp) => cmp.tagName).filter((c) => !excludeComponents.includes(c.tagName) && !c.internal); } async function copyResources(config, outputTarget) { if (!config.sys || !config.sys.copy || !config.sys.glob) { throw new Error('stencil is not properly initialized at this step. Notify the developer'); } 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'); return config.sys.copy([ { src: srcDirectory, dest: destDirectory, keepDirStructure: false, warn: false, ignore: [], }, ], srcDirectory); } function generateProxies(components, pkgData, outputTarget, rootDir) { const distTypesDir = path__default["default"].dirname(pkgData.types); const dtsFilePath = path__default["default"].join(rootDir, distTypesDir, GENERATED_DTS); const { outputType } = outputTarget; const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts'); const includeSingleComponentAngularModules = outputType === OutputTypes.Scam; const isCustomElementsBuild = isOutputTypeCustomElementsBuild(outputType); const isStandaloneBuild = outputType === OutputTypes.Standalone; const includeOutputImports = components.some((component) => component.events.some((event) => !event.internal)); /** * The collection of named imports from @angular/core. */ const angularCoreImports = ['ChangeDetectionStrategy', 'ChangeDetectorRef', 'Component', 'ElementRef']; if (includeOutputImports) { angularCoreImports.push('EventEmitter', 'Output'); } angularCoreImports.push('NgZone'); /** * The collection of named imports from the angular-component-lib/utils. */ const componentLibImports = ['ProxyCmp']; if (includeSingleComponentAngularModules) { angularCoreImports.push('NgModule'); } const imports = `/* tslint:disable */ /* auto-generated angular directive proxies */ ${createImportStatement(angularCoreImports, '@angular/core')} ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`; /** * Generate JSX import type from correct location. * When using custom elements build, we need to import from * either the "components" directory or customElementsDir * otherwise we risk bundlers pulling in lazy loaded imports. */ const generateTypeImports = () => { let importLocation = outputTarget.componentCorePackage ? normalizePath(outputTarget.componentCorePackage) : normalizePath(componentsTypeFile); importLocation += isCustomElementsBuild ? `/${outputTarget.customElementsDir}` : ''; return `import ${isCustomElementsBuild ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`; }; const typeImports = generateTypeImports(); let sourceImports = ''; /** * Build an array of Custom Elements build imports and namespace them * so that they do not conflict with the Angular wrapper names. For example, * IonButton would be imported as IonButtonCmp so as to not conflict with the * IonButton Angular Component that takes in the Web Component as a parameter. */ if (isCustomElementsBuild && outputTarget.componentCorePackage !== undefined) { const cmpImports = components.map((component) => { const pascalImport = dashToPascalCase(component.tagName); return `import { defineCustomElement as define${pascalImport} } from '${normalizePath(outputTarget.componentCorePackage)}/${outputTarget.customElementsDir}/${component.tagName}.js';`; }); sourceImports = cmpImports.join('\n'); } const proxyFileOutput = []; const filterInternalProps = (prop) => !prop.internal; // Ensure that virtual properties has required as false. const mapInputProp = (prop) => { var _a; return ({ name: prop.name, required: (_a = prop.required) !== null && _a !== void 0 ? _a : false, }); }; const { componentCorePackage, customElementsDir } = outputTarget; for (let cmpMeta of components) { const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); const internalProps = []; if (cmpMeta.properties) { internalProps.push(...cmpMeta.properties.filter(filterInternalProps)); } const inputs = internalProps.map(mapInputProp); if (cmpMeta.virtualProperties) { inputs.push(...cmpMeta.virtualProperties.map(mapInputProp)); } const orderedInputs = sortBy(inputs, (cip) => cip.name); const methods = []; if (cmpMeta.methods) { methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName)); } const inlineComponentProps = outputTarget.inlineProperties ? internalProps : []; /** * For each component, we need to generate: * 1. The @Component decorated class * 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true) * 3. The component interface (using declaration merging for types). */ const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []); const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName); const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir); proxyFileOutput.push(componentDefinition, '\n'); if (includeSingleComponentAngularModules) { proxyFileOutput.push(moduleDefinition, '\n'); } proxyFileOutput.push(componentTypeDefinition, '\n'); } const final = [imports, typeImports, sourceImports, ...proxyFileOutput]; return final.join('\n') + '\n'; } /** * Generate a single component proxy file for ES modules output */ function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir) { var _a; const { outputType, componentCorePackage, customElementsDir } = outputTarget; 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 includeSingleComponentAngularModules = outputType === OutputTypes.Scam; const isCustomElementsBuild = isOutputTypeCustomElementsBuild(outputType); const isStandaloneBuild = outputType === OutputTypes.Standalone; const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName); const hasOutputs = (_a = cmpMeta.events) === null || _a === void 0 ? void 0 : _a.some((event) => !event.internal); // Angular core imports for this component const angularCoreImports = ['ChangeDetectionStrategy', 'ChangeDetectorRef', 'Component', 'ElementRef', 'NgZone']; if (hasOutputs) { angularCoreImports.push('EventEmitter', 'Output'); } if (includeSingleComponentAngularModules) { angularCoreImports.push('NgModule'); } const imports = `/* tslint:disable */ /* auto-generated angular directive proxies */ ${createImportStatement(angularCoreImports, '@angular/core')} ${createImportStatement(['ProxyCmp'], './angular-component-lib/utils')}\n`; // Type imports let importLocation = componentCorePackage ? normalizePath(componentCorePackage) : normalizePath(componentsTypeFile); importLocation += isCustomElementsBuild ? `/${customElementsDir}` : ''; const typeImports = `import ${isCustomElementsBuild ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`; // defineCustomElement import let sourceImport = ''; if (isCustomElementsBuild && componentCorePackage !== undefined) { sourceImport = `import { defineCustomElement as define${tagNameAsPascal} } from '${normalizePath(componentCorePackage)}/${customElementsDir}/${cmpMeta.tagName}.js';\n`; } // Generate component definition const filterInternalProps = (prop) => !prop.internal; const mapInputProp = (prop) => { var _a; return ({ name: prop.name, required: (_a = prop.required) !== null && _a !== void 0 ? _a : false, }); }; const internalProps = []; if (cmpMeta.properties) { internalProps.push(...cmpMeta.properties.filter(filterInternalProps)); } const inputs = internalProps.map(mapInputProp); if (cmpMeta.virtualProperties) { inputs.push(...cmpMeta.virtualProperties.map(mapInputProp)); } const orderedInputs = sortBy(inputs, (cip) => cip.name); const methods = []; if (cmpMeta.methods) { methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName)); } const inlineComponentProps = outputTarget.inlineProperties ? internalProps : []; const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []); const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName); const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir); const proxyFileOutput = [componentDefinition, '\n']; if (includeSingleComponentAngularModules) { proxyFileOutput.push(moduleDefinition, '\n'); } proxyFileOutput.push(componentTypeDefinition, '\n'); const final = [imports, typeImports, sourceImport, ...proxyFileOutput]; return final.join('\n') + '\n'; } /** * Generate a barrel file that re-exports all components */ function generateBarrelFile(components, outputTarget) { const { outputType } = outputTarget; const includeSingleComponentAngularModules = outputType === OutputTypes.Scam; const header = `/* tslint:disable */ /** * This file was automatically generated by the Stencil Angular Output Target. * Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. */\n\n`; const exports = components .map((component) => { const pascalName = dashToPascalCase(component.tagName); const moduleExport = includeSingleComponentAngularModules ? `, ${pascalName}Module` : ''; return `export { ${pascalName}${moduleExport} } from './${component.tagName}.js';`; }) .join('\n'); return header + exports + '\n'; } const GENERATED_DTS = 'components.d.ts'; const IMPORT_TYPES = 'Components'; const angularOutputTarget = (outputTarget) => { let validatedOutputTarget; return { type: 'custom', name: 'angular-library', validate(config) { validatedOutputTarget = normalizeOutputTarget(config, outputTarget); }, async generator(config, compilerCtx, buildCtx) { const timespan = buildCtx.createTimeSpan(`generate angular proxies started`, true); await angularDirectiveProxyOutput(compilerCtx, validatedOutputTarget, buildCtx.components, config); timespan.finish(`generate angular proxies finished`); }, }; }; function normalizeOutputTarget(config, outputTarget) { var _a, _b; const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], valueAccessorConfigs: outputTarget.valueAccessorConfigs || [], customElementsDir: (_a = outputTarget.customElementsDir) !== null && _a !== void 0 ? _a : 'components', outputType: (_b = outputTarget.outputType) !== null && _b !== void 0 ? _b : OutputTypes.Standalone }); 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. Please set it in the Stencil config.'); } if (outputTarget.directivesProxyFile && !path__default["default"].isAbsolute(outputTarget.directivesProxyFile)) { results.directivesProxyFile = normalizePath(path__default["default"].join(config.rootDir, outputTarget.directivesProxyFile)); } if (outputTarget.directivesArrayFile && !path__default["default"].isAbsolute(outputTarget.directivesArrayFile)) { results.directivesArrayFile = normalizePath(path__default["default"].join(config.rootDir, outputTarget.directivesArrayFile)); } if (outputTarget.includeSingleComponentAngularModules !== undefined) { throw new Error("The 'includeSingleComponentAngularModules' option has been removed. Please use 'outputType' instead."); } return results; } exports.angularOutputTarget = angularOutputTarget;