@softarc/native-federation 4.3.1 → 4.4.0
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.
- package/README.md +71 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/internal.d.ts +5 -1
- package/dist/internal.js +24 -0
- package/dist/lib/config/expand-mappings.d.ts +24 -0
- package/dist/lib/config/expand-mappings.js +88 -0
- package/dist/lib/config/get-used-dependencies.d.ts +0 -2
- package/dist/lib/config/get-used-dependencies.js +4 -40
- package/dist/lib/config/mapped-paths.d.ts +7 -2
- package/dist/lib/config/mapped-paths.js +21 -4
- package/dist/lib/config/mapping-utils.d.ts +32 -0
- package/dist/lib/config/mapping-utils.js +68 -0
- package/dist/lib/config/match-mapping.d.ts +9 -0
- package/dist/lib/config/match-mapping.js +46 -0
- package/dist/lib/config/remove-unused-deps.d.ts +2 -1
- package/dist/lib/config/remove-unused-deps.js +35 -2
- package/dist/lib/config/validate-mappings.d.ts +15 -0
- package/dist/lib/config/validate-mappings.js +28 -0
- package/dist/lib/config/with-native-federation.js +20 -7
- package/dist/lib/core/build/build-for-federation.d.ts +7 -0
- package/dist/lib/core/build/build-for-federation.js +32 -134
- package/dist/lib/core/build/bundle-exposed-and-mappings.js +21 -8
- package/dist/lib/core/build/bundle-shared.d.ts +7 -0
- package/dist/lib/core/build/bundle-shared.js +45 -8
- package/dist/lib/core/build/rebuild-for-federation.d.ts +8 -0
- package/dist/lib/core/build/rebuild-for-federation.js +26 -0
- package/dist/lib/core/build/resolve-shared-dirs.d.ts +32 -0
- package/dist/lib/core/build/resolve-shared-dirs.js +70 -0
- package/dist/lib/core/build/shared-bundle-plan.d.ts +22 -0
- package/dist/lib/core/build/shared-bundle-plan.js +70 -0
- package/dist/lib/core/build/synthesize-cjs-exports.d.ts +12 -0
- package/dist/lib/core/build/synthesize-cjs-exports.js +58 -0
- package/dist/lib/core/cache/cache-persistence.d.ts +4 -2
- package/dist/lib/core/cache/cache-persistence.js +28 -6
- package/dist/lib/core/federation-builder.d.ts +2 -1
- package/dist/lib/core/federation-builder.js +3 -0
- package/dist/lib/core/normalize-options.d.ts +2 -2
- package/dist/lib/core/normalize-options.js +15 -9
- package/dist/lib/core/output/densify-externals.js +1 -0
- package/dist/lib/domain/config/federation-config.contract.d.ts +29 -2
- package/dist/lib/domain/utils/file-watcher.contract.d.ts +19 -1
- package/dist/lib/domain/utils/io-port.contract.d.ts +20 -6
- package/dist/lib/utils/file-watcher.d.ts +15 -4
- package/dist/lib/utils/file-watcher.js +126 -16
- package/dist/lib/utils/io/node-io-adapter.js +85 -24
- package/dist/lib/utils/package/cjs-named-exports.d.ts +21 -0
- package/dist/lib/utils/package/cjs-named-exports.js +36 -0
- package/dist/lib/utils/package/entry-point-resolver.js +2 -5
- package/dist/lib/utils/package/esm-detection.d.ts +13 -0
- package/dist/lib/utils/package/esm-detection.js +29 -0
- package/dist/lib/utils/package/package-info.d.ts +13 -0
- package/dist/lib/utils/package/package-info.js +30 -4
- package/dist/lib/utils/package/resolve-wildcard-keys.js +8 -2
- package/dist/lib/utils/path-patterns.d.ts +14 -0
- package/dist/lib/utils/path-patterns.js +12 -0
- package/package.json +6 -6
|
@@ -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
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { NormalizedExternalConfig } from '../../domain/config/external-config.contract.js';
|
|
2
|
+
import type { NormalizedFederationConfig } from '../../domain/config/federation-config.contract.js';
|
|
2
3
|
import type { ChunkInfo, IntegrityMap, SharedInfo } from '../../domain/core/federation-info.contract.js';
|
|
3
4
|
import type { FileReaderPort, FileWriterPort, HashPort } from '../../domain/utils/io-port.contract.js';
|
|
4
5
|
export declare const getDefaultCachePath: (workspaceRoot: string) => string;
|
|
5
6
|
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
|
|
7
|
+
export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, features?: FeatureFlags, contentSignals?: Record<string, string>, resolvedVersions?: Record<string, string>) => string;
|
|
8
|
+
export type FeatureFlags = Partial<NormalizedFederationConfig['features']>;
|
|
9
|
+
export declare const getChecksumCore: (hash: HashPort, shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, features?: FeatureFlags, contentSignals?: Record<string, string>, resolvedVersions?: Record<string, string>) => string;
|
|
8
10
|
export type CacheMetadata = {
|
|
9
11
|
checksum: string;
|
|
10
12
|
externals: SharedInfo[];
|
|
@@ -6,12 +6,31 @@ 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
|
|
9
|
+
const getChecksum = (shared, dev, builderVersion = "", features = {}, contentSignals = {}, resolvedVersions = {}) => getChecksumCore(nodeIo, shared, dev, builderVersion, features, contentSignals, resolvedVersions);
|
|
10
|
+
const featureState = (features) => Object.entries(features).sort(([a], [b]) => a < b ? -1 : 1).map(([flag, on]) => `${flag}=${on ? "1" : "0"}`).join(",");
|
|
11
|
+
const SHARED_INFO_FIELDS = [
|
|
12
|
+
"requiredVersion",
|
|
13
|
+
"singleton",
|
|
14
|
+
"strictVersion",
|
|
15
|
+
"shareScope",
|
|
16
|
+
"pool"
|
|
17
|
+
];
|
|
18
|
+
const sharedInfoState = (config) => {
|
|
19
|
+
const values = SHARED_INFO_FIELDS.map((field) => config[field] ?? null);
|
|
20
|
+
return values.every((value) => value === null) ? "" : `!${JSON.stringify(values)}`;
|
|
21
|
+
};
|
|
22
|
+
const getChecksumCore = (hash, shared, dev, builderVersion = "", features = {}, contentSignals = {}, resolvedVersions = {}) => {
|
|
11
23
|
const denseExternals = Object.keys(shared).sort().reduce((clean, external) => {
|
|
12
|
-
|
|
24
|
+
const installed = resolvedVersions[external];
|
|
25
|
+
const declared = shared[external].version;
|
|
26
|
+
const version = installed ? `~${installed}` : declared ? `@${declared}` : "";
|
|
27
|
+
const signal = contentSignals[external] ? `#${contentSignals[external]}` : "";
|
|
28
|
+
return clean + ":" + external + version + sharedInfoState(shared[external]) + signal;
|
|
13
29
|
}, "deps");
|
|
14
|
-
return hash.hash(
|
|
30
|
+
return hash.hash(
|
|
31
|
+
"sha256",
|
|
32
|
+
denseExternals + `:dev=${dev}:builder=${builderVersion}:features=${featureState(features)}`
|
|
33
|
+
).hex();
|
|
15
34
|
};
|
|
16
35
|
const cacheEntryCore = (io, pathToCache, fileName) => {
|
|
17
36
|
const metadataFile = path.join(pathToCache, fileName);
|
|
@@ -33,8 +52,11 @@ const cacheEntryCore = (io, pathToCache, fileName) => {
|
|
|
33
52
|
io.mkdirp(fullOutputPath);
|
|
34
53
|
cachedResult.files.forEach((file) => {
|
|
35
54
|
const cachedFile = path.join(pathToCache, file);
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
if (!io.exists(cachedFile))
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Cached artifact '${file}' recorded in '${metadataFile}' is missing. Delete '${pathToCache}' and rebuild.`
|
|
58
|
+
);
|
|
59
|
+
io.copyFile(cachedFile, path.join(fullOutputPath, file));
|
|
38
60
|
});
|
|
39
61
|
},
|
|
40
62
|
clear: () => {
|
|
@@ -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 {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
|
|
2
2
|
import type { FederationOptions, NormalizedFederationOptions } from '../domain/core/federation-options.contract.js';
|
|
3
|
-
import type { FileReaderPort } from '../domain/utils/io-port.contract.js';
|
|
3
|
+
import type { FileReaderPort, GlobPort } from '../domain/utils/io-port.contract.js';
|
|
4
4
|
import { type FederationCache } from '../../domain.js';
|
|
5
5
|
import { getUsedDependenciesFactory } from '../config/get-used-dependencies.js';
|
|
6
6
|
type ConfigLoader = (fullConfigPath: string) => Promise<NormalizedFederationConfig>;
|
|
7
7
|
interface NormalizeFederationDeps {
|
|
8
|
-
io: FileReaderPort;
|
|
8
|
+
io: FileReaderPort & GlobPort;
|
|
9
9
|
loadConfig: ConfigLoader;
|
|
10
10
|
usedDependenciesFactory?: typeof getUsedDependenciesFactory;
|
|
11
11
|
}
|
|
@@ -2,6 +2,8 @@ import * as path from "path";
|
|
|
2
2
|
import { pathToFileURL } from "url";
|
|
3
3
|
import { nodeIo } from "../utils/io/node-io-adapter.js";
|
|
4
4
|
import { removeUnusedDeps } from "../config/remove-unused-deps.js";
|
|
5
|
+
import { expandOrDropWildcards } from "../config/expand-mappings.js";
|
|
6
|
+
import { assertBarrelMappings } from "../config/validate-mappings.js";
|
|
5
7
|
import { createFederationCache } from "./cache/federation-cache.js";
|
|
6
8
|
import { getDefaultCachePath } from "./cache/cache-persistence.js";
|
|
7
9
|
import { getUsedDependenciesFactory } from "../config/get-used-dependencies.js";
|
|
@@ -31,25 +33,29 @@ async function normalizeFederationOptionsCore(deps, options, cache) {
|
|
|
31
33
|
cacheExternalArtifacts: options.cacheExternalArtifacts ?? true,
|
|
32
34
|
federationCache
|
|
33
35
|
};
|
|
34
|
-
|
|
36
|
+
const nothingShared = Object.keys(config.shared).length === 0 && Object.keys(config.sharedMappings).length === 0;
|
|
37
|
+
if (nothingShared) {
|
|
38
|
+
logger.debug("Nothing is shared, skipping the used dependency scan.");
|
|
39
|
+
} else if (config.features.ignoreUnusedDeps) {
|
|
35
40
|
const getUsedDeps = (deps.usedDependenciesFactory ?? getUsedDependenciesFactory)(
|
|
36
41
|
options.workspaceRoot,
|
|
37
42
|
options.entryPoints
|
|
38
43
|
);
|
|
39
|
-
config = removeUnusedDeps(getUsedDeps(config), config
|
|
44
|
+
config = removeUnusedDeps(getUsedDeps(config), config, {
|
|
45
|
+
io: deps.io,
|
|
46
|
+
workspaceRoot: options.workspaceRoot
|
|
47
|
+
});
|
|
40
48
|
logger.info("Removed unused dependencies.");
|
|
41
49
|
logger.debug(
|
|
42
50
|
'This can be disabled per dependency/external using the "includeSecondaries: {keepAll: true}" property. Or in general by disabling the "ignoreUnusedDeps" feature. '
|
|
43
51
|
);
|
|
44
52
|
} else {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
50
|
-
config.sharedMappings = Object.entries(config.sharedMappings).filter(([_path]) => !_path.includes("*")).reduce((acc, [_path, _import]) => ({ ...acc, [_path]: _import }), {});
|
|
51
|
-
}
|
|
53
|
+
config.sharedMappings = expandOrDropWildcards(config, {
|
|
54
|
+
io: deps.io,
|
|
55
|
+
workspaceRoot: options.workspaceRoot
|
|
56
|
+
});
|
|
52
57
|
}
|
|
58
|
+
assertBarrelMappings(config.sharedMappings);
|
|
53
59
|
return { config, options: normalizedOptions };
|
|
54
60
|
}
|
|
55
61
|
function resolveProjectName(name) {
|
|
@@ -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);
|
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import type { PreparedSkipList, SkipList } from './skip-list.contract.js';
|
|
2
2
|
import type { PathToImport } from '../utils/mapped-path.contract.js';
|
|
3
|
-
import type { NormalizedSharedExternalsConfig, SharedExternalsConfig } from './external-config.contract.js';
|
|
3
|
+
import type { ExternalConfig, NormalizedSharedExternalsConfig, SharedExternalsConfig } from './external-config.contract.js';
|
|
4
4
|
export type ExposeEntry = {
|
|
5
5
|
file: string;
|
|
6
6
|
element?: string;
|
|
7
7
|
};
|
|
8
|
+
export type SharedMappingEntry = string | [string[], ExternalConfig];
|
|
9
|
+
/** Selection pattern -> config, kept in declaration order: first match wins. */
|
|
10
|
+
export type SharedMappingConfigs = Record<string, ExternalConfig>;
|
|
11
|
+
/**
|
|
12
|
+
* Only the subset of `ExternalConfig` a mapping can act on — mappings all share the
|
|
13
|
+
* single `mapping-or-exposed` bundle, so `build`/`platform`/`chunks`/`packageInfo`
|
|
14
|
+
* have nothing to select. `requiredVersion` and `version` stay optional because their
|
|
15
|
+
* defaults are read from the mapped lib's package.json at build time.
|
|
16
|
+
*
|
|
17
|
+
* `includeSecondaries` collapses to a boolean exactly as it does for a shared external:
|
|
18
|
+
* it means "exempt from `ignoreUnusedDeps` pruning". `resolveGlob` is lifted out of it
|
|
19
|
+
* because a mapping has no secondary entry points for it to apply to.
|
|
20
|
+
*/
|
|
21
|
+
export interface NormalizedMappingConfig {
|
|
22
|
+
singleton: boolean;
|
|
23
|
+
strictVersion: boolean;
|
|
24
|
+
requiredVersion?: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
shareScope?: string;
|
|
27
|
+
pool?: string;
|
|
28
|
+
includeSecondaries?: boolean;
|
|
29
|
+
resolveGlob?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export type NormalizedSharedMappingConfigs = Record<string, NormalizedMappingConfig>;
|
|
8
32
|
export interface FederationConfig {
|
|
9
33
|
name?: string;
|
|
10
34
|
exposes?: Record<string, string | ExposeEntry>;
|
|
11
35
|
shared?: SharedExternalsConfig;
|
|
12
36
|
platform?: 'browser' | 'node';
|
|
13
|
-
sharedMappings?: Array<
|
|
37
|
+
sharedMappings?: Array<SharedMappingEntry>;
|
|
14
38
|
chunks?: boolean;
|
|
15
39
|
skip?: SkipList;
|
|
16
40
|
externals?: string[];
|
|
@@ -21,6 +45,7 @@ export interface FederationConfig {
|
|
|
21
45
|
denseChunking?: boolean;
|
|
22
46
|
denseExternals?: boolean;
|
|
23
47
|
integrityHashes?: boolean;
|
|
48
|
+
synthesizeCjsExports?: boolean;
|
|
24
49
|
};
|
|
25
50
|
}
|
|
26
51
|
export interface NormalizedFederationConfig {
|
|
@@ -29,6 +54,7 @@ export interface NormalizedFederationConfig {
|
|
|
29
54
|
exposes: Record<string, ExposeEntry>;
|
|
30
55
|
shared: NormalizedSharedExternalsConfig;
|
|
31
56
|
sharedMappings: PathToImport;
|
|
57
|
+
sharedMappingsConfig: NormalizedSharedMappingConfigs;
|
|
32
58
|
skip: PreparedSkipList;
|
|
33
59
|
chunks: boolean;
|
|
34
60
|
externals: string[];
|
|
@@ -39,5 +65,6 @@ export interface NormalizedFederationConfig {
|
|
|
39
65
|
denseChunking: boolean;
|
|
40
66
|
denseExternals: boolean;
|
|
41
67
|
integrityHashes: boolean;
|
|
68
|
+
synthesizeCjsExports: boolean;
|
|
42
69
|
};
|
|
43
70
|
}
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
export interface NfFileWatcherOptions {
|
|
2
2
|
onChange?: (path: string) => void;
|
|
3
|
+
pollIntervalMs?: number;
|
|
4
|
+
debounceMs?: number;
|
|
5
|
+
/** Drop events whose mtime is unchanged since the last one seen for that path.
|
|
6
|
+
* macOS FSEvents re-delivers 'changed' for recently edited files roughly every
|
|
7
|
+
* 30s; with a watch list of a few thousand sources that replay alone keeps a
|
|
8
|
+
* rebuild loop awake forever. Default: true. */
|
|
9
|
+
dedupeReplays?: boolean;
|
|
10
|
+
/** Grace period after a path's recorded mtime during which an event whose mtime
|
|
11
|
+
* and byte length both match still passes, covering a second save inside one
|
|
12
|
+
* mtime tick. The 2000 default is twice the coarsest mtime granularity in play
|
|
13
|
+
* (1s on gRPC-FUSE/NFS/WSL2 drvfs/HFS+); raise it for a filesystem coarser than
|
|
14
|
+
* that, or for a network mount whose server clock runs behind the client.
|
|
15
|
+
* See AGENTS.md "Replay dedupe". Default: 2000. */
|
|
16
|
+
replayGraceMs?: number;
|
|
17
|
+
}
|
|
18
|
+
interface AddPathsOptions {
|
|
19
|
+
poll?: boolean;
|
|
3
20
|
}
|
|
4
21
|
export interface NfFileWatcher {
|
|
5
|
-
addPaths(paths: string | readonly string[]): void;
|
|
22
|
+
addPaths(paths: string | readonly string[], opts?: AddPathsOptions): void;
|
|
6
23
|
close(): Promise<void>;
|
|
7
24
|
get(): ReadonlySet<string>;
|
|
8
25
|
clear(): void;
|
|
9
26
|
mutate(fn: (dirtyPaths: Set<string>) => void): void;
|
|
10
27
|
}
|
|
28
|
+
export {};
|
|
@@ -3,6 +3,12 @@ export interface Digest {
|
|
|
3
3
|
hex(): string;
|
|
4
4
|
base64(): string;
|
|
5
5
|
}
|
|
6
|
+
export interface StatInfo {
|
|
7
|
+
mtimeMs: number;
|
|
8
|
+
/** Byte length; 0 for directories. */
|
|
9
|
+
size: number;
|
|
10
|
+
isSymbolicLink: boolean;
|
|
11
|
+
}
|
|
6
12
|
export interface FileReaderPort {
|
|
7
13
|
readText(path: string): string;
|
|
8
14
|
readBytes(path: string): Uint8Array;
|
|
@@ -13,6 +19,8 @@ export interface FileReaderPort {
|
|
|
13
19
|
isDirectory(path: string): boolean;
|
|
14
20
|
/** Immediate child entry names (not full paths). Empty array on ENOENT, never throws. */
|
|
15
21
|
readDir(path: string): string[];
|
|
22
|
+
realpath(path: string): string;
|
|
23
|
+
stat(path: string): StatInfo | null;
|
|
16
24
|
}
|
|
17
25
|
export interface FileWriterPort {
|
|
18
26
|
writeText(path: string, data: string): void;
|
|
@@ -23,6 +31,7 @@ export interface FileWriterPort {
|
|
|
23
31
|
export interface GlobPort {
|
|
24
32
|
globFiles(pattern: string, opts: {
|
|
25
33
|
cwd: string;
|
|
34
|
+
ignore?: string[];
|
|
26
35
|
}): string[];
|
|
27
36
|
}
|
|
28
37
|
export interface HashPort {
|
|
@@ -31,15 +40,20 @@ export interface HashPort {
|
|
|
31
40
|
export interface WatchHandle {
|
|
32
41
|
close(): void;
|
|
33
42
|
}
|
|
43
|
+
interface WatchOptions {
|
|
44
|
+
recursive: boolean;
|
|
45
|
+
poll?: {
|
|
46
|
+
intervalMs: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
34
49
|
export interface WatchPort {
|
|
35
50
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
51
|
+
* `onEvent` receives the changed entry's filename relative to `path` (null when
|
|
52
|
+
* the platform omits it). Watching a file reports that file's own basename, so a
|
|
53
|
+
* caller that registered a file can ignore the argument.
|
|
39
54
|
*/
|
|
40
|
-
watch(path: string, opts:
|
|
41
|
-
recursive: boolean;
|
|
42
|
-
}, onEvent: (filename: string | null) => void): WatchHandle;
|
|
55
|
+
watch(path: string, opts: WatchOptions, onEvent: (filename: string | null) => void): WatchHandle;
|
|
43
56
|
}
|
|
44
57
|
export interface IoPort extends FileReaderPort, FileWriterPort, GlobPort, HashPort, WatchPort {
|
|
45
58
|
}
|
|
59
|
+
export {};
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { WatchPort, FileReaderPort } from '../domain/utils/io-port.contract.js';
|
|
2
2
|
import type { NfFileWatcher, NfFileWatcherOptions } from '../domain/utils/file-watcher.contract.js';
|
|
3
3
|
export declare function createNfWatcher(options?: NfFileWatcherOptions): NfFileWatcher;
|
|
4
|
-
export declare function createNfWatcherCore(io: WatchPort & FileReaderPort, options?: NfFileWatcherOptions): NfFileWatcher;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export declare function createNfWatcherCore(io: WatchPort & FileReaderPort, options?: NfFileWatcherOptions, now?: () => number): NfFileWatcher;
|
|
5
|
+
/** A bundler cache keyed by input path, or the input paths themselves. */
|
|
6
|
+
export type WatchSources = Iterable<string> | {
|
|
7
|
+
keys(): Iterable<string>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe the watcher to the inputs the last build compiled.
|
|
11
|
+
*
|
|
12
|
+
* Passing a bundler cache only works when it is keyed by input path. A cache that
|
|
13
|
+
* records its inputs elsewhere yields an empty watch set and, silently, a dev server
|
|
14
|
+
* that serves stale bundles until restart — Angular's `SourceFileCache` extends `Map`
|
|
15
|
+
* but keeps its inputs in `typeScriptFileCache`/`referencedFiles`, which is
|
|
16
|
+
* angular-adapter#94. Adapters over such a cache must expand it and pass the paths.
|
|
17
|
+
*/
|
|
18
|
+
export declare function syncNfFileWatcher(watcher: NfFileWatcher, sources: WatchSources, linkedDirs?: readonly string[]): void;
|
|
@@ -1,31 +1,130 @@
|
|
|
1
|
-
import { join } from "path";
|
|
1
|
+
import { dirname, join } from "path";
|
|
2
2
|
import { nodeIo } from "./io/node-io-adapter.js";
|
|
3
3
|
import { logger } from "./logger.js";
|
|
4
|
-
import { toPosix } from "./path-patterns.js";
|
|
4
|
+
import { isUnderDir, toPosix } from "./path-patterns.js";
|
|
5
5
|
function createNfWatcher(options = {}) {
|
|
6
6
|
return createNfWatcherCore(nodeIo, options);
|
|
7
7
|
}
|
|
8
|
-
function createNfWatcherCore(io, options = {}) {
|
|
8
|
+
function createNfWatcherCore(io, options = {}, now = Date.now) {
|
|
9
9
|
const { onChange } = options;
|
|
10
|
+
const pollIntervalMs = options.pollIntervalMs ?? 300;
|
|
11
|
+
const debounceMs = options.debounceMs ?? 0;
|
|
12
|
+
const dedupeReplays = options.dedupeReplays ?? true;
|
|
13
|
+
const replayGraceMs = options.replayGraceMs ?? 2e3;
|
|
10
14
|
const watchers = /* @__PURE__ */ new Map();
|
|
11
15
|
const dirtyPaths = /* @__PURE__ */ new Set();
|
|
12
|
-
const
|
|
16
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
17
|
+
const trackedFiles = /* @__PURE__ */ new Set();
|
|
18
|
+
const fileDirWatchers = /* @__PURE__ */ new Map();
|
|
19
|
+
const dirKey = (p) => {
|
|
20
|
+
const posix = toPosix(p);
|
|
21
|
+
return posix.length > 1 ? posix.replace(/\/+$/, "") : posix;
|
|
22
|
+
};
|
|
23
|
+
const covers = (path, poll) => {
|
|
24
|
+
for (const [dir, watch] of watchers) {
|
|
25
|
+
if ((watch.poll || !poll) && isUnderDir(path, dir)) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
const supersede = (dir, poll) => {
|
|
30
|
+
for (const map of [watchers, fileDirWatchers]) {
|
|
31
|
+
for (const [key, watch] of map) {
|
|
32
|
+
if (watch.poll && !poll) continue;
|
|
33
|
+
if (map === watchers && key === dir) continue;
|
|
34
|
+
if (!isUnderDir(key, dir)) continue;
|
|
35
|
+
watch.handle.close();
|
|
36
|
+
map.delete(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const identityOf = (path) => {
|
|
41
|
+
let stat = io.stat(path);
|
|
42
|
+
if (stat?.isSymbolicLink) stat = io.stat(io.realpath(path));
|
|
43
|
+
return stat ? { mtimeMs: stat.mtimeMs, size: stat.size } : null;
|
|
44
|
+
};
|
|
45
|
+
const isReplay = (path, at) => {
|
|
46
|
+
const current = identityOf(path);
|
|
47
|
+
if (!current) {
|
|
48
|
+
lastSeen.delete(path);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const previous = lastSeen.get(path);
|
|
52
|
+
lastSeen.set(path, current);
|
|
53
|
+
if (!previous || previous.mtimeMs !== current.mtimeMs || previous.size !== current.size) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return at - current.mtimeMs >= replayGraceMs;
|
|
57
|
+
};
|
|
58
|
+
let watchFailures = 0;
|
|
59
|
+
const watchFailed = (path) => {
|
|
60
|
+
if (++watchFailures > 1) return logger.debug(`Could not watch path '${path}'.`);
|
|
61
|
+
logger.warn(
|
|
62
|
+
`Could not watch '${path}'. Changes there will not trigger a rebuild; run with verbose logging to see any further watch failures.`
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
const deliver = (path, at) => {
|
|
66
|
+
if (dedupeReplays && isReplay(path, at)) return;
|
|
67
|
+
dirtyPaths.add(path);
|
|
13
68
|
if (onChange) onChange(path);
|
|
14
|
-
|
|
69
|
+
};
|
|
70
|
+
const pending = /* @__PURE__ */ new Map();
|
|
71
|
+
let flushTimer;
|
|
72
|
+
const flush = () => {
|
|
73
|
+
for (const [p, at] of pending) deliver(p, at);
|
|
74
|
+
pending.clear();
|
|
75
|
+
};
|
|
76
|
+
const notify = (path) => {
|
|
77
|
+
const at = now();
|
|
78
|
+
if (debounceMs <= 0) return deliver(path, at);
|
|
79
|
+
if (!pending.has(path)) pending.set(path, at);
|
|
80
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
81
|
+
flushTimer = setTimeout(flush, debounceMs);
|
|
82
|
+
flushTimer.unref?.();
|
|
15
83
|
};
|
|
16
84
|
return {
|
|
17
|
-
addPaths(paths) {
|
|
85
|
+
addPaths(paths, opts) {
|
|
18
86
|
const list = typeof paths === "string" ? [paths] : [...paths];
|
|
87
|
+
const shouldPoll = !!opts?.poll;
|
|
88
|
+
const poll = shouldPoll ? { intervalMs: pollIntervalMs } : void 0;
|
|
19
89
|
for (const p of list) {
|
|
20
|
-
if (
|
|
90
|
+
if (io.isDirectory(p)) {
|
|
91
|
+
const dir2 = dirKey(p);
|
|
92
|
+
if (watchers.has(dir2) || covers(dir2, shouldPoll)) continue;
|
|
93
|
+
try {
|
|
94
|
+
watchers.set(dir2, {
|
|
95
|
+
handle: io.watch(p, { recursive: true, poll }, (filename) => {
|
|
96
|
+
if (filename) notify(toPosix(join(p, filename)));
|
|
97
|
+
}),
|
|
98
|
+
poll: shouldPoll
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
watchFailed(p);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
supersede(dir2, shouldPoll);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const key = toPosix(p);
|
|
108
|
+
if (trackedFiles.has(key)) continue;
|
|
109
|
+
trackedFiles.add(key);
|
|
110
|
+
if (dedupeReplays && !lastSeen.has(key)) {
|
|
111
|
+
const identity = identityOf(key);
|
|
112
|
+
if (identity) lastSeen.set(key, identity);
|
|
113
|
+
}
|
|
114
|
+
if (covers(key, shouldPoll)) continue;
|
|
115
|
+
const dir = dirKey(dirname(p));
|
|
116
|
+
if (fileDirWatchers.has(dir)) continue;
|
|
21
117
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
118
|
+
fileDirWatchers.set(dir, {
|
|
119
|
+
handle: io.watch(dir, { recursive: false, poll }, (filename) => {
|
|
120
|
+
if (!filename) return;
|
|
121
|
+
const changed = toPosix(join(dir, filename));
|
|
122
|
+
if (trackedFiles.has(changed)) notify(changed);
|
|
123
|
+
}),
|
|
124
|
+
poll: shouldPoll
|
|
125
|
+
});
|
|
27
126
|
} catch {
|
|
28
|
-
|
|
127
|
+
watchFailed(dir);
|
|
29
128
|
}
|
|
30
129
|
}
|
|
31
130
|
},
|
|
@@ -33,16 +132,27 @@ function createNfWatcherCore(io, options = {}) {
|
|
|
33
132
|
clear: () => dirtyPaths.clear(),
|
|
34
133
|
mutate: (fn) => fn(dirtyPaths),
|
|
35
134
|
async close() {
|
|
36
|
-
|
|
135
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
136
|
+
for (const { handle } of [...watchers.values(), ...fileDirWatchers.values()]) {
|
|
37
137
|
handle.close();
|
|
38
138
|
}
|
|
39
139
|
watchers.clear();
|
|
140
|
+
fileDirWatchers.clear();
|
|
141
|
+
trackedFiles.clear();
|
|
142
|
+
lastSeen.clear();
|
|
143
|
+
watchFailures = 0;
|
|
40
144
|
}
|
|
41
145
|
};
|
|
42
146
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
147
|
+
function toPaths(sources) {
|
|
148
|
+
if (Array.isArray(sources)) return sources;
|
|
149
|
+
const cache = sources;
|
|
150
|
+
return typeof cache.keys === "function" ? cache.keys() : sources;
|
|
151
|
+
}
|
|
152
|
+
function syncNfFileWatcher(watcher, sources, linkedDirs = []) {
|
|
153
|
+
const files = [...toPaths(sources)].filter((k) => !k.includes("node_modules"));
|
|
45
154
|
if (files.length) watcher.addPaths(files);
|
|
155
|
+
if (linkedDirs.length) watcher.addPaths(linkedDirs, { poll: true });
|
|
46
156
|
}
|
|
47
157
|
export {
|
|
48
158
|
createNfWatcher,
|