@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.
- package/dist/internal.d.ts +1 -0
- package/dist/lib/config/configuration-context.d.ts +2 -1
- package/dist/lib/config/configuration-context.js +4 -2
- package/dist/lib/config/get-used-dependencies.js +20 -0
- package/dist/lib/config/project-paths.d.ts +1 -1
- package/dist/lib/config/project-paths.js +4 -3
- package/dist/lib/config/remove-unused-deps.js +10 -5
- package/dist/lib/config/share-utils.js +28 -8
- package/dist/lib/config/version-lookup.d.ts +4 -0
- package/dist/lib/config/version-lookup.js +27 -0
- package/dist/lib/core/build/assemble-federation-info.d.ts +8 -0
- package/dist/lib/core/build/assemble-federation-info.js +34 -0
- package/dist/lib/core/build/build-for-federation.js +7 -45
- package/dist/lib/core/build/bundle-exposed-and-mappings.d.ts +1 -3
- package/dist/lib/core/build/bundle-exposed-and-mappings.js +0 -25
- package/dist/lib/core/build/rebuild-for-federation.js +6 -33
- package/dist/lib/core/build/resolve-shared-dirs.d.ts +6 -3
- package/dist/lib/core/build/resolve-shared-dirs.js +25 -11
- package/dist/lib/core/normalize-options.js +19 -7
- package/dist/lib/core/output/write-federation-outputs.d.ts +7 -0
- package/dist/lib/core/output/write-federation-outputs.js +9 -0
- package/dist/lib/domain/config/external-config.contract.d.ts +14 -2
- package/dist/lib/domain/core/federation-options.contract.d.ts +6 -0
- package/dist/lib/domain/utils/file-watcher.contract.d.ts +7 -0
- package/dist/lib/domain/utils/io-port.contract.d.ts +2 -0
- package/dist/lib/utils/disk-case.d.ts +7 -0
- package/dist/lib/utils/disk-case.js +14 -0
- package/dist/lib/utils/file-watcher.js +10 -9
- package/dist/lib/utils/io/node-io-adapter.js +10 -0
- package/dist/lib/utils/path-patterns.d.ts +8 -0
- package/dist/lib/utils/path-patterns.js +2 -0
- package/package.json +1 -1
package/dist/internal.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
63
|
-
const
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
|
35
|
-
|
|
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(`[
|
|
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("[
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
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("[
|
|
31
|
-
const
|
|
32
|
-
|
|
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
|
|
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
|
|
15
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
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
|
|
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
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
53
|
+
workspaceRoot,
|
|
42
54
|
options.entryPoints
|
|
43
55
|
);
|
|
44
56
|
config = removeUnusedDeps(getUsedDeps(config), config, {
|
|
45
57
|
io: deps.io,
|
|
46
|
-
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
|
|
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
|
-
|
|
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,
|
|
25
|
-
if ((
|
|
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,
|
|
32
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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,
|