UNPKG

@stencil/angular-output-target

Version:

Angular output target for @stencil/core components.

1,153 lines 50.7 kB
import path from 'path';
import { fileURLToPath } from 'url';
import { EOL } from 'os';

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.relative(path.dirname(pathFrom), path.dirname(pathTo));
    if (relativePath === '') {
        relativePath = '.';
    }
    else if (relativePath[0] !== '.') {
        relativePath = './' + relativePath;
    }
    return normalizePath(`${relativePath}/${path.basename(pathTo, ext)}`);
}
async function readPackageJson(config, rootDir) {
    const pkgJsonPath = path.join(rootDir, 'package.json');
    let pkgJson;
    try {
        pkgJson = (await config.sys?.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}` : '');
    /**
     * Each component's events are typed with the per-component CustomEvent type
     * generated by Stencil (e.g. `MyComponentCustomEvent<Detail>`), which carries a
     * narrowed `target`. Import it once per component that emits public events.
     */
    if (events.length > 0) {
        imports.push(`import type { ${componentTagName}CustomEvent } from '${importPathName}';`);
    }
    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};`;
    }
}
/**
 * The transform function to reference in generated code, exported from
 * `angular-component-lib/boolean-attribute`.
 */
const INPUT_TRANSFORM_FUNCTION = 'nullableBooleanAttribute';
/**
 * Creates a formatted inputs text with required and transform declarations.
 *
 * Inputs are emitted as bare strings unless they carry options, in which case Angular's object
 * form is used (e.g. `{ name: 'myInput', required: true }`).
 *
 * @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) => {
        const options = [];
        if (item.required) {
            options.push('required: true');
        }
        if (item.transform) {
            options.push(`transform: ${INPUT_TRANSFORM_FUNCTION}`);
        }
        if (options.length === 0) {
            return `'${item.name}'`;
        }
        return `{ name: '${item.name}', ${options.join(', ')} }`;
    })
        .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<${tagNameAsPascal}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<MyComponentCustomEvent<MyEvent<T>>>` to
             * `EventEmitter<MyComponentCustomEvent<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<${tagNameAsPascal}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);
}

const __filename$1 = fileURLToPath(import.meta.url);
const __dirname$1 = path.dirname(__filename$1);
async function generateValueAccessors(compilerCtx, components, outputTarget, config) {
    if (!Array.isArray(outputTarget.valueAccessorConfigs) || outputTarget.valueAccessorConfigs.length === 0) {
        return;
    }
    const targetDir = path.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 {
            ...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.join(targetDir, targetFileName);
        const srcFilePath = path.join(__dirname$1, '../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(`,${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.join(__dirname$1, '../resources/control-value-accessors/', rf),
            dest: path.join(directory, rf),
            keepDirStructure: false,
            warn: false,
            ignore: [],
        };
    });
    return config.sys.copy(copyTasks, path.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.join(path.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.join(scriptsDirectory, 'patch-transform-selectors.mjs'), patchSelectorsContent);
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const filterInternalProps = (prop) => !prop.internal;
/**
 * Whether a property declares an input transform.
 *
 * This is the single definition of the rule. Both the generated declaration and the import that
 * satisfies it are derived from it, so widening the rule can't leave a generated file referencing
 * a transform it never imports.
 *
 * @param prop The property compiler metadata.
 * @param booleanAttributes Whether boolean properties should declare an input transform.
 * @returns `true` when the property should be transformed.
 */
const isTransformedProp = (prop, booleanAttributes) => booleanAttributes && prop.type === 'boolean';
/**
 * Maps a Stencil property to an Angular input declaration.
 *
 * Virtual properties are mapped with `booleanAttributes` left at its default of `false`. They
 * carry a free-form type string and no `required` flag, so they are never transformed and always
 * come out as optional.
 *
 * @param prop The property compiler metadata.
 * @param booleanAttributes Whether boolean properties should declare an input transform.
 * @returns The Angular input declaration.
 */
const mapInputProp = (prop, booleanAttributes = false) => ({
    name: prop.name,
    required: prop.required ?? false,
    transform: isTransformedProp(prop, booleanAttributes) ? true : undefined,
});
/**
 * Whether any of a component's properties will declare an input transform, which determines
 * whether the transform needs to be imported.
 *
 * @param components The components in the generated file.
 * @param booleanAttributes Whether boolean properties should declare an input transform.
 * @returns `true` when at least one property is transformed.
 */
const usesInputTransform = (components, booleanAttributes) => components.some((cmpMeta) => (cmpMeta.properties ?? []).filter(filterInternalProps).some((prop) => isTransformedProp(prop, booleanAttributes)));
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.dirname(outputTarget.directivesProxyFile);
        for (const component of filteredComponents) {
            const componentFile = path.join(proxiesDir, `${component.tagName}.ts`);
            const componentText = generateComponentProxy(component, pkgData, outputTarget, rootDir, config);
            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, config);
        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.dirname(path.dirname(path.dirname(outputTarget.directivesProxyFile)));
        const angularPkgJsonPath = path.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.join(__dirname, '..', 'angular-component-lib');
    const destDirectory = path.join(path.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, config) {
    const distTypesDir = path.dirname(pkgData.types);
    const dtsFilePath = path.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'];
    /**
     * The input transform lives in its own module so that it carries no runtime imports.
     */
    let transformImport = '';
    const booleanAttributes = outputTarget.booleanAttributes === true;
    if (usesInputTransform(components, booleanAttributes)) {
        transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
    }
    if (includeSingleComponentAngularModules) {
        angularCoreImports.push('NgModule');
    }
    const imports = `/* tslint:disable */
/* auto-generated angular directive proxies */
${createImportStatement(angularCoreImports, '@angular/core')}

${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\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 = () => {
        const importLocation = outputTarget.componentCorePackage
            ? getPathToComponentTypes(config, outputTarget)
            : normalizePath(componentsTypeFile);
        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 { 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((prop) => mapInputProp(prop, booleanAttributes));
        if (cmpMeta.virtualProperties) {
            inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
        }
        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, config) {
    const { outputType, componentCorePackage, customElementsDir } = outputTarget;
    const distTypesDir = path.dirname(pkgData.types);
    const dtsFilePath = path.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 = cmpMeta.events?.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 booleanAttributes = outputTarget.booleanAttributes === true;
    const componentLibImports = ['ProxyCmp'];
    // The input transform lives in its own module so that it carries no runtime imports.
    let transformImport = '';
    if (usesInputTransform([cmpMeta], booleanAttributes)) {
        transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
    }
    const imports = `/* tslint:disable */
/* auto-generated angular directive proxies */
${createImportStatement(angularCoreImports, '@angular/core')}

${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
    // Type imports
    const importLocation = componentCorePackage
        ? getPathToComponentTypes(config, outputTarget)
        : normalizePath(componentsTypeFile);
    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 internalProps = [];
    if (cmpMeta.properties) {
        internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
    }
    const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
    if (cmpMeta.virtualProperties) {
        inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
    }
    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}';`;
    })
        .join('\n');
    return header + exports + '\n';
}
function getPathToComponentTypes(config, outputTarget) {
    const basePkg = outputTarget.componentCorePackage || '';
    // in v5, all types (including components.d.ts) are generated in the dist/types directory
    const typesTarget = config.outputTargets?.find((o) => o.type === 'types');
    if (typesTarget) {
        const rawDir = typesTarget.dir || 'dist/types';
        const relDir = config.rootDir && path.isAbsolute(rawDir) ? path.relative(config.rootDir, rawDir) : rawDir;
        return normalizePath(path.join(basePkg, relDir, 'components'));
    }
    // v4: append customElementsDir for custom elements build, otherwise package root
    const isCustomElementsBuild = isOutputTypeCustomElementsBuild(outputTarget.outputType);
    if (isCustomElementsBuild && outputTarget.customElementsDir) {
        return normalizePath(path.join(basePkg, outputTarget.customElementsDir));
    }
    return normalizePath(basePkg);
}
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) {
    const results = {
        ...outputTarget,
        excludeComponents: outputTarget.excludeComponents || [],
        valueAccessorConfigs: outputTarget.valueAccessorConfigs || [],
        customElementsDir: outputTarget.customElementsDir ?? 'components',
        outputType: outputTarget.outputType ?? 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.isAbsolute(outputTarget.directivesProxyFile)) {
        results.directivesProxyFile = normalizePath(path.join(config.rootDir, outputTarget.directivesProxyFile));
    }
    if (outputTarget.directivesArrayFile && !path.isAbsolute(outputTarget.directivesArrayFile)) {
        results.directivesArrayFile = normalizePath(path.join(config.rootDir, outputTarget.directivesArrayFile));
    }
    if (outputTarget.includeSingleComponentAngularModules !== undefined) {
        throw new Error("The 'includeSingleComponentAngularModules' option has been removed. Please use 'outputType' instead.");
    }
    return results;
}

export { angularOutputTarget };