@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
@@ -1,16 +1,40 @@
1
1
  import type { PreparedSkipList, SkipList } from './skip-list.contract.js';
2
2
  import type { PathToImport } from '../utils/mapped-path.contract.js';
3
- import type { NormalizedSharedExternalsConfig, SharedExternalsConfig } from './external-config.contract.js';
3
+ import type { ExternalConfig, NormalizedSharedExternalsConfig, SharedExternalsConfig } from './external-config.contract.js';
4
4
  export type ExposeEntry = {
5
5
  file: string;
6
6
  element?: string;
7
7
  };
8
+ export type SharedMappingEntry = string | [string[], ExternalConfig];
9
+ /** Selection pattern -> config, kept in declaration order: first match wins. */
10
+ export type SharedMappingConfigs = Record<string, ExternalConfig>;
11
+ /**
12
+ * Only the subset of `ExternalConfig` a mapping can act on — mappings all share the
13
+ * single `mapping-or-exposed` bundle, so `build`/`platform`/`chunks`/`packageInfo`
14
+ * have nothing to select. `requiredVersion` and `version` stay optional because their
15
+ * defaults are read from the mapped lib's package.json at build time.
16
+ *
17
+ * `includeSecondaries` collapses to a boolean exactly as it does for a shared external:
18
+ * it means "exempt from `ignoreUnusedDeps` pruning". `resolveGlob` is lifted out of it
19
+ * because a mapping has no secondary entry points for it to apply to.
20
+ */
21
+ export interface NormalizedMappingConfig {
22
+ singleton: boolean;
23
+ strictVersion: boolean;
24
+ requiredVersion?: string;
25
+ version?: string;
26
+ shareScope?: string;
27
+ pool?: string;
28
+ includeSecondaries?: boolean;
29
+ resolveGlob?: boolean;
30
+ }
31
+ export type NormalizedSharedMappingConfigs = Record<string, NormalizedMappingConfig>;
8
32
  export interface FederationConfig {
9
33
  name?: string;
10
34
  exposes?: Record<string, string | ExposeEntry>;
11
35
  shared?: SharedExternalsConfig;
12
36
  platform?: 'browser' | 'node';
13
- sharedMappings?: Array<string>;
37
+ sharedMappings?: Array<SharedMappingEntry>;
14
38
  chunks?: boolean;
15
39
  skip?: SkipList;
16
40
  externals?: string[];
@@ -30,6 +54,7 @@ export interface NormalizedFederationConfig {
30
54
  exposes: Record<string, ExposeEntry>;
31
55
  shared: NormalizedSharedExternalsConfig;
32
56
  sharedMappings: PathToImport;
57
+ sharedMappingsConfig: NormalizedSharedMappingConfigs;
33
58
  skip: PreparedSkipList;
34
59
  chunks: boolean;
35
60
  externals: string[];
@@ -2,6 +2,18 @@ export interface NfFileWatcherOptions {
2
2
  onChange?: (path: string) => void;
3
3
  pollIntervalMs?: number;
4
4
  debounceMs?: number;
5
+ /** Drop events whose mtime is unchanged since the last one seen for that path.
6
+ * macOS FSEvents re-delivers 'changed' for recently edited files roughly every
7
+ * 30s; with a watch list of a few thousand sources that replay alone keeps a
8
+ * rebuild loop awake forever. Default: true. */
9
+ dedupeReplays?: boolean;
10
+ /** Grace period after a path's recorded mtime during which an event whose mtime
11
+ * and byte length both match still passes, covering a second save inside one
12
+ * mtime tick. The 2000 default is twice the coarsest mtime granularity in play
13
+ * (1s on gRPC-FUSE/NFS/WSL2 drvfs/HFS+); raise it for a filesystem coarser than
14
+ * that, or for a network mount whose server clock runs behind the client.
15
+ * See AGENTS.md "Replay dedupe". Default: 2000. */
16
+ replayGraceMs?: number;
5
17
  }
6
18
  interface AddPathsOptions {
7
19
  poll?: boolean;
@@ -5,6 +5,8 @@ export interface Digest {
5
5
  }
6
6
  export interface StatInfo {
7
7
  mtimeMs: number;
8
+ /** Byte length; 0 for directories. */
9
+ size: number;
8
10
  isSymbolicLink: boolean;
9
11
  }
10
12
  export interface FileReaderPort {
@@ -29,6 +31,7 @@ export interface FileWriterPort {
29
31
  export interface GlobPort {
30
32
  globFiles(pattern: string, opts: {
31
33
  cwd: string;
34
+ ignore?: string[];
32
35
  }): string[];
33
36
  }
34
37
  export interface HashPort {
@@ -45,9 +48,9 @@ interface WatchOptions {
45
48
  }
46
49
  export interface WatchPort {
47
50
  /**
48
- * For a recursive directory watch `onEvent` receives the changed entry's
49
- * filename relative to `path`; for a file it receives the path itself (or
50
- * null when the platform omits it).
51
+ * `onEvent` receives the changed entry's filename relative to `path` (null when
52
+ * the platform omits it). Watching a file reports that file's own basename, so a
53
+ * caller that registered a file can ignore the argument.
51
54
  */
52
55
  watch(path: string, opts: WatchOptions, onEvent: (filename: string | null) => void): WatchHandle;
53
56
  }
@@ -1,7 +1,18 @@
1
1
  import type { WatchPort, FileReaderPort } from '../domain/utils/io-port.contract.js';
2
2
  import type { NfFileWatcher, NfFileWatcherOptions } from '../domain/utils/file-watcher.contract.js';
3
3
  export declare function createNfWatcher(options?: NfFileWatcherOptions): NfFileWatcher;
4
- export declare function createNfWatcherCore(io: WatchPort & FileReaderPort, options?: NfFileWatcherOptions): NfFileWatcher;
5
- export declare function syncNfFileWatcher(watcher: NfFileWatcher, bundlerCache: {
6
- keys(): IterableIterator<string>;
7
- }, linkedDirs?: readonly string[]): void;
4
+ export declare function createNfWatcherCore(io: WatchPort & FileReaderPort, options?: NfFileWatcherOptions, now?: () => number): NfFileWatcher;
5
+ /** A bundler cache keyed by input path, or the input paths themselves. */
6
+ export type WatchSources = Iterable<string> | {
7
+ keys(): Iterable<string>;
8
+ };
9
+ /**
10
+ * Subscribe the watcher to the inputs the last build compiled.
11
+ *
12
+ * Passing a bundler cache only works when it is keyed by input path. A cache that
13
+ * records its inputs elsewhere yields an empty watch set and, silently, a dev server
14
+ * that serves stale bundles until restart — Angular's `SourceFileCache` extends `Map`
15
+ * but keeps its inputs in `typeScriptFileCache`/`referencedFiles`, which is
16
+ * angular-adapter#94. Adapters over such a cache must expand it and pass the paths.
17
+ */
18
+ export declare function syncNfFileWatcher(watcher: NfFileWatcher, sources: WatchSources, linkedDirs?: readonly string[]): void;
@@ -1,29 +1,82 @@
1
- import { join } from "path";
1
+ import { dirname, join } from "path";
2
2
  import { nodeIo } from "./io/node-io-adapter.js";
3
3
  import { logger } from "./logger.js";
4
- import { toPosix } from "./path-patterns.js";
4
+ import { isUnderDir, toPosix } from "./path-patterns.js";
5
5
  function createNfWatcher(options = {}) {
6
6
  return createNfWatcherCore(nodeIo, options);
7
7
  }
8
- function createNfWatcherCore(io, options = {}) {
8
+ function createNfWatcherCore(io, options = {}, now = Date.now) {
9
9
  const { onChange } = options;
10
10
  const pollIntervalMs = options.pollIntervalMs ?? 300;
11
11
  const debounceMs = options.debounceMs ?? 0;
12
+ const dedupeReplays = options.dedupeReplays ?? true;
13
+ const replayGraceMs = options.replayGraceMs ?? 2e3;
12
14
  const watchers = /* @__PURE__ */ new Map();
13
15
  const dirtyPaths = /* @__PURE__ */ new Set();
14
- const deliver = (path) => {
16
+ const lastSeen = /* @__PURE__ */ new Map();
17
+ const trackedFiles = /* @__PURE__ */ new Set();
18
+ const fileDirWatchers = /* @__PURE__ */ new Map();
19
+ const dirKey = (p) => {
20
+ const posix = toPosix(p);
21
+ return posix.length > 1 ? posix.replace(/\/+$/, "") : posix;
22
+ };
23
+ const covers = (path, poll) => {
24
+ for (const [dir, watch] of watchers) {
25
+ if ((watch.poll || !poll) && isUnderDir(path, dir)) return true;
26
+ }
27
+ return false;
28
+ };
29
+ const supersede = (dir, poll) => {
30
+ for (const map of [watchers, fileDirWatchers]) {
31
+ for (const [key, watch] of map) {
32
+ if (watch.poll && !poll) continue;
33
+ if (map === watchers && key === dir) continue;
34
+ if (!isUnderDir(key, dir)) continue;
35
+ watch.handle.close();
36
+ map.delete(key);
37
+ }
38
+ }
39
+ };
40
+ const identityOf = (path) => {
41
+ let stat = io.stat(path);
42
+ if (stat?.isSymbolicLink) stat = io.stat(io.realpath(path));
43
+ return stat ? { mtimeMs: stat.mtimeMs, size: stat.size } : null;
44
+ };
45
+ const isReplay = (path, at) => {
46
+ const current = identityOf(path);
47
+ if (!current) {
48
+ lastSeen.delete(path);
49
+ return false;
50
+ }
51
+ const previous = lastSeen.get(path);
52
+ lastSeen.set(path, current);
53
+ if (!previous || previous.mtimeMs !== current.mtimeMs || previous.size !== current.size) {
54
+ return false;
55
+ }
56
+ return at - current.mtimeMs >= replayGraceMs;
57
+ };
58
+ let watchFailures = 0;
59
+ const watchFailed = (path) => {
60
+ if (++watchFailures > 1) return logger.debug(`Could not watch path '${path}'.`);
61
+ logger.warn(
62
+ `Could not watch '${path}'. Changes there will not trigger a rebuild; run with verbose logging to see any further watch failures.`
63
+ );
64
+ };
65
+ const deliver = (path, at) => {
66
+ if (dedupeReplays && isReplay(path, at)) return;
67
+ dirtyPaths.add(path);
15
68
  if (onChange) onChange(path);
16
- else dirtyPaths.add(path);
17
69
  };
18
- const pending = /* @__PURE__ */ new Set();
70
+ const pending = /* @__PURE__ */ new Map();
19
71
  let flushTimer;
20
72
  const flush = () => {
21
- for (const p of pending) deliver(p);
73
+ for (const [p, at] of pending) deliver(p, at);
22
74
  pending.clear();
23
75
  };
24
76
  const notify = (path) => {
25
- if (debounceMs <= 0) return deliver(path);
26
- pending.add(path);
77
+ const at = now();
78
+ if (debounceMs <= 0) return deliver(path, at);
79
+ if (!pending.has(path)) pending.set(path, at);
27
80
  if (flushTimer) clearTimeout(flushTimer);
28
81
  flushTimer = setTimeout(flush, debounceMs);
29
82
  flushTimer.unref?.();
@@ -31,16 +84,47 @@ function createNfWatcherCore(io, options = {}) {
31
84
  return {
32
85
  addPaths(paths, opts) {
33
86
  const list = typeof paths === "string" ? [paths] : [...paths];
34
- const poll = opts?.poll ? { intervalMs: pollIntervalMs } : void 0;
87
+ const shouldPoll = !!opts?.poll;
88
+ const poll = shouldPoll ? { intervalMs: pollIntervalMs } : void 0;
35
89
  for (const p of list) {
36
- if (watchers.has(p)) continue;
90
+ if (io.isDirectory(p)) {
91
+ const dir2 = dirKey(p);
92
+ if (watchers.has(dir2) || covers(dir2, shouldPoll)) continue;
93
+ try {
94
+ watchers.set(dir2, {
95
+ handle: io.watch(p, { recursive: true, poll }, (filename) => {
96
+ if (filename) notify(toPosix(join(p, filename)));
97
+ }),
98
+ poll: shouldPoll
99
+ });
100
+ } catch {
101
+ watchFailed(p);
102
+ continue;
103
+ }
104
+ supersede(dir2, shouldPoll);
105
+ continue;
106
+ }
107
+ const key = toPosix(p);
108
+ if (trackedFiles.has(key)) continue;
109
+ trackedFiles.add(key);
110
+ if (dedupeReplays && !lastSeen.has(key)) {
111
+ const identity = identityOf(key);
112
+ if (identity) lastSeen.set(key, identity);
113
+ }
114
+ if (covers(key, shouldPoll)) continue;
115
+ const dir = dirKey(dirname(p));
116
+ if (fileDirWatchers.has(dir)) continue;
37
117
  try {
38
- const handle = io.isDirectory(p) ? io.watch(p, { recursive: true, poll }, (filename) => {
39
- if (filename) notify(toPosix(join(p, filename)));
40
- }) : io.watch(p, { recursive: false, poll }, () => notify(toPosix(p)));
41
- watchers.set(p, handle);
118
+ fileDirWatchers.set(dir, {
119
+ handle: io.watch(dir, { recursive: false, poll }, (filename) => {
120
+ if (!filename) return;
121
+ const changed = toPosix(join(dir, filename));
122
+ if (trackedFiles.has(changed)) notify(changed);
123
+ }),
124
+ poll: shouldPoll
125
+ });
42
126
  } catch {
43
- logger.debug(`Could not watch path '${p}'.`);
127
+ watchFailed(dir);
44
128
  }
45
129
  }
46
130
  },
@@ -49,15 +133,24 @@ function createNfWatcherCore(io, options = {}) {
49
133
  mutate: (fn) => fn(dirtyPaths),
50
134
  async close() {
51
135
  if (flushTimer) clearTimeout(flushTimer);
52
- for (const handle of watchers.values()) {
136
+ for (const { handle } of [...watchers.values(), ...fileDirWatchers.values()]) {
53
137
  handle.close();
54
138
  }
55
139
  watchers.clear();
140
+ fileDirWatchers.clear();
141
+ trackedFiles.clear();
142
+ lastSeen.clear();
143
+ watchFailures = 0;
56
144
  }
57
145
  };
58
146
  }
59
- function syncNfFileWatcher(watcher, bundlerCache, linkedDirs = []) {
60
- const files = [...bundlerCache.keys()].filter((k) => !k.includes("node_modules"));
147
+ function toPaths(sources) {
148
+ if (Array.isArray(sources)) return sources;
149
+ const cache = sources;
150
+ return typeof cache.keys === "function" ? cache.keys() : sources;
151
+ }
152
+ function syncNfFileWatcher(watcher, sources, linkedDirs = []) {
153
+ const files = [...toPaths(sources)].filter((k) => !k.includes("node_modules"));
61
154
  if (files.length) watcher.addPaths(files);
62
155
  if (linkedDirs.length) watcher.addPaths(linkedDirs, { poll: true });
63
156
  }
@@ -43,7 +43,7 @@ const nodeIo = {
43
43
  stat(path2) {
44
44
  try {
45
45
  const s = fs.lstatSync(path2);
46
- return { mtimeMs: s.mtimeMs, isSymbolicLink: s.isSymbolicLink() };
46
+ return { mtimeMs: s.mtimeMs, size: s.size, isSymbolicLink: s.isSymbolicLink() };
47
47
  } catch {
48
48
  return null;
49
49
  }
@@ -61,7 +61,12 @@ const nodeIo = {
61
61
  fs.unlinkSync(path2);
62
62
  },
63
63
  globFiles(pattern, opts) {
64
- return fg.sync(pattern, { cwd: opts.cwd, onlyFiles: true, deep: Infinity });
64
+ return fg.sync(pattern, {
65
+ cwd: opts.cwd,
66
+ ignore: opts.ignore,
67
+ onlyFiles: true,
68
+ deep: Infinity
69
+ });
65
70
  },
66
71
  hash(algorithm, data) {
67
72
  const sum = crypto.createHash(algorithm).update(data);
@@ -72,11 +77,11 @@ const nodeIo = {
72
77
  },
73
78
  watch(watchPath, opts, onEvent) {
74
79
  if (opts.poll) return pollWatch(watchPath, opts.recursive, opts.poll.intervalMs, onEvent);
75
- const watcher = opts.recursive ? fs.watch(
80
+ const watcher = fs.watch(
76
81
  watchPath,
77
- { recursive: true },
82
+ { recursive: opts.recursive },
78
83
  (_event, filename) => onEvent(filename ? filename.toString() : null)
79
- ) : fs.watch(watchPath, () => onEvent(watchPath));
84
+ );
80
85
  return { close: () => watcher.close() };
81
86
  }
82
87
  };
@@ -44,7 +44,7 @@ function resolvePackageInfo(repo, packageName, directory) {
44
44
  const version = mainPkgJson["version"];
45
45
  const esm = mainPkgJson["type"] === "module";
46
46
  if (!version) {
47
- logger.warn("No version found for " + packageName);
47
+ logger.debug("No version found for " + packageName + " in " + directory);
48
48
  return null;
49
49
  }
50
50
  const pathToSecondary = path.relative(mainPkgName, packageName);
@@ -67,10 +67,7 @@ function resolvePackageInfo(repo, packageName, directory) {
67
67
  const result = strategy(ctx);
68
68
  if (result) return result;
69
69
  }
70
- logger.warn("No entry point found for " + packageName);
71
- logger.warn(
72
- "If you don't need this package, skip it in your federation.config.js or consider moving it into depDependencies in your package.json"
73
- );
70
+ logger.debug("No entry point found for " + packageName + " in " + directory);
74
71
  return null;
75
72
  }
76
73
  export {
@@ -2,6 +2,19 @@ import type { PackageInfo, PackageJsonRepository, VersionMap } from '../../domai
2
2
  export { sharedPackageJsonRepository } from '../io/package-json-repository.js';
3
3
  export type { PackageInfo, VersionMap, ExportCondition, ExportEntry, } from '../../domain/utils/package-json.contract.js';
4
4
  export { isESMExport } from './esm-detection.js';
5
+ export declare function tryGetPackageInfo(packageName: string, workspaceRoot: string, repo?: PackageJsonRepository): PackageInfo | null;
6
+ /**
7
+ * Resolve a package that is expected to be resolvable, warning once if it is not.
8
+ * Only use this where a failure is genuinely actionable, i.e. for packages the user
9
+ * asked to share.
10
+ */
5
11
  export declare function getPackageInfo(packageName: string, workspaceRoot: string, repo?: PackageJsonRepository): PackageInfo | null;
12
+ /**
13
+ * Installed version per key, from each key's package root. Resolution is deduped by root:
14
+ * `findDepPackageJson` trims to the root anyway, and that root package.json is also where
15
+ * `resolvePackageInfo` reads the `version` it records as `PackageInfo.version`, so a secondary
16
+ * yields the same string as its main entry point. Unresolvable keys map to ''.
17
+ */
18
+ export declare function installedVersions(packageNames: string[], workspaceRoot: string, repo?: PackageJsonRepository): Record<string, string>;
6
19
  export declare function getVersionMaps(project: string, workspace: string, repo?: PackageJsonRepository): VersionMap[];
7
20
  export declare function findDepPackageJson(packageName: string, projectRoot: string, repo?: PackageJsonRepository): string | null;
@@ -1,11 +1,11 @@
1
1
  import { logger } from "../logger.js";
2
2
  import { normalize } from "../normalize.js";
3
- import { sharedPackageJsonRepository } from "../io/package-json-repository.js";
3
+ import { getPkgFolder, sharedPackageJsonRepository } from "../io/package-json-repository.js";
4
4
  import { resolvePackageInfo } from "./entry-point-resolver.js";
5
5
  import { getVersionMaps as getVersionMapsFromRepo } from "./version-maps.js";
6
6
  import { sharedPackageJsonRepository as sharedPackageJsonRepository2 } from "../io/package-json-repository.js";
7
7
  import { isESMExport } from "./esm-detection.js";
8
- function getPackageInfo(packageName, workspaceRoot, repo = sharedPackageJsonRepository) {
8
+ function tryGetPackageInfo(packageName, workspaceRoot, repo = sharedPackageJsonRepository) {
9
9
  workspaceRoot = normalize(workspaceRoot, true);
10
10
  for (const info of repo.getPackageJsonFiles(workspaceRoot, workspaceRoot)) {
11
11
  const cand = resolvePackageInfo(repo, packageName, info.directory);
@@ -13,9 +13,33 @@ function getPackageInfo(packageName, workspaceRoot, repo = sharedPackageJsonRepo
13
13
  return cand;
14
14
  }
15
15
  }
16
- logger.warn("No meta data found for shared lib " + packageName);
17
16
  return null;
18
17
  }
18
+ function getPackageInfo(packageName, workspaceRoot, repo = sharedPackageJsonRepository) {
19
+ const info = tryGetPackageInfo(packageName, workspaceRoot, repo);
20
+ if (!info) {
21
+ logger.warn("No meta data found for shared lib " + packageName);
22
+ logger.warn(
23
+ "If you don't need this package, skip it in your federation.config.js or consider moving it into depDependencies in your package.json"
24
+ );
25
+ }
26
+ return info;
27
+ }
28
+ function installedVersions(packageNames, workspaceRoot, repo = sharedPackageJsonRepository) {
29
+ workspaceRoot = normalize(workspaceRoot, true);
30
+ const byRoot = /* @__PURE__ */ new Map();
31
+ const result = {};
32
+ for (const packageName of packageNames) {
33
+ const root = getPkgFolder(packageName);
34
+ if (!byRoot.has(root)) {
35
+ const pkgJsonPath = repo.findDepPackageJson(root, workspaceRoot);
36
+ const version = pkgJsonPath ? repo.readJson(pkgJsonPath)["version"] : "";
37
+ byRoot.set(root, version ?? "");
38
+ }
39
+ result[packageName] = byRoot.get(root);
40
+ }
41
+ return result;
42
+ }
19
43
  function getVersionMaps(project, workspace, repo = sharedPackageJsonRepository) {
20
44
  return getVersionMapsFromRepo(repo, project, workspace);
21
45
  }
@@ -26,6 +50,8 @@ export {
26
50
  findDepPackageJson,
27
51
  getPackageInfo,
28
52
  getVersionMaps,
53
+ installedVersions,
29
54
  isESMExport,
30
- sharedPackageJsonRepository2 as sharedPackageJsonRepository
55
+ sharedPackageJsonRepository2 as sharedPackageJsonRepository,
56
+ tryGetPackageInfo
31
57
  };
@@ -1,10 +1,16 @@
1
- import { captureWildcard, parseWildcard, substituteWildcard, toPosix } from "../path-patterns.js";
1
+ import {
2
+ captureWildcard,
3
+ parseWildcard,
4
+ substituteWildcard,
5
+ toGlobPattern,
6
+ toPosix
7
+ } from "../path-patterns.js";
2
8
  function resolvePackageJsonExportsWildcardCore(io, keyPattern, valuePattern, cwd) {
3
9
  const pattern = parseWildcard(valuePattern.replace(/^\.?\/+/, ""));
4
10
  if (!pattern.hasWildcard) {
5
11
  return [];
6
12
  }
7
- const files = io.globFiles(pattern.prefix + "**/*" + pattern.suffix, { cwd });
13
+ const files = io.globFiles(toGlobPattern(pattern), { cwd, ignore: ["**/node_modules/**"] });
8
14
  const keys = [];
9
15
  for (const file of files) {
10
16
  const relPath = toPosix(file).replace(/^\.\//, "");
@@ -1,4 +1,11 @@
1
1
  export declare const toPosix: (p: string) => string;
2
+ /**
3
+ * True when `file` is `dir` itself or lives under it. Both sides are normalized, so a
4
+ * caller cannot splice in `path.sep` and get a predicate that is silently always-false
5
+ * on Windows -- `linkedSharedDirs` and the file watcher both emit posix paths.
6
+ */
7
+ export declare function isUnderDir(file: string, dir: string): boolean;
8
+ export declare const isUnderAnyDir: (file: string, dirs: readonly string[]) => boolean;
2
9
  export interface WildcardPattern {
3
10
  prefix: string;
4
11
  suffix: string;
@@ -12,3 +19,10 @@ export declare function matchesWildcard(value: string, pattern: string): boolean
12
19
  */
13
20
  export declare function captureWildcard(value: string, pattern: WildcardPattern): string | null;
14
21
  export declare function substituteWildcard(template: string, captured: string): string;
22
+ /**
23
+ * A glob that is a superset of the pattern, for callers that re-check the match themselves
24
+ * (`captureWildcard`, `matchMapping`). `**` only acts as a globstar on a segment of its own,
25
+ * so a prefix stopping mid-segment (`libs/ui-`) has to be widened back to its directory --
26
+ * `libs/ui-**` reads as `libs/ui-*` and silently matches nothing one level down.
27
+ */
28
+ export declare function toGlobPattern({ prefix, suffix }: WildcardPattern): string;
@@ -1,4 +1,10 @@
1
1
  const toPosix = (p) => p.replace(/\\/g, "/");
2
+ function isUnderDir(file, dir) {
3
+ const f = toPosix(file);
4
+ const d = toPosix(dir).replace(/\/+$/, "");
5
+ return f === d || f.startsWith(d + "/");
6
+ }
7
+ const isUnderAnyDir = (file, dirs) => dirs.some((d) => isUnderDir(file, d));
2
8
  function parseWildcard(pattern) {
3
9
  const i = pattern.indexOf("*");
4
10
  if (i === -1) return { prefix: pattern, suffix: "", hasWildcard: false };
@@ -19,10 +25,16 @@ function captureWildcard(value, pattern) {
19
25
  function substituteWildcard(template, captured) {
20
26
  return template.replace("*", captured);
21
27
  }
28
+ function toGlobPattern({ prefix, suffix }) {
29
+ return prefix.slice(0, prefix.lastIndexOf("/") + 1) + "**/*" + suffix;
30
+ }
22
31
  export {
23
32
  captureWildcard,
33
+ isUnderAnyDir,
34
+ isUnderDir,
24
35
  matchesWildcard,
25
36
  parseWildcard,
26
37
  substituteWildcard,
38
+ toGlobPattern,
27
39
  toPosix
28
40
  };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@softarc/native-federation",
3
- "version": "4.3.2",
3
+ "version": "4.4.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "packageManager": "pnpm@11.11.0",
6
+ "packageManager": "pnpm@11.18.0",
7
7
  "scripts": {
8
8
  "build": "node esbuild.config.mjs && tsc -p tsconfig.build.json",
9
9
  "lint": "eslint src",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@softarc/sheriff-core": "^0.19.6",
16
- "chalk": "^5.6.2",
16
+ "chalk": "^6.0.0",
17
17
  "esbuild": "^0.28.0",
18
18
  "fast-glob": "^3.3.3",
19
19
  "json5": "^2.2.3"
@@ -27,12 +27,12 @@
27
27
  "globals": "^17.3.0",
28
28
  "jiti": "^2.6.1",
29
29
  "jsdom": "^29.0.0",
30
- "knip": "^6.20.0",
30
+ "knip": "^6.26.0",
31
31
  "prettier": "^3.9.4",
32
32
  "tslib": "^2.3.0",
33
33
  "typescript": "~6.0.0",
34
34
  "typescript-eslint": "^8.61.0",
35
- "vite": "^8.1.3",
35
+ "vite": "^8.2.0",
36
36
  "vitest": "^4.0.0"
37
37
  },
38
38
  "exports": {