@softarc/native-federation 4.4.0 → 4.5.0-next.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 (32) hide show
  1. package/dist/internal.d.ts +1 -0
  2. package/dist/lib/config/configuration-context.d.ts +2 -1
  3. package/dist/lib/config/configuration-context.js +4 -2
  4. package/dist/lib/config/get-used-dependencies.js +20 -0
  5. package/dist/lib/config/project-paths.d.ts +1 -1
  6. package/dist/lib/config/project-paths.js +4 -3
  7. package/dist/lib/config/remove-unused-deps.js +10 -5
  8. package/dist/lib/config/share-utils.js +28 -8
  9. package/dist/lib/config/version-lookup.d.ts +4 -0
  10. package/dist/lib/config/version-lookup.js +27 -0
  11. package/dist/lib/core/build/assemble-federation-info.d.ts +8 -0
  12. package/dist/lib/core/build/assemble-federation-info.js +34 -0
  13. package/dist/lib/core/build/build-for-federation.js +7 -45
  14. package/dist/lib/core/build/bundle-exposed-and-mappings.d.ts +1 -3
  15. package/dist/lib/core/build/bundle-exposed-and-mappings.js +0 -25
  16. package/dist/lib/core/build/rebuild-for-federation.js +6 -33
  17. package/dist/lib/core/build/resolve-shared-dirs.d.ts +6 -3
  18. package/dist/lib/core/build/resolve-shared-dirs.js +25 -11
  19. package/dist/lib/core/normalize-options.js +19 -7
  20. package/dist/lib/core/output/write-federation-outputs.d.ts +7 -0
  21. package/dist/lib/core/output/write-federation-outputs.js +9 -0
  22. package/dist/lib/domain/config/external-config.contract.d.ts +14 -2
  23. package/dist/lib/domain/core/federation-options.contract.d.ts +6 -0
  24. package/dist/lib/domain/utils/file-watcher.contract.d.ts +7 -0
  25. package/dist/lib/domain/utils/io-port.contract.d.ts +2 -0
  26. package/dist/lib/utils/disk-case.d.ts +7 -0
  27. package/dist/lib/utils/disk-case.js +14 -0
  28. package/dist/lib/utils/file-watcher.js +10 -9
  29. package/dist/lib/utils/io/node-io-adapter.js +10 -0
  30. package/dist/lib/utils/path-patterns.d.ts +8 -0
  31. package/dist/lib/utils/path-patterns.js +2 -0
  32. package/package.json +1 -1
@@ -15,5 +15,6 @@ export { isCjsCandidate, classifyByExtension, hasEsmSyntax, type ModuleFormat, }
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
17
  export { syncNfFileWatcher, createNfWatcher, type WatchSources } from './lib/utils/file-watcher.js';
18
+ export type { WatchPort, WatchHandle } from './lib/domain/utils/io-port.contract.js';
18
19
  export { linkedSharedDirs, sharedMappingDirs } from './lib/core/build/resolve-shared-dirs.js';
19
20
  export { isUnderDir, isUnderAnyDir } from './lib/utils/path-patterns.js';
@@ -1,7 +1,8 @@
1
+ import type { FileReaderPort } from '../domain/utils/io-port.contract.js';
1
2
  export interface ConfigurationContext {
2
3
  workspaceRoot?: string;
3
4
  packageJson?: string;
4
5
  }
5
- export declare function useWorkspace(workspaceRoot: string): void;
6
+ export declare function useWorkspace(workspaceRoot: string, io?: FileReaderPort): void;
6
7
  export declare function usePackageJson(packageJson?: string): void;
7
8
  export declare function getConfigContext(): ConfigurationContext;
@@ -1,6 +1,8 @@
1
+ import { nodeIo } from "../utils/io/node-io-adapter.js";
2
+ import { toDiskCase } from "../utils/disk-case.js";
1
3
  let _context = {};
2
- function useWorkspace(workspaceRoot) {
3
- _context = { ..._context, workspaceRoot };
4
+ function useWorkspace(workspaceRoot, io = nodeIo) {
5
+ _context = { ..._context, workspaceRoot: toDiskCase(io, workspaceRoot) };
4
6
  }
5
7
  function usePackageJson(packageJson) {
6
8
  _context = { ..._context, packageJson };
@@ -4,6 +4,7 @@ import { sharedPackageJsonRepository, tryGetPackageInfo } from "../utils/package
4
4
  import { getExternalImportsCore } from "./get-external-imports.js";
5
5
  import { nodeIo } from "../utils/io/node-io-adapter.js";
6
6
  import { isSharedMapping, matchMapping } from "./match-mapping.js";
7
+ import { logger } from "../utils/logger.js";
7
8
  import * as path from "path";
8
9
  const defaultDeps = {
9
10
  io: nodeIo,
@@ -71,6 +72,8 @@ function addTransientDeps(packages, workspaceRoot, deps) {
71
72
  }
72
73
  function resolveUsedMappings(fileInfos, workspaceRoot, sharedMappings) {
73
74
  const usedMappings = {};
75
+ const matchesIgnoringCase = createCaseInsensitiveMatcher(sharedMappings);
76
+ const caseOnlyMisses = /* @__PURE__ */ new Set();
74
77
  for (const fileName of Object.keys(fileInfos)) {
75
78
  const fullFileName = path.join(workspaceRoot, fileName);
76
79
  if (isSharedMapping(fullFileName, sharedMappings)) continue;
@@ -80,10 +83,27 @@ function resolveUsedMappings(fileInfos, workspaceRoot, sharedMappings) {
80
83
  const fullImport = path.join(workspaceRoot, imp);
81
84
  const match = matchMapping(fullImport, sharedMappings);
82
85
  if (match) usedMappings[fullImport] = match;
86
+ else if (matchesIgnoringCase(fullImport)) caseOnlyMisses.add(fullImport);
83
87
  }
84
88
  }
89
+ warnOnCaseOnlyMisses(caseOnlyMisses);
85
90
  return usedMappings;
86
91
  }
92
+ function createCaseInsensitiveMatcher(sharedMappings) {
93
+ const lowerCased = Object.fromEntries(
94
+ Object.entries(sharedMappings).map(([sharedPath, sharedImport]) => [
95
+ sharedPath.toLowerCase(),
96
+ sharedImport
97
+ ])
98
+ );
99
+ return (filePath) => matchMapping(filePath.toLowerCase(), lowerCased) !== null;
100
+ }
101
+ function warnOnCaseOnlyMisses(misses) {
102
+ if (misses.size === 0) return;
103
+ logger.warn(
104
+ `${misses.size} import(s) match a shared mapping only when case is ignored, so those libraries were pruned from remoteEntry.json -- e.g. '${[...misses][0]}'.`
105
+ );
106
+ }
87
107
  export {
88
108
  getUsedDependenciesFactory,
89
109
  getUsedDependenciesFactoryCore
@@ -2,4 +2,4 @@ import type { FileReaderPort } from '../domain/utils/io-port.contract.js';
2
2
  export declare function findRootTsConfigJson(): string;
3
3
  export declare function findRootTsConfigJsonCore(io: FileReaderPort): string;
4
4
  export declare function findPackageJson(io: FileReaderPort, folder: string): string;
5
- export declare function inferProjectPath(projectPath: string | undefined): string;
5
+ export declare function inferProjectPath(projectPath: string | undefined, io?: FileReaderPort): string;
@@ -2,11 +2,12 @@ import * as path from "path";
2
2
  import { cwd } from "process";
3
3
  import { getConfigContext } from "./configuration-context.js";
4
4
  import { nodeIo } from "../utils/io/node-io-adapter.js";
5
+ import { toDiskCase } from "../utils/disk-case.js";
5
6
  function findRootTsConfigJson() {
6
7
  return findRootTsConfigJsonCore(nodeIo);
7
8
  }
8
9
  function findRootTsConfigJsonCore(io) {
9
- const packageJson = findPackageJson(io, cwd());
10
+ const packageJson = findPackageJson(io, toDiskCase(io, cwd()));
10
11
  const projectRoot = path.dirname(packageJson);
11
12
  const tsConfigBaseJson = path.join(projectRoot, "tsconfig.base.json");
12
13
  const tsConfigJson = path.join(projectRoot, "tsconfig.json");
@@ -29,7 +30,7 @@ function findPackageJson(io, folder) {
29
30
  "no package.json found. Searched the following folder and all parents: " + folder
30
31
  );
31
32
  }
32
- function inferProjectPath(projectPath) {
33
+ function inferProjectPath(projectPath, io = nodeIo) {
33
34
  if (!projectPath && getConfigContext().packageJson) {
34
35
  projectPath = path.dirname(getConfigContext().packageJson || "");
35
36
  }
@@ -37,7 +38,7 @@ function inferProjectPath(projectPath) {
37
38
  projectPath = getConfigContext().workspaceRoot || "";
38
39
  }
39
40
  if (!projectPath) {
40
- projectPath = cwd();
41
+ projectPath = toDiskCase(io, cwd());
41
42
  }
42
43
  return projectPath;
43
44
  }
@@ -6,14 +6,19 @@ import {
6
6
  import { logger } from "../utils/logger.js";
7
7
  function removeUnusedDeps(usedDependencies, config, ctx) {
8
8
  const filteredDependencies = Object.entries(config.shared).filter(([shared, meta]) => !!meta.includeSecondaries || usedDependencies.external.has(shared)).reduce((acc, [shared, meta]) => ({ ...acc, [shared]: meta }), {});
9
+ const sharedMappings = withoutSkippedMappings(
10
+ { ...keptMappings(config, ctx), ...usedDependencies.internal },
11
+ config.skip
12
+ );
13
+ if (Object.keys(config.sharedMappings).length > 0 && Object.keys(sharedMappings).length === 0) {
14
+ logger.warn(
15
+ "No shared mapping is reachable from the entry points, so remoteEntry.json will ship without this workspace's libraries. Disable 'ignoreUnusedDeps' to publish them anyway."
16
+ );
17
+ }
9
18
  return {
10
19
  ...config,
11
20
  shared: filteredDependencies,
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
- )
21
+ sharedMappings
17
22
  };
18
23
  }
19
24
  function keptMappings(config, ctx) {
@@ -8,7 +8,7 @@ import {
8
8
  import { logger } from "../utils/logger.js";
9
9
  import { nodeIo } from "../utils/io/node-io-adapter.js";
10
10
  import { findPackageJson, inferProjectPath } from "./project-paths.js";
11
- import { isInferVersion, lookupVersion } from "./version-lookup.js";
11
+ import { isInferVersion, lookupVersion, applyAutoRequiredOptions } from "./version-lookup.js";
12
12
  import { addSecondaries, getSecondaries } from "./secondaries.js";
13
13
  const fromPackageJson = (baseCfg, projectPath) => {
14
14
  const skipList = [...DEFAULT_SKIP_LIST];
@@ -59,8 +59,19 @@ function shareAllCore(io, config, opts = {}, repo = sharedPackageJsonRepository)
59
59
  if (!!opts.overrides && Object.keys(opts.overrides).some((o) => key.startsWith(o))) {
60
60
  continue;
61
61
  }
62
- const inferVersion = !config.requiredVersion || config.requiredVersion === "auto";
63
- const requiredVersion = inferVersion ? versions[key] : config.requiredVersion;
62
+ const requiredVersionCfg = config.requiredVersion ?? void 0;
63
+ const isAutoObject = typeof requiredVersionCfg === "object";
64
+ const inferVersion = !requiredVersionCfg || requiredVersionCfg === "auto" || isAutoObject;
65
+ let requiredVersion;
66
+ if (inferVersion) {
67
+ const base = versions[key];
68
+ requiredVersion = isAutoObject ? applyAutoRequiredOptions(base, {
69
+ range: requiredVersionCfg.range,
70
+ version: requiredVersionCfg.version ?? config.version
71
+ }) : base;
72
+ } else {
73
+ requiredVersion = requiredVersionCfg;
74
+ }
64
75
  if (!sharedExternals[key]) {
65
76
  sharedExternals[key] = { ...config, requiredVersion };
66
77
  }
@@ -100,11 +111,20 @@ function shareCore(io, configuredShareObjects, projectPath = "", skipList = DEFA
100
111
  const result = {};
101
112
  for (const key in shareObjects) {
102
113
  let includeSecondaries = false;
103
- const shareObject = shareObjects[key];
104
- if (shareObject.requiredVersion === "auto" || isInferVersion() && typeof shareObject.requiredVersion === "undefined" || (shareObject.requiredVersion?.length ?? 1) < 1) {
105
- const version = lookupVersion(key, projectPath, repo);
106
- shareObject.requiredVersion = version;
107
- shareObject.version = version.replace(/^\D*/, "");
114
+ const { requiredVersion: requiredVersionCfg, ...rest } = shareObjects[key];
115
+ const shareObject = {
116
+ ...rest,
117
+ ...typeof requiredVersionCfg === "string" && {
118
+ requiredVersion: requiredVersionCfg
119
+ }
120
+ };
121
+ if (requiredVersionCfg === "auto" || isInferVersion() && typeof requiredVersionCfg === "undefined" || typeof requiredVersionCfg === "object" || (requiredVersionCfg?.length ?? 1) < 1) {
122
+ const isAutoObject = typeof requiredVersionCfg === "object";
123
+ const explicitVersion = isAutoObject ? requiredVersionCfg.version ?? shareObject.version : void 0;
124
+ const resolvedVersion = explicitVersion && explicitVersion !== "auto" ? explicitVersion : void 0;
125
+ const raw = resolvedVersion ?? lookupVersion(key, projectPath, repo);
126
+ shareObject.requiredVersion = isAutoObject && requiredVersionCfg.range ? applyAutoRequiredOptions(raw, { range: requiredVersionCfg.range }) : raw;
127
+ shareObject.version = raw.replace(/^\D*/, "");
108
128
  }
109
129
  if (typeof shareObject.includeSecondaries === "undefined") {
110
130
  shareObject.includeSecondaries = true;
@@ -2,3 +2,7 @@ import type { PackageJsonRepository } from '../domain/utils/package-json.contrac
2
2
  export declare function setInferVersion(infer: boolean): void;
3
3
  export declare function isInferVersion(): boolean;
4
4
  export declare function lookupVersion(key: string, workspaceRoot: string, repo: PackageJsonRepository): string;
5
+ export declare function applyAutoRequiredOptions(baseVersion: string, opts?: {
6
+ range?: 'exact' | '^' | '~' | 'minor' | 'patch';
7
+ version?: string;
8
+ }): string;
@@ -30,7 +30,34 @@ function lookupVersionInMap(key, versions) {
30
30
  }
31
31
  return versions[key];
32
32
  }
33
+ function applyAutoRequiredOptions(baseVersion, opts) {
34
+ const explicit = opts?.version && opts.version !== "auto" ? opts.version : void 0;
35
+ const raw = (explicit ?? baseVersion ?? "").trim();
36
+ if (!opts || !opts.range) return raw;
37
+ const requested = opts.range;
38
+ const singleTokenMatch = raw.match(
39
+ /^(?:[~^<>=]*\s*)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?((?:[-+][\w.]+)?)$/
40
+ );
41
+ if (!singleTokenMatch) {
42
+ return raw;
43
+ }
44
+ const [, major, minor = "0", patch = "0", extra] = singleTokenMatch;
45
+ const bareVersion = `${major}.${minor}.${patch}${extra}`;
46
+ switch (requested) {
47
+ case "exact":
48
+ return bareVersion;
49
+ case "^":
50
+ case "minor":
51
+ return `^${bareVersion}`;
52
+ case "~":
53
+ case "patch":
54
+ return `~${bareVersion}`;
55
+ default:
56
+ return raw;
57
+ }
58
+ }
33
59
  export {
60
+ applyAutoRequiredOptions,
34
61
  isInferVersion,
35
62
  lookupVersion,
36
63
  setInferVersion
@@ -0,0 +1,8 @@
1
+ import type { ArtifactInfo, FederationInfo } from '../../domain/core/federation-info.contract.js';
2
+ import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
3
+ import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
4
+ /**
5
+ * Derives the `remoteEntry.json` payload from the populated federation cache. Shared by the initial
6
+ * build and the watch rebuild.
7
+ */
8
+ export declare function assembleFederationInfo(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, artifactInfo: ArtifactInfo): FederationInfo;
@@ -0,0 +1,34 @@
1
+ import { densifyExternals } from "../output/densify-externals.js";
2
+ function assembleFederationInfo(config, fedOptions, artifactInfo) {
3
+ const federationCache = fedOptions.federationCache;
4
+ const sharedExternals = applyShareScope(
5
+ [...federationCache.externals, ...artifactInfo.mappings],
6
+ config.shareScope
7
+ );
8
+ const federationInfo = {
9
+ name: config.name,
10
+ shared: config.features.denseExternals ? densifyExternals(sharedExternals) : sharedExternals,
11
+ exposes: artifactInfo.exposes,
12
+ buildNotificationsEndpoint: fedOptions.buildNotifications?.enable && fedOptions.dev ? fedOptions.buildNotifications?.endpoint : void 0
13
+ };
14
+ if (federationCache.chunks) {
15
+ federationInfo.chunks = federationCache.chunks;
16
+ }
17
+ if (artifactInfo.chunks) {
18
+ federationInfo.chunks = { ...federationInfo.chunks ?? {}, ...artifactInfo.chunks };
19
+ }
20
+ if (config.features.integrityHashes) {
21
+ federationInfo.integrity = {
22
+ ...federationCache.integrity ?? {},
23
+ ...artifactInfo.integrity ?? {}
24
+ };
25
+ }
26
+ return federationInfo;
27
+ }
28
+ function applyShareScope(externals, shareScope) {
29
+ if (!shareScope) return externals;
30
+ return externals.map((external) => external.shareScope ? external : { ...external, shareScope });
31
+ }
32
+ export {
33
+ assembleFederationInfo
34
+ };
@@ -1,22 +1,12 @@
1
- import {
2
- bundleExposedAndMappings,
3
- describeExposed,
4
- describeSharedMappings
5
- } from "./bundle-exposed-and-mappings.js";
1
+ import { bundleExposedAndMappings } from "./bundle-exposed-and-mappings.js";
6
2
  import { bundleShared } from "./bundle-shared.js";
7
- import { densifyExternals } from "../output/densify-externals.js";
8
- import { writeFederationInfo } from "../output/write-federation-info.js";
9
- import { writeImportMap } from "../output/write-import-map.js";
3
+ import { assembleFederationInfo } from "./assemble-federation-info.js";
4
+ import { writeFederationOutputs } from "../output/write-federation-outputs.js";
10
5
  import { logger } from "../../utils/logger.js";
11
6
  import { AbortedError } from "../../utils/errors.js";
12
7
  import { addExternalsToCache } from "../cache/federation-cache.js";
13
8
  import { planSharedBundles } from "./shared-bundle-plan.js";
14
- import path from "path";
15
9
  async function buildForFederation(config, fedOptions, externals, signal) {
16
- fedOptions.federationCache.cachePath = path.join(
17
- fedOptions.federationCache.cachePath,
18
- fedOptions.projectName
19
- );
20
10
  logger.info("Building federation artifacts");
21
11
  logger.notice("Skip packages you don't want to share in your federation config");
22
12
  await executeSharedBundlePlans(planSharedBundles(config, externals), config, fedOptions, signal);
@@ -31,36 +21,8 @@ async function buildForFederation(config, fedOptions, externals, signal) {
31
21
  logger.measure(start, "Step 3) Bundling all internal libraries and exposed modules.");
32
22
  if (signal?.aborted)
33
23
  throw new AbortedError("[buildForFederation] After exposed-and-mappings bundle");
34
- const exposedInfo = !artifactInfo ? describeExposed(config, fedOptions) : artifactInfo.exposes;
35
- const sharedMappingInfo = !artifactInfo ? describeSharedMappings(config, fedOptions) : artifactInfo.mappings;
36
- const sharedExternals = [...fedOptions.federationCache.externals, ...sharedMappingInfo];
37
- if (config?.shareScope) {
38
- Object.values(sharedExternals).forEach((external) => {
39
- if (!external.shareScope) external.shareScope = config.shareScope;
40
- });
41
- }
42
- const shared = config.features.denseExternals ? densifyExternals(sharedExternals) : sharedExternals;
43
- const buildNotificationsEndpoint = fedOptions.buildNotifications?.enable && fedOptions.dev ? fedOptions.buildNotifications?.endpoint : void 0;
44
- const federationInfo = {
45
- name: config.name,
46
- shared,
47
- exposes: exposedInfo,
48
- buildNotificationsEndpoint
49
- };
50
- if (fedOptions.federationCache.chunks) {
51
- federationInfo.chunks = fedOptions.federationCache.chunks;
52
- }
53
- if (artifactInfo?.chunks) {
54
- federationInfo.chunks = { ...federationInfo.chunks ?? {}, ...artifactInfo?.chunks };
55
- }
56
- if (config.features.integrityHashes) {
57
- federationInfo.integrity = {
58
- ...fedOptions.federationCache.integrity ?? {},
59
- ...artifactInfo?.integrity ?? {}
60
- };
61
- }
62
- writeFederationInfo(federationInfo, fedOptions);
63
- writeImportMap(fedOptions.federationCache, fedOptions, federationInfo.integrity);
24
+ const federationInfo = assembleFederationInfo(config, fedOptions, artifactInfo);
25
+ writeFederationOutputs(federationInfo, fedOptions);
64
26
  return federationInfo;
65
27
  }
66
28
  async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
@@ -75,7 +37,7 @@ async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
75
37
  logger.measure(start, `Step 2.1) Bundling '${plan.bundleName}' externals`);
76
38
  addExternalsToCache(fedOptions.federationCache, info);
77
39
  if (signal?.aborted)
78
- throw new AbortedError(`[buildForFederation] After ${plan.bundleName} bundle`);
40
+ throw new AbortedError(`[executeSharedBundlePlans] After ${plan.bundleName} bundle`);
79
41
  }
80
42
  const separatePlans = plans.filter((p) => p.kind === "separate");
81
43
  if (separatePlans.length > 0) {
@@ -91,7 +53,7 @@ async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
91
53
  );
92
54
  logger.measure(start, "Step 2.2) Bundling all separate external packages");
93
55
  for (const info of results) addExternalsToCache(fedOptions.federationCache, info);
94
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After separate bundle");
56
+ if (signal?.aborted) throw new AbortedError("[executeSharedBundlePlans] After separate bundle");
95
57
  }
96
58
  }
97
59
  export {
@@ -1,4 +1,4 @@
1
- import type { ArtifactInfo, ExposesInfo, SharedInfo } from '../../domain/core/federation-info.contract.js';
1
+ import type { ArtifactInfo } from '../../domain/core/federation-info.contract.js';
2
2
  import type { FileReaderPort } from '../../domain/utils/io-port.contract.js';
3
3
  import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
4
4
  import { type NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
@@ -7,7 +7,5 @@ export declare function bundleExposedAndMappings(config: NormalizedFederationCon
7
7
  export declare function bundleExposedAndMappingsCore(deps: {
8
8
  adapter: NFBuildAdapter;
9
9
  }, config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], modifiedFiles?: string[], signal?: AbortSignal): Promise<ArtifactInfo>;
10
- export declare function describeExposed(config: NormalizedFederationConfig, options: NormalizedFederationOptions): Array<ExposesInfo>;
11
- export declare function describeSharedMappings(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions): Array<SharedInfo>;
12
10
  export declare function getMappingVersionCore(io: FileReaderPort, fileName: string, workspaceRoot: string): string;
13
11
  export declare function getMappingVersion(fileName: string, workspaceRoot: string): string;
@@ -115,29 +115,6 @@ async function bundleExposedAndMappingsCore(deps, config, fedOptions, externals,
115
115
  const integrity = config.features.integrityHashes ? computeIntegrityMap([...entryFiles, ...chunkPaths], "") : void 0;
116
116
  return { mappings: sharedResult, exposes: exposedResult, chunks: exportedChunks, integrity };
117
117
  }
118
- function describeExposed(config, options) {
119
- const result = [];
120
- for (const key in config.exposes) {
121
- const expose = config.exposes[key];
122
- const localPath = normalize(path.normalize(path.join(options.workspaceRoot, expose.file)));
123
- result.push({
124
- key,
125
- outFileName: "",
126
- ...expose.element && { element: expose.element },
127
- dev: !options.dev ? void 0 : {
128
- entryPoint: localPath
129
- }
130
- });
131
- }
132
- return result;
133
- }
134
- function describeSharedMappings(config, fedOptions) {
135
- const result = [];
136
- for (const [mappedPath, mappedImport] of Object.entries(config.sharedMappings)) {
137
- result.push(toSharedMappingInfo(mappedPath, mappedImport, "", config, fedOptions));
138
- }
139
- return result;
140
- }
141
118
  function toSharedMappingInfo(mappedPath, mappedImport, outFileName, config, fedOptions) {
142
119
  const mappingVersion = config.features.mappingVersion ? getMappingVersion(mappedPath, fedOptions.workspaceRoot) : "";
143
120
  const mappingConfig = resolveMappingConfig(
@@ -184,8 +161,6 @@ function getMappingVersion(fileName, workspaceRoot) {
184
161
  export {
185
162
  bundleExposedAndMappings,
186
163
  bundleExposedAndMappingsCore,
187
- describeExposed,
188
- describeSharedMappings,
189
164
  getMappingVersion,
190
165
  getMappingVersionCore
191
166
  };
@@ -1,10 +1,6 @@
1
- import {
2
- bundleExposedAndMappings,
3
- describeExposed,
4
- describeSharedMappings
5
- } from "./bundle-exposed-and-mappings.js";
6
- import { writeFederationInfo } from "../output/write-federation-info.js";
7
- import { writeImportMap } from "../output/write-import-map.js";
1
+ import { bundleExposedAndMappings } from "./bundle-exposed-and-mappings.js";
2
+ import { assembleFederationInfo } from "./assemble-federation-info.js";
3
+ import { writeFederationOutputs } from "../output/write-federation-outputs.js";
8
4
  import { logger } from "../../utils/logger.js";
9
5
  import { AbortedError } from "../../utils/errors.js";
10
6
  import { planSharedBundles } from "./shared-bundle-plan.js";
@@ -14,7 +10,6 @@ import { cacheEntryCore, getFilename } from "../cache/cache-persistence.js";
14
10
  import { nodeIo } from "../../utils/io/node-io-adapter.js";
15
11
  import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
16
12
  async function rebuildForFederation(config, fedOptions, externals, modifiedFiles, signal) {
17
- const federationCache = fedOptions.federationCache;
18
13
  await rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal);
19
14
  logger.info(`Re-bundling all internal libraries and exposed modules..'`);
20
15
  const start = process.hrtime();
@@ -27,31 +22,9 @@ async function rebuildForFederation(config, fedOptions, externals, modifiedFiles
27
22
  );
28
23
  logger.measure(start, "To re-bundle all internal libraries and exposed modules.");
29
24
  if (signal?.aborted)
30
- throw new AbortedError("[buildForFederation] After exposed-and-mappings bundle");
31
- const exposedInfo = !artifactInfo ? describeExposed(config, fedOptions) : artifactInfo.exposes;
32
- const sharedMappingInfo = !artifactInfo ? describeSharedMappings(config, fedOptions) : artifactInfo.mappings;
33
- const sharedExternals = [...federationCache.externals, ...sharedMappingInfo];
34
- const buildNotificationsEndpoint = fedOptions.buildNotifications?.enable && fedOptions.dev ? fedOptions.buildNotifications?.endpoint : void 0;
35
- const federationInfo = {
36
- name: config.name,
37
- shared: sharedExternals,
38
- exposes: exposedInfo,
39
- buildNotificationsEndpoint
40
- };
41
- if (federationCache.chunks) {
42
- federationInfo.chunks = federationCache.chunks;
43
- }
44
- if (artifactInfo?.chunks) {
45
- federationInfo.chunks = { ...federationInfo.chunks ?? {}, ...artifactInfo?.chunks };
46
- }
47
- if (config.features.integrityHashes) {
48
- federationInfo.integrity = {
49
- ...federationCache.integrity ?? {},
50
- ...artifactInfo?.integrity ?? {}
51
- };
52
- }
53
- writeFederationInfo(federationInfo, fedOptions);
54
- writeImportMap(federationCache, fedOptions, federationInfo.integrity);
25
+ throw new AbortedError("[rebuildForFederation] After exposed-and-mappings bundle");
26
+ const federationInfo = assembleFederationInfo(config, fedOptions, artifactInfo);
27
+ writeFederationOutputs(federationInfo, fedOptions);
55
28
  return federationInfo;
56
29
  }
57
30
  async function rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal, io = nodeIo, repo = sharedPackageJsonRepository) {
@@ -6,7 +6,7 @@ import type { PackageJsonRepository } from '../../domain/utils/package-json.cont
6
6
  * file-mapping agree on one identity for symlinked (npm-linked) deps. */
7
7
  export declare function resolveSharedPackageDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): Map<string, string>;
8
8
  /** Realpath'd dirs of symlinked shared packages — the bounded watch set.
9
- * Deduped, since secondaries share a package dir. */
9
+ * Deduped, since secondaries share a package dir. Empty unless `watchLinkedDeps` is on. */
10
10
  export declare function linkedSharedDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): string[];
11
11
  /**
12
12
  * Source directories of the workspace libs in `config.sharedMappings` — the watch set
@@ -24,9 +24,12 @@ export declare function linkedSharedDirs(config: NormalizedFederationConfig, fed
24
24
  * they are source trees, not the dist output `linkedSharedDirs` exists for.
25
25
  */
26
26
  export declare function sharedMappingDirs(config: NormalizedFederationConfig): string[];
27
- /** Per-key content signal (max mtime of the resolved dir) for symlinked deps only.
27
+ /** Per-key content signal (max mtime of the resolved dir) for linked checkouts only.
28
28
  * Registry deps get no signal, keeping their checksum version-only. (Every key is
29
- * still resolved: detecting the symlink requires the realpath + lstat.) */
29
+ * still resolved: detecting the symlink requires the realpath + lstat.)
30
+ *
31
+ * Deliberately not gated on `watchLinkedDeps`: that option decides whether an edit is
32
+ * noticed live, never whether the next build is correct. */
30
33
  export declare function linkedContentSignals(keys: string[], folder: string, io?: FileReaderPort, repo?: PackageJsonRepository): Record<string, string>;
31
34
  /** `config.shared` keys whose package directory contains at least one modified file. */
32
35
  export declare function affectedSharedKeys(modifiedFiles: readonly string[], dirs: Map<string, string>, io?: FileReaderPort): Set<string>;
@@ -1,7 +1,7 @@
1
1
  import * as path from "path";
2
2
  import { nodeIo } from "../../utils/io/node-io-adapter.js";
3
3
  import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
4
- import { isUnderDir, toPosix } from "../../utils/path-patterns.js";
4
+ import { isOutsideNodeModules, isUnderDir, toPosix } from "../../utils/path-patterns.js";
5
5
  const folderOf = (fedOptions) => fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
6
6
  function resolveEntries(keys, folder, io, repo) {
7
7
  const out = [];
@@ -9,10 +9,12 @@ function resolveEntries(keys, folder, io, repo) {
9
9
  const pkgJsonPath = repo.findDepPackageJson(key, folder);
10
10
  if (!pkgJsonPath) continue;
11
11
  const pkgDir = path.dirname(pkgJsonPath);
12
+ const realDir = toPosix(io.realpath(pkgDir));
12
13
  out.push({
13
14
  key,
14
- realDir: toPosix(io.realpath(pkgDir)),
15
- isSymlink: !!io.stat(pkgDir)?.isSymbolicLink
15
+ realDir,
16
+ // pnpm's default linker symlinks every dep, so the link alone proves nothing.
17
+ isLinkedCheckout: !!io.stat(pkgDir)?.isSymbolicLink && isOutsideNodeModules(realDir)
16
18
  });
17
19
  }
18
20
  return out;
@@ -22,24 +24,29 @@ function resolveSharedPackageDirs(config, fedOptions, io = nodeIo, repo = shared
22
24
  return new Map(entries.map((e) => [e.key, e.realDir]));
23
25
  }
24
26
  function linkedSharedDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
27
+ if (!fedOptions.watchLinkedDeps) return [];
25
28
  const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
26
- return [...new Set(entries.filter((e) => e.isSymlink).map((e) => e.realDir))];
29
+ return [...new Set(entries.filter((e) => e.isLinkedCheckout).map((e) => e.realDir))];
27
30
  }
28
31
  function sharedMappingDirs(config) {
29
- const dirs = Object.keys(config.sharedMappings).map((entryPoint) => toPosix(path.dirname(entryPoint))).filter((dir) => !dir.includes("node_modules"));
32
+ const dirs = Object.keys(config.sharedMappings).map((entryPoint) => toPosix(path.dirname(entryPoint))).filter(isOutsideNodeModules);
30
33
  return [...new Set(dirs)];
31
34
  }
32
35
  function maxMtime(io, dir) {
33
36
  let max = 0;
37
+ const bump = (s) => {
38
+ if (s && s.mtimeMs > max) max = s.mtimeMs;
39
+ };
34
40
  const walk = (d) => {
35
41
  for (const name of io.readDir(d)) {
36
42
  const full = path.join(d, name);
37
- if (io.isDirectory(full)) walk(full);
38
- else {
39
- let s = io.stat(full);
40
- if (s?.isSymbolicLink) s = io.stat(io.realpath(full));
41
- if (s && s.mtimeMs > max) max = s.mtimeMs;
43
+ const entry = io.stat(full);
44
+ if (entry?.isSymbolicLink) {
45
+ if (!io.isDirectory(full)) bump(io.stat(io.realpath(full)));
46
+ continue;
42
47
  }
48
+ if (io.isDirectory(full)) walk(full);
49
+ else bump(entry);
43
50
  }
44
51
  };
45
52
  walk(dir);
@@ -47,8 +54,15 @@ function maxMtime(io, dir) {
47
54
  }
48
55
  function linkedContentSignals(keys, folder, io = nodeIo, repo = sharedPackageJsonRepository) {
49
56
  const signals = {};
57
+ const byDir = /* @__PURE__ */ new Map();
50
58
  for (const entry of resolveEntries(keys, folder, io, repo)) {
51
- if (entry.isSymlink) signals[entry.key] = String(maxMtime(io, entry.realDir));
59
+ if (!entry.isLinkedCheckout) continue;
60
+ let signal = byDir.get(entry.realDir);
61
+ if (signal === void 0) {
62
+ signal = String(maxMtime(io, entry.realDir));
63
+ byDir.set(entry.realDir, signal);
64
+ }
65
+ signals[entry.key] = signal;
52
66
  }
53
67
  return signals;
54
68
  }
@@ -9,6 +9,7 @@ import { getDefaultCachePath } from "./cache/cache-persistence.js";
9
9
  import { getUsedDependenciesFactory } from "../config/get-used-dependencies.js";
10
10
  import { logger } from "../utils/logger.js";
11
11
  import { normalizePackageName } from "../utils/normalize.js";
12
+ import { toDiskCase } from "../utils/disk-case.js";
12
13
  const defaultConfigLoader = async (fullConfigPath) => (await import(pathToFileURL(fullConfigPath).href))?.default;
13
14
  async function normalizeFederationOptions(options, cache) {
14
15
  return normalizeFederationOptionsCore(
@@ -18,19 +19,30 @@ async function normalizeFederationOptions(options, cache) {
18
19
  );
19
20
  }
20
21
  async function normalizeFederationOptionsCore(deps, options, cache) {
21
- const fullConfigPath = path.join(options.workspaceRoot, options.federationConfig);
22
+ const workspaceRoot = toDiskCase(deps.io, options.workspaceRoot);
23
+ const packageJson = options.packageJson && toDiskCase(deps.io, options.packageJson);
24
+ const fullConfigPath = path.join(workspaceRoot, options.federationConfig);
22
25
  if (!deps.io.exists(fullConfigPath)) {
23
26
  throw new Error("Expected " + fullConfigPath);
24
27
  }
25
28
  let config = await deps.loadConfig(fullConfigPath);
26
- const federationCache = cache ?? createFederationCache(
27
- getDefaultCachePath(options.workspaceRoot)
29
+ const projectName = resolveProjectName(options.projectName ?? config.name);
30
+ const suppliedCache = cache ?? createFederationCache(
31
+ getDefaultCachePath(workspaceRoot)
28
32
  );
33
+ const federationCache = {
34
+ ...suppliedCache,
35
+ cachePath: path.join(suppliedCache.cachePath, projectName),
36
+ externals: [...suppliedCache.externals]
37
+ };
29
38
  const normalizedOptions = {
30
39
  ...options,
40
+ workspaceRoot,
41
+ ...packageJson && { packageJson },
31
42
  entryPoints: options.entryPoints ?? Object.values(config.exposes ?? {}).map((e) => e.file),
32
- projectName: resolveProjectName(options.projectName ?? config.name),
43
+ projectName,
33
44
  cacheExternalArtifacts: options.cacheExternalArtifacts ?? true,
45
+ watchLinkedDeps: options.watchLinkedDeps ?? false,
34
46
  federationCache
35
47
  };
36
48
  const nothingShared = Object.keys(config.shared).length === 0 && Object.keys(config.sharedMappings).length === 0;
@@ -38,12 +50,12 @@ async function normalizeFederationOptionsCore(deps, options, cache) {
38
50
  logger.debug("Nothing is shared, skipping the used dependency scan.");
39
51
  } else if (config.features.ignoreUnusedDeps) {
40
52
  const getUsedDeps = (deps.usedDependenciesFactory ?? getUsedDependenciesFactory)(
41
- options.workspaceRoot,
53
+ workspaceRoot,
42
54
  options.entryPoints
43
55
  );
44
56
  config = removeUnusedDeps(getUsedDeps(config), config, {
45
57
  io: deps.io,
46
- workspaceRoot: options.workspaceRoot
58
+ workspaceRoot
47
59
  });
48
60
  logger.info("Removed unused dependencies.");
49
61
  logger.debug(
@@ -52,7 +64,7 @@ async function normalizeFederationOptionsCore(deps, options, cache) {
52
64
  } else {
53
65
  config.sharedMappings = expandOrDropWildcards(config, {
54
66
  io: deps.io,
55
- workspaceRoot: options.workspaceRoot
67
+ workspaceRoot
56
68
  });
57
69
  }
58
70
  assertBarrelMappings(config.sharedMappings);
@@ -0,0 +1,7 @@
1
+ import type { FederationInfo } from '../../domain/core/federation-info.contract.js';
2
+ import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
+ /**
4
+ * The import map is built from the flat federation cache rather than `federationInfo.shared`: a
5
+ * dense group has no import-map representation, every specifier needs its own entry.
6
+ */
7
+ export declare function writeFederationOutputs(federationInfo: FederationInfo, fedOptions: NormalizedFederationOptions): void;
@@ -0,0 +1,9 @@
1
+ import { writeFederationInfo } from "./write-federation-info.js";
2
+ import { writeImportMap } from "./write-import-map.js";
3
+ function writeFederationOutputs(federationInfo, fedOptions) {
4
+ writeFederationInfo(federationInfo, fedOptions);
5
+ writeImportMap(fedOptions.federationCache, fedOptions, federationInfo.integrity);
6
+ }
7
+ export {
8
+ writeFederationOutputs
9
+ };
@@ -3,6 +3,16 @@ export type IncludeSecondariesOptions = {
3
3
  resolveGlob?: boolean;
4
4
  keepAll?: boolean;
5
5
  } | boolean;
6
+ export interface AutoRequiredOptions {
7
+ version?: 'auto' | string;
8
+ /** Controls how the resolved package.json version is emitted.
9
+ * - 'exact' => "1.2.3"
10
+ * - '^' | '~' => '^1.2.3' or '~1.2.3'
11
+ * - 'minor' => maps to '^' (allow minor bumps)
12
+ * - 'patch' => maps to '~' (allow patch bumps)
13
+ */
14
+ range?: 'exact' | '^' | '~' | 'minor' | 'patch';
15
+ }
6
16
  export interface ExternalConfig {
7
17
  singleton?: boolean;
8
18
  strictVersion?: boolean;
@@ -39,8 +49,10 @@ export interface NormalizedExternalConfig {
39
49
  }
40
50
  export type SharedExternalsConfig = Record<string, ExternalConfig>;
41
51
  export type NormalizedSharedExternalsConfig = Record<string, NormalizedExternalConfig>;
42
- export type ShareAllExternalsOptions = ExternalConfig;
43
- export type ShareExternalsOptions = SharedExternalsConfig;
52
+ export type ShareAllExternalsOptions = Omit<ExternalConfig, 'requiredVersion'> & {
53
+ requiredVersion?: string | AutoRequiredOptions;
54
+ };
55
+ export type ShareExternalsOptions = Record<string, ShareAllExternalsOptions>;
44
56
  export type ResolvedExternalConfig = Omit<ExternalConfig, 'includeSecondaries'> & {
45
57
  includeSecondaries?: boolean;
46
58
  };
@@ -10,6 +10,11 @@ export interface FederationOptions {
10
10
  verbose?: boolean;
11
11
  dev?: boolean;
12
12
  watch?: boolean;
13
+ /** Poll-watch npm-linked shared deps so rebuilding the linked lib live-reloads the host.
14
+ * Off by default: a registry dep is bundled once and cached by checksum, so watching
15
+ * node_modules cannot change an outcome. With this off a linked lib still re-bundles on
16
+ * the next build, it just does not live-reload. */
17
+ watchLinkedDeps?: boolean;
13
18
  packageJson?: string;
14
19
  entryPoints?: string[];
15
20
  buildNotifications?: BuildNotificationOptions;
@@ -19,4 +24,5 @@ export interface NormalizedFederationOptions<TBundlerCache = unknown> extends Fe
19
24
  entryPoints: string[];
20
25
  projectName: string;
21
26
  cacheExternalArtifacts: boolean;
27
+ watchLinkedDeps: boolean;
22
28
  }
@@ -1,4 +1,11 @@
1
+ import type { WatchPort } from './io-port.contract.js';
1
2
  export interface NfFileWatcherOptions {
3
+ /** Watch implementation, defaulting to Node's fs. The built-in poll is dependency-free
4
+ * but sweeps the tree every `pollIntervalMs`; a host that already ships a real watcher
5
+ * should pass it here. An event-driven implementation may ignore `opts.poll`, but must
6
+ * then survive inode replacement on its own: a polled dir supersedes the native watches
7
+ * beneath it, so a missed rename-replace is never re-covered. */
8
+ watch?: WatchPort['watch'];
2
9
  onChange?: (path: string) => void;
3
10
  pollIntervalMs?: number;
4
11
  debounceMs?: number;
@@ -20,6 +20,8 @@ export interface FileReaderPort {
20
20
  /** Immediate child entry names (not full paths). Empty array on ENOENT, never throws. */
21
21
  readDir(path: string): string[];
22
22
  realpath(path: string): string;
23
+ /** The path as disk spells it, including case on case-insensitive filesystems. */
24
+ realpathNative(path: string): string;
23
25
  stat(path: string): StatInfo | null;
24
26
  }
25
27
  export interface FileWriterPort {
@@ -0,0 +1,7 @@
1
+ import type { FileReaderPort } from '../domain/utils/io-port.contract.js';
2
+ /**
3
+ * Every absolute path that gets string-compared later descends from a root supplied by the
4
+ * invoking tool, and on Windows two tools can report one root with different drive-letter case.
5
+ * Correcting it at the root keeps the comparison sites unchanged.
6
+ */
7
+ export declare function toDiskCase(io: FileReaderPort, p: string): string;
@@ -0,0 +1,14 @@
1
+ import * as path from "path";
2
+ import { toPosix } from "./path-patterns.js";
3
+ function toDiskCase(io, p) {
4
+ const real = io.realpathNative(p);
5
+ if (real === p || !differsOnlyByCase(real, p)) return p;
6
+ return path.normalize(real);
7
+ }
8
+ function differsOnlyByCase(a, b) {
9
+ const strip = (s) => toPosix(s).replace(/\/+$/, "").toLowerCase();
10
+ return strip(a) === strip(b);
11
+ }
12
+ export {
13
+ toDiskCase
14
+ };
@@ -1,12 +1,13 @@
1
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 { isUnderDir, toPosix } from "./path-patterns.js";
4
+ import { isOutsideNodeModules, isUnderDir, toPosix } from "./path-patterns.js";
5
5
  function createNfWatcher(options = {}) {
6
6
  return createNfWatcherCore(nodeIo, options);
7
7
  }
8
8
  function createNfWatcherCore(io, options = {}, now = Date.now) {
9
9
  const { onChange } = options;
10
+ const watch = options.watch ?? ((p, o, cb) => io.watch(p, o, cb));
10
11
  const pollIntervalMs = options.pollIntervalMs ?? 300;
11
12
  const debounceMs = options.debounceMs ?? 0;
12
13
  const dedupeReplays = options.dedupeReplays ?? true;
@@ -21,18 +22,18 @@ function createNfWatcherCore(io, options = {}, now = Date.now) {
21
22
  return posix.length > 1 ? posix.replace(/\/+$/, "") : posix;
22
23
  };
23
24
  const covers = (path, poll) => {
24
- for (const [dir, watch] of watchers) {
25
- if ((watch.poll || !poll) && isUnderDir(path, dir)) return true;
25
+ for (const [dir, watch2] of watchers) {
26
+ if ((watch2.poll || !poll) && isUnderDir(path, dir)) return true;
26
27
  }
27
28
  return false;
28
29
  };
29
30
  const supersede = (dir, poll) => {
30
31
  for (const map of [watchers, fileDirWatchers]) {
31
- for (const [key, watch] of map) {
32
- if (watch.poll && !poll) continue;
32
+ for (const [key, watch2] of map) {
33
+ if (watch2.poll && !poll) continue;
33
34
  if (map === watchers && key === dir) continue;
34
35
  if (!isUnderDir(key, dir)) continue;
35
- watch.handle.close();
36
+ watch2.handle.close();
36
37
  map.delete(key);
37
38
  }
38
39
  }
@@ -92,7 +93,7 @@ function createNfWatcherCore(io, options = {}, now = Date.now) {
92
93
  if (watchers.has(dir2) || covers(dir2, shouldPoll)) continue;
93
94
  try {
94
95
  watchers.set(dir2, {
95
- handle: io.watch(p, { recursive: true, poll }, (filename) => {
96
+ handle: watch(p, { recursive: true, poll }, (filename) => {
96
97
  if (filename) notify(toPosix(join(p, filename)));
97
98
  }),
98
99
  poll: shouldPoll
@@ -116,7 +117,7 @@ function createNfWatcherCore(io, options = {}, now = Date.now) {
116
117
  if (fileDirWatchers.has(dir)) continue;
117
118
  try {
118
119
  fileDirWatchers.set(dir, {
119
- handle: io.watch(dir, { recursive: false, poll }, (filename) => {
120
+ handle: watch(dir, { recursive: false, poll }, (filename) => {
120
121
  if (!filename) return;
121
122
  const changed = toPosix(join(dir, filename));
122
123
  if (trackedFiles.has(changed)) notify(changed);
@@ -150,7 +151,7 @@ function toPaths(sources) {
150
151
  return typeof cache.keys === "function" ? cache.keys() : sources;
151
152
  }
152
153
  function syncNfFileWatcher(watcher, sources, linkedDirs = []) {
153
- const files = [...toPaths(sources)].filter((k) => !k.includes("node_modules"));
154
+ const files = [...toPaths(sources)].filter(isOutsideNodeModules);
154
155
  if (files.length) watcher.addPaths(files);
155
156
  if (linkedDirs.length) watcher.addPaths(linkedDirs, { poll: true });
156
157
  }
@@ -40,6 +40,15 @@ const nodeIo = {
40
40
  return path2;
41
41
  }
42
42
  },
43
+ // Only the native variant reports the spelling stored on disk: the JS realpathSync walks the
44
+ // components of the input string and rewrites only the ones that are symlinks.
45
+ realpathNative(path2) {
46
+ try {
47
+ return fs.realpathSync.native(path2);
48
+ } catch {
49
+ return path2;
50
+ }
51
+ },
43
52
  stat(path2) {
44
53
  try {
45
54
  const s = fs.lstatSync(path2);
@@ -96,6 +105,7 @@ function pollWatch(root, recursive, intervalMs, onEvent) {
96
105
  return;
97
106
  }
98
107
  for (const entry of entries) {
108
+ if (entry.name === "node_modules") continue;
99
109
  const full = path.join(dir, entry.name);
100
110
  if (entry.isDirectory()) {
101
111
  if (recursive) walk(full);
@@ -26,3 +26,11 @@ export declare function substituteWildcard(template: string, captured: string):
26
26
  * `libs/ui-**` reads as `libs/ui-*` and silently matches nothing one level down.
27
27
  */
28
28
  export declare function toGlobPattern({ prefix, suffix }: WildcardPattern): string;
29
+ /**
30
+ * A dev checkout rather than an installed package. A symlink alone cannot tell them apart:
31
+ * pnpm's default linker points every dep at `…/node_modules/.pnpm/<pkg>@<ver>/node_modules/
32
+ * <pkg>`, while `npm link` resolves to the checkout. Matched anywhere in the path, since a
33
+ * monorepo's store can sit above the workspace root, and by segment so a checkout under
34
+ * `node_modules_backup` still counts.
35
+ */
36
+ export declare const isOutsideNodeModules: (dir: string) => boolean;
@@ -28,8 +28,10 @@ function substituteWildcard(template, captured) {
28
28
  function toGlobPattern({ prefix, suffix }) {
29
29
  return prefix.slice(0, prefix.lastIndexOf("/") + 1) + "**/*" + suffix;
30
30
  }
31
+ const isOutsideNodeModules = (dir) => !toPosix(dir).split("/").includes("node_modules");
31
32
  export {
32
33
  captureWildcard,
34
+ isOutsideNodeModules,
33
35
  isUnderAnyDir,
34
36
  isUnderDir,
35
37
  matchesWildcard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softarc/native-federation",
3
- "version": "4.4.0",
3
+ "version": "4.5.0-next.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@11.18.0",