@softarc/native-federation 4.3.1 → 4.3.2

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 (31) hide show
  1. package/dist/internal.d.ts +3 -0
  2. package/dist/internal.js +20 -0
  3. package/dist/lib/config/with-native-federation.js +2 -1
  4. package/dist/lib/core/build/build-for-federation.d.ts +7 -0
  5. package/dist/lib/core/build/build-for-federation.js +32 -134
  6. package/dist/lib/core/build/bundle-shared.d.ts +7 -0
  7. package/dist/lib/core/build/bundle-shared.js +38 -7
  8. package/dist/lib/core/build/rebuild-for-federation.d.ts +8 -0
  9. package/dist/lib/core/build/rebuild-for-federation.js +26 -0
  10. package/dist/lib/core/build/resolve-shared-dirs.d.ts +16 -0
  11. package/dist/lib/core/build/resolve-shared-dirs.js +66 -0
  12. package/dist/lib/core/build/shared-bundle-plan.d.ts +22 -0
  13. package/dist/lib/core/build/shared-bundle-plan.js +70 -0
  14. package/dist/lib/core/build/synthesize-cjs-exports.d.ts +12 -0
  15. package/dist/lib/core/build/synthesize-cjs-exports.js +58 -0
  16. package/dist/lib/core/cache/cache-persistence.d.ts +2 -2
  17. package/dist/lib/core/cache/cache-persistence.js +7 -4
  18. package/dist/lib/core/federation-builder.d.ts +2 -1
  19. package/dist/lib/core/federation-builder.js +3 -0
  20. package/dist/lib/core/output/densify-externals.js +1 -0
  21. package/dist/lib/domain/config/federation-config.contract.d.ts +2 -0
  22. package/dist/lib/domain/utils/file-watcher.contract.d.ts +7 -1
  23. package/dist/lib/domain/utils/io-port.contract.d.ts +14 -3
  24. package/dist/lib/utils/file-watcher.d.ts +1 -1
  25. package/dist/lib/utils/file-watcher.js +23 -6
  26. package/dist/lib/utils/io/node-io-adapter.js +77 -21
  27. package/dist/lib/utils/package/cjs-named-exports.d.ts +21 -0
  28. package/dist/lib/utils/package/cjs-named-exports.js +36 -0
  29. package/dist/lib/utils/package/esm-detection.d.ts +13 -0
  30. package/dist/lib/utils/package/esm-detection.js +29 -0
  31. package/package.json +3 -3
@@ -11,5 +11,8 @@ export { RebuildQueue, type TrackResult } from './lib/core/rebuild-queue.js';
11
11
  export { writeImportMap } from './lib/core/output/write-import-map.js';
12
12
  export { getDefaultCachePath, getChecksum } from './lib/core/cache/cache-persistence.js';
13
13
  export { isESMExport, type ExportCondition, type ExportEntry, } from './lib/utils/package/package-info.js';
14
+ export { isCjsCandidate, classifyByExtension, hasEsmSyntax, type ModuleFormat, } from './lib/utils/package/esm-detection.js';
15
+ export { isIdentifierName, planCjsWrap, buildSyntheticCjsEntry, isEsmInteropError, } from './lib/utils/package/cjs-named-exports.js';
14
16
  export type { NfFileWatcher, NfFileWatcherOptions, } from './lib/domain/utils/file-watcher.contract.js';
15
17
  export { syncNfFileWatcher, createNfWatcher } from './lib/utils/file-watcher.js';
18
+ export { linkedSharedDirs } from './lib/core/build/resolve-shared-dirs.js';
package/dist/internal.js CHANGED
@@ -8,15 +8,35 @@ import { getDefaultCachePath, getChecksum } from "./lib/core/cache/cache-persist
8
8
  import {
9
9
  isESMExport
10
10
  } from "./lib/utils/package/package-info.js";
11
+ import {
12
+ isCjsCandidate,
13
+ classifyByExtension,
14
+ hasEsmSyntax
15
+ } from "./lib/utils/package/esm-detection.js";
16
+ import {
17
+ isIdentifierName,
18
+ planCjsWrap,
19
+ buildSyntheticCjsEntry,
20
+ isEsmInteropError
21
+ } from "./lib/utils/package/cjs-named-exports.js";
11
22
  import { syncNfFileWatcher, createNfWatcher } from "./lib/utils/file-watcher.js";
23
+ import { linkedSharedDirs } from "./lib/core/build/resolve-shared-dirs.js";
12
24
  export {
13
25
  RebuildQueue,
26
+ buildSyntheticCjsEntry,
27
+ classifyByExtension,
14
28
  createNfWatcher,
15
29
  getChecksum,
16
30
  getDefaultCachePath,
31
+ hasEsmSyntax,
17
32
  hashFile,
33
+ isCjsCandidate,
18
34
  isESMExport,
35
+ isEsmInteropError,
36
+ isIdentifierName,
37
+ linkedSharedDirs,
19
38
  logger,
39
+ planCjsWrap,
20
40
  setLogLevel,
21
41
  syncNfFileWatcher,
22
42
  writeImportMap
@@ -20,7 +20,8 @@ function withNativeFederation(config) {
20
20
  ignoreUnusedDeps: config.features?.ignoreUnusedDeps ?? true,
21
21
  denseChunking: config.features?.denseChunking ?? false,
22
22
  denseExternals: config.features?.denseExternals ?? false,
23
- integrityHashes: config.features?.integrityHashes ?? false
23
+ integrityHashes: config.features?.integrityHashes ?? false,
24
+ synthesizeCjsExports: config.features?.synthesizeCjsExports ?? true
24
25
  },
25
26
  ...config.shareScope && { shareScope: config.shareScope }
26
27
  };
@@ -1,4 +1,11 @@
1
1
  import type { FederationInfo } from '../../domain/core/federation-info.contract.js';
2
2
  import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
3
  import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
4
+ import { type SharedBundlePlan } from './shared-bundle-plan.js';
4
5
  export declare function buildForFederation(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], signal?: AbortSignal): Promise<FederationInfo>;
6
+ /**
7
+ * Bundles shared/separate externals per plan and populates the federation cache.
8
+ * Shared bundles run sequentially (with signal checks); separate bundles run in
9
+ * parallel. Shared by the initial build and the watch rebuild.
10
+ */
11
+ export declare function executeSharedBundlePlans(plans: SharedBundlePlan[], config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, signal?: AbortSignal): Promise<void>;
@@ -8,9 +8,9 @@ import { densifyExternals } from "../output/densify-externals.js";
8
8
  import { writeFederationInfo } from "../output/write-federation-info.js";
9
9
  import { writeImportMap } from "../output/write-import-map.js";
10
10
  import { logger } from "../../utils/logger.js";
11
- import { inferPackageFromSecondary, normalizePackageName } from "../../utils/normalize.js";
12
11
  import { AbortedError } from "../../utils/errors.js";
13
12
  import { addExternalsToCache } from "../cache/federation-cache.js";
13
+ import { planSharedBundles } from "./shared-bundle-plan.js";
14
14
  import path from "path";
15
15
  async function buildForFederation(config, fedOptions, externals, signal) {
16
16
  fedOptions.federationCache.cachePath = path.join(
@@ -19,66 +19,7 @@ async function buildForFederation(config, fedOptions, externals, signal) {
19
19
  );
20
20
  logger.info("Building federation artifacts");
21
21
  logger.notice("Skip packages you don't want to share in your federation config");
22
- const { sharedBrowser, sharedServer, separateBrowser, separateServer } = splitShared(
23
- config.shared
24
- );
25
- if (Object.keys(sharedBrowser).length > 0) {
26
- logger.info(`Bundling external npm packages with bundle type 'browser-shared'`);
27
- const start2 = process.hrtime();
28
- const sharedPackageInfoBrowser = await bundleShared(
29
- sharedBrowser,
30
- config,
31
- fedOptions,
32
- externals,
33
- { platform: "browser", bundleName: "browser-shared", chunks: config.chunks }
34
- );
35
- logger.measure(start2, "Step 2.1) Bundling all shared browser externals");
36
- addExternalsToCache(fedOptions.federationCache, sharedPackageInfoBrowser);
37
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After shared-browser bundle");
38
- }
39
- if (Object.keys(sharedServer).length > 0) {
40
- logger.info(`Bundling external npm packages with bundle type 'server-shared'`);
41
- const start2 = process.hrtime();
42
- const sharedPackageInfoServer = await bundleShared(
43
- sharedServer,
44
- config,
45
- fedOptions,
46
- externals,
47
- { platform: "node", bundleName: "node-shared", chunks: config.chunks }
48
- );
49
- logger.measure(start2, "Step 2.1) Bundling all shared node externals");
50
- addExternalsToCache(fedOptions.federationCache, sharedPackageInfoServer);
51
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After shared-node bundle");
52
- }
53
- if (Object.keys(separateBrowser).length > 0) {
54
- logger.info(`Bundling external npm packages with bundle type 'browser-separate'`);
55
- const start2 = process.hrtime();
56
- const separatePackageInfoBrowser = await bundleSeparatePackages(
57
- separateBrowser,
58
- externals,
59
- config,
60
- fedOptions,
61
- { platform: "browser" }
62
- );
63
- logger.measure(start2, "Step 2.2) Bundling all separate browser external packages");
64
- addExternalsToCache(fedOptions.federationCache, separatePackageInfoBrowser);
65
- if (signal?.aborted)
66
- throw new AbortedError("[buildForFederation] After separate-browser bundle");
67
- }
68
- if (Object.keys(separateServer).length > 0) {
69
- logger.info(`Bundling external npm packages with bundle type 'node-separate'`);
70
- const start2 = process.hrtime();
71
- const separatePackageInfoServer = await bundleSeparatePackages(
72
- separateServer,
73
- externals,
74
- config,
75
- fedOptions,
76
- { platform: "node" }
77
- );
78
- logger.measure(start2, "Step 2.2) Bundling all separate node external packages");
79
- addExternalsToCache(fedOptions.federationCache, separatePackageInfoServer);
80
- }
81
- if (signal?.aborted) throw new AbortedError("[buildForFederation] After separate-node bundle");
22
+ await executeSharedBundlePlans(planSharedBundles(config, externals), config, fedOptions, signal);
82
23
  const start = process.hrtime();
83
24
  const artifactInfo = await bundleExposedAndMappings(
84
25
  config,
@@ -122,81 +63,38 @@ async function buildForFederation(config, fedOptions, externals, signal) {
122
63
  writeImportMap(fedOptions.federationCache, fedOptions, federationInfo.integrity);
123
64
  return federationInfo;
124
65
  }
125
- async function bundleSeparatePackages(separateBrowser, externals, config, fedOptions, buildOptions) {
126
- const groupedByPackage = {};
127
- for (const [key, shared] of Object.entries(separateBrowser)) {
128
- const packageName = shared.build === "separate" ? key : inferPackageFromSecondary(key);
129
- if (!groupedByPackage[packageName]) {
130
- groupedByPackage[packageName] = {
131
- chunks: shared.chunks,
132
- entries: {}
133
- };
134
- }
135
- groupedByPackage[packageName].entries[key] = shared;
66
+ async function executeSharedBundlePlans(plans, config, fedOptions, signal) {
67
+ for (const plan of plans.filter((p) => p.kind === "shared")) {
68
+ logger.info(`Bundling external npm packages with bundle type '${plan.bundleName}'`);
69
+ const start = process.hrtime();
70
+ const info = await bundleShared(plan.entries, config, fedOptions, plan.externals, {
71
+ platform: plan.platform,
72
+ bundleName: plan.bundleName,
73
+ chunks: plan.chunks
74
+ });
75
+ logger.measure(start, `Step 2.1) Bundling '${plan.bundleName}' externals`);
76
+ addExternalsToCache(fedOptions.federationCache, info);
77
+ if (signal?.aborted)
78
+ throw new AbortedError(`[buildForFederation] After ${plan.bundleName} bundle`);
136
79
  }
137
- const bundlePromises = Object.entries(groupedByPackage).map(
138
- async ([packageName, packageConfig]) => {
139
- return bundleShared(
140
- packageConfig.entries,
141
- config,
142
- fedOptions,
143
- externals.filter((e) => !e.startsWith(packageName)),
144
- {
145
- platform: buildOptions.platform,
146
- chunks: packageConfig.chunks,
147
- bundleName: `${buildOptions.platform}-${normalizePackageName(packageName)}`
148
- }
149
- );
150
- }
151
- );
152
- const buildResults = await Promise.all(bundlePromises);
153
- return buildResults.reduce(
154
- (acc, r) => {
155
- let chunks = acc.chunks;
156
- if (r.chunks) {
157
- chunks = { ...acc.chunks ?? {}, ...r.chunks };
158
- }
159
- let integrity = acc.integrity;
160
- if (r.integrity) {
161
- integrity = { ...acc.integrity ?? {}, ...r.integrity };
162
- }
163
- return {
164
- externals: [...acc.externals, ...r.externals],
165
- chunks,
166
- integrity
167
- };
168
- },
169
- { externals: [] }
170
- );
171
- }
172
- function splitShared(shared) {
173
- const sharedServer = {};
174
- const sharedBrowser = {};
175
- const separateBrowser = {};
176
- const separateServer = {};
177
- for (const key in shared) {
178
- const obj = shared[key];
179
- if (obj?.platform === "node") {
180
- if (obj.build === "default") {
181
- sharedServer[key] = obj;
182
- } else {
183
- separateServer[key] = obj;
184
- }
185
- } else if (obj?.platform === "browser") {
186
- if (obj.build === "default") {
187
- sharedBrowser[key] = obj;
188
- } else {
189
- separateBrowser[key] = obj;
190
- }
191
- }
80
+ const separatePlans = plans.filter((p) => p.kind === "separate");
81
+ if (separatePlans.length > 0) {
82
+ const start = process.hrtime();
83
+ const results = await Promise.all(
84
+ separatePlans.map(
85
+ (plan) => bundleShared(plan.entries, config, fedOptions, plan.externals, {
86
+ platform: plan.platform,
87
+ bundleName: plan.bundleName,
88
+ chunks: plan.chunks
89
+ })
90
+ )
91
+ );
92
+ logger.measure(start, "Step 2.2) Bundling all separate external packages");
93
+ for (const info of results) addExternalsToCache(fedOptions.federationCache, info);
94
+ if (signal?.aborted) throw new AbortedError("[buildForFederation] After separate bundle");
192
95
  }
193
- return {
194
- sharedBrowser,
195
- sharedServer,
196
- separateBrowser,
197
- separateServer
198
- };
199
96
  }
200
97
  export {
201
- buildForFederation
98
+ buildForFederation,
99
+ executeSharedBundlePlans
202
100
  };
@@ -5,6 +5,7 @@ import type { HashPort, IoPort } from '../../domain/utils/io-port.contract.js';
5
5
  import { type NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
6
6
  import type { NormalizedExternalConfig } from '../../domain/config/external-config.contract.js';
7
7
  import type { NFBuildAdapter } from '../../domain/core/build-adapter.contract.js';
8
+ import { type ModuleEvaluator } from './synthesize-cjs-exports.js';
8
9
  export declare function bundleShared(sharedBundles: Record<string, NormalizedExternalConfig>, config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], buildOptions: {
9
10
  platform: 'browser' | 'node';
10
11
  bundleName: string;
@@ -18,6 +19,12 @@ interface BundleSharedDeps {
18
19
  io: IoPort;
19
20
  repo: PackageJsonRepository;
20
21
  adapter: NFBuildAdapter;
22
+ /**
23
+ * Loads a module by absolute path at build time (Node `require`) and returns its
24
+ * runtime exports, from which a CJS external's named exports are then enumerated.
25
+ * Omitted → no named-export synthesis; externals stay default-only.
26
+ */
27
+ evaluateModule?: ModuleEvaluator;
21
28
  }
22
29
  export declare function bundleSharedCore(deps: BundleSharedDeps, sharedBundles: Record<string, NormalizedExternalConfig>, config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], buildOptions: {
23
30
  platform: 'browser' | 'node';
@@ -9,12 +9,21 @@ import { DEFAULT_EXTERNAL_LIST } from "./default-external-list.js";
9
9
  import { isSourceFile, transformChunkImports } from "./rewrite-chunk-imports.js";
10
10
  import { toChunkImport } from "../../domain/core/chunk.js";
11
11
  import { cacheEntryCore, getChecksumCore, getFilename } from "../cache/cache-persistence.js";
12
+ import { linkedContentSignals } from "./resolve-shared-dirs.js";
12
13
  import { computeIntegrityMapCore } from "./compute-integrity.js";
13
14
  import { fileURLToPath } from "url";
14
15
  import { getBuildAdapter } from "./build-adapter.js";
16
+ import { synthesizeCjsNamedExportsEntry } from "./synthesize-cjs-exports.js";
17
+ import { createRequire } from "module";
15
18
  async function bundleShared(sharedBundles, config, fedOptions, externals, buildOptions) {
19
+ const requireFromWorkspace = createRequire(path.join(fedOptions.workspaceRoot, "index.js"));
16
20
  return bundleSharedCore(
17
- { io: nodeIo, repo: sharedPackageJsonRepository, adapter: getBuildAdapter() },
21
+ {
22
+ io: nodeIo,
23
+ repo: sharedPackageJsonRepository,
24
+ adapter: getBuildAdapter(),
25
+ evaluateModule: (absPath) => requireFromWorkspace(absPath)
26
+ },
18
27
  sharedBundles,
19
28
  config,
20
29
  fedOptions,
@@ -25,13 +34,21 @@ async function bundleShared(sharedBundles, config, fedOptions, externals, buildO
25
34
  async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externals, buildOptions) {
26
35
  const builderPackageJson = readBuilderPackageJson(deps.io, fileURLToPath(import.meta.url));
27
36
  const builderVersion = parseBuilderVersion(builderPackageJson);
37
+ const folder = fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
38
+ const contentSignals = linkedContentSignals(
39
+ Object.keys(sharedBundles),
40
+ folder,
41
+ deps.io,
42
+ deps.repo
43
+ );
28
44
  const checksum = getChecksumCore(
29
45
  deps.io,
30
46
  sharedBundles,
31
47
  fedOptions.dev ? "1" : "0",
32
- builderVersion
48
+ builderVersion,
49
+ config.features.synthesizeCjsExports,
50
+ contentSignals
33
51
  );
34
- const folder = fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
35
52
  const bundleCache = cacheEntryCore(
36
53
  deps.io,
37
54
  fedOptions.federationCache.cachePath,
@@ -67,8 +84,22 @@ async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externa
67
84
  const configState = `BUNDLER_CHUNKS;${builderVersion};${JSON.stringify(config)}`;
68
85
  const entryPoints = packageInfos.map((pi) => {
69
86
  const encName = pi.packageName.replace(/[^A-Za-z0-9]/g, "_");
70
- const outName = createOutName(deps.io, pi, configState, fedOptions, encName);
71
- return { fileName: pi.entryPoint, outName };
87
+ const outName = createOutName(
88
+ deps.io,
89
+ pi,
90
+ configState,
91
+ fedOptions,
92
+ encName,
93
+ contentSignals[pi.packageName] ?? ""
94
+ );
95
+ const synthetic = deps.evaluateModule && config.features.synthesizeCjsExports ? synthesizeCjsNamedExportsEntry(
96
+ deps.io,
97
+ deps.evaluateModule,
98
+ pi,
99
+ fedOptions.federationCache.cachePath,
100
+ outName
101
+ ) : null;
102
+ return { fileName: synthetic ?? pi.entryPoint, outName };
72
103
  });
73
104
  const fullOutputPath = path.join(fedOptions.workspaceRoot, fedOptions.outputPath);
74
105
  const useDefaultExternalList = buildOptions.platform === "browser" && !config.features.ignoreUnusedDeps;
@@ -177,8 +208,8 @@ function applyRenames(bundleResult, entryPoints, renamed) {
177
208
  if (next) ep.outName = next;
178
209
  }
179
210
  }
180
- function createOutName(io, pi, configState, fedOptions, encName) {
181
- const hashBase = pi.version + "_" + pi.entryPoint + "_" + configState;
211
+ function createOutName(io, pi, configState, fedOptions, encName, contentSignal = "") {
212
+ const hashBase = pi.version + "_" + pi.entryPoint + "_" + configState + (contentSignal ? "_" + contentSignal : "");
182
213
  const hash = calcHashCore(io, hashBase);
183
214
  const outName = fedOptions.dev ? `${encName}.${hash}-dev.js` : `${encName}.${hash}.js`;
184
215
  return outName;
@@ -1,4 +1,12 @@
1
1
  import type { FederationInfo } from '../../domain/core/federation-info.contract.js';
2
2
  import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
3
  import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
4
+ import type { IoPort } from '../../domain/utils/io-port.contract.js';
5
+ import type { PackageJsonRepository } from '../../domain/utils/package-json.contract.js';
4
6
  export declare function rebuildForFederation(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], modifiedFiles: string[], signal?: AbortSignal): Promise<FederationInfo>;
7
+ /**
8
+ * When a modified file belongs to a shared package (e.g. npm-linked dev dep),
9
+ * re-bundle the affected bundle(s); unchanged bundles hit their cache, so
10
+ * `federationCache.externals` regenerates cheaply. No package touched → no-op.
11
+ */
12
+ export declare function rebuildAffectedExternals(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, externals: string[], modifiedFiles: string[], signal?: AbortSignal, io?: IoPort, repo?: PackageJsonRepository): Promise<void>;
@@ -7,8 +7,15 @@ import { writeFederationInfo } from "../output/write-federation-info.js";
7
7
  import { writeImportMap } from "../output/write-import-map.js";
8
8
  import { logger } from "../../utils/logger.js";
9
9
  import { AbortedError } from "../../utils/errors.js";
10
+ import { planSharedBundles } from "./shared-bundle-plan.js";
11
+ import { executeSharedBundlePlans } from "./build-for-federation.js";
12
+ import { affectedSharedKeys, resolveSharedPackageDirs } from "./resolve-shared-dirs.js";
13
+ import { cacheEntryCore, getFilename } from "../cache/cache-persistence.js";
14
+ import { nodeIo } from "../../utils/io/node-io-adapter.js";
15
+ import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
10
16
  async function rebuildForFederation(config, fedOptions, externals, modifiedFiles, signal) {
11
17
  const federationCache = fedOptions.federationCache;
18
+ await rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal);
12
19
  logger.info(`Re-bundling all internal libraries and exposed modules..'`);
13
20
  const start = process.hrtime();
14
21
  const artifactInfo = await bundleExposedAndMappings(
@@ -47,6 +54,25 @@ async function rebuildForFederation(config, fedOptions, externals, modifiedFiles
47
54
  writeImportMap(federationCache, fedOptions, federationInfo.integrity);
48
55
  return federationInfo;
49
56
  }
57
+ async function rebuildAffectedExternals(config, fedOptions, externals, modifiedFiles, signal, io = nodeIo, repo = sharedPackageJsonRepository) {
58
+ if (modifiedFiles.length === 0) return;
59
+ const plans = planSharedBundles(config, externals);
60
+ if (plans.length === 0) return;
61
+ const dirs = resolveSharedPackageDirs(config, fedOptions, io, repo);
62
+ const affected = affectedSharedKeys(modifiedFiles, dirs, io);
63
+ const affectedPlans = plans.filter((p) => p.keys.some((k) => affected.has(k)));
64
+ if (affectedPlans.length === 0) return;
65
+ const federationCache = fedOptions.federationCache;
66
+ for (const plan of affectedPlans) {
67
+ logger.info(`Detected change in linked shared package(s); re-bundling '${plan.bundleName}'.`);
68
+ cacheEntryCore(io, federationCache.cachePath, getFilename(plan.bundleName, fedOptions.dev)).clear();
69
+ }
70
+ federationCache.externals = [];
71
+ federationCache.chunks = void 0;
72
+ federationCache.integrity = void 0;
73
+ await executeSharedBundlePlans(plans, config, fedOptions, signal);
74
+ }
50
75
  export {
76
+ rebuildAffectedExternals,
51
77
  rebuildForFederation
52
78
  };
@@ -0,0 +1,16 @@
1
+ import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
2
+ import type { NormalizedFederationOptions } from '../../domain/core/federation-options.contract.js';
3
+ import type { FileReaderPort } from '../../domain/utils/io-port.contract.js';
4
+ import type { PackageJsonRepository } from '../../domain/utils/package-json.contract.js';
5
+ /** Each `config.shared` key → its realpath'd package dir, so watcher, checksum, and
6
+ * file-mapping agree on one identity for symlinked (npm-linked) deps. */
7
+ export declare function resolveSharedPackageDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): Map<string, string>;
8
+ /** Realpath'd dirs of symlinked shared packages — the bounded watch set.
9
+ * Deduped, since secondaries share a package dir. */
10
+ export declare function linkedSharedDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): string[];
11
+ /** Per-key content signal (max mtime of the resolved dir) for symlinked deps only.
12
+ * Registry deps get no signal, keeping their checksum version-only. (Every key is
13
+ * still resolved: detecting the symlink requires the realpath + lstat.) */
14
+ export declare function linkedContentSignals(keys: string[], folder: string, io?: FileReaderPort, repo?: PackageJsonRepository): Record<string, string>;
15
+ /** `config.shared` keys whose package directory contains at least one modified file. */
16
+ export declare function affectedSharedKeys(modifiedFiles: readonly string[], dirs: Map<string, string>, io?: FileReaderPort): Set<string>;
@@ -0,0 +1,66 @@
1
+ import * as path from "path";
2
+ import { nodeIo } from "../../utils/io/node-io-adapter.js";
3
+ import { sharedPackageJsonRepository } from "../../utils/package/package-info.js";
4
+ import { toPosix } from "../../utils/path-patterns.js";
5
+ const folderOf = (fedOptions) => fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
6
+ function resolveEntries(keys, folder, io, repo) {
7
+ const out = [];
8
+ for (const key of keys) {
9
+ const pkgJsonPath = repo.findDepPackageJson(key, folder);
10
+ if (!pkgJsonPath) continue;
11
+ const pkgDir = path.dirname(pkgJsonPath);
12
+ out.push({
13
+ key,
14
+ realDir: toPosix(io.realpath(pkgDir)),
15
+ isSymlink: !!io.stat(pkgDir)?.isSymbolicLink
16
+ });
17
+ }
18
+ return out;
19
+ }
20
+ function resolveSharedPackageDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
21
+ const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
22
+ return new Map(entries.map((e) => [e.key, e.realDir]));
23
+ }
24
+ function linkedSharedDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
25
+ const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
26
+ return [...new Set(entries.filter((e) => e.isSymlink).map((e) => e.realDir))];
27
+ }
28
+ function maxMtime(io, dir) {
29
+ let max = 0;
30
+ const walk = (d) => {
31
+ for (const name of io.readDir(d)) {
32
+ const full = path.join(d, name);
33
+ if (io.isDirectory(full)) walk(full);
34
+ else {
35
+ let s = io.stat(full);
36
+ if (s?.isSymbolicLink) s = io.stat(io.realpath(full));
37
+ if (s && s.mtimeMs > max) max = s.mtimeMs;
38
+ }
39
+ }
40
+ };
41
+ walk(dir);
42
+ return max;
43
+ }
44
+ function linkedContentSignals(keys, folder, io = nodeIo, repo = sharedPackageJsonRepository) {
45
+ const signals = {};
46
+ for (const entry of resolveEntries(keys, folder, io, repo)) {
47
+ if (entry.isSymlink) signals[entry.key] = String(maxMtime(io, entry.realDir));
48
+ }
49
+ return signals;
50
+ }
51
+ function affectedSharedKeys(modifiedFiles, dirs, io = nodeIo) {
52
+ const affected = /* @__PURE__ */ new Set();
53
+ if (modifiedFiles.length === 0 || dirs.size === 0) return affected;
54
+ const realFiles = modifiedFiles.map((f) => toPosix(io.realpath(f)));
55
+ for (const [key, dir] of dirs) {
56
+ const prefix = dir.endsWith("/") ? dir : dir + "/";
57
+ if (realFiles.some((f) => f === dir || f.startsWith(prefix))) affected.add(key);
58
+ }
59
+ return affected;
60
+ }
61
+ export {
62
+ affectedSharedKeys,
63
+ linkedContentSignals,
64
+ linkedSharedDirs,
65
+ resolveSharedPackageDirs
66
+ };
@@ -0,0 +1,22 @@
1
+ import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
2
+ import type { NormalizedExternalConfig } from '../../domain/config/external-config.contract.js';
3
+ export interface SharedBundlePlan {
4
+ bundleName: string;
5
+ platform: 'browser' | 'node';
6
+ chunks: boolean;
7
+ entries: Record<string, NormalizedExternalConfig>;
8
+ externals: string[];
9
+ keys: string[];
10
+ kind: 'shared' | 'separate';
11
+ }
12
+ type SplitSharedResult = {
13
+ sharedServer: Record<string, NormalizedExternalConfig>;
14
+ sharedBrowser: Record<string, NormalizedExternalConfig>;
15
+ separateBrowser: Record<string, NormalizedExternalConfig>;
16
+ separateServer: Record<string, NormalizedExternalConfig>;
17
+ };
18
+ export declare function splitShared(shared: Record<string, NormalizedExternalConfig>): SplitSharedResult;
19
+ /** Single source of truth for package → bundle(name) mapping, shared by the initial
20
+ * build and the watch rebuild so both target the same cache entries. */
21
+ export declare function planSharedBundles(config: NormalizedFederationConfig, externals: string[]): SharedBundlePlan[];
22
+ export {};
@@ -0,0 +1,70 @@
1
+ import { inferPackageFromSecondary, normalizePackageName } from "../../utils/normalize.js";
2
+ function splitShared(shared) {
3
+ const sharedServer = {};
4
+ const sharedBrowser = {};
5
+ const separateBrowser = {};
6
+ const separateServer = {};
7
+ for (const key in shared) {
8
+ const obj = shared[key];
9
+ if (obj?.platform === "node") {
10
+ if (obj.build === "default") sharedServer[key] = obj;
11
+ else separateServer[key] = obj;
12
+ } else if (obj?.platform === "browser") {
13
+ if (obj.build === "default") sharedBrowser[key] = obj;
14
+ else separateBrowser[key] = obj;
15
+ }
16
+ }
17
+ return { sharedBrowser, sharedServer, separateBrowser, separateServer };
18
+ }
19
+ function planSeparate(separate, platform, externals) {
20
+ const grouped = {};
21
+ for (const [key, shared] of Object.entries(separate)) {
22
+ const packageName = shared.build === "separate" ? key : inferPackageFromSecondary(key);
23
+ if (!grouped[packageName]) grouped[packageName] = { chunks: shared.chunks, entries: {} };
24
+ grouped[packageName].entries[key] = shared;
25
+ }
26
+ return Object.entries(grouped).map(([packageName, group]) => ({
27
+ bundleName: `${platform}-${normalizePackageName(packageName)}`,
28
+ platform,
29
+ chunks: group.chunks,
30
+ entries: group.entries,
31
+ externals: externals.filter((e) => !e.startsWith(packageName)),
32
+ keys: Object.keys(group.entries),
33
+ kind: "separate"
34
+ }));
35
+ }
36
+ function planSharedBundles(config, externals) {
37
+ const { sharedBrowser, sharedServer, separateBrowser, separateServer } = splitShared(
38
+ config.shared
39
+ );
40
+ const plans = [];
41
+ if (Object.keys(sharedBrowser).length > 0) {
42
+ plans.push({
43
+ bundleName: "browser-shared",
44
+ platform: "browser",
45
+ chunks: config.chunks,
46
+ entries: sharedBrowser,
47
+ externals,
48
+ keys: Object.keys(sharedBrowser),
49
+ kind: "shared"
50
+ });
51
+ }
52
+ if (Object.keys(sharedServer).length > 0) {
53
+ plans.push({
54
+ bundleName: "node-shared",
55
+ platform: "node",
56
+ chunks: config.chunks,
57
+ entries: sharedServer,
58
+ externals,
59
+ keys: Object.keys(sharedServer),
60
+ kind: "shared"
61
+ });
62
+ }
63
+ plans.push(...planSeparate(separateBrowser, "browser", externals));
64
+ plans.push(...planSeparate(separateServer, "node", externals));
65
+ return plans;
66
+ }
67
+ export {
68
+ planSharedBundles,
69
+ splitShared
70
+ };
@@ -0,0 +1,12 @@
1
+ import type { IoPort } from '../../domain/utils/io-port.contract.js';
2
+ import type { PackageInfo } from '../../domain/utils/package-json.contract.js';
3
+ /** Evaluates a module at build time (Node `require`) and returns its value, or throws. */
4
+ export type ModuleEvaluator = (absPath: string) => unknown;
5
+ /**
6
+ * For a CommonJS shared external, `require()`s it, enumerates its runtime named
7
+ * exports, and writes a synthetic ESM entry re-exporting them.
8
+ *
9
+ * Returns the synthetic entry path, or `null` to keep the original entry (not a CJS
10
+ * candidate, no names to wrap, or a `require()` failure → default-only fallback).
11
+ */
12
+ export declare function synthesizeCjsNamedExportsEntry(io: IoPort, evaluateModule: ModuleEvaluator, pi: PackageInfo, cachePath: string, outName: string): string | null;
@@ -0,0 +1,58 @@
1
+ import * as path from "path";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { isCjsCandidate } from "../../utils/package/esm-detection.js";
4
+ import {
5
+ planCjsWrap,
6
+ buildSyntheticCjsEntry,
7
+ isEsmInteropError
8
+ } from "../../utils/package/cjs-named-exports.js";
9
+ const SYNTHETIC_DIR = ".nf-cjs-entries";
10
+ const warned = /* @__PURE__ */ new Set();
11
+ function nearestPackageType(io, fromDir) {
12
+ let dir = fromDir;
13
+ for (; ; ) {
14
+ const pkg = path.join(dir, "package.json");
15
+ if (io.exists(pkg)) {
16
+ try {
17
+ const type = JSON.parse(io.readText(pkg))?.type;
18
+ return type === "module" || type === "commonjs" ? type : void 0;
19
+ } catch {
20
+ }
21
+ }
22
+ const parent = path.dirname(dir);
23
+ if (parent === dir) return void 0;
24
+ dir = parent;
25
+ }
26
+ }
27
+ function synthesizeCjsNamedExportsEntry(io, evaluateModule, pi, cachePath, outName) {
28
+ const entryPoint = pi.entryPoint;
29
+ const candidate = isCjsCandidate({
30
+ esm: pi.esm,
31
+ entryPoint,
32
+ packageType: nearestPackageType(io, path.dirname(entryPoint)),
33
+ readSource: () => io.readText(entryPoint)
34
+ });
35
+ if (!candidate) return null;
36
+ let plan;
37
+ try {
38
+ plan = planCjsWrap(evaluateModule(entryPoint));
39
+ } catch (err) {
40
+ if (!isEsmInteropError(err) && !warned.has(entryPoint)) {
41
+ warned.add(entryPoint);
42
+ const detail = err instanceof Error ? err.message : String(err);
43
+ logger.warn(
44
+ `[native-federation] Could not enumerate named exports of "${entryPoint}" at build time (${detail}); falling back to default-only. Named imports from this package may fail across the module boundary.`
45
+ );
46
+ }
47
+ return null;
48
+ }
49
+ if (!plan.wrap) return null;
50
+ const dir = path.join(cachePath, SYNTHETIC_DIR);
51
+ io.mkdirp(dir);
52
+ const syntheticPath = path.join(dir, outName);
53
+ io.writeText(syntheticPath, buildSyntheticCjsEntry(entryPoint, plan.keys));
54
+ return syntheticPath;
55
+ }
56
+ export {
57
+ synthesizeCjsNamedExportsEntry
58
+ };
@@ -3,8 +3,8 @@ import type { ChunkInfo, IntegrityMap, SharedInfo } from '../../domain/core/fede
3
3
  import type { FileReaderPort, FileWriterPort, HashPort } from '../../domain/utils/io-port.contract.js';
4
4
  export declare const getDefaultCachePath: (workspaceRoot: string) => string;
5
5
  export declare const getFilename: (title: string, dev?: boolean) => string;
6
- export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string) => string;
7
- export declare const getChecksumCore: (hash: HashPort, shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string) => string;
6
+ export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, synthesizeCjsExports?: boolean, contentSignals?: Record<string, string>) => string;
7
+ export declare const getChecksumCore: (hash: HashPort, shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, synthesizeCjsExports?: boolean, contentSignals?: Record<string, string>) => string;
8
8
  export type CacheMetadata = {
9
9
  checksum: string;
10
10
  externals: SharedInfo[];
@@ -6,12 +6,15 @@ const getFilename = (title, dev) => {
6
6
  const devSuffix = dev ? "-dev" : "";
7
7
  return `${title}${devSuffix}.meta.json`;
8
8
  };
9
- const getChecksum = (shared, dev, builderVersion = "") => getChecksumCore(nodeIo, shared, dev, builderVersion);
10
- const getChecksumCore = (hash, shared, dev, builderVersion = "") => {
9
+ const getChecksum = (shared, dev, builderVersion = "", synthesizeCjsExports = true, contentSignals = {}) => getChecksumCore(nodeIo, shared, dev, builderVersion, synthesizeCjsExports, contentSignals);
10
+ const getChecksumCore = (hash, shared, dev, builderVersion = "", synthesizeCjsExports = true, contentSignals = {}) => {
11
11
  const denseExternals = Object.keys(shared).sort().reduce((clean, external) => {
12
- return clean + ":" + external + (shared[external].version ? `@${shared[external].version}` : "");
12
+ const version = shared[external].version ? `@${shared[external].version}` : "";
13
+ const signal = contentSignals[external] ? `#${contentSignals[external]}` : "";
14
+ return clean + ":" + external + version + signal;
13
15
  }, "deps");
14
- return hash.hash("sha256", denseExternals + `:dev=${dev}:builder=${builderVersion}`).hex();
16
+ const cjs = synthesizeCjsExports ? "1" : "0";
17
+ return hash.hash("sha256", denseExternals + `:dev=${dev}:builder=${builderVersion}:cjs=${cjs}`).hex();
15
18
  };
16
19
  const cacheEntryCore = (io, pathToCache, fileName) => {
17
20
  const metadataFile = path.join(pathToCache, fileName);
@@ -1,6 +1,6 @@
1
1
  import type { FederationInfo } from '../domain/core/federation-info.contract.js';
2
2
  import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
3
- import { type FederationOptions } from '../domain/core/federation-options.contract.js';
3
+ import { type FederationOptions, type NormalizedFederationOptions } from '../domain/core/federation-options.contract.js';
4
4
  import type { NFBuildAdapter } from '../domain/core/build-adapter.contract.js';
5
5
  export interface BuildHelperParams {
6
6
  options: FederationOptions;
@@ -19,5 +19,6 @@ export declare const federationBuilder: {
19
19
  readonly federationInfo: FederationInfo;
20
20
  readonly externals: string[];
21
21
  readonly config: NormalizedFederationConfig;
22
+ readonly options: NormalizedFederationOptions;
22
23
  };
23
24
  export {};
@@ -46,6 +46,9 @@ const federationBuilder = {
46
46
  },
47
47
  get config() {
48
48
  return config;
49
+ },
50
+ get options() {
51
+ return options;
49
52
  }
50
53
  };
51
54
  export {
@@ -40,6 +40,7 @@ function densifyExternals(shared) {
40
40
  if (entry.version !== void 0) dense.version = entry.version;
41
41
  if (entry.shareScope !== void 0) dense.shareScope = entry.shareScope;
42
42
  if (entry.bundle !== void 0) dense.bundle = entry.bundle;
43
+ if (entry.pool !== void 0) dense.pool = entry.pool;
43
44
  if (entry.dev !== void 0) dense.dev = entry.dev;
44
45
  groupIndex.set(key, result.length);
45
46
  result.push(dense);
@@ -21,6 +21,7 @@ export interface FederationConfig {
21
21
  denseChunking?: boolean;
22
22
  denseExternals?: boolean;
23
23
  integrityHashes?: boolean;
24
+ synthesizeCjsExports?: boolean;
24
25
  };
25
26
  }
26
27
  export interface NormalizedFederationConfig {
@@ -39,5 +40,6 @@ export interface NormalizedFederationConfig {
39
40
  denseChunking: boolean;
40
41
  denseExternals: boolean;
41
42
  integrityHashes: boolean;
43
+ synthesizeCjsExports: boolean;
42
44
  };
43
45
  }
@@ -1,10 +1,16 @@
1
1
  export interface NfFileWatcherOptions {
2
2
  onChange?: (path: string) => void;
3
+ pollIntervalMs?: number;
4
+ debounceMs?: number;
5
+ }
6
+ interface AddPathsOptions {
7
+ poll?: boolean;
3
8
  }
4
9
  export interface NfFileWatcher {
5
- addPaths(paths: string | readonly string[]): void;
10
+ addPaths(paths: string | readonly string[], opts?: AddPathsOptions): void;
6
11
  close(): Promise<void>;
7
12
  get(): ReadonlySet<string>;
8
13
  clear(): void;
9
14
  mutate(fn: (dirtyPaths: Set<string>) => void): void;
10
15
  }
16
+ export {};
@@ -3,6 +3,10 @@ export interface Digest {
3
3
  hex(): string;
4
4
  base64(): string;
5
5
  }
6
+ export interface StatInfo {
7
+ mtimeMs: number;
8
+ isSymbolicLink: boolean;
9
+ }
6
10
  export interface FileReaderPort {
7
11
  readText(path: string): string;
8
12
  readBytes(path: string): Uint8Array;
@@ -13,6 +17,8 @@ export interface FileReaderPort {
13
17
  isDirectory(path: string): boolean;
14
18
  /** Immediate child entry names (not full paths). Empty array on ENOENT, never throws. */
15
19
  readDir(path: string): string[];
20
+ realpath(path: string): string;
21
+ stat(path: string): StatInfo | null;
16
22
  }
17
23
  export interface FileWriterPort {
18
24
  writeText(path: string, data: string): void;
@@ -31,15 +37,20 @@ export interface HashPort {
31
37
  export interface WatchHandle {
32
38
  close(): void;
33
39
  }
40
+ interface WatchOptions {
41
+ recursive: boolean;
42
+ poll?: {
43
+ intervalMs: number;
44
+ };
45
+ }
34
46
  export interface WatchPort {
35
47
  /**
36
48
  * For a recursive directory watch `onEvent` receives the changed entry's
37
49
  * filename relative to `path`; for a file it receives the path itself (or
38
50
  * null when the platform omits it).
39
51
  */
40
- watch(path: string, opts: {
41
- recursive: boolean;
42
- }, onEvent: (filename: string | null) => void): WatchHandle;
52
+ watch(path: string, opts: WatchOptions, onEvent: (filename: string | null) => void): WatchHandle;
43
53
  }
44
54
  export interface IoPort extends FileReaderPort, FileWriterPort, GlobPort, HashPort, WatchPort {
45
55
  }
56
+ export {};
@@ -4,4 +4,4 @@ export declare function createNfWatcher(options?: NfFileWatcherOptions): NfFileW
4
4
  export declare function createNfWatcherCore(io: WatchPort & FileReaderPort, options?: NfFileWatcherOptions): NfFileWatcher;
5
5
  export declare function syncNfFileWatcher(watcher: NfFileWatcher, bundlerCache: {
6
6
  keys(): IterableIterator<string>;
7
- }): void;
7
+ }, linkedDirs?: readonly string[]): void;
@@ -7,22 +7,37 @@ function createNfWatcher(options = {}) {
7
7
  }
8
8
  function createNfWatcherCore(io, options = {}) {
9
9
  const { onChange } = options;
10
+ const pollIntervalMs = options.pollIntervalMs ?? 300;
11
+ const debounceMs = options.debounceMs ?? 0;
10
12
  const watchers = /* @__PURE__ */ new Map();
11
13
  const dirtyPaths = /* @__PURE__ */ new Set();
12
- const notify = (path) => {
14
+ const deliver = (path) => {
13
15
  if (onChange) onChange(path);
14
16
  else dirtyPaths.add(path);
15
17
  };
18
+ const pending = /* @__PURE__ */ new Set();
19
+ let flushTimer;
20
+ const flush = () => {
21
+ for (const p of pending) deliver(p);
22
+ pending.clear();
23
+ };
24
+ const notify = (path) => {
25
+ if (debounceMs <= 0) return deliver(path);
26
+ pending.add(path);
27
+ if (flushTimer) clearTimeout(flushTimer);
28
+ flushTimer = setTimeout(flush, debounceMs);
29
+ flushTimer.unref?.();
30
+ };
16
31
  return {
17
- addPaths(paths) {
32
+ addPaths(paths, opts) {
18
33
  const list = typeof paths === "string" ? [paths] : [...paths];
34
+ const poll = opts?.poll ? { intervalMs: pollIntervalMs } : void 0;
19
35
  for (const p of list) {
20
36
  if (watchers.has(p)) continue;
21
37
  try {
22
- const isDir = io.isDirectory(p);
23
- const handle = isDir ? io.watch(p, { recursive: true }, (filename) => {
38
+ const handle = io.isDirectory(p) ? io.watch(p, { recursive: true, poll }, (filename) => {
24
39
  if (filename) notify(toPosix(join(p, filename)));
25
- }) : io.watch(p, { recursive: false }, () => notify(toPosix(p)));
40
+ }) : io.watch(p, { recursive: false, poll }, () => notify(toPosix(p)));
26
41
  watchers.set(p, handle);
27
42
  } catch {
28
43
  logger.debug(`Could not watch path '${p}'.`);
@@ -33,6 +48,7 @@ function createNfWatcherCore(io, options = {}) {
33
48
  clear: () => dirtyPaths.clear(),
34
49
  mutate: (fn) => fn(dirtyPaths),
35
50
  async close() {
51
+ if (flushTimer) clearTimeout(flushTimer);
36
52
  for (const handle of watchers.values()) {
37
53
  handle.close();
38
54
  }
@@ -40,9 +56,10 @@ function createNfWatcherCore(io, options = {}) {
40
56
  }
41
57
  };
42
58
  }
43
- function syncNfFileWatcher(watcher, bundlerCache) {
59
+ function syncNfFileWatcher(watcher, bundlerCache, linkedDirs = []) {
44
60
  const files = [...bundlerCache.keys()].filter((k) => !k.includes("node_modules"));
45
61
  if (files.length) watcher.addPaths(files);
62
+ if (linkedDirs.length) watcher.addPaths(linkedDirs, { poll: true });
46
63
  }
47
64
  export {
48
65
  createNfWatcher,
@@ -1,48 +1,64 @@
1
1
  import * as fs from "fs";
2
+ import * as path from "path";
2
3
  import * as crypto from "crypto";
3
4
  import fg from "fast-glob";
4
5
  const nodeIo = {
5
- readText(path) {
6
- return fs.readFileSync(path, "utf-8");
6
+ readText(path2) {
7
+ return fs.readFileSync(path2, "utf-8");
7
8
  },
8
- readBytes(path) {
9
- return fs.readFileSync(path);
9
+ readBytes(path2) {
10
+ return fs.readFileSync(path2);
10
11
  },
11
- exists(path) {
12
- return fs.existsSync(path);
12
+ exists(path2) {
13
+ return fs.existsSync(path2);
13
14
  },
14
- isFile(path) {
15
+ isFile(path2) {
15
16
  try {
16
- return fs.statSync(path).isFile();
17
+ return fs.statSync(path2).isFile();
17
18
  } catch {
18
19
  return false;
19
20
  }
20
21
  },
21
- isDirectory(path) {
22
+ isDirectory(path2) {
22
23
  try {
23
- return fs.statSync(path).isDirectory();
24
+ return fs.statSync(path2).isDirectory();
24
25
  } catch {
25
26
  return false;
26
27
  }
27
28
  },
28
- readDir(path) {
29
+ readDir(path2) {
29
30
  try {
30
- return fs.readdirSync(path);
31
+ return fs.readdirSync(path2);
31
32
  } catch {
32
33
  return [];
33
34
  }
34
35
  },
35
- writeText(path, data) {
36
- fs.writeFileSync(path, data, "utf-8");
36
+ realpath(path2) {
37
+ try {
38
+ return fs.realpathSync(path2);
39
+ } catch {
40
+ return path2;
41
+ }
37
42
  },
38
- mkdirp(path) {
39
- fs.mkdirSync(path, { recursive: true });
43
+ stat(path2) {
44
+ try {
45
+ const s = fs.lstatSync(path2);
46
+ return { mtimeMs: s.mtimeMs, isSymbolicLink: s.isSymbolicLink() };
47
+ } catch {
48
+ return null;
49
+ }
50
+ },
51
+ writeText(path2, data) {
52
+ fs.writeFileSync(path2, data, "utf-8");
53
+ },
54
+ mkdirp(path2) {
55
+ fs.mkdirSync(path2, { recursive: true });
40
56
  },
41
57
  copyFile(from, to) {
42
58
  fs.copyFileSync(from, to);
43
59
  },
44
- remove(path) {
45
- fs.unlinkSync(path);
60
+ remove(path2) {
61
+ fs.unlinkSync(path2);
46
62
  },
47
63
  globFiles(pattern, opts) {
48
64
  return fg.sync(pattern, { cwd: opts.cwd, onlyFiles: true, deep: Infinity });
@@ -54,15 +70,55 @@ const nodeIo = {
54
70
  base64: () => sum.digest("base64")
55
71
  };
56
72
  },
57
- watch(path, opts, onEvent) {
73
+ watch(watchPath, opts, onEvent) {
74
+ if (opts.poll) return pollWatch(watchPath, opts.recursive, opts.poll.intervalMs, onEvent);
58
75
  const watcher = opts.recursive ? fs.watch(
59
- path,
76
+ watchPath,
60
77
  { recursive: true },
61
78
  (_event, filename) => onEvent(filename ? filename.toString() : null)
62
- ) : fs.watch(path, () => onEvent(path));
79
+ ) : fs.watch(watchPath, () => onEvent(watchPath));
63
80
  return { close: () => watcher.close() };
64
81
  }
65
82
  };
83
+ function pollWatch(root, recursive, intervalMs, onEvent) {
84
+ const snapshot = () => {
85
+ const out = /* @__PURE__ */ new Map();
86
+ const walk = (dir) => {
87
+ let entries;
88
+ try {
89
+ entries = fs.readdirSync(dir, { withFileTypes: true });
90
+ } catch {
91
+ return;
92
+ }
93
+ for (const entry of entries) {
94
+ const full = path.join(dir, entry.name);
95
+ if (entry.isDirectory()) {
96
+ if (recursive) walk(full);
97
+ } else {
98
+ try {
99
+ out.set(path.relative(root, full), fs.statSync(full).mtimeMs);
100
+ } catch {
101
+ }
102
+ }
103
+ }
104
+ };
105
+ walk(root);
106
+ return out;
107
+ };
108
+ let prev = snapshot();
109
+ const timer = setInterval(() => {
110
+ const next = snapshot();
111
+ for (const [rel, mtime] of next) {
112
+ if (prev.get(rel) !== mtime) onEvent(rel);
113
+ }
114
+ for (const rel of prev.keys()) {
115
+ if (!next.has(rel)) onEvent(rel);
116
+ }
117
+ prev = next;
118
+ }, intervalMs);
119
+ timer.unref?.();
120
+ return { close: () => clearInterval(timer) };
121
+ }
66
122
  export {
67
123
  nodeIo
68
124
  };
@@ -0,0 +1,21 @@
1
+ /** A key is re-exportable only if it is a syntactically valid identifier name. */
2
+ export declare const isIdentifierName: (name: string) => boolean;
3
+ /**
4
+ * Decides whether a `require()`d CommonJS value needs a synthetic named-export
5
+ * wrapper, and which names to re-export. UMD/CJS packages (e.g. dayjs) assign
6
+ * named exports dynamically, invisible to static lexers; enumerating the value
7
+ * recovers them. ES-module namespaces and `__esModule` interop objects are
8
+ * skipped — a bundler already exposes those statically.
9
+ */
10
+ export declare const planCjsWrap: (mod: unknown) => {
11
+ wrap: boolean;
12
+ keys: string[];
13
+ };
14
+ /**
15
+ * Synthetic ESM entry re-exporting the CommonJS default plus each discovered name.
16
+ * The `export { local as key }` clause lets reserved-word keys (`class`, `default`)
17
+ * pass without escaping.
18
+ */
19
+ export declare const buildSyntheticCjsEntry: (importPath: string, keys: string[]) => string;
20
+ /** True when a `require()` failure just means the file was ESM after all. */
21
+ export declare const isEsmInteropError: (err: unknown) => boolean;
@@ -0,0 +1,36 @@
1
+ const isIdentifierName = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
2
+ const planCjsWrap = (mod) => {
3
+ if (mod === null || typeof mod !== "object" && typeof mod !== "function") {
4
+ return { wrap: false, keys: [] };
5
+ }
6
+ if (mod[Symbol.toStringTag] === "Module") {
7
+ return { wrap: false, keys: [] };
8
+ }
9
+ if (mod["__esModule"]) {
10
+ return { wrap: false, keys: [] };
11
+ }
12
+ const keys = Object.keys(mod).filter(
13
+ (key) => key !== "default" && key !== "__esModule" && isIdentifierName(key)
14
+ );
15
+ return { wrap: keys.length > 0, keys };
16
+ };
17
+ const buildSyntheticCjsEntry = (importPath, keys) => {
18
+ const spec = JSON.stringify(importPath);
19
+ const lines = [`import _nfDefault from ${spec};`, `export default _nfDefault;`];
20
+ if (keys.length > 0) {
21
+ keys.forEach((key, i) => lines.push(`const _nf${i} = _nfDefault[${JSON.stringify(key)}];`));
22
+ lines.push(`export { ${keys.map((key, i) => `_nf${i} as ${key}`).join(", ")} };`);
23
+ }
24
+ return lines.join("\n") + "\n";
25
+ };
26
+ const isEsmInteropError = (err) => {
27
+ const code = err?.code;
28
+ if (code === "ERR_REQUIRE_ESM" || code === "ERR_REQUIRE_ASYNC_MODULE") return true;
29
+ return err instanceof SyntaxError && /Unexpected token 'export'|Cannot use import statement|export|import statement/.test(err.message);
30
+ };
31
+ export {
32
+ buildSyntheticCjsEntry,
33
+ isEsmInteropError,
34
+ isIdentifierName,
35
+ planCjsWrap
36
+ };
@@ -3,3 +3,16 @@
3
3
  * or ambiguous (`undefined`).
4
4
  */
5
5
  export declare const isESMExport: (e: string) => boolean | undefined;
6
+ export type ModuleFormat = 'esm' | 'cjs' | 'unknown';
7
+ /** Node's format rule keyed on extension alone; `.js` stays ambiguous. */
8
+ export declare const classifyByExtension: (entryPoint: string) => ModuleFormat;
9
+ /** Does the source contain top-level ESM `import`/`export`? Dynamic `import()` is excluded (legal in CJS). */
10
+ export declare const hasEsmSyntax: (source: string) => boolean;
11
+ export declare const isCjsCandidate: (input: {
12
+ esm?: boolean;
13
+ entryPoint: string;
14
+ /** Nearest package.json `type` for an ambiguous `.js`. */
15
+ packageType?: "module" | "commonjs";
16
+ /** Lazy source for fallback content sniff; only read for an ambiguous `.js`. */
17
+ readSource?: () => string;
18
+ }) => boolean;
@@ -5,6 +5,35 @@ const isESMExport = (e) => {
5
5
  if (e === "cjs" || e === "commonjs") return false;
6
6
  return void 0;
7
7
  };
8
+ const classifyByExtension = (entryPoint) => {
9
+ if (entryPoint.endsWith(".mjs")) return "esm";
10
+ if (entryPoint.endsWith(".cjs")) return "cjs";
11
+ if (entryPoint.endsWith(".js")) return "unknown";
12
+ return "esm";
13
+ };
14
+ const hasEsmSyntax = (source) => {
15
+ const head = source.slice(0, 16384);
16
+ const exportStmt = /(?:^|[;\n}])\s*export\s*(?:\{|\*|default\b|const\b|let\b|var\b|function\b|async\b|class\b)/;
17
+ const importStmt = /(?:^|[;\n}])\s*import\s*(?:[A-Za-z_$]|\{|\*|['"])/;
18
+ return exportStmt.test(head) || importStmt.test(head);
19
+ };
20
+ const isCjsCandidate = (input) => {
21
+ if (input.esm === true) return false;
22
+ const fmt = classifyByExtension(input.entryPoint);
23
+ if (fmt === "esm") return false;
24
+ if (fmt === "cjs") return true;
25
+ if (input.packageType === "module") return false;
26
+ if (input.readSource) {
27
+ try {
28
+ if (hasEsmSyntax(input.readSource())) return false;
29
+ } catch {
30
+ }
31
+ }
32
+ return true;
33
+ };
8
34
  export {
35
+ classifyByExtension,
36
+ hasEsmSyntax,
37
+ isCjsCandidate,
9
38
  isESMExport
10
39
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softarc/native-federation",
3
- "version": "4.3.1",
3
+ "version": "4.3.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@11.11.0",
@@ -28,11 +28,11 @@
28
28
  "jiti": "^2.6.1",
29
29
  "jsdom": "^29.0.0",
30
30
  "knip": "^6.20.0",
31
- "prettier": "^3.8.1",
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.0.0",
35
+ "vite": "^8.1.3",
36
36
  "vitest": "^4.0.0"
37
37
  },
38
38
  "exports": {