@stencil/angular-output-target
Version:
Angular output target for @stencil/core components.
308 lines (269 loc) • 10.5 kB
JavaScript
import path from 'path';
import { dashToPascalCase } from './utils';
/**
* 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').
*/
export 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);
}