@softarc/native-federation 4.3.2 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +71 -1
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.js +2 -0
  4. package/dist/internal.d.ts +3 -2
  5. package/dist/internal.js +5 -1
  6. package/dist/lib/config/expand-mappings.d.ts +24 -0
  7. package/dist/lib/config/expand-mappings.js +88 -0
  8. package/dist/lib/config/get-used-dependencies.d.ts +0 -2
  9. package/dist/lib/config/get-used-dependencies.js +4 -40
  10. package/dist/lib/config/mapped-paths.d.ts +7 -2
  11. package/dist/lib/config/mapped-paths.js +21 -4
  12. package/dist/lib/config/mapping-utils.d.ts +32 -0
  13. package/dist/lib/config/mapping-utils.js +68 -0
  14. package/dist/lib/config/match-mapping.d.ts +9 -0
  15. package/dist/lib/config/match-mapping.js +46 -0
  16. package/dist/lib/config/remove-unused-deps.d.ts +2 -1
  17. package/dist/lib/config/remove-unused-deps.js +35 -2
  18. package/dist/lib/config/validate-mappings.d.ts +15 -0
  19. package/dist/lib/config/validate-mappings.js +28 -0
  20. package/dist/lib/config/with-native-federation.js +18 -6
  21. package/dist/lib/core/build/assemble-federation-info.d.ts +8 -0
  22. package/dist/lib/core/build/assemble-federation-info.js +34 -0
  23. package/dist/lib/core/build/build-for-federation.js +7 -45
  24. package/dist/lib/core/build/bundle-exposed-and-mappings.d.ts +1 -3
  25. package/dist/lib/core/build/bundle-exposed-and-mappings.js +20 -32
  26. package/dist/lib/core/build/bundle-shared.js +9 -3
  27. package/dist/lib/core/build/rebuild-for-federation.js +6 -33
  28. package/dist/lib/core/build/resolve-shared-dirs.d.ts +16 -0
  29. package/dist/lib/core/build/resolve-shared-dirs.js +8 -4
  30. package/dist/lib/core/cache/cache-persistence.d.ts +4 -2
  31. package/dist/lib/core/cache/cache-persistence.js +27 -8
  32. package/dist/lib/core/normalize-options.d.ts +2 -2
  33. package/dist/lib/core/normalize-options.js +23 -11
  34. package/dist/lib/core/output/write-federation-outputs.d.ts +7 -0
  35. package/dist/lib/core/output/write-federation-outputs.js +9 -0
  36. package/dist/lib/domain/config/federation-config.contract.d.ts +27 -2
  37. package/dist/lib/domain/utils/file-watcher.contract.d.ts +12 -0
  38. package/dist/lib/domain/utils/io-port.contract.d.ts +6 -3
  39. package/dist/lib/utils/file-watcher.d.ts +15 -4
  40. package/dist/lib/utils/file-watcher.js +112 -19
  41. package/dist/lib/utils/io/node-io-adapter.js +10 -5
  42. package/dist/lib/utils/package/entry-point-resolver.js +2 -5
  43. package/dist/lib/utils/package/package-info.d.ts +13 -0
  44. package/dist/lib/utils/package/package-info.js +30 -4
  45. package/dist/lib/utils/package/resolve-wildcard-keys.js +8 -2
  46. package/dist/lib/utils/path-patterns.d.ts +14 -0
  47. package/dist/lib/utils/path-patterns.js +12 -0
  48. package/package.json +5 -5
package/README.md CHANGED
@@ -437,11 +437,81 @@ module.exports = withNativeFederation({
437
437
  Notes:
438
438
 
439
439
  - `sharedMappings` is optional. If you omit it, all mapped paths are shared.
440
+ - Entries are matched as patterns, so `'@my-org/*'` selects every mapped path under that scope.
440
441
  - You can use wildcard suffixes (for example, `@my-org/ui/*`) to include multiple mapped paths.
441
- - `skip` still applies and can be used to exclude mapped paths even if they were selected via `sharedMappings`.
442
+ - `skip` still applies and can be used to exclude mapped paths even if they were selected via `sharedMappings`. For wildcard mappings it is matched against each resolved import (`@my-org/ui/button`), not the pattern.
442
443
  - Mapped paths are read from the workspace root tsconfig file: `tsconfig.base.json` if present, otherwise `tsconfig.json`.
443
444
  - The workspace root is detected by searching upward from the current working directory until a `package.json` is found.
444
445
 
446
+ #### Configuring shared mappings
447
+
448
+ A mapped path can carry the same kind of metadata as a shared npm package. Pair a list of patterns with a config object:
449
+
450
+ ```js
451
+ module.exports = withNativeFederation({
452
+ sharedMappings: ['@my-org/auth-lib', [['@my-org/ui/*'], { singleton: false }]],
453
+ });
454
+ ```
455
+
456
+ Plain strings and annotated pairs can be mixed freely. When several entries match the same mapped path, **the first one wins**, so put the specific entries before the general ones.
457
+
458
+ The honoured properties are `singleton`, `strictVersion`, `requiredVersion`, `version`, `shareScope`, `pool` and `includeSecondaries`. Anything omitted keeps its current default: `singleton: true`, `strictVersion` following the `mappingVersion` flag, and the version read from the mapped library's nearest `package.json`. Setting `version` explicitly also drives `requiredVersion` unless you set that too.
459
+
460
+ `build`, `platform`, `chunks` and `packageInfo` are **not** honoured for mapped paths — every mapping is built into the same bundle, so there is nothing for them to select.
461
+
462
+ For anything beyond a couple of entries, `mappingsFromWorkspace` is easier to read. It produces exactly the array form above:
463
+
464
+ ```js
465
+ import { withNativeFederation, mappingsFromWorkspace } from '@softarc/native-federation/config';
466
+
467
+ module.exports = withNativeFederation({
468
+ sharedMappings: mappingsFromWorkspace({ singleton: true, strictVersion: true })
469
+ .filter(['@my-org/ui/*', '@my-org/auth-lib'])
470
+ .patch(['@my-org/ui/*'], { singleton: false })
471
+ .get(),
472
+ });
473
+ ```
474
+
475
+ - Omit `.filter()` to select every mapped path — the same default as omitting `sharedMappings`.
476
+ - `.patch()` annotates a subset; it never widens the selection, so patching a pattern that `.filter()` excluded is ignored with a warning.
477
+
478
+ #### Keeping mappings that nothing imports
479
+
480
+ With the `ignoreUnusedDeps` feature on (the default), mapped paths are pruned to those actually reachable from your entry points. A host that exposes little of its own but is expected to supply libraries to its remotes can opt out per mapping, the same way `shared` packages do:
481
+
482
+ ```js
483
+ module.exports = withNativeFederation({
484
+ sharedMappings: mappingsFromWorkspace({
485
+ includeSecondaries: { keepAll: true, resolveGlob: true },
486
+ }).get(),
487
+ });
488
+ ```
489
+
490
+ - `keepAll` keeps the mapping even when nothing imports it. This is read exactly as it is for a shared package, so a bare `includeSecondaries: true` opts out too.
491
+ - `resolveGlob` is additionally required for **wildcard** mappings. A wildcard is a pattern rather than an entry point, and normally only the reachability scan turns it into concrete files; `resolveGlob` expands it against the filesystem instead. Without it, a wildcard mapping is dropped with a warning.
492
+
493
+ An expanded wildcard is named by the same rule the reachability scan uses, so `libs/ui/*` matching `libs/ui/button/index.ts` is shared as `@my-org/ui/button`.
494
+
495
+ Expansion only accepts **entry points**. A glob cannot tell a library's public surface from its internals, so a match whose specifier still contains a dot in its last segment — `@my-org/ui/button/button.component`, `.spec`, `.d` — is skipped rather than shared.
496
+
497
+ That restriction is not cosmetic: **only barrel imports can be shared as a mapped path.** A mapped path is advertised under its import specifier and marked external, so the specifier has to be one a browser import map can resolve, and a dot in the last segment does not resolve (see [vitejs/vite#21036](https://github.com/vitejs/vite/issues/21036)).
498
+
499
+ So the rule is simply *would this end up in `remoteEntry.json`?* If it would, a non-barrel specifier fails the build:
500
+
501
+ ```
502
+ Invalid 'shared mappings' config. Only barrel imports can be shared as a sharedMapping:
503
+ '@my-org/ui/button/button.component'.
504
+ ```
505
+
506
+ If it would not, nothing is reported — there is no reason to fail a build over a path that was never going to be published:
507
+
508
+ - **pruned away** by `ignoreUnusedDeps` — nothing imports it, so it is already gone.
509
+ - **skipped by a wildcard expansion** — `resolveGlob` is a guess about your public surface, so it drops non-barrel matches rather than inventing a build error out of `*.service.ts` files nobody imports.
510
+
511
+ What is left is the case worth stopping for: something genuinely imports `@my-org/ui/button/button.component`, so it is about to be published and would break at runtime. Import the barrel (`@my-org/ui/button`) and re-export from it. Note that with `ignoreUnusedDeps: false` nothing is pruned, so every mapped path is published and therefore checked.
512
+
513
+ Note that a host providing libraries its remotes depend on couples the two: the remote can no longer run standalone. Letting each application share the entry points it imports and leaving the orchestrator to deduplicate at runtime is usually the better default.
514
+
445
515
  The `mappingVersion` feature flag controls whether mapped paths get a version. It is **enabled by default**: Native Federation reads the version from the mapped library's nearest `package.json` and shares it with strict versioning, just like a published library.
446
516
 
447
517
  If your mapped paths point at plain internal source that isn't distributed as a versioned, buildable library, you can disable it. The mapped paths are then shared without a version constraint.
package/dist/config.d.ts CHANGED
@@ -3,4 +3,5 @@ export { withNativeFederation } from './lib/config/with-native-federation.js';
3
3
  export { findRootTsConfigJson } from './lib/config/project-paths.js';
4
4
  export { setInferVersion } from './lib/config/version-lookup.js';
5
5
  export { share, shareAll, fromPackageJson } from './lib/config/share-utils.js';
6
+ export { mappingsFromWorkspace } from './lib/config/mapping-utils.js';
6
7
  export { DEFAULT_SKIP_LIST } from './lib/config/default-skip-list.js';
package/dist/config.js CHANGED
@@ -3,11 +3,13 @@ import { withNativeFederation } from "./lib/config/with-native-federation.js";
3
3
  import { findRootTsConfigJson } from "./lib/config/project-paths.js";
4
4
  import { setInferVersion } from "./lib/config/version-lookup.js";
5
5
  import { share, shareAll, fromPackageJson } from "./lib/config/share-utils.js";
6
+ import { mappingsFromWorkspace } from "./lib/config/mapping-utils.js";
6
7
  import { DEFAULT_SKIP_LIST } from "./lib/config/default-skip-list.js";
7
8
  export {
8
9
  DEFAULT_SKIP_LIST,
9
10
  findRootTsConfigJson,
10
11
  fromPackageJson,
12
+ mappingsFromWorkspace,
11
13
  setInferVersion,
12
14
  share,
13
15
  shareAll,
@@ -14,5 +14,6 @@ export { isESMExport, type ExportCondition, type ExportEntry, } from './lib/util
14
14
  export { isCjsCandidate, classifyByExtension, hasEsmSyntax, type ModuleFormat, } from './lib/utils/package/esm-detection.js';
15
15
  export { isIdentifierName, planCjsWrap, buildSyntheticCjsEntry, isEsmInteropError, } from './lib/utils/package/cjs-named-exports.js';
16
16
  export type { NfFileWatcher, NfFileWatcherOptions, } from './lib/domain/utils/file-watcher.contract.js';
17
- export { syncNfFileWatcher, createNfWatcher } from './lib/utils/file-watcher.js';
18
- export { linkedSharedDirs } from './lib/core/build/resolve-shared-dirs.js';
17
+ export { syncNfFileWatcher, createNfWatcher, type WatchSources } from './lib/utils/file-watcher.js';
18
+ export { linkedSharedDirs, sharedMappingDirs } from './lib/core/build/resolve-shared-dirs.js';
19
+ export { isUnderDir, isUnderAnyDir } from './lib/utils/path-patterns.js';
package/dist/internal.js CHANGED
@@ -20,7 +20,8 @@ import {
20
20
  isEsmInteropError
21
21
  } from "./lib/utils/package/cjs-named-exports.js";
22
22
  import { syncNfFileWatcher, createNfWatcher } from "./lib/utils/file-watcher.js";
23
- import { linkedSharedDirs } from "./lib/core/build/resolve-shared-dirs.js";
23
+ import { linkedSharedDirs, sharedMappingDirs } from "./lib/core/build/resolve-shared-dirs.js";
24
+ import { isUnderDir, isUnderAnyDir } from "./lib/utils/path-patterns.js";
24
25
  export {
25
26
  RebuildQueue,
26
27
  buildSyntheticCjsEntry,
@@ -34,10 +35,13 @@ export {
34
35
  isESMExport,
35
36
  isEsmInteropError,
36
37
  isIdentifierName,
38
+ isUnderAnyDir,
39
+ isUnderDir,
37
40
  linkedSharedDirs,
38
41
  logger,
39
42
  planCjsWrap,
40
43
  setLogLevel,
44
+ sharedMappingDirs,
41
45
  syncNfFileWatcher,
42
46
  writeImportMap
43
47
  };
@@ -0,0 +1,24 @@
1
+ import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
2
+ import type { GlobPort } from '../domain/utils/io-port.contract.js';
3
+ import type { PathToImport } from '../domain/utils/mapped-path.contract.js';
4
+ export interface MappingExpansionContext {
5
+ io: GlobPort;
6
+ workspaceRoot: string;
7
+ }
8
+ export declare function isWildcardMapping(mappedPath: string, mappedImport: string): boolean;
9
+ /**
10
+ * Turns a wildcard mapping into the concrete entry points it stands for. A path containing a
11
+ * wildcard segment is not something the bundler can resolve, so it has to be walked on disk;
12
+ * the reachability pass is the only other thing that can materialise one.
13
+ *
14
+ * Naming goes through `matchMapping`, the same rule reachability uses, so an entry point is
15
+ * never advertised under a specifier the other path would not have produced. The glob is a
16
+ * guess about what consumers import, so it only yields entry-point-shaped specifiers; anything
17
+ * genuinely deep-imported is added by the reachability walk, which has the evidence.
18
+ */
19
+ export declare function expandWildcardMapping(mappedPath: string, mappedImport: string, ctx: MappingExpansionContext): PathToImport;
20
+ /**
21
+ * The `ignoreUnusedDeps: false` path: nothing is pruned, but wildcard mappings still have to
22
+ * become real entry points, and only `resolveGlob` can do that here.
23
+ */
24
+ export declare function expandOrDropWildcards(config: NormalizedFederationConfig, ctx: MappingExpansionContext): PathToImport;
@@ -0,0 +1,88 @@
1
+ import * as path from "path";
2
+ import { resolveMappingConfig, withoutSkippedMappings } from "./mapping-utils.js";
3
+ import { isModuleFile, matchMapping } from "./match-mapping.js";
4
+ import { isNonBarrelImport } from "./validate-mappings.js";
5
+ import { parseWildcard, toGlobPattern, toPosix } from "../utils/path-patterns.js";
6
+ import { logger } from "../utils/logger.js";
7
+ const IGNORED_DIRS = ["**/node_modules/**"];
8
+ function isWildcardMapping(mappedPath, mappedImport) {
9
+ return mappedPath.includes("*") || mappedImport.includes("*");
10
+ }
11
+ function expandWildcardMapping(mappedPath, mappedImport, ctx) {
12
+ const pattern = parseWildcard(toPosix(path.relative(ctx.workspaceRoot, mappedPath)));
13
+ if (!pattern.hasWildcard) {
14
+ logger.warn(`Mapping '${mappedImport}' has no wildcard to expand and was not shared.`);
15
+ return {};
16
+ }
17
+ const files = ctx.io.globFiles(toGlobPattern(pattern), {
18
+ cwd: ctx.workspaceRoot,
19
+ ignore: IGNORED_DIRS
20
+ });
21
+ const expanded = {};
22
+ const takenBy = {};
23
+ const collisions = [];
24
+ const notEntryPoints = [];
25
+ for (const file of files) {
26
+ if (!isModuleFile(file)) continue;
27
+ const absPath = path.join(ctx.workspaceRoot, toPosix(file).replace(/^\.\//, ""));
28
+ const importName = matchMapping(absPath, { [mappedPath]: mappedImport });
29
+ if (!importName) continue;
30
+ if (isNonBarrelImport(importName)) {
31
+ notEntryPoints.push(importName);
32
+ continue;
33
+ }
34
+ if (takenBy[importName]) {
35
+ collisions.push(`${importName} (${absPath}, kept ${takenBy[importName]})`);
36
+ continue;
37
+ }
38
+ takenBy[importName] = absPath;
39
+ expanded[absPath] = importName;
40
+ }
41
+ if (notEntryPoints.length > 0) {
42
+ logger.debug(
43
+ `Mapping '${mappedImport}' skipped ${notEntryPoints.length} match(es) that are not entry points: ${notEntryPoints.join(", ")}.`
44
+ );
45
+ }
46
+ if (collisions.length > 0) {
47
+ logger.warn(
48
+ `Mapping '${mappedImport}' expanded to duplicate imports; extra matches dropped: ${collisions.join(", ")}.`
49
+ );
50
+ }
51
+ if (Object.keys(expanded).length === 0) {
52
+ logger.warn(
53
+ notEntryPoints.length > 0 ? `Mapping '${mappedImport}' matched only implementation files, no entry points, and was not shared.` : `Mapping '${mappedImport}' matched no files on disk and was not shared.`
54
+ );
55
+ }
56
+ return expanded;
57
+ }
58
+ function expandOrDropWildcards(config, ctx) {
59
+ const result = {};
60
+ const dropped = [];
61
+ for (const [mappedPath, mappedImport] of Object.entries(config.sharedMappings)) {
62
+ if (!isWildcardMapping(mappedPath, mappedImport)) {
63
+ result[mappedPath] = mappedImport;
64
+ continue;
65
+ }
66
+ const mappingConfig = resolveMappingConfig(
67
+ mappedImport,
68
+ config.sharedMappingsConfig,
69
+ config.features.mappingVersion
70
+ );
71
+ if (mappingConfig.resolveGlob) {
72
+ Object.assign(result, expandWildcardMapping(mappedPath, mappedImport, ctx));
73
+ continue;
74
+ }
75
+ dropped.push(mappedImport);
76
+ }
77
+ if (dropped.length > 0) {
78
+ logger.warn(
79
+ `Sharing mapped paths with wildcards (*) needs either the ignoreUnusedDeps feature or 'includeSecondaries: { resolveGlob: true }'. Dropped: ${dropped.join(", ")}.`
80
+ );
81
+ }
82
+ return withoutSkippedMappings(result, config.skip);
83
+ }
84
+ export {
85
+ expandOrDropWildcards,
86
+ expandWildcardMapping,
87
+ isWildcardMapping
88
+ };
@@ -19,6 +19,4 @@ type UsedDependenciesConfig = {
19
19
  };
20
20
  export declare function getUsedDependenciesFactory(workspaceRoot: string, fallbackEntryPoints?: string[]): (config: UsedDependenciesConfig) => UsedDependencies;
21
21
  export declare function getUsedDependenciesFactoryCore(deps: UsedDependenciesDeps, workspaceRoot: string, fallbackEntryPoints?: string[]): (config: UsedDependenciesConfig) => UsedDependencies;
22
- export declare function isSharedMapping(filePath: string, sharedMappings: PathToImport): boolean;
23
- export declare function matchMapping(filePath: string, sharedMappings: PathToImport): string | null;
24
22
  export {};
@@ -1,9 +1,9 @@
1
1
  import { getProjectData as sheriffGetProjectData } from "@softarc/sheriff-core";
2
2
  import { cwd } from "process";
3
- import { sharedPackageJsonRepository, getPackageInfo } from "../utils/package/package-info.js";
3
+ import { sharedPackageJsonRepository, tryGetPackageInfo } from "../utils/package/package-info.js";
4
4
  import { getExternalImportsCore } from "./get-external-imports.js";
5
5
  import { nodeIo } from "../utils/io/node-io-adapter.js";
6
- import { parseWildcard, substituteWildcard, toPosix } from "../utils/path-patterns.js";
6
+ import { isSharedMapping, matchMapping } from "./match-mapping.js";
7
7
  import * as path from "path";
8
8
  const defaultDeps = {
9
9
  io: nodeIo,
@@ -54,7 +54,7 @@ function addTransientDeps(packages, workspaceRoot, deps) {
54
54
  if (!dep) {
55
55
  continue;
56
56
  }
57
- const pInfo = getPackageInfo(dep, workspaceRoot, deps.repo);
57
+ const pInfo = tryGetPackageInfo(dep, workspaceRoot, deps.repo);
58
58
  if (!pInfo) {
59
59
  continue;
60
60
  }
@@ -84,43 +84,7 @@ function resolveUsedMappings(fileInfos, workspaceRoot, sharedMappings) {
84
84
  }
85
85
  return usedMappings;
86
86
  }
87
- function isSharedMapping(filePath, sharedMappings) {
88
- for (const sharedPath of Object.keys(sharedMappings)) {
89
- const { prefix, hasWildcard } = parseWildcard(sharedPath);
90
- if (hasWildcard) {
91
- if (filePath.startsWith(prefix)) return true;
92
- } else if (filePath.startsWith(sharedPath + path.sep) || filePath === sharedPath) {
93
- return true;
94
- }
95
- }
96
- return false;
97
- }
98
- function matchMapping(filePath, sharedMappings) {
99
- for (const [sharedPath, sharedImport] of Object.entries(sharedMappings)) {
100
- const { prefix, suffix, hasWildcard } = parseWildcard(sharedPath);
101
- if (hasWildcard) {
102
- if (!filePath.startsWith(prefix)) continue;
103
- if (suffix && !filePath.includes(suffix)) continue;
104
- const captured = suffix ? filePath.slice(prefix.length, filePath.indexOf(suffix, prefix.length)) : filePath.slice(prefix.length);
105
- return substituteWildcard(sharedImport, toImportPath(captured));
106
- } else if (filePath === sharedPath || isIndexOf(filePath, sharedPath)) {
107
- return sharedImport;
108
- }
109
- }
110
- return null;
111
- }
112
- const INDEX_PATTERN = /\/index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
113
- function isIndexOf(filePath, dirPath) {
114
- return filePath.startsWith(dirPath + path.sep) && INDEX_PATTERN.test(filePath);
115
- }
116
- function toImportPath(filePath) {
117
- const withoutExt = filePath.replace(/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/, "");
118
- const normalized = toPosix(withoutExt);
119
- return normalized.endsWith("/index") ? normalized.slice(0, -6) : normalized;
120
- }
121
87
  export {
122
88
  getUsedDependenciesFactory,
123
- getUsedDependenciesFactoryCore,
124
- isSharedMapping,
125
- matchMapping
89
+ getUsedDependenciesFactoryCore
126
90
  };
@@ -1,7 +1,12 @@
1
1
  import type { FileReaderPort } from '../domain/utils/io-port.contract.js';
2
2
  import type { PathToImport } from '../domain/utils/mapped-path.contract.js';
3
+ import type { SharedMappingConfigs, SharedMappingEntry } from '../domain/config/federation-config.contract.js';
4
+ export interface RawMappedPaths {
5
+ paths: PathToImport;
6
+ configs: SharedMappingConfigs;
7
+ }
3
8
  /**
4
9
  * Will return user defined and tsconfig defined paths including their imports, might contain wildcards
5
10
  */
6
- export declare function getRawMappedPaths(rootTsConfigPath: string, configuredSharedMappings?: string[], rootPath?: string): PathToImport;
7
- export declare function getRawMappedPathsCore(io: FileReaderPort, rootTsConfigPath: string, configuredSharedMappings?: string[], rootPath?: string): PathToImport;
11
+ export declare function getRawMappedPaths(rootTsConfigPath: string, configuredSharedMappings?: SharedMappingEntry[], rootPath?: string): RawMappedPaths;
12
+ export declare function getRawMappedPathsCore(io: FileReaderPort, rootTsConfigPath: string, configuredSharedMappings?: SharedMappingEntry[], rootPath?: string): RawMappedPaths;
@@ -1,6 +1,7 @@
1
1
  import * as path from "path";
2
2
  import JSON5 from "json5";
3
3
  import { nodeIo } from "../utils/io/node-io-adapter.js";
4
+ import { matchesWildcard } from "../utils/path-patterns.js";
4
5
  function getRawMappedPaths(rootTsConfigPath, configuredSharedMappings, rootPath) {
5
6
  return getRawMappedPathsCore(nodeIo, rootTsConfigPath, configuredSharedMappings, rootPath);
6
7
  }
@@ -11,19 +12,35 @@ function getRawMappedPathsCore(io, rootTsConfigPath, configuredSharedMappings, r
11
12
  }
12
13
  const basePath = rootPath ?? path.normalize(path.dirname(rootTsConfigPath));
13
14
  const shareAll = !configuredSharedMappings;
14
- const sharedMappings = configuredSharedMappings ?? [];
15
+ const { patterns, configs } = flattenEntries(configuredSharedMappings ?? []);
15
16
  const tsConfig = JSON5.parse(io.readText(rootTsConfigPath));
16
17
  const mappings = tsConfig?.compilerOptions?.paths;
17
18
  if (!mappings) {
18
- return mappedPaths;
19
+ return { paths: mappedPaths, configs };
19
20
  }
20
21
  for (const key in mappings) {
21
22
  const libPath = path.normalize(path.join(basePath, mappings[key][0]));
22
- if (shareAll || sharedMappings.includes(key)) {
23
+ if (shareAll || patterns.some((pattern) => matchesWildcard(key, pattern))) {
23
24
  mappedPaths[libPath] = key;
24
25
  }
25
26
  }
26
- return mappedPaths;
27
+ return { paths: mappedPaths, configs };
28
+ }
29
+ function flattenEntries(entries) {
30
+ const patterns = [];
31
+ const configs = /* @__PURE__ */ Object.create(null);
32
+ for (const entry of entries) {
33
+ if (typeof entry === "string") {
34
+ patterns.push(entry);
35
+ continue;
36
+ }
37
+ const [keys, config] = entry;
38
+ for (const key of keys) {
39
+ patterns.push(key);
40
+ if (!(key in configs)) configs[key] = config;
41
+ }
42
+ }
43
+ return { patterns, configs };
27
44
  }
28
45
  export {
29
46
  getRawMappedPaths,
@@ -0,0 +1,32 @@
1
+ import type { ExternalConfig } from '../domain/config/external-config.contract.js';
2
+ import type { NormalizedMappingConfig, NormalizedSharedMappingConfigs, SharedMappingEntry } from '../domain/config/federation-config.contract.js';
3
+ import type { PreparedSkipList } from '../domain/config/skip-list.contract.js';
4
+ import type { PathToImport } from '../domain/utils/mapped-path.contract.js';
5
+ export interface WorkspaceMappingsBuilder {
6
+ filter(patterns: string[]): WorkspaceMappingsBuilder;
7
+ patch(patterns: string[], cfg: Partial<ExternalConfig>): WorkspaceMappingsBuilder;
8
+ get(): SharedMappingEntry[];
9
+ }
10
+ /**
11
+ * Sugar over the `sharedMappings` array form: `get()` returns entries that could equally
12
+ * be written by hand. Without `filter()` the selection is every tsconfig path mapping.
13
+ */
14
+ export declare function mappingsFromWorkspace(baseCfg?: ExternalConfig): WorkspaceMappingsBuilder;
15
+ /**
16
+ * Wildcard mappings are only turned into concrete imports after the skip list has already been
17
+ * applied to the raw patterns, so the expanded entries have to be filtered again here.
18
+ */
19
+ export declare function withoutSkippedMappings(paths: PathToImport, skipList: PreparedSkipList): PathToImport;
20
+ /**
21
+ * Sole owner of the mapping defaults. `includeSecondaries` collapses to a boolean the same way
22
+ * `normalizeShared` does it, so both halves of `removeUnusedDeps` read one flag; `resolveGlob`
23
+ * is lifted out because it steers wildcard expansion, not secondary entry points.
24
+ */
25
+ export declare function normalizeMappingConfig(cfg: ExternalConfig, mappingVersion: boolean): NormalizedMappingConfig;
26
+ /**
27
+ * Looks a mapping's config up by its import name. Wildcard substitution rewrites imports
28
+ * (`@org/ui/*` -> `@org/ui/button`), so the table is keyed by the pattern the user wrote and
29
+ * matched, not compared. Declaration order decides: the first matching pattern wins.
30
+ * Falls back to the defaults so callers never re-state them.
31
+ */
32
+ export declare function resolveMappingConfig(importName: string, configs: NormalizedSharedMappingConfigs, mappingVersion: boolean): NormalizedMappingConfig;
@@ -0,0 +1,68 @@
1
+ import { isInSkipList } from "./default-skip-list.js";
2
+ import { matchesWildcard } from "../utils/path-patterns.js";
3
+ import { logger } from "../utils/logger.js";
4
+ const ALL = "*";
5
+ function mappingsFromWorkspace(baseCfg = {}) {
6
+ const selection = [];
7
+ const patches = [];
8
+ const builder = {
9
+ filter(patterns) {
10
+ selection.push(...patterns);
11
+ return builder;
12
+ },
13
+ patch(patterns, cfg) {
14
+ patches.push({ patterns, cfg });
15
+ return builder;
16
+ },
17
+ get() {
18
+ const selected = selection.length > 0 ? [...selection] : [ALL];
19
+ const entries = [];
20
+ for (const { patterns, cfg } of patches) {
21
+ const kept = patterns.filter((p) => isSelected(p, selected));
22
+ for (const dropped of patterns.filter((p) => !isSelected(p, selected))) {
23
+ logger.warn(
24
+ `[mappingsFromWorkspace] patch('${dropped}') is not covered by filter() and was ignored.`
25
+ );
26
+ }
27
+ if (kept.length > 0) entries.push([kept, { ...baseCfg, ...cfg }]);
28
+ }
29
+ entries.push([selected, baseCfg]);
30
+ return entries;
31
+ }
32
+ };
33
+ return builder;
34
+ }
35
+ function isSelected(pattern, selection) {
36
+ return selection.some((s) => matchesWildcard(pattern, s));
37
+ }
38
+ function withoutSkippedMappings(paths, skipList) {
39
+ return Object.entries(paths).filter(([, mappedImport]) => !isInSkipList(mappedImport, skipList)).reduce((acc, [mappedPath, mappedImport]) => {
40
+ acc[mappedPath] = mappedImport;
41
+ return acc;
42
+ }, {});
43
+ }
44
+ function normalizeMappingConfig(cfg, mappingVersion) {
45
+ const includeSecondaries = typeof cfg.includeSecondaries === "object" ? !!cfg.includeSecondaries.keepAll : cfg.includeSecondaries;
46
+ return {
47
+ singleton: cfg.singleton ?? true,
48
+ strictVersion: cfg.strictVersion ?? mappingVersion,
49
+ ...cfg.requiredVersion !== void 0 && { requiredVersion: cfg.requiredVersion },
50
+ ...cfg.version !== void 0 && { version: cfg.version },
51
+ ...cfg.shareScope && { shareScope: cfg.shareScope },
52
+ ...cfg.pool && { pool: cfg.pool },
53
+ ...includeSecondaries !== void 0 && { includeSecondaries },
54
+ ...typeof cfg.includeSecondaries === "object" && cfg.includeSecondaries.resolveGlob && { resolveGlob: true }
55
+ };
56
+ }
57
+ function resolveMappingConfig(importName, configs, mappingVersion) {
58
+ for (const [pattern, config] of Object.entries(configs)) {
59
+ if (matchesWildcard(importName, pattern)) return config;
60
+ }
61
+ return normalizeMappingConfig({}, mappingVersion);
62
+ }
63
+ export {
64
+ mappingsFromWorkspace,
65
+ normalizeMappingConfig,
66
+ resolveMappingConfig,
67
+ withoutSkippedMappings
68
+ };
@@ -0,0 +1,9 @@
1
+ import type { PathToImport } from '../domain/utils/mapped-path.contract.js';
2
+ export declare function isModuleFile(filePath: string): boolean;
3
+ export declare function isSharedMapping(filePath: string, sharedMappings: PathToImport): boolean;
4
+ /**
5
+ * The single rule that turns a file into the import specifier it is shared under. Both the
6
+ * reachability walk and the `resolveGlob` expansion go through here, so an entry point cannot
7
+ * end up advertised under a name the other side would not have produced.
8
+ */
9
+ export declare function matchMapping(filePath: string, sharedMappings: PathToImport): string | null;
@@ -0,0 +1,46 @@
1
+ import * as path from "path";
2
+ import { parseWildcard, substituteWildcard, toPosix } from "../utils/path-patterns.js";
3
+ const MODULE_EXTENSION_PATTERN = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
4
+ const DECLARATION_PATTERN = /\.d\.(ts|mts|cts)$/;
5
+ function isModuleFile(filePath) {
6
+ return MODULE_EXTENSION_PATTERN.test(filePath) && !DECLARATION_PATTERN.test(filePath);
7
+ }
8
+ function isSharedMapping(filePath, sharedMappings) {
9
+ for (const sharedPath of Object.keys(sharedMappings)) {
10
+ const { prefix, hasWildcard } = parseWildcard(sharedPath);
11
+ if (hasWildcard) {
12
+ if (filePath.startsWith(prefix)) return true;
13
+ } else if (filePath.startsWith(sharedPath + path.sep) || filePath === sharedPath) {
14
+ return true;
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+ function matchMapping(filePath, sharedMappings) {
20
+ for (const [sharedPath, sharedImport] of Object.entries(sharedMappings)) {
21
+ const { prefix, suffix, hasWildcard } = parseWildcard(sharedPath);
22
+ if (hasWildcard) {
23
+ if (!filePath.startsWith(prefix)) continue;
24
+ if (suffix && !filePath.includes(suffix)) continue;
25
+ const captured = suffix ? filePath.slice(prefix.length, filePath.indexOf(suffix, prefix.length)) : filePath.slice(prefix.length);
26
+ return substituteWildcard(sharedImport, toImportPath(captured));
27
+ } else if (filePath === sharedPath || isIndexOf(filePath, sharedPath)) {
28
+ return sharedImport;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ const INDEX_PATTERN = /\/index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
34
+ function isIndexOf(filePath, dirPath) {
35
+ return filePath.startsWith(dirPath + path.sep) && INDEX_PATTERN.test(filePath);
36
+ }
37
+ function toImportPath(filePath) {
38
+ const withoutExt = filePath.replace(MODULE_EXTENSION_PATTERN, "");
39
+ const normalized = toPosix(withoutExt);
40
+ return normalized.endsWith("/index") ? normalized.slice(0, -6) : normalized;
41
+ }
42
+ export {
43
+ isModuleFile,
44
+ isSharedMapping,
45
+ matchMapping
46
+ };
@@ -1,3 +1,4 @@
1
1
  import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
2
2
  import type { UsedDependencies } from '../domain/utils/used-dependencies.contract.js';
3
- export declare function removeUnusedDeps(usedDependencies: UsedDependencies, config: NormalizedFederationConfig): NormalizedFederationConfig;
3
+ import { type MappingExpansionContext } from './expand-mappings.js';
4
+ export declare function removeUnusedDeps(usedDependencies: UsedDependencies, config: NormalizedFederationConfig, ctx: MappingExpansionContext): NormalizedFederationConfig;
@@ -1,11 +1,44 @@
1
- function removeUnusedDeps(usedDependencies, config) {
1
+ import { resolveMappingConfig, withoutSkippedMappings } from "./mapping-utils.js";
2
+ import {
3
+ expandWildcardMapping,
4
+ isWildcardMapping
5
+ } from "./expand-mappings.js";
6
+ import { logger } from "../utils/logger.js";
7
+ function removeUnusedDeps(usedDependencies, config, ctx) {
2
8
  const filteredDependencies = Object.entries(config.shared).filter(([shared, meta]) => !!meta.includeSecondaries || usedDependencies.external.has(shared)).reduce((acc, [shared, meta]) => ({ ...acc, [shared]: meta }), {});
3
9
  return {
4
10
  ...config,
5
11
  shared: filteredDependencies,
6
- sharedMappings: usedDependencies.internal
12
+ // Both halves can contain wildcard-expanded imports, which the skip list has not seen yet.
13
+ sharedMappings: withoutSkippedMappings(
14
+ { ...keptMappings(config, ctx), ...usedDependencies.internal },
15
+ config.skip
16
+ )
7
17
  };
8
18
  }
19
+ function keptMappings(config, ctx) {
20
+ const kept = {};
21
+ for (const [mappedPath, mappedImport] of Object.entries(config.sharedMappings)) {
22
+ const mappingConfig = resolveMappingConfig(
23
+ mappedImport,
24
+ config.sharedMappingsConfig,
25
+ config.features.mappingVersion
26
+ );
27
+ if (!mappingConfig.includeSecondaries) continue;
28
+ if (!isWildcardMapping(mappedPath, mappedImport)) {
29
+ kept[mappedPath] = mappedImport;
30
+ continue;
31
+ }
32
+ if (!mappingConfig.resolveGlob) {
33
+ logger.warn(
34
+ `Mapping '${mappedImport}' opts out of pruning, but wildcard mappings need 'includeSecondaries: { resolveGlob: true }' to be expanded, and will be pruned.`
35
+ );
36
+ continue;
37
+ }
38
+ Object.assign(kept, expandWildcardMapping(mappedPath, mappedImport, ctx));
39
+ }
40
+ return kept;
41
+ }
9
42
  export {
10
43
  removeUnusedDeps
11
44
  };
@@ -0,0 +1,15 @@
1
+ import type { PathToImport } from '../domain/utils/mapped-path.contract.js';
2
+ /**
3
+ * A mapped path is advertised under its import specifier and marked external, so the specifier
4
+ * has to be one a browser import map can resolve. Only barrel-shaped specifiers are: a dot in
5
+ * the last segment breaks resolution, see https://github.com/vitejs/vite/issues/21036.
6
+ */
7
+ export declare function isNonBarrelImport(importName: string): boolean;
8
+ /**
9
+ * Runs on the final mapping set, which is exactly what gets advertised in `remoteEntry.json`.
10
+ * Anything still here will be published and resolved from an import map, so a specifier that
11
+ * cannot be resolved is a build error. Sources that legitimately decline to share — pruning,
12
+ * and the `resolveGlob` guess — have already dropped their non-barrel candidates by now, so
13
+ * this never fires for a path nobody asked to publish.
14
+ */
15
+ export declare function assertBarrelMappings(paths: PathToImport): void;
@@ -0,0 +1,28 @@
1
+ import { logger } from "../utils/logger.js";
2
+ const IMPORTABLE_EXTENSIONS = /* @__PURE__ */ new Set(["mjs", "js", "mts", "ts", "jsx", "tsx", "json"]);
3
+ const MAX_LISTED = 5;
4
+ function isNonBarrelImport(importName) {
5
+ if (!importName.includes(".")) return false;
6
+ const queryIndex = importName.search(/[?#]/);
7
+ const sanitized = queryIndex >= 0 ? importName.slice(0, queryIndex) : importName;
8
+ const lastSegment = sanitized.slice(sanitized.lastIndexOf("/") + 1);
9
+ const dotIndex = lastSegment.lastIndexOf(".");
10
+ if (dotIndex < 0) return false;
11
+ return !IMPORTABLE_EXTENSIONS.has(lastSegment.slice(dotIndex + 1));
12
+ }
13
+ function assertBarrelMappings(paths) {
14
+ const invalid = Object.values(paths).filter(isNonBarrelImport);
15
+ if (invalid.length === 0) return;
16
+ for (const importName of invalid) {
17
+ logger.warn(`Only barrel imports can be shared as a sharedMapping: '${importName}'.`);
18
+ }
19
+ const shown = invalid.slice(0, MAX_LISTED).map((i) => `'${i}'`).join(", ");
20
+ const rest = invalid.length - MAX_LISTED;
21
+ throw new Error(
22
+ `Invalid 'shared mappings' config. Only barrel imports can be shared as a sharedMapping: ${shown}${rest > 0 ? ` and ${rest} more` : ""}.`
23
+ );
24
+ }
25
+ export {
26
+ assertBarrelMappings,
27
+ isNonBarrelImport
28
+ };