Kometa UI is an Angular 22 component kit. Up to 0.1.x it was published the way the Angular CLI scaffolds a library: one public-api.ts that re-exports everything, built by ng-packagr into one package entry. In 0.2.0 every source file became its own secondary entry point, 85 of them. This post covers why, what the layout looks like, and the one rule that makes it build.
The problem: lazy routes paying for the initial chunk
The app I extracted the kit from has an admin area with lazy-loaded routes. Most kit components are only used inside those routes. You would expect each component's code to load with its route.
It did not. ng-packagr builds each entry point into a single flat ES module. With one entry, that is one module containing every component. Tree-shaking removes the components nobody imports, but it does not split the rest: a bundler places a module's code in one chunk. As soon as eagerly loaded code (the shell, the root component) imported anything from kometa-ui, the module belonged to the initial chunk, and with it every kit component that any lazy route used.
In that admin app, this put 78 kB (raw, not compressed) into the initial chunk that belonged to lazy routes.
The fix is to give the bundler module boundaries that match component boundaries. If KometaDrawer lives in its own module, a route that is the only user of the drawer gets the drawer.
The layout
Each component folder under projects/kometa-ui/ is now an entry point:
projects/kometa-ui/ ng-package.json # primary entry: src/public-api.ts, assets, inlineStyleLanguage src/public-api.ts # re-exports every entry button/ kometa-button.ts kometa-button.spec.ts index.ts ng-package.json drawer/ kometa-drawer.ts kometa-drawer.spec.ts index.ts ng-package.json ... The two extra files are as small as they can be. button/index.ts:
export * from './kometa-button'; button/ng-package.json:
{ "lib": { "entryFile": "index.ts" } } ng-packagr discovers secondary entry points by finding ng-package.json files below the primary one, so nothing else has to list them. Each folder publishes as kometa-ui/<folder>, where the folder is the file name without the kometa- prefix: kometa-ui/button, kometa-ui/paging, kometa-ui/provide-kometa-ui.
The primary entry still exists and re-exports everything, by package path:
export { provideKometaUi, KOMETA_LAYER_ORDER } from 'kometa-ui/provide-kometa-ui'; export * from 'kometa-ui/accordion'; export * from 'kometa-ui/button'; // ... So import { KometaButton } from 'kometa-ui' from 0.1 still compiles. It just brings back the original problem, so the docs and the agent guide recommend the entry imports:
import { KometaButton } from 'kometa-ui/button'; import { KometaDrawer } from 'kometa-ui/drawer'; The rule: import other entries by package path, never relatively
Components use each other. The accordion uses the fold's body; the drawer uses the motion directive. Before the split these were relative imports between files in src/lib/. After it, they cross entry-point boundaries, and ng-packagr refuses a relative import that reaches into another entry point: each entry is compiled as its own unit and must reference the others the way a consumer would.
So inside the library:
// projects/kometa-ui/accordion/kometa-accordion.ts import { KometaFoldBody } from 'kometa-ui/fold'; // projects/kometa-ui/drawer/kometa-drawer.ts import { KometaMotion } from 'kometa-ui/motion'; ng-packagr works out the build order from these imports. In the published package, kometa-ui/accordion imports kometa-ui/fold like any other dependency, and a bundler sees two modules that can go to different chunks.
Making the workspace agree
Two places outside the library need to know the entry points exist.
TypeScript paths. The demo app imports the library from source, so there is no rebuild while developing. The root tsconfig.json maps both the primary and the secondary entries:
"kometa-ui": ["./projects/kometa-ui/src/public-api.ts"], "kometa-ui/*": ["./projects/kometa-ui/*/index.ts"] The second line also resolves the library's own from 'kometa-ui/fold' imports during development and in tests.
Test include. Specs moved next to their components, out of src/, so the library's test target in angular.json picks them up from the sibling folders:
"include": ["**/*.spec.ts", "../*/*.spec.ts"] src/lib/ now holds only the test setup and two specs that cover several components at once.
Adding a component now
A folder with four files (the component, its spec, index.ts, ng-package.json) and one export * line in src/public-api.ts. The docs generator reads the folder name too, so each docs page prints the import line for the right entry.
Is it worth it for your library?
If your library is a handful of components that every page uses, probably not: everything ends up in the initial chunk anyway. If it is a kit where most components appear on one or two screens, and apps lazy-load those screens, a single barrel quietly moves code into the initial load, and the build output will not warn you about it. Comparing the initial chunk before and after is a quick way to find out.
Kometa UI is MIT, on npm as kometa-ui (now 0.3.0, which also adds ng add kometa-ui). Docs: https://inomjonismaylov.github.io/kometa-ui-kit/ · Try it on StackBlitz: https://stackblitz.com/github/InomjonIsmaylov/kometa-ui-kit/tree/main/examples/starter?file=src/app/app.ts
