From 4ca581dd1eaa2b871adf21794bc376bcedce7e12 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Sun, 11 Oct 2026 15:07:31 -0700 Subject: [PATCH] chore: split create-files.mjs into icons.mjs and barrels.mjs, as `pnpm generate` create-files.mjs did two unrelated jobs. icons.mjs now writes the icon components from @carbon/icons and their module declarations; barrels.mjs writes the four barrels (components, ai-chat, charts and icons). The scripts that write source files share a `generate` prefix, apart from `build`, which writes dist/: - `generate` runs `generate:icons`, then `generate:barrels` (was `create-autogenerated-files`) - `generate:skill-references` (was `create-skill-references`; the script is now scripts/skill-references.mjs) CI, prepack, AGENTS.md, CONTRIBUTING.md and the parity agent's prompt use the new names. The icon barrel now goes through the same writeBarrel as the others, in place of its own copy of the naming code, and gains the duplicate-name check. All the generated files are byte-for-byte the same as before. barrels.mjs fails, rather than writing an empty icon barrel, if it runs before icons.mjs. Signed-off-by: Alexey Kulakov --- .github/workflows/docs.yml | 6 +- .github/workflows/nodejs.yml | 6 +- .github/workflows/pr-preview.yml | 2 +- .github/workflows/templates/agent-prompt.md | 4 +- AGENTS.md | 4 +- CONTRIBUTING.md | 2 +- carbon-components-ember/.gitignore | 4 +- carbon-components-ember/.prettierignore | 2 +- .../.storybook/manifests.ts | 2 +- carbon-components-ember/eslint.config.mjs | 2 +- carbon-components-ember/package.json | 10 +- carbon-components-ember/scripts/barrels.mjs | 101 +++++++++ .../scripts/create-files.mjs | 193 ------------------ carbon-components-ember/scripts/icons.mjs | 99 +++++++++ ...ll-references.mjs => skill-references.mjs} | 2 +- 15 files changed, 224 insertions(+), 215 deletions(-) create mode 100644 carbon-components-ember/scripts/barrels.mjs delete mode 100644 carbon-components-ember/scripts/create-files.mjs create mode 100644 carbon-components-ember/scripts/icons.mjs rename carbon-components-ember/scripts/{create-skill-references.mjs => skill-references.mjs} (98%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d6fc35f1ba..4983e0bf3e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -54,7 +54,7 @@ jobs: pnpm install --frozen-lockfile # See the same step's comment in the `test` job above. pnpm --filter carbon-components-ember exec playwright install chromium - pnpm --filter carbon-components-ember create-autogenerated-files + pnpm --filter carbon-components-ember generate - name: Test stories run: pnpm --filter carbon-components-ember test:storybook --shard=${{ matrix.shard }}/${{ strategy.job-total }} @@ -73,7 +73,7 @@ jobs: - name: Install dependencies run: | pnpm install --frozen-lockfile - pnpm --filter carbon-components-ember create-autogenerated-files + pnpm --filter carbon-components-ember generate - name: Build Storybook run: pnpm build:storybook @@ -91,7 +91,7 @@ jobs: # Packing the addon writes its agent skill's references from this # build; fail here, not at release, if the manifest stops fitting. - name: Write the skill's component references - run: pnpm --filter carbon-components-ember create-skill-references + run: pnpm --filter carbon-components-ember generate:skill-references # The deploy below publishes this build rather than building again. - name: Upload the site diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index d13a124253..97ba7a8944 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -40,7 +40,7 @@ jobs: - name: Install dependencies run: | pnpm install --frozen-lockfile - pnpm --filter carbon-components-ember create-autogenerated-files + pnpm --filter carbon-components-ember generate - name: Lint env: @@ -110,7 +110,7 @@ jobs: pnpm install --frozen-lockfile # pnpm's side-effects cache can skip playwright-chromium's install script on a store cache hit, even though the browsers it downloads live outside the store and don't persist across runners. pnpm --filter carbon-components-ember exec playwright install chromium - pnpm --filter carbon-components-ember create-autogenerated-files + pnpm --filter carbon-components-ember generate - name: Run tests run: pnpm run test @@ -150,7 +150,7 @@ jobs: pnpm install --no-frozen-lockfile # See the same step's comment in the `test` job above. pnpm --filter carbon-components-ember exec playwright install chromium - pnpm --filter carbon-components-ember create-autogenerated-files + pnpm --filter carbon-components-ember generate - name: Run tests run: pnpm test diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index d6f1f492eb..93e027be8c 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | pnpm install --frozen-lockfile - pnpm --filter carbon-components-ember create-autogenerated-files + pnpm --filter carbon-components-ember generate - name: Build Storybook run: pnpm build:storybook diff --git a/.github/workflows/templates/agent-prompt.md b/.github/workflows/templates/agent-prompt.md index 981170922e..91864ce507 100644 --- a/.github/workflows/templates/agent-prompt.md +++ b/.github/workflows/templates/agent-prompt.md @@ -143,7 +143,7 @@ component synced and push that update (see Phase 4, step 3). - If a prop/story implies an icon-rendering slot, use the icon components from `carbon-components-ember/icons` (see AGENTS.md pitfall on icon registration below — the same registration step applies here) 3. **Export Component** - - Run `pnpm --filter carbon-components-ember create-autogenerated-files`; + - Run `pnpm --filter carbon-components-ember generate`; it adds the component to the generated barrel, named after its file 4. **Create Tests** @@ -195,7 +195,7 @@ Use this path only when the component genuinely doesn't belong as a standalone E `{{COMPONENT_NAME_KEBAB}}/item.gts`. The barrel names each export after its file, and this fails if two files would export the same name: ```bash - pnpm --filter carbon-components-ember create-autogenerated-files + pnpm --filter carbon-components-ember generate ``` ### Phase 4: Documentation (Required) diff --git a/AGENTS.md b/AGENTS.md index 6864d5637f..37a2185693 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,7 +186,7 @@ Signatures follow the [Ember guides](https://guides.emberjs.com/release/typescri Template keywords such as `fn`, `on`, `eq`, and `element` resolve to a same-named JavaScript binding in scope. Alias such imports when using the template keyword: import Storybook's spy as `fn as spy`. Never import `fn` or `on` from Ember (`ember/no-legacy-helper-imports`). ### ❌ Pitfall 2: Naming a File Differently from Its Export -File names are kebab-case, and `pnpm create-autogenerated-files` names each export of the `carbon-components-ember/components`, `/ai-chat` and `/charts` barrels after its file: `text-input.gts` exports `TextInput`, and a top-level `ai-` or `ui-` file spells the acronym (`AILabel`, `UIShell`). It fails when two files would export the same name from one barrel. So name a file after its export in full: `toggletip/toggletip-button.gts`, not `toggletip/button.gts`. Two files sharing a basename (`tile/group.gts` and `radio-button/group.gts`) once made the production rollup build mis-name exports. +File names are kebab-case, and `pnpm generate` names each export of the `carbon-components-ember/components`, `/ai-chat` and `/charts` barrels after its file: `text-input.gts` exports `TextInput`, and a top-level `ai-` or `ui-` file spells the acronym (`AILabel`, `UIShell`). It fails when two files would export the same name from one barrel. So name a file after its export in full: `toggletip/toggletip-button.gts`, not `toggletip/button.gts`. Two files sharing a basename (`tile/group.gts` and `radio-button/group.gts`) once made the production rollup build mis-name exports. ### ❌ Pitfall 3: MutationObservers to inspect Child DOM Nodes Fights the Glimmer render pipeline. Instead, use contextual components so children render with correct classes/attributes declaratively. @@ -286,7 +286,7 @@ AI Chat's components live in `src/components/ai-chat/` and are exported from the - [ ] Create `.gts` file in `carbon-components-ember/src/components/` - [ ] Define TypeScript signature — fully typed, no `any` - [ ] Use `cds--` prefix for CSS classes -- [ ] Export it: run `pnpm create-autogenerated-files` to regenerate public barrel exports +- [ ] Export it: run `pnpm generate` to regenerate public barrel exports - [ ] Match React's props: `pnpm --filter dom-parity args --component ` - [ ] When the PR closes a `[Parity Check]` issue, run `node scripts/parity-check.mjs --mark-synced ` (with `GITHUB_TOKEN`) and commit `.parity-check-data.json`, or the weekly check reopens it; a React directory with no same-named export needs `--exclude --reason "..."` - [ ] Create test file in `tests/components/` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2177f64b9..1757af042c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ pnpm switches to it automatically (or run `corepack enable` once). Node is pinned in `package.json#devEngines.runtime`: pnpm downloads that version and runs every script with it, whatever Node you have installed. -- `pnpm --filter carbon-components-ember create-autogenerated-files` – generates the icon components and index files +- `pnpm --filter carbon-components-ember generate` – generates the icon components and the barrels ## Linting diff --git a/carbon-components-ember/.gitignore b/carbon-components-ember/.gitignore index 12c90ef257..31ec931ed0 100644 --- a/carbon-components-ember/.gitignore +++ b/carbon-components-ember/.gitignore @@ -3,11 +3,11 @@ dist/ dist-tests/ declarations/ -# generated by `pnpm create-autogenerated-files` +# generated by `pnpm generate` src/components/icons/ unpublished-development-types/carbon-icons.d.ts -# generated by `pnpm create-skill-references` from the Storybook build +# generated by `pnpm generate:skill-references` from the Storybook build skills/carbon-components-ember/references/components.md skills/carbon-components-ember/references/components/ diff --git a/carbon-components-ember/.prettierignore b/carbon-components-ember/.prettierignore index 48f31c21a7..cd419dfe7b 100644 --- a/carbon-components-ember/.prettierignore +++ b/carbon-components-ember/.prettierignore @@ -7,7 +7,7 @@ /declarations/ /storybook-static/ -# generated by `pnpm create-autogenerated-files` +# generated by `pnpm generate` /src/components/icons/ /src/components.ts /src/ai-chat.ts diff --git a/carbon-components-ember/.storybook/manifests.ts b/carbon-components-ember/.storybook/manifests.ts index add129031d..3abb7270dd 100644 --- a/carbon-components-ember/.storybook/manifests.ts +++ b/carbon-components-ember/.storybook/manifests.ts @@ -9,7 +9,7 @@ import { readFileSync } from 'node:fs'; import type { Manifests } from 'storybook/internal/types'; -// Written by `pnpm create-autogenerated-files`. +// Written by `pnpm generate`. const BARRELS = { components: '../src/components.ts', 'ai-chat': '../src/ai-chat.ts', diff --git a/carbon-components-ember/eslint.config.mjs b/carbon-components-ember/eslint.config.mjs index 0bba2ef803..71e1f45afe 100644 --- a/carbon-components-ember/eslint.config.mjs +++ b/carbon-components-ember/eslint.config.mjs @@ -42,7 +42,7 @@ export default defineConfig([ 'coverage/', 'storybook-static/', '!**/.*', - // generated by `pnpm create-autogenerated-files` + // generated by `pnpm generate` 'src/components/icons/', ]), js.configs.recommended, diff --git a/carbon-components-ember/package.json b/carbon-components-ember/package.json index 2c8d76f177..420accfbd5 100644 --- a/carbon-components-ember/package.json +++ b/carbon-components-ember/package.json @@ -38,10 +38,12 @@ ], "scripts": { "build": "rollup --config", - "build:skill": "storybook build --quiet && pnpm create-skill-references", + "build:skill": "storybook build --quiet && pnpm generate:skill-references", "build:watch": "rollup --config --watch --no-watch.clearScreen", - "create-autogenerated-files": "node ./scripts/create-files.mjs", - "create-skill-references": "node ./scripts/create-skill-references.mjs", + "generate": "pnpm generate:icons && pnpm generate:barrels", + "generate:barrels": "node ./scripts/barrels.mjs", + "generate:icons": "node ./scripts/icons.mjs", + "generate:skill-references": "node ./scripts/skill-references.mjs", "format": "prettier . --cache --write", "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm run format", @@ -50,7 +52,7 @@ "lint:js:fix": "eslint . --fix", "lint:publish": "pnpm build && publint run --level error", "lint:types": "ember-tsc --noEmit", - "prepack": "pnpm create-autogenerated-files && rollup --config && pnpm build:skill", + "prepack": "pnpm generate && rollup --config && pnpm build:skill", "start": "vite dev", "test": "vite build --mode=development --out-dir dist-tests && testem --file testem.cjs ci --port 0", "test:update-snapshots": "vite build --mode=development --out-dir dist-tests && UPDATE_SNAPSHOTS=true testem --file testem.cjs ci --port 0", diff --git a/carbon-components-ember/scripts/barrels.mjs b/carbon-components-ember/scripts/barrels.mjs new file mode 100644 index 0000000000..e7ec612b8d --- /dev/null +++ b/carbon-components-ember/scripts/barrels.mjs @@ -0,0 +1,101 @@ +/** + * Writes the barrels: src/components.ts, src/ai-chat.ts, src/charts.ts and + * src/icons.ts, which apps import as `carbon-components-ember/components`, + * `/ai-chat`, `/charts` and `/icons` (dist/*.js, via the package.json `./*` + * export). Run it after icons.mjs, which writes the icon components; + * `pnpm generate` runs both. + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +function createIndexFiles() { + // Sorted, so the generated barrels are the same on every OS. + const files = fs.readdirSync('./src', { recursive: true }).sort(); + const groups = { + components: [], + aiChat: [], + charts: [], + icons: [], + }; + for (const addonFilename of files) { + if (!addonFilename.includes('.')) continue; + if (addonFilename.includes('.gitkeep')) continue; + if (/\.stories\.[gt]?[jt]s$|\.mdx$/.test(addonFilename)) continue; + if (addonFilename.includes('render-svg-part.ts')) continue; + if ( + addonFilename.startsWith('components/icons') && + !addonFilename.includes('/-') + ) { + groups.icons.push(addonFilename); + continue; + } + if ( + addonFilename.startsWith('components/') && + !addonFilename.includes('/-') + ) { + // AI Chat and the charts have their own barrels, as upstream's are + // packages of their own (`@carbon/ai-chat-components`, + // `@carbon/charts-react`), so AI Chat keeps upstream's names (`Card`, + // `CodeSnippet`). + const group = addonFilename.startsWith('components/ai-chat/') + ? groups.aiChat + : addonFilename.startsWith('components/charts/') + ? groups.charts + : groups.components; + group.push(addonFilename); + } + } + if (groups.icons.length === 0) { + throw new Error( + 'src/components/icons/ has no icon components; run scripts/icons.mjs first', + ); + } + writeBarrel(groups.components, './src/components.ts'); + writeBarrel(groups.aiChat, './src/ai-chat.ts'); + writeBarrel(groups.charts, './src/charts.ts'); + writeBarrel(groups.icons, './src/icons.ts'); +} + +/** + * Writes a barrel exporting each component under its file name in + * PascalCase (`text-input.gts` is `TextInput`), so name the file after the + * export. A top-level `ai-` or `ui-` file spells the acronym as Carbon React + * does (`AILabel`, `UIShell`). + */ +function writeBarrel(components, file) { + const lines = []; + const exportedFrom = new Map(); + for (const comp of components) { + const camelCased = path + .basename(comp) + .replace(/\..*$/, '') + .split('-') + .map((word, i) => + i === 0 && /^(ai|ui)$/.test(word) && path.dirname(comp) === 'components' + ? word.toUpperCase() + : word[0].toUpperCase() + word.slice(1), + ) + .join(''); + if (exportedFrom.has(camelCased)) { + throw new Error( + `${comp} and ${exportedFrom.get(camelCased)} would both export ${camelCased} from ${file}; rename one`, + ); + } + exportedFrom.set(camelCased, comp); + const relativePath = comp.replace('components/', ''); + const source = fs + .readFileSync(path.join('./src/components', relativePath)) + .toString(); + const namedExports = [ + ...source.matchAll(/^export (?:class|const|function) ([A-Za-z0-9_]+)/gm), + ].map((m) => m[1]); + const specifiers = [`default as ${camelCased}`, ...namedExports]; + lines.push( + `export { ${specifiers.join(', ')} } from './components/${relativePath}'`, + ); + } + fs.writeFileSync(file, lines.join('\n')); +} + +createIndexFiles(); diff --git a/carbon-components-ember/scripts/create-files.mjs b/carbon-components-ember/scripts/create-files.mjs deleted file mode 100644 index 649557cf0a..0000000000 --- a/carbon-components-ember/scripts/create-files.mjs +++ /dev/null @@ -1,193 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -function createIndexFiles() { - // Sorted, so the generated barrels are the same on every OS. - const files = fs.readdirSync('./src', { recursive: true }).sort(); - const groups = { - components: [], - aiChat: [], - charts: [], - icons: [], - }; - for (const addonFilename of files) { - if (!addonFilename.includes('.')) continue; - if (addonFilename.includes('.gitkeep')) continue; - if (/\.stories\.[gt]?[jt]s$|\.mdx$/.test(addonFilename)) continue; - if (addonFilename.includes('render-svg-part.ts')) continue; - if ( - addonFilename.startsWith('components/icons') && - !addonFilename.includes('/-') - ) { - groups.icons.push(addonFilename); - continue; - } - if ( - addonFilename.startsWith('components/') && - !addonFilename.includes('/-') - ) { - // AI Chat and the charts have their own barrels, as upstream's are - // packages of their own (`@carbon/ai-chat-components`, - // `@carbon/charts-react`), so AI Chat keeps upstream's names (`Card`, - // `CodeSnippet`). - const group = addonFilename.startsWith('components/ai-chat/') - ? groups.aiChat - : addonFilename.startsWith('components/charts/') - ? groups.charts - : groups.components; - group.push(addonFilename); - } - } - // `carbon-components-ember/components`, `/ai-chat` and `/charts` - // (dist/components.js etc., via the package.json `./*` export). - writeBarrel(groups.components, './src/components.ts'); - writeBarrel(groups.aiChat, './src/ai-chat.ts'); - writeBarrel(groups.charts, './src/charts.ts'); - - const iconsIndexFile = []; - for (const comp of groups.icons) { - let camelCased = comp - .split('/') - .at(-1) - .split('.') - .at() - .replace(/-([a-zA-Z])/g, function (g) { - return g[1].toUpperCase(); - }); - camelCased = camelCased[0].toUpperCase() + camelCased.slice(1); - iconsIndexFile.push(`export { default as ${camelCased} } from './${comp}'`); - } - fs.writeFileSync('./src/icons.ts', iconsIndexFile.join('\n')); -} - -/** - * Writes a barrel exporting each component under its file name in - * PascalCase (`text-input.gts` is `TextInput`), so name the file after the - * export. A top-level `ai-` or `ui-` file spells the acronym as Carbon React - * does (`AILabel`, `UIShell`). - */ -function writeBarrel(components, file) { - const lines = []; - const exportedFrom = new Map(); - for (const comp of components) { - const camelCased = path - .basename(comp) - .replace(/\..*$/, '') - .split('-') - .map((word, i) => - i === 0 && /^(ai|ui)$/.test(word) && path.dirname(comp) === 'components' - ? word.toUpperCase() - : word[0].toUpperCase() + word.slice(1), - ) - .join(''); - if (exportedFrom.has(camelCased)) { - throw new Error( - `${comp} and ${exportedFrom.get(camelCased)} would both export ${camelCased} from ${file}; rename one`, - ); - } - exportedFrom.set(camelCased, comp); - const relativePath = comp.replace('components/', ''); - const source = fs - .readFileSync(path.join('./src/components', relativePath)) - .toString(); - const namedExports = [ - ...source.matchAll(/^export (?:class|const|function) ([A-Za-z0-9_]+)/gm), - ].map((m) => m[1]); - const specifiers = [`default as ${camelCased}`, ...namedExports]; - lines.push( - `export { ${specifiers.join(', ')} } from './components/${relativePath}'`, - ); - } - fs.writeFileSync(file, lines.join('\n')); -} - -function createIconIndex() { - const iconsIndex = path.resolve('./node_modules/@carbon/icons/es/index.js'); - const content = fs.readFileSync(iconsIndex).toString(); - - // Step 1: parse all import lines → varName → relative path (without .js) - const varToPath = {}; - for (const m of content.matchAll(/^import (\S+) from "\.\/([^"]+)";$/gm)) { - varToPath[m[1]] = m[2].replace('.js', ''); - } - - // Step 2: parse export block → varName → export name - // The export block is a single line: export { var1 as Name1, var2 as Name2, ... } - const varToExport = {}; - for (const m of content.matchAll(/\b(\S+?) as ([A-Za-z]\w*)/g)) { - varToExport[m[1]] = m[2]; - } - - // Step 3: build matchList as [exportName, '@carbon/icons/es/path/size'] - const matchList = []; - for (const [varName, exportName] of Object.entries(varToExport)) { - const iconPath = varToPath[varName]; - if (iconPath) { - matchList.push([exportName, '@carbon/icons/es/' + iconPath]); - } - } - - let typesContent = ` -type Icon = { - name: IconNames; - elem: string; - attrs: Record; - content: { - elem: string; - attrs: Record; - }[]; - size: number; -}; - `; - const icons = {}; - const iconNames = {}; - for (const [exportName, fullPath] of matchList) { - const size = fullPath.split('/').at(-1); - const commonPath = fullPath.split('/').slice(0, -1).join('/'); - icons[commonPath] = icons[commonPath] || []; - icons[commonPath].push(size); - iconNames[commonPath] = exportName.replace(size, '').replace('Glyph', ''); - } - // Start clean, so icons renamed or removed upstream don't leave stale files. - fs.rmSync('./src/components/icons', { recursive: true, force: true }); - fs.mkdirSync('./src/components/icons', { recursive: true }); - fs.writeFileSync('./src/components/icons/.gitkeep', 'auto generated icons\n'); - for (const [icon, sizes] of Object.entries(icons)) { - const iconSizeMap = sizes - .map( - (size) => - `'${size}': new TrackedPromise(() => import('${icon}/${size}')),`, - ) - .join('\n '); - const index = sizes.includes('index') ? 'index' : '32'; - const content = ` -import Icon from '../icon.gts'; -import { TrackedPromise } from '../../utils/tracked.ts'; - -const icons = { - ${iconSizeMap} -}; - -export default class ${iconNames[icon]} extends Icon { - get svg() { - return (icons[(this.args.size || 24).toString() as keyof typeof icons] || icons['${index}']).getValue()?.default; - } -} - `; - let dashed = iconNames[icon] - .replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) - .replace(/^-/, ''); - fs.writeFileSync(`./src/components/icons/${dashed}.ts`, content); - } - for (const [, fullPath] of matchList) { - typesContent += '\n'; - typesContent += `declare module '${fullPath}' { export default {} as Icon };\n`; - } - fs.writeFileSync( - './unpublished-development-types/carbon-icons.d.ts', - typesContent, - ); -} - -createIconIndex(); -createIndexFiles(); diff --git a/carbon-components-ember/scripts/icons.mjs b/carbon-components-ember/scripts/icons.mjs new file mode 100644 index 0000000000..64b2200025 --- /dev/null +++ b/carbon-components-ember/scripts/icons.mjs @@ -0,0 +1,99 @@ +/** + * Writes an Ember component for each `@carbon/icons` icon into + * src/components/icons/, and the module declarations for their SVG data + * into unpublished-development-types/carbon-icons.d.ts. barrels.mjs then + * exports them from `carbon-components-ember/icons`. + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +function createIconComponents() { + const iconsIndex = path.resolve('./node_modules/@carbon/icons/es/index.js'); + const content = fs.readFileSync(iconsIndex).toString(); + + // Step 1: parse all import lines → varName → relative path (without .js) + const varToPath = {}; + for (const m of content.matchAll(/^import (\S+) from "\.\/([^"]+)";$/gm)) { + varToPath[m[1]] = m[2].replace('.js', ''); + } + + // Step 2: parse export block → varName → export name + // The export block is a single line: export { var1 as Name1, var2 as Name2, ... } + const varToExport = {}; + for (const m of content.matchAll(/\b(\S+?) as ([A-Za-z]\w*)/g)) { + varToExport[m[1]] = m[2]; + } + + // Step 3: build matchList as [exportName, '@carbon/icons/es/path/size'] + const matchList = []; + for (const [varName, exportName] of Object.entries(varToExport)) { + const iconPath = varToPath[varName]; + if (iconPath) { + matchList.push([exportName, '@carbon/icons/es/' + iconPath]); + } + } + + let typesContent = ` +type Icon = { + name: IconNames; + elem: string; + attrs: Record; + content: { + elem: string; + attrs: Record; + }[]; + size: number; +}; + `; + const icons = {}; + const iconNames = {}; + for (const [exportName, fullPath] of matchList) { + const size = fullPath.split('/').at(-1); + const commonPath = fullPath.split('/').slice(0, -1).join('/'); + icons[commonPath] = icons[commonPath] || []; + icons[commonPath].push(size); + iconNames[commonPath] = exportName.replace(size, '').replace('Glyph', ''); + } + // Start clean, so icons renamed or removed upstream don't leave stale files. + fs.rmSync('./src/components/icons', { recursive: true, force: true }); + fs.mkdirSync('./src/components/icons', { recursive: true }); + fs.writeFileSync('./src/components/icons/.gitkeep', 'auto generated icons\n'); + for (const [icon, sizes] of Object.entries(icons)) { + const iconSizeMap = sizes + .map( + (size) => + `'${size}': new TrackedPromise(() => import('${icon}/${size}')),`, + ) + .join('\n '); + const index = sizes.includes('index') ? 'index' : '32'; + const content = ` +import Icon from '../icon.gts'; +import { TrackedPromise } from '../../utils/tracked.ts'; + +const icons = { + ${iconSizeMap} +}; + +export default class ${iconNames[icon]} extends Icon { + get svg() { + return (icons[(this.args.size || 24).toString() as keyof typeof icons] || icons['${index}']).getValue()?.default; + } +} + `; + let dashed = iconNames[icon] + .replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) + .replace(/^-/, ''); + fs.writeFileSync(`./src/components/icons/${dashed}.ts`, content); + } + for (const [, fullPath] of matchList) { + typesContent += '\n'; + typesContent += `declare module '${fullPath}' { export default {} as Icon };\n`; + } + fs.writeFileSync( + './unpublished-development-types/carbon-icons.d.ts', + typesContent, + ); +} + +createIconComponents(); diff --git a/carbon-components-ember/scripts/create-skill-references.mjs b/carbon-components-ember/scripts/skill-references.mjs similarity index 98% rename from carbon-components-ember/scripts/create-skill-references.mjs rename to carbon-components-ember/scripts/skill-references.mjs index b74b383f81..ed01f92f71 100644 --- a/carbon-components-ember/scripts/create-skill-references.mjs +++ b/carbon-components-ember/scripts/skill-references.mjs @@ -4,7 +4,7 @@ * and `references/components/.md` holds one page's import, arguments, * blocks and examples, as the components manifest has them. * - * Usage: node scripts/create-skill-references.mjs [storybook-static] + * Usage: node scripts/skill-references.mjs [storybook-static] */ import fs from 'node:fs';