Description
When a theme's icons: field is fed by a relative import, astryx theme build writes that import into the module it generates:
// dist/neutral.js
import { neutralIconRegistry } from './icons';
Producing a loadable module at that path is the caller's responsibility — theme build does not compile the icon registry. That's a reasonable division of labour, and the theme packages satisfy it with their own pipeline (astryx theme build && tsup && tsc, where tsup emits dist/icons.mjs).
But nothing says so. The docs list the four emitted files (.css, .js, .d.ts, optional .variants.d.ts) without mentioning that one of them carries an external requirement, and a reader would have to infer it from the theme packages' build scripts.
What a theme author actually has to do
Two steps, starting from a stock astryx theme add scaffold:
$ astryx theme build src/neutralTheme.ts
$ sed -n 8p src/neutral.js
import { neutralIconRegistry } from './icons';
$ node -e "import('./src/neutral.js')"
ERR_MODULE_NOT_FOUND # icons are still TypeScript
# 1. compile the icon registry (2.0 kB)
$ esbuild src/icons.tsx --bundle --format=esm --outfile=src/icons.mjs \
--external:react --external:lucide-react --jsx=automatic
# 2. point the generated module at it
$ sed -i '' "s#from './icons'#from './icons.mjs'#" src/neutral.js
$ node -e "import('./src/neutral.js')"
LOADS — theme=neutral, icons=26
$ esbuild src/neutral.js --bundle --format=esm --outfile=/dev/null
BUNDLES OK
Do both and the module works in Node and in bundlers. Skip step 1 and it works only in bundlers, which resolve ./icons to the neighbouring icons.tsx. Skip step 2 and it never works in Node, because ./icons is not a valid ESM specifier — that part is #4620, and step 2 isn't currently possible without hand-editing a @generated file.
The most visible symptom of the missing documentation is that this exits 0:
$ astryx theme build src/myTheme.ts -o dist/theme.css # no compile step in this project
$ ls dist
my.d.ts my.js my.variants.d.ts theme.css # nothing named icons
That module resolves in nothing — Node, esbuild and Vite all fail — and the build reported success.
Suggested
Document the contract: the generated module imports the icon registry from a sibling path, producing a module there is the caller's job, and here is the one-line esbuild invocation that does it. The transcript above is most of the docs already.
That's the whole ask. In particular, theme build should not check this itself — it runs first in the pipeline, so at codegen time the target legitimately doesn't exist yet. Immediately after step 1 in a theme package, dist/ holds only the four generated files and no icons module, so a resolve-check inside the generator would fail on every correct build of all seven packages. Emitting a forward reference is right; whether the finished artifact resolves is the pipeline's business.
If a check is wanted, it belongs after the build, and there's already a home for it: scripts/verify-exports.mjs runs in CI as "Verify package exports" and walks every exports map, but only existsSync()s each target and never opens it. Extending it to await import() each resolvable target is ~15 lines, can't false-positive because it runs on a finished build, and catches both this and #4569's failure mode. (scripts/check-fully-specified.mjs can't cover it — that one is purely textual and checks only that specifiers have an extension, never that the target exists.)
If the contract is considered self-evident and the --out-into-an-uncompiled-directory case is user error, this is fine to close — #4620 is then the whole bug. Raising it only because the current behaviour is silent.
Environment
@astryxdesign/{cli,core,theme-*} 0.2.0
- Node 22.17.1, macOS 26.6 (Darwin 25.6.0, arm64), esbuild 0.28.1, Vite 8.2.0
Description
When a theme's
icons:field is fed by a relative import,astryx theme buildwrites that import into the module it generates:Producing a loadable module at that path is the caller's responsibility —
theme builddoes not compile the icon registry. That's a reasonable division of labour, and the theme packages satisfy it with their own pipeline (astryx theme build && tsup && tsc, wheretsupemitsdist/icons.mjs).But nothing says so. The docs list the four emitted files (
.css,.js,.d.ts, optional.variants.d.ts) without mentioning that one of them carries an external requirement, and a reader would have to infer it from the theme packages' build scripts.What a theme author actually has to do
Two steps, starting from a stock
astryx theme addscaffold:$ astryx theme build src/neutralTheme.ts $ sed -n 8p src/neutral.js import { neutralIconRegistry } from './icons'; $ node -e "import('./src/neutral.js')" ERR_MODULE_NOT_FOUND # icons are still TypeScript # 1. compile the icon registry (2.0 kB) $ esbuild src/icons.tsx --bundle --format=esm --outfile=src/icons.mjs \ --external:react --external:lucide-react --jsx=automatic # 2. point the generated module at it $ sed -i '' "s#from './icons'#from './icons.mjs'#" src/neutral.js $ node -e "import('./src/neutral.js')" LOADS — theme=neutral, icons=26 $ esbuild src/neutral.js --bundle --format=esm --outfile=/dev/null BUNDLES OKDo both and the module works in Node and in bundlers. Skip step 1 and it works only in bundlers, which resolve
./iconsto the neighbouringicons.tsx. Skip step 2 and it never works in Node, because./iconsis not a valid ESM specifier — that part is #4620, and step 2 isn't currently possible without hand-editing a@generatedfile.The most visible symptom of the missing documentation is that this exits 0:
That module resolves in nothing — Node, esbuild and Vite all fail — and the build reported success.
Suggested
Document the contract: the generated module imports the icon registry from a sibling path, producing a module there is the caller's job, and here is the one-line esbuild invocation that does it. The transcript above is most of the docs already.
That's the whole ask. In particular,
theme buildshould not check this itself — it runs first in the pipeline, so at codegen time the target legitimately doesn't exist yet. Immediately after step 1 in a theme package,dist/holds only the four generated files and no icons module, so a resolve-check inside the generator would fail on every correct build of all seven packages. Emitting a forward reference is right; whether the finished artifact resolves is the pipeline's business.If a check is wanted, it belongs after the build, and there's already a home for it:
scripts/verify-exports.mjsruns in CI as "Verify package exports" and walks everyexportsmap, but onlyexistsSync()s each target and never opens it. Extending it toawait import()each resolvable target is ~15 lines, can't false-positive because it runs on a finished build, and catches both this and #4569's failure mode. (scripts/check-fully-specified.mjscan't cover it — that one is purely textual and checks only that specifiers have an extension, never that the target exists.)If the contract is considered self-evident and the
--out-into-an-uncompiled-directory case is user error, this is fine to close — #4620 is then the whole bug. Raising it only because the current behaviour is silent.Environment
@astryxdesign/{cli,core,theme-*}0.2.0