nera-common
v0.36.0
Published
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Readme
nera-frontend-common
Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? Use the template at the bottom!
Add your files
- Create or upload files
- Add files using the command line or push an existing Git repository with the following command:
cd existing_repo
git remote add origin https://gitlab.com/fluxvane/nera-iam/nera-frontend-common.git
git branch -M develop
git push -uf origin developIntegrate with your tools
Collaborate with your team
- Invite team members and collaborators
- Create a new merge request
- Automatically close issues from merge requests
- Enable merge request approvals
- Set auto-merge
Test and Deploy
Use the built-in continuous integration in GitLab.
- Get started with GitLab CI/CD
- Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)
- Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy
- Use pull-based deployments for improved Kubernetes management
- Set up protected environments
Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to makeareadme.com for this template.
Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
Name
Choose a self-explaining name for your project.
Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
Installation
Consumers pin nera-common to ONE exact version ("nera-common": "0.36.0"), never a caret range: on a 0.x package a caret (^0.21.2) resolves only >=0.21.2 <0.22.0, so the consumer is frozen on its old minor and silently misses every release (a breaking change bumps the MINOR while the major is 0).
After tagging a release, move every consumer in one step and let the lockfiles follow:
pnpm bump:consumers --install <consumerDir>... # pins this package's version everywhere under each dir
pnpm bump:consumers --check <consumerDir>... # exit 1 if any pin is a range or differs from this versionUsage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
License
For open source projects, say how it is licensed.
Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork this project or volunteer to work on it as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
Publishing the Angular components to the shared library
Status: PLAN — not implemented. This section documents what has to happen before the Angular components (
DynamicTableComponent,PageHeaderComponent,BreadcrumbComponent) can leave the fe-web application and live innera-common/angularlike the other shared Angular code. Do NOT assume the components are published — they are not.
What is published today
nera-common/angular is built with tsc, not with ng-packagr:
tsc -p tsconfig.json → dist/angular/*.js + dist/angular/*.d.tsThat works for plain TypeScript Angular utilities — services, interceptors, pipes and helpers — because they carry no Angular compiler metadata:
| Already in nera-common/angular | File |
| --- | --- |
| BaseDestroy | src/angular/base-destroy.ts |
| RestApiService | src/angular/rest-api.service.ts |
| GenericCrudService | src/angular/generic-crud.service.ts |
| pagination helpers | src/angular/pagination.ts |
| HTTP param helpers | src/angular/http-params.ts |
| list-page helpers | src/angular/list-page.ts |
| customValidators / REGEX_VALIDATORS | src/angular/validators.ts |
| interceptors, tokens, API response | src/angular/*.ts |
The validators are the reason they are described structurally (an
AbstractControlLike with a value, a ValidatorFnLike): the angular
subpath must not depend on @angular/forms, which is not a peer of this
package. The returned validator is still assignable to Angular's ValidatorFn.
The entry point is self-contained
ng-packagr compiles every entry point on its own (the compiler's rootDir is
the entry point's own folder), so the angular entry point may not import
from the package's root entry point. The modules it shares with the root
(lib/types/api, lib/errors/*, lib/api/notification-paths,
lib/auth/*) therefore have an entry-point-local copy under
src/angular/internal/. Keep those copies in sync with the canonical ones.
The ports a consumer binds
The components never depend on an app service — they take ports:
| Port | Token | Used for |
| --- | --- | --- |
| translator | NERA_TRANSLATOR | every label the components render (column labels, cell values, empty/error text) |
| toast | NERA_TOAST_SERVICE | the table's copy-to-clipboard feedback |
| timezone | NERA_TIMEZONE | the display zone of the shared date pipe |
providers: [
{
provide: NERA_TRANSLATOR,
useFactory: (transloco: TranslocoService) => ({
translate: (key: string, params?: Record<string, unknown>) =>
transloco.translate(key, params),
getActiveLang: () => transloco.getActiveLang(),
}),
deps: [TranslocoService],
},
{ provide: NERA_TOAST_SERVICE, useExisting: ToastService },
{
provide: NERA_TIMEZONE,
useFactory: () => ({ zone: () => inject(TimezoneService).zone() }),
},
]Without a translator the components render the key itself (the fallback an i18n library uses for an unknown key), so a consumer that has no i18n wiring yet still gets a usable surface.
Peers
@angular/core, @angular/common (already there) plus @angular/router and
@angular/material — all optional (peerDependenciesMeta), because the
breadcrumb's RouterLink and the table's Material modules are only needed by
the apps that use those components.
Rollback
The release is tag-gated, so a bad angular artifact is fixed with a new
release, not an unpublish. pnpm build:angular:tsc builds the subpath with
plain tsc again (the pre-ng-packagr artifact shape: one file per module);
point the "./angular" export map at ./dist/angular/index.tsc.js to cut
such a release. The shared components are not part of that legacy build (a
templateUrl template cannot be shipped by tsc) — the legacy barrel
src/angular/index.tsc.ts leaves them out.
Verification
pnpm build→dist/angular/{fesm2022,index.d.ts,package.json}.pnpm test→ the Angular unit tests (vitest,import '@angular/compiler'plus aresolveComponentResourceshelper for thetemplateUrlcomponents).- In a consumer app:
npx ng buildmust succeed AOT — noNG0303/ "not a known element" in the console, which is the symptom of atsc-built component.
The plan (steps, in order)
- Add the toolchain:
ng-packagr+@angular/compiler-clias devDependencies, and asrc/angular/ng-package.jsondescribing the secondary entry point ("lib": { "entryFile": "src/angular/public-api.ts" }). - Split the build: keep
tscfor the root /react/serversubpaths and add a second build step for the Angular package (ng-packagr -p src/angular/ng-package.json→dist/angular/with the FESM bundle and the.d.ts). Thetsconfig.jsonused bytscmust then exclude the component sources, or they would be emitted twice. - Move the components into the library
(
src/angular/components/…), stripping every app-specific dependency: noapp/core/...imports, no feature services, no environment-specific configuration. The components must only import from@angular/core(and@angular/common/@angular/formswhere they need them). - Declare the peers:
@angular/core,@angular/commonand@angular/formsbecome (optional) peer dependencies of the package, with the matchingpeerDependenciesMetaentries — ng-packagr refuses to bundle them. - Keep the barrel in sync:
src/angular/index.tsstays the public surface (export * from './components/...'), and theexportsmap keeps pointing./angularat the ng-packagr output. - Migrate the consumers: fe-web imports the components from
nera-common/angularand deletes its local copies; the other Angular apps can adopt them the same way. Each consumer bumps the pinnednera-commonversion — the release is tag-gated (x.y.z), as usual. - Verify:
npx ng-packagr -p src/angular/ng-package.jsonproducesdist/angularwith a FESM bundle and type declarations, and a consumer app compiles the components AOT (noNG0303/ "not a known element" in the console).
Risks / open questions
- ng-packagr wants its own
tsconfig(compilationMode: "partial") — the library's currenttscsetup has to be reconciled with it. - The components in fe-web may pull in app-level services (i18n, permissions, API base). Those have to become injectable library services or inputs first, otherwise the move is not possible.
- The
angularsubpath's public surface grows: everything exported fromsrc/angular/index.tsbecomes semver-relevant.
Plan: the IAM portal's table + page-scaffold variants, and the ng-packagr migration of the angular subpath (card 4830)
Status: Half A (React) is IMPLEMENTED; Half B (ng-packagr) is still a PLAN. Half A moved the IAM portal's table + page-scaffold variants into
react/lib/components/data-table/resource-tableandreact/lib/components/scaffold; the portal now consumes them and its fork is deleted. Half B — moving theangularsubpath to ng-packagr — is untouched and still a plan. The section above ("Publishing the Angular components to the shared library") is the general outline; this one is the file-level, config-level version of card 4830 and of the Angular half that goes with it.
Why two halves
Card 4830 is a React card. The IAM portal
(modules/iam/nera-iam-portal, Next.js) keeps a 1 090-LOC fork of its own
table + page-scaffold family because nera-common/react does not expose the
variants the portal actually uses — so the fork keeps living. React components
need no special build: the react subpath is compiled by plain tsc and
can host them as they are.
The Angular half is the second blocker in the same area. nera-common/angular
is compiled by tsc, and tsc cannot emit AOT components — so no Angular
component (fe-web's DynamicTableComponent, PageHeaderComponent,
BreadcrumbComponent) can ever live in the library, no matter how the code is
written. Publishing them means moving that subpath to ng-packagr, which is
specified in Half B below.
Half A and Half B are independent and can ship in separate releases.
Half A — React: the DataTable + scaffold variants the IAM portal needs
A.0 What shipped (the plan below is kept as the design record)
The plan's file table is the design record; this is what actually landed.
| Target in the kit | What it is |
| --- | --- |
| react/lib/components/data-table/resource-table/ | ResourceTable + its body / toolbar / pagination / row-action menu / confirm hook / labels / column utils / types. The fork's DataTable → ResourceTable, Column → ResourceColumn, RowAction → ResourceRowAction, BulkAction → ResourceBulkAction, DataTableLabels → ResourceTableLabels, DataTablePaginationLabels → ResourceTablePaginationLabels, DataTableProps → ResourceTableProps. |
| react/lib/components/scaffold/PageHeaderStats.tsx | The portal's stat tile. |
| react/lib/components/scaffold/PageLayout.tsx | Page, PageContent, Breadcrumb — new: the portal's shared/page/page-layout.tsx had already been deleted (91155703), so there was nothing to move. |
| react/lib/components/scaffold/ListPage.tsx | The list page (header + stats + table), without the portal's next-intl calls — the translated copy arrives as props. |
| PageHeader / PageScaffold | Gained stats?: ReactNode (the plan's slot) and gutter?: boolean (an app whose shell owns the horizontal padding renders the header flush). |
Decisions the plan left open:
- The query shape. The plan asked for ONE spelling.
ListQueryParams/SortOrdernow live in the kit and the IAM portal re-exports them (@/shared/types/table); the portal no longer declares its own copy. It is deliberately NOTListQueryState(theuseListQueryhook's URL state, which speakssortDirection): this one is the filter contract a table renders and an API consumes —sortOrderis what the shared filter schemas (UserFilterParamsSchema) speak. - The confirm dialog. The fork used the portal's own
ConfirmDialogwrapper; the kit'sConfirmDialogalready carries the reason prompt, so the moved code uses it directly (the wrapper only added a translated cancel label, which the table passes itself). next-intl. The kit stays i18n-agnostic: the copy arrives as props and the portal keeps a thinuseDataTableI18n()adapter.
A.1 The gap (verified against the portal's fork)
| What the IAM portal needs | nera-common/react today |
| --- | --- |
| column shape key / header / render | DataTableColumn = id / header / accessor (+ sortValue) |
| bulk-actions bar inside the table | BulkActionsBar exists as a standalone scaffold piece; DataTable has no bulkActions prop and never renders it |
| search toolbar | none — FilterBar is a separate scaffold piece, DataTable has no search |
| date-range filter | none |
| i18n label slots (labels, paginationLabels, actionsLabel, searchPlaceholder, emptyMessage / emptyHint) | only the useNeraUiLabels() context — no per-table overrides |
| column visibility (hideBelow, className, headerClassName, minWidth) | none (the lib has size / truncate / monospace / numeric instead) |
| PageHeaderStats | none (StatCard is a different shape) |
| Page / PageContent / Breadcrumb | none (PageScaffold is a different composition) |
| ListPage | none |
The lib's DataTable (TanStack-based, id/accessor) is kept as is — it
is the shape sa-portal and photoqa already use. The portal's shape is added as
a second, self-contained component so neither API has to grow a second
set of column semantics.
A.2 The files that move
Source: modules/iam/nera-iam-portal/src/components/.
Target: projects/nera-common/src/react/lib/components/.
| Source file | Target file | LOC | Change while moving |
| --- | --- | --- | --- |
| ui/data-table/types.ts | data-table/resource-table/types.ts | 123 | Column → ResourceColumn, ColumnVisibility → ResourceColumnVisibility, DataTableLabels → ResourceTableLabels, DataTableProps → ResourceTableProps; ConfirmConfig / ConfirmableAction keep their names (no clash in the lib) |
| ui/data-table/default-labels.ts | data-table/resource-table/default-labels.ts | 32 | DEFAULT_LABELS → RESOURCE_TABLE_DEFAULT_LABELS, resolveLabels → resolveResourceTableLabels (exported name — a stable seam other components use) |
| ui/data-table/column-utils.ts | data-table/resource-table/column-utils.ts | 18 | columnVisibilityClass, minWidthValue — unchanged |
| ui/data-table/use-confirmable-action.ts | data-table/resource-table/use-confirmable-action.ts | 93 | unchanged |
| ui/data-table/data-table.tsx | data-table/resource-table/resource-table.tsx | 123 | DataTable → ResourceTable |
| ui/data-table/data-table-body.tsx | data-table/resource-table/resource-table-body.tsx | 229 | unchanged |
| ui/data-table/data-table-toolbar.tsx | data-table/resource-table/resource-table-toolbar.tsx | 161 | search + date-range filter + bulk bar |
| ui/data-table/data-table-pagination.tsx | data-table/resource-table/resource-table-pagination.tsx | 173 | unchanged |
| ui/data-table/row-action-menu.tsx | data-table/resource-table/row-action-menu.tsx | 124 | unchanged |
| ui/data-table/__tests__/use-confirmable-action.test.tsx | data-table/resource-table/__tests__/use-confirmable-action.test.tsx | 62 | unchanged |
| shared/page/page-header.tsx (the PageHeaderStats half) | scaffold/PageHeaderStats.tsx | ~90 of 191 | new component; the portal's PageHeader itself is not moved — the portal migrates onto the lib's PageHeader |
| shared/page/page-layout.tsx | scaffold/PageLayout.tsx | 105 | Page, PageContent, Breadcrumb |
| shared/page/list-page.tsx | scaffold/ListPage.tsx | 275 | ListPage, ListPageProps, ListPageStat, ListPageTableProps |
| shared/page/__tests__/list-page.test.tsx | scaffold/__tests__/list-page.test.tsx | 81 | unchanged |
| shared/page/entity-detail-header.tsx | stays in the portal | 120 | it depends on @/features/auth (PermissionGuard) — portal-specific, not a shared primitive |
New public API (exported from src/react/index.ts through the existing
data-table/ and scaffold/ barrels — no export-map change):
// data-table/resource-table/types.ts
export type ResourceColumnVisibility = 'always' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
export interface ResourceColumn<T> {
key: keyof T | string
header: string
render?: (row: T) => ReactNode
sortable?: boolean
hideBelow?: ResourceColumnVisibility
className?: string
headerClassName?: string
minWidth?: number | string
}
export interface ResourceTableLabels { selected?: string; moreOptions?: string; searchAria?: string
fromDate?: string; toDate?: string; dateRangeSeparator?: string; selectAll?: string
selectRow?: string; confirmTitle?: string; confirmMessage?: string; cancel?: string }
export interface ResourceTablePaginationLabels { showing?: string; loading?: string; previous?: string
next?: string; previousPage?: string; nextPage?: string; rowsPerPage?: string; perPage?: string }
export interface ResourceTableProps<T> { columns: ResourceColumn<T>[]; items: T[]; total: number
initialQuery?: ListQueryParams; onQueryChange?: (query: ListQueryParams) => void
toolbar?: ReactNode; getRowActions?: (row: T) => ResourceRowAction<T>[]
bulkActions?: ResourceBulkAction<T>[]; showDateFilters?: boolean; hideSearch?: boolean
isLoading?: boolean; emptyMessage?: string; emptyHint?: string; actionsLabel?: string
searchPlaceholder?: string; paginationLabels?: ResourceTablePaginationLabels
labels?: ResourceTableLabels; className?: string }
// scaffold
export { PageHeaderStats } from './PageHeaderStats'
export { Page, PageContent, Breadcrumb } from './PageLayout'
export { ListPage } from './ListPage'PageHeader (and PageScaffold, which forwards to it) gains one slot:
stats?: ReactNode — the portal renders its stat tiles under the title and
the lib's header has no place for them today.
A.3 What must change while moving (the fork's app-level imports)
| Fork import | Replacement in the lib |
| --- | --- |
| cn from @/shared/utils | cn from nera-common (already in the lib) |
| useTranslations from next-intl (4 files) | no next-intl in the lib — the copy arrives as props (labels, paginationLabels, actionsLabel, …). The portal keeps a thin useDataTableI18n() that maps its next-intl messages onto those props; that hook stays in the portal |
| ConfirmDialog from @/components/shared/feedback/confirm-dialog | the lib has ConfirmDeleteDialog; the fork's dialog adds an optional reason prompt — either extend ConfirmDeleteDialog or add feedback/ConfirmDialog.tsx with the same ConfirmConfig contract |
| useDebounce from @/shared/hooks/shared-ui-hooks | useDebounce from nera-common/react (already in the lib) |
| ListQueryParams / SortOrder from @/shared/types/table | the lib's use-list-query (ListQueryState) — decide one shape: either the lib's ListQueryState is adopted, or ListQueryParams is added next to it. Do not ship two spellings of the same query object |
| Button / Checkbox / Input / Skeleton from nera-common/react | already the lib's — no change |
A.4 Tests
Move the two existing test files with the code, and add:
data-table/resource-table/__tests__/resource-table.test.tsx— renders the table with a fake row set and asserts: the bulk bar appears with the selection count, the search box filters the rows, the date-range filter reports its range throughonQueryChange,hideBelowadds the responsive class,labels/paginationLabelsoverride the English fallbacks, and aColumnwithkey+renderrenders the cell.scaffold/__tests__/page-header-stats.test.tsx— the tile renders the value, and renders the skeleton whileloadingis true (never a fake0).scaffold/__tests__/page-layout.test.tsx—Pagerenders header + content,Breadcrumbmarks the last itemaria-current="page".scaffold/__tests__/list-page.test.tsx— moved from the portal.
A.5 The portal migration (separate repo, separate MR)
modules/iam/nera-iam-portal: switch the imports tonera-common/react, deletesrc/components/ui/data-table/and the page-scaffold copies, keep the portal-onlyentity-detail-header.tsxand theuseDataTableI18nhook.- Bump the pinned
nera-commonversion in the portal and tag the release (x.y.z— the release is tag-gated). - The fork's files are deleted in the portal's MR only — the library keeps the components, so a rollback re-adds the portal copies without touching the lib.
Half B — Angular: moving the angular subpath to ng-packagr
B.1 What moves (and what has to be stripped first)
Source: projects/core/pfl01y25-fe-web/src/app/shared/components/
(the path in card 4830 — src/app/core/components/dynamic-table — does not
exist; this is the real one).
| Source file | Target file |
| --- | --- |
| dynamic-table/dynamic-table.component.ts / .html / .scss | src/angular/components/dynamic-table/dynamic-table.component.ts / .html / .scss |
| page-header/page-header.component.ts | src/angular/components/page-header/page-header.component.ts |
| breadcrumb/breadcrumb.component.ts / .html | src/angular/components/breadcrumb/breadcrumb.component.ts / .html |
| page-header/page-header.component.spec.ts | src/angular/components/page-header/__tests__/page-header.component.spec.ts |
The app-level dependencies of those components have to become library inputs / injected tokens before the move:
| App import (fe-web) | Becomes |
| --- | --- |
| IPagination from app/core/models/general.model | the lib's IPaginationMetadata (src/angular/api-response.ts) |
| DEFAULT_PAGINATOR from app/shared/constants | the lib's DEFAULT_PAGINATOR (src/angular/pagination.ts) — already there |
| AppDatePipe from app/shared/pipes/app-date.pipe | moves into the lib (src/angular/app-date.pipe.ts) |
| formatMoney from app/shared/utils/money.utils | moves into the lib (src/angular/format.ts) |
| ToastService from app/core/services/toast.service | an @Output() / callback input — the lib must not depend on an app service |
| getAngularLocale from app/core/i18n/app-locale | an @Input() (locale) or an injection token |
| TranslocoModule / TranslocoService | label @Input()s — the lib must not depend on Transloco (not a peer) |
| @angular/material/* (table, paginator, button, icon, tooltip, progress-bar, progress-spinner) | a new optional peer dependency; or the components are re-written without Material |
| @angular/router (RouterLink in the breadcrumb) | a new optional peer dependency, or plain <a href> |
B.2 The ng-packagr configuration
src/angular/ng-package.json (new):
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/angular",
"lib": { "entryFile": "index.ts" }
}src/angular/tsconfig.lib.json (new):
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/angular",
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"declaration": true,
"types": []
},
"angularCompilerOptions": {
"compilationMode": "partial",
"strictTemplates": true
},
"include": ["**/*.ts", "**/*.html"],
"exclude": ["**/*.spec.ts", "**/__tests__/**"]
}package.json:
"scripts": {
// the root tsc build stops emitting the angular subpath (see B.3)
"build": "tsc -p tsconfig.json && ng-packagr -p src/angular/ng-package.json -c src/angular/tsconfig.lib.json && node dist/tokens/write-tokens-css.js",
// kept until the ng-packagr output is proven in a consumer app — this is
// the rollback switch (see B.6)
"build:angular:tsc": "tsc -p tsconfig.angular-tsc.json"
}devDependencies: ng-packagr + @angular/compiler-cli (both pinned to the
Angular major the consumers run — ^20.x today).
peerDependencies (+ the matching peerDependenciesMeta optional entries):
@angular/core, @angular/common, ^20.0.0 (already there), plus
@angular/forms, @angular/router, @angular/material — each only if the
moved components still need them.
B.3 The blocker to clear first: cross-entry-point imports
ng-packagr requires every entry point to be self-contained — an entry
point may not import from another entry point of the same package ("Importing
from a secondary entry point is not allowed"). The angular entry point
imports four modules from the ROOT entry point today:
| File | Imports |
| --- | --- |
| src/angular/api-response.ts | ../lib/types/api (type-only) |
| src/angular/api-error.interceptor.ts | ../lib/errors/error-code-message (runtime) |
| src/angular/notification-inbox.ts | ../lib/types/api (type-only) |
| src/angular/notification-inbox.service.ts | ../lib/api/notification-paths (runtime) |
Resolution: the angular entry point gets its own copies of the three
shared modules (the envelope type aliases, the error-code map, the notification
path constants). The type-only copies cost nothing at runtime; the two
runtime maps are small. Spike first: run ng-packagr -p
src/angular/ng-package.json on the current sources and read the errors — if
ng-packagr turns out to accept them, the duplication is unnecessary.
The root tsconfig.json must then exclude src/angular/** from the
tsc build, or the two builds emit into dist/angular twice:
"exclude": ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "src/angular/**"]Nothing outside src/angular imports src/angular (the root barrel does not
re-export it), so the exclusion is safe.
B.4 The export map change
Before (today — tsc output, one file per module):
"./angular": {
"types": "./dist/angular/index.d.ts",
"import": "./dist/angular/index.js",
"default": "./dist/angular/index.js"
}After (ng-packagr output — one FESM bundle + the type declarations):
"./angular": {
"types": "./dist/angular/index.d.ts",
"import": "./dist/angular/fesm2022/<bundle>.mjs",
"default": "./dist/angular/fesm2022/<bundle>.mjs"
}<bundle> is the file ng-packagr actually emits (it derives the name from the
package name and the entry point — read it from the build output, do not
guess it into the map).
Consequences:
- Deep imports break.
nera-common/angular/rest-api.serviceresolves through the"./*"export map to./dist/angular/rest-api.service.js, which no longer exists. No consumer does that today — the only importer of the subpath is fe-web, and it imports the mapped'nera-common/angular'(14 files, 0 deep imports) — but the"./*"pattern stays in the map for the other subpaths, so the breakage is invisible until someone tries it. - The root
"."export is untouched: the root barrel is stilltsc-built.
B.5 Versioning impact
| Change | Semver |
| --- | --- |
| Half A (new React components + PageHeader.stats) | additive → minor |
| Half B (the angular subpath's published artifact changes shape) | breaking for deep importers → minor as well, because the package is 0.x (the repo rule: while the major version is 0, a breaking change bumps the MINOR version) |
- Version 0.33.0 → 0.34.0 for Half A. (
0.33.0already shipped — the shared Angular inbox client — so the plan's original "0.32.1 → 0.33.0" text is one release behind.) A consumer pinning^0.33.0does not pick0.34.0up (caret on a0.xversion pins the minor), so every consumer has to bump explicitly — that is the point of the minor bump. - The changelog entry must call out the
angularsubpath change explicitly (artifact shape, deep imports gone) — a consumer that deep-imports would otherwise find out at build time. - The release stays tag-gated (
x.y.z), as usual; MAIN owns the version bump — this plan does not touchpackage.json's version.
B.6 Rollback
The release is tag-gated and npm's unpublish window is 72 h — do not rely
on unpublishing a bad 0.33.0.
- Before the migration ships: keep the old build runnable
(
build:angular:tsc+ thetsconfig.angular-tsc.jsonit needs) so a release can always be cut from the previous shape of the subpath. - If the ng-packagr build breaks a consumer: revert the
buildscript, thetsconfig.jsonexclusion and the"./angular"export-map entry in the library, and publish 0.33.1 with thetsc-built subpath. The consumers only bump their pin — no source change on their side. - If a moved component misbehaves (AOT metadata, template compile error): keep the component in the library but do not delete the app's copy in the same release — the app switches to the library copy in a follow-up MR, so the rollback is "point the import back at the app copy".
- Half A rollback is independent: the new React components are additive,
so a bad release is reverted by publishing
0.33.1without them; the portal's fork is only deleted in the portal's own MR, so the portal can go back to it independently.
B.7 Verification
pnpm buildinnera-common→dist/angular/{fesm2022,index.d.ts,package.json}.pnpm testinnera-common→ the Angular unit tests keep passing (vitest loads the JIT compiler explicitly —import '@angular/compiler'— the same trick the existingsrc/angular/__tests__use).- In fe-web:
npx ng buildmust succeed AOT — noNG0303/ "not a known element" in the console, which is the symptom of atsc-built component. - In the IAM portal:
pnpm build+pnpm testafter the fork is deleted.
B.8 Order of work
- Half A in
nera-common(new components + tests) →0.34.0. - Portal migration in
modules/iam/nera-iam-portal(import from the lib, delete the fork) — its own repo, its own MR, its own version bump. The portal now pins[email protected]. - Half B in
nera-common(ng-packagr) →0.35.0. Half B does not depend on Half A. - fe-web migration (import the Angular components, delete the app copies) — after Half B ships.
