From b979cdd0a355bb640d77b596c7515467807fbafe Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 15 Sep 2026 18:24:54 +0200 Subject: [PATCH 1/2] fix(builders): externalize Angular's synthesized imports into shared mappings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit esbuild's `external` matches the unresolved specifier, so it only caught imports spelled `@myorg/ui`. Angular emits a deep relative path for any reference it has to synthesize — a template dependency reached through an imported NgModule — and those were inlined into the app alongside the federated copy, giving the lib two module instances and NG0201. The same duplication hits `providedIn: 'root'` services and pipes. Only synthesized references are affected, which is why it looks intermittent: a standalone component named in `imports: []` and an injected service both emit bare specifiers and are fine. createSharedMappingsPlugin covered this until 73f5c69 commented out both call sites and a6aa619 deleted them; it has shipped disabled since v20.3.8 / v21.1.8, so 21.x is affected too. Wire it back into the app build. The rule itself lives in core. createMappingImportResolver owns containment, entry-point resolution, the self-import check and the publication guard; what is left here is the relative filter, the import-statement check, a platform bail because Angular applies code plugins to the server bundle too, and reset() on onStart. reset() rather than a caller-supplied change set: esbuild re-resolves the whole graph on every rebuild, so nothing survives from a previous build, and the only changed-path set this builder holds is the federation rebuild's buffer, which races the Angular rebuild and is drained by it. Staleness is not symmetric — a stale decline leaves an import inlined, which is the duplication being fixed, while a stale rewrite emits a specifier no bundler validates and surfaces as `undefined` at runtime. That asymmetry is also why a file the entry point does not publish is left inlined rather than rewritten: Angular emits the deep import whether or not the barrel re-exports the file, and pointing it at a namespace with no such name fails at runtime instead of merely shipping twice. The bail is on `ngServerMode`, not `platform`: SSR targeting an edge runtime builds the server bundle as 'neutral', and Angular uses that platform for browser-side global scripts too, so it does not identify the server bundle. A rewrite there would emit a bare specifier no import map resolves. Resolves outside the file namespace are declined as well. Angular's virtual modules resolve against the workspace root, so the joined path names a file the importer never asked for, and one that happens to sit under a mapping would otherwise be rewritten. The mapping-or-exposed build keeps its externals as they are: core bundles every mapping and expose as entry points of one esbuild context with splitting on by default, which already collapses cross-mapping relative imports onto a single shared chunk. Closes #128 --- src/builders/build/builder.ts | 4 + .../esbuild/shared-mappings-plugin.spec.ts | 361 ++++++++++++++++-- src/tools/esbuild/shared-mappings-plugin.ts | 57 +-- 3 files changed, 356 insertions(+), 66 deletions(-) diff --git a/src/builders/build/builder.ts b/src/builders/build/builder.ts index eab0c8a..9e9ba64 100644 --- a/src/builders/build/builder.ts +++ b/src/builders/build/builder.ts @@ -59,6 +59,7 @@ import { } from "./watch-decisions.js"; import type { NfBuilderSchema, NfInternalOptions } from "./schema.js"; import { createAngularBuildAdapter } from "../../tools/esbuild/angular-esbuild-adapter.js"; +import { createSharedMappingsPlugin } from "../../tools/esbuild/shared-mappings-plugin.js"; import { getI18nConfig, translateFederationArtifacts } from "./i18n.js"; import { updateScriptTags } from "./update-index-html.js"; @@ -325,6 +326,9 @@ export async function* runBuilder( } }, }, + ...(Object.keys(normalized.config.sharedMappings).length > 0 + ? [createSharedMappingsPlugin(normalized.config.sharedMappings)] + : []), // Inject custom esbuild plugins ...(Array.isArray(nfBuilderOptions.plugins) ? nfBuilderOptions.plugins diff --git a/src/tools/esbuild/shared-mappings-plugin.spec.ts b/src/tools/esbuild/shared-mappings-plugin.spec.ts index a8e7b10..9738761 100644 --- a/src/tools/esbuild/shared-mappings-plugin.spec.ts +++ b/src/tools/esbuild/shared-mappings-plugin.spec.ts @@ -1,18 +1,86 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { createSharedMappingsPlugin } from './shared-mappings-plugin.js'; import type { PathToImport } from '@softarc/native-federation/internal'; -import type { OnResolveArgs, OnResolveOptions, PluginBuild } from 'esbuild'; +import type { BuildOptions, OnResolveArgs, OnResolveOptions, PluginBuild } from 'esbuild'; type ResolveHandler = (args: OnResolveArgs) => Promise<{ path?: string; external?: boolean }>; -function setupPlugin(mappedPaths: PathToImport): { - options: OnResolveOptions; - handler: ResolveHandler; -} { +/** + * Core's resolver reads the barrels off disk and compares declarations, so the mappings have to + * point at real files holding real exports — an empty file publishes nothing and is declined as + * a side-effect import. + * + * - `foo` is a plain lib whose entry point is a non-index barrel. + * - `foo-utils` only exists to share a path prefix with `foo`. + * - `ui` re-exports its module and its component, and deliberately hides a third file. + * - `ui/lib/testing` is a secondary entry point nested under `ui`'s barrel. + * - `modonly` is the NgModule shape whose barrel publishes only the module. + * - `renamed` re-exports under a different name than the class is declared with. + * - `facade` mixes a re-export of another package with a relative one, which leaves its export + * surface incomplete — core cannot read through the package specifier. + */ +let ws: string; + +function write(relative: string, contents = ''): void { + const file = path.join(ws, relative); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, contents); +} + +beforeAll(() => { + ws = fs.mkdtempSync(path.join(os.tmpdir(), 'nf-shared-mappings-')); + + write('libs/foo/src/public-api.ts', "export * from './lib/thing';\n"); + write('libs/foo/src/lib/thing.ts', 'export class Thing {}\n'); + write('libs/foo-utils/src/helper.ts', 'export class Helper {}\n'); + + write( + 'libs/ui/src/index.ts', + "export * from './lib/ui.module';\nexport * from './lib/badge.component';\n" + ); + write('libs/ui/src/lib/ui.module.ts', 'export class UiModule {}\n'); + write('libs/ui/src/lib/badge.component.ts', 'export class BadgeComponent {}\n'); + write('libs/ui/src/lib/hidden.component.ts', 'export class HiddenComponent {}\n'); + write('libs/ui/src/lib/testing/index.ts', "export * from './harness';\n"); + write('libs/ui/src/lib/testing/harness.ts', 'export class Harness {}\n'); + + write('libs/modonly/src/index.ts', "export * from './lib/ui.module';\n"); + write('libs/modonly/src/lib/ui.module.ts', 'export class UiModule {}\n'); + write('libs/modonly/src/lib/badge.component.ts', 'export class BadgeComponent {}\n'); + + write( + 'libs/renamed/src/index.ts', + "export { BadgeComponent as Badge } from './lib/badge.component';\n" + ); + write('libs/renamed/src/lib/badge.component.ts', 'export class BadgeComponent {}\n'); + + write( + 'libs/facade/src/index.ts', + "export * from '@angular/core';\nexport * from './lib/badge.component';\n" + ); + write('libs/facade/src/lib/badge.component.ts', 'export class BadgeComponent {}\n'); + + write('apps/app/src/main.ts'); +}); + +afterAll(() => fs.rmSync(ws, { recursive: true, force: true })); + +function setupPlugin( + mappedPaths: PathToImport, + initialOptions: BuildOptions = { platform: 'browser', define: { ngServerMode: 'false' } } +): { options?: OnResolveOptions; handler?: ResolveHandler; start?: () => void } { const plugin = createSharedMappingsPlugin(mappedPaths); - let options!: OnResolveOptions; - let handler!: ResolveHandler; + let options: OnResolveOptions | undefined; + let handler: ResolveHandler | undefined; + let start: (() => void) | undefined; const build = { + initialOptions, + onStart(cb: () => void) { + start = cb; + }, onResolve(opts: OnResolveOptions, cb: ResolveHandler) { options = opts; handler = cb; @@ -20,68 +88,281 @@ function setupPlugin(mappedPaths: PathToImport): { } as unknown as PluginBuild; plugin.setup(build); - return { options, handler }; + return { options, handler, start }; } -const MAPPED: PathToImport = { - '/ws/libs/foo/src/public-api.ts': 'foo-remote', -}; +const foo = (): PathToImport => ({ [path.join(ws, 'libs/foo/src/public-api.ts')]: 'foo-remote' }); + +const ui = (): PathToImport => ({ + [path.join(ws, 'libs/ui/src/index.ts')]: '@myorg/ui', + [path.join(ws, 'libs/ui/src/lib/testing/index.ts')]: '@myorg/ui/testing', +}); + +function resolve( + handler: ResolveHandler, + args: { + from: string; + import: string; + kind?: OnResolveArgs['kind']; + namespace?: string; + resolveDir?: string; + } +) { + const importer = path.join(ws, args.from); + + return handler({ + kind: args.kind ?? 'import-statement', + namespace: args.namespace ?? 'file', + resolveDir: args.resolveDir ?? path.dirname(importer), + path: args.import, + importer, + } as OnResolveArgs); +} describe('createSharedMappingsPlugin', () => { it('registers an onResolve handler for relative imports', () => { - const { options } = setupPlugin(MAPPED); - expect(options.filter).toEqual(/^[.]/); + const { options } = setupPlugin(foo()); + expect(options?.filter).toEqual(/^[.]/); }); it('maps a relative import pointing into a shared lib to an external path', async () => { - const { handler } = setupPlugin(MAPPED); + const { handler } = setupPlugin(foo()); - const result = await handler({ - kind: 'import-statement', - resolveDir: '/ws/apps/app/src', - path: '../../../libs/foo/src/public-api', - importer: '/ws/apps/app/src/main.ts', - } as OnResolveArgs); + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/foo/src/lib/thing', + }); + + expect(result).toEqual({ path: 'foo-remote', external: true }); + }); + + it('maps a relative import of the mapped entry point itself', async () => { + const { handler } = setupPlugin(foo()); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/foo/src/public-api', + }); expect(result).toEqual({ path: 'foo-remote', external: true }); }); it('does not externalize imports originating from within the same lib (self-import)', async () => { - const { handler } = setupPlugin(MAPPED); + const { handler } = setupPlugin(foo()); - const result = await handler({ - kind: 'import-statement', - resolveDir: '/ws/libs/foo/src', - path: './public-api', - importer: '/ws/libs/foo/src/internal.ts', - } as OnResolveArgs); + const result = await resolve(handler!, { + from: 'libs/foo/src/lib/other.ts', + import: './thing', + }); expect(result).toEqual({}); }); it('ignores non-import-statement kinds', async () => { - const { handler } = setupPlugin(MAPPED); + const { handler } = setupPlugin(foo()); - const result = await handler({ + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/foo/src/lib/thing', kind: 'require-call', - resolveDir: '/ws/apps/app/src', - path: '../../../libs/foo/src/public-api', - importer: '/ws/apps/app/src/main.ts', - } as OnResolveArgs); + }); expect(result).toEqual({}); }); it('returns an empty result for unmapped relative imports', async () => { - const { handler } = setupPlugin(MAPPED); + const { handler } = setupPlugin(foo()); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: './local-file', + }); + + expect(result).toEqual({}); + }); + + it('leaves a sibling lib whose path merely shares a prefix alone', async () => { + const { handler } = setupPlugin(foo()); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/foo-utils/src/helper', + }); + + expect(result).toEqual({}); + }); + + it('maps a component the barrel re-exports alongside its module', async () => { + const { handler } = setupPlugin(ui()); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/ui/src/lib/badge.component', + }); + + expect(result).toEqual({ path: '@myorg/ui', external: true }); + }); - const result = await handler({ - kind: 'import-statement', - resolveDir: '/ws/apps/app/src', - path: './local-file', - importer: '/ws/apps/app/src/main.ts', - } as OnResolveArgs); + it('leaves a file the barrel does not re-export inlined', async () => { + const { handler } = setupPlugin(ui()); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/ui/src/lib/hidden.component', + }); expect(result).toEqual({}); }); + + // The known decline: ngtsc emits the deep import whether or not the barrel publishes the + // component, so an NgModule lib exporting only its module stays duplicated. Rewriting anyway + // would point `i1.BadgeComponent` at a namespace that has no such name. + it('declines an NgModule lib whose barrel publishes only the module', async () => { + const { handler } = setupPlugin({ + [path.join(ws, 'libs/modonly/src/index.ts')]: '@myorg/modonly', + }); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/modonly/src/lib/badge.component', + }); + + expect(result).toEqual({}); + }); + + // The rewrite swaps the specifier but keeps the property access, so a renamed re-export + // leaves the file reachable while `ns.BadgeComponent` is undefined. + it('declines a file the barrel re-exports under a different name', async () => { + const { handler } = setupPlugin({ + [path.join(ws, 'libs/renamed/src/index.ts')]: '@myorg/renamed', + }); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/renamed/src/lib/badge.component', + }); + + expect(result).toEqual({}); + }); + + it('prefers the closest mapping when a secondary entry point sits under a barrel', async () => { + const { handler } = setupPlugin(ui()); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/ui/src/lib/testing/harness', + }); + + expect(result).toEqual({ path: '@myorg/ui/testing', external: true }); + }); + + it('externalizes a barrel file reaching into a secondary of the same lib', async () => { + const { handler } = setupPlugin(ui()); + + const result = await resolve(handler!, { + from: 'libs/ui/src/lib/badge.component.ts', + import: './testing/harness', + }); + + expect(result).toEqual({ path: '@myorg/ui/testing', external: true }); + }); + + it('registers nothing for the server bundle', () => { + const { options, handler, start } = setupPlugin(foo(), { + platform: 'node', + define: { ngServerMode: 'true' }, + }); + + expect(options).toBeUndefined(); + expect(handler).toBeUndefined(); + expect(start).toBeUndefined(); + }); + + // SSR targeting an edge runtime (`ssr.platform: "neutral"`) builds the server bundle with + // `platform: 'neutral'`, and a rewrite there emits a bare specifier no import map resolves. + it('registers nothing for a server bundle built for a non-node runtime', () => { + const { options, handler, start } = setupPlugin(foo(), { + platform: 'neutral', + define: { ngServerMode: 'true' }, + }); + + expect(options).toBeUndefined(); + expect(handler).toBeUndefined(); + expect(start).toBeUndefined(); + }); + + // Angular's virtual modules resolve against the workspace root, so the joined path would name + // a file the importer never asked for — here one that does sit under a mapping. + it('declines an import from a virtual module', async () => { + const { handler } = setupPlugin(foo()); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: './libs/foo/src/lib/thing', + namespace: 'angular:polyfills', + resolveDir: ws, + }); + + expect(result).toEqual({}); + }); + + // A barrel re-exporting another package cannot be read through, so its surface is incomplete. + // The entry point still publishes itself, and a file it re-exports relatively is still + // reachable, so neither shape depends on reading past the package specifier. + it('maps an entry point whose barrel also re-exports another package', async () => { + const { handler } = setupPlugin({ + [path.join(ws, 'libs/facade/src/index.ts')]: '@myorg/facade', + }); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/facade/src/index', + }); + + expect(result).toEqual({ path: '@myorg/facade', external: true }); + }); + + it('maps a deep import through a barrel that also re-exports another package', async () => { + const { handler } = setupPlugin({ + [path.join(ws, 'libs/facade/src/index.ts')]: '@myorg/facade', + }); + + const result = await resolve(handler!, { + from: 'apps/app/src/main.ts', + import: '../../../libs/facade/src/lib/badge.component', + }); + + expect(result).toEqual({ path: '@myorg/facade', external: true }); + }); + + // Export surfaces are cached for the resolver's lifetime and an esbuild context outlives any + // one rebuild, so without the onStart reset a barrel edited under `ng serve` would keep + // answering from the first build until restart. + it('picks up a barrel edited between rebuilds', async () => { + write('libs/watched/src/index.ts', "export * from './lib/ui.module';\n"); + write('libs/watched/src/lib/ui.module.ts', 'export class UiModule {}\n'); + write('libs/watched/src/lib/badge.component.ts', 'export class BadgeComponent {}\n'); + + const { handler, start } = setupPlugin({ + [path.join(ws, 'libs/watched/src/index.ts')]: '@myorg/watched', + }); + + const deepImport = { + from: 'apps/app/src/main.ts', + import: '../../../libs/watched/src/lib/badge.component', + }; + + expect(await resolve(handler!, deepImport)).toEqual({}); + + write( + 'libs/watched/src/index.ts', + "export * from './lib/ui.module';\nexport * from './lib/badge.component';\n" + ); + start!(); + + expect(await resolve(handler!, deepImport)).toEqual({ + path: '@myorg/watched', + external: true, + }); + }); }); diff --git a/src/tools/esbuild/shared-mappings-plugin.ts b/src/tools/esbuild/shared-mappings-plugin.ts index 14c963b..8c5b468 100644 --- a/src/tools/esbuild/shared-mappings-plugin.ts +++ b/src/tools/esbuild/shared-mappings-plugin.ts @@ -1,39 +1,44 @@ import type { Plugin, PluginBuild } from 'esbuild'; import * as path from 'path'; -import type { PathToImport } from '@softarc/native-federation/internal'; +import { + createMappingImportResolver, + type PathToImport, +} from '@softarc/native-federation/internal'; + +// esbuild's `external` only matches the unresolved specifier, so it misses the deep relative +// paths Angular emits for references it synthesizes — a template dependency reached through an +// imported NgModule, say — which would then be inlined alongside the federated copy. +export function createSharedMappingsPlugin(sharedMappings: PathToImport): Plugin { + const resolveMapping = createMappingImportResolver(sharedMappings); -// TODO: `createSharedMappingsPlugin` currently has no callers. Before deleting, -// verify its responsibility (rewriting relative imports of shared/exposed paths -// to externals) isn't already handled elsewhere — e.g. the federation adapter's -// `external` list / mapped-paths handling in `angular-bundler.ts`. If covered, -// remove this file and its spec; otherwise wire it back into the esbuild config. -export function createSharedMappingsPlugin(mappedPaths: PathToImport): Plugin { return { - name: 'custom', + name: 'nf-shared-mappings', setup(build: PluginBuild) { - build.onResolve({ filter: /^[.]/ }, async args => { - let mappedPath: string | undefined = undefined; - let isSelf = false; + // Angular applies code plugins to the server bundle too, which resolves externals itself. + // `platform` does not identify it — SSR on an edge runtime builds as 'neutral', and + // Angular uses that for browser-side global scripts as well. `ngServerMode` is defined + // 'true' on the server bundles and 'false' on the browser one. + if (build.initialOptions.define?.['ngServerMode'] === 'true') { + return; + } - if (args.kind === 'import-statement') { - const importPath = path.join(args.resolveDir, args.path); - if (mappedPaths) { - mappedPath = Object.keys(mappedPaths).find(p => importPath.startsWith(path.dirname(p))); - } - } + // The context outlives every rebuild it serves, so a barrel edited under `ng serve` would + // otherwise keep answering from the surface cached on the first build. + build.onStart(() => { + resolveMapping.reset(); + }); - if (mappedPath) { - isSelf = args.importer.startsWith(path.dirname(mappedPath)); + build.onResolve({ filter: /^[.]/ }, args => { + // Angular's virtual modules ('angular:polyfills' and friends) resolve against the + // workspace root, so the path joined below would not be the one the importer meant. + if (args.kind !== 'import-statement' || args.namespace !== 'file') { + return {}; } - if (mappedPath && !isSelf) { - return { - path: mappedPaths[mappedPath], - external: true, - }; - } + // Unresolved: the resolver does its own extension and index resolution. + const importName = resolveMapping(path.join(args.resolveDir, args.path), args.importer); - return {}; + return importName ? { path: importName, external: true } : {}; }); }, }; From 239f3c92d37d3ed0c911be9e97a871942cfe9289 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 16 Sep 2026 11:36:57 +0200 Subject: [PATCH 2/2] chore: Updated native-federation core --- package.json | 4 ++-- pnpm-lock.yaml | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 41ab371..d6f39ba 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "@angular-devkit/core": "~22.1.0", "@angular-devkit/schematics": "~22.1.0", "@chialab/esbuild-plugin-commonjs": "^0.19.0", - "@softarc/native-federation": "~4.5.0", - "@softarc/native-federation-orchestrator": "^4.5.2", + "@softarc/native-federation": "~4.6.0", + "@softarc/native-federation-orchestrator": "^4.6.0", "es-module-shims": "^2.8.0", "esbuild": "^0.28.0", "mrmime": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cd51ef..31ca244 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,10 +24,10 @@ importers: specifier: ^0.19.0 version: 0.19.1 '@softarc/native-federation': - specifier: ~4.5.0 - version: 4.5.0(typescript@6.0.3) + specifier: ~4.6.0 + version: 4.6.0 '@softarc/native-federation-orchestrator': - specifier: ^4.5.2 + specifier: ^4.6.0 version: 4.6.1 es-module-shims: specifier: ^2.8.0 @@ -1781,8 +1781,8 @@ packages: resolution: {integrity: sha512-n5XZvWCFPeLvZ5y42fH79X9L1MG+BMBLlbhKOxIVXbju+52BAQL6D+NoYOuOPTwtX5wFafwhzAs15bkggF3VSw==, tarball: https://registry.npmjs.org/@softarc/native-federation-orchestrator/-/native-federation-orchestrator-4.6.1.tgz} engines: {node: '>=24.16.0'} - '@softarc/native-federation@4.5.0': - resolution: {integrity: sha512-FixGtwyRT7o8nANxecr0UDTZWTzhzLqH+cE3clntzhx/t4BNkNQwqaOZlcyjHNTI6l6nH4zIY4Ct7jaHCjPO3A==, tarball: https://registry.npmjs.org/@softarc/native-federation/-/native-federation-4.5.0.tgz} + '@softarc/native-federation@4.6.0': + resolution: {integrity: sha512-Cd+z7CRpQYHagZUkjR9jCO6fWxl7rh0VfXcv4uhnCIopzWwwCMN0ltBvfSCjC5hso1gfPdOSlxGzhrqtprbZcw==, tarball: https://registry.npmjs.org/@softarc/native-federation/-/native-federation-4.6.0.tgz} '@softarc/sheriff-core@0.19.6': resolution: {integrity: sha512-KACxHG9sS7kNWgnnBODzdr14kMLMrJVlQKc+tViUP03p2fRwNhESOA49bz51Yn7dro1mbtMmmFjICLmZSJDZZA==, tarball: https://registry.npmjs.org/@softarc/sheriff-core/-/sheriff-core-0.19.6.tgz} @@ -4625,15 +4625,14 @@ snapshots: dependencies: semver: 7.8.5 - '@softarc/native-federation@4.5.0(typescript@6.0.3)': + '@softarc/native-federation@4.6.0': dependencies: '@softarc/sheriff-core': 0.19.6(typescript@6.0.3) chalk: 6.0.0 esbuild: 0.28.2 fast-glob: 3.3.3 json5: 2.2.3 - transitivePeerDependencies: - - typescript + typescript: 6.0.3 '@softarc/sheriff-core@0.19.6(typescript@6.0.3)': dependencies: