@softarc/native-federation 4.3.2 → 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 +3 -2
- package/dist/internal.js +5 -1
- 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 +18 -6
- package/dist/lib/core/build/bundle-exposed-and-mappings.js +21 -8
- package/dist/lib/core/build/bundle-shared.js +9 -3
- package/dist/lib/core/build/resolve-shared-dirs.d.ts +16 -0
- package/dist/lib/core/build/resolve-shared-dirs.js +8 -4
- package/dist/lib/core/cache/cache-persistence.d.ts +4 -2
- package/dist/lib/core/cache/cache-persistence.js +27 -8
- package/dist/lib/core/normalize-options.d.ts +2 -2
- package/dist/lib/core/normalize-options.js +15 -9
- package/dist/lib/domain/config/federation-config.contract.d.ts +27 -2
- package/dist/lib/domain/utils/file-watcher.contract.d.ts +12 -0
- package/dist/lib/domain/utils/io-port.contract.d.ts +6 -3
- package/dist/lib/utils/file-watcher.d.ts +15 -4
- package/dist/lib/utils/file-watcher.js +112 -19
- package/dist/lib/utils/io/node-io-adapter.js +10 -5
- package/dist/lib/utils/package/entry-point-resolver.js +2 -5
- 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 +5 -5
|
@@ -2,21 +2,25 @@ import { getRawMappedPaths } from "./mapped-paths.js";
|
|
|
2
2
|
import { fromPackageJson } from "./share-utils.js";
|
|
3
3
|
import { findRootTsConfigJson } from "./project-paths.js";
|
|
4
4
|
import { isInSkipList, prepareSkipList } from "./default-skip-list.js";
|
|
5
|
+
import { normalizeMappingConfig, withoutSkippedMappings } from "./mapping-utils.js";
|
|
5
6
|
import { logger } from "../utils/logger.js";
|
|
6
7
|
function withNativeFederation(config) {
|
|
7
8
|
const skip = prepareSkipList(config.skip ?? []);
|
|
8
9
|
const chunks = config.chunks ?? true;
|
|
10
|
+
const mappingVersion = config.features?.mappingVersion ?? true;
|
|
11
|
+
const { paths, configs } = getRawMappedPaths(findRootTsConfigJson(), config.sharedMappings);
|
|
9
12
|
const normalized = {
|
|
10
13
|
$type: "classic",
|
|
11
14
|
name: config.name ?? "",
|
|
12
15
|
exposes: normalizeExposes(config.exposes),
|
|
13
16
|
shared: normalizeShared(config, skip, chunks),
|
|
14
|
-
sharedMappings:
|
|
17
|
+
sharedMappings: withoutSkippedMappings(paths, skip),
|
|
18
|
+
sharedMappingsConfig: normalizeMappingConfigs(configs, mappingVersion),
|
|
15
19
|
chunks,
|
|
16
20
|
skip,
|
|
17
21
|
externals: config.externals ?? [],
|
|
18
22
|
features: {
|
|
19
|
-
mappingVersion
|
|
23
|
+
mappingVersion,
|
|
20
24
|
ignoreUnusedDeps: config.features?.ignoreUnusedDeps ?? true,
|
|
21
25
|
denseChunking: config.features?.denseChunking ?? false,
|
|
22
26
|
denseExternals: config.features?.denseExternals ?? false,
|
|
@@ -74,10 +78,18 @@ function normalizeShared(config, skip, chunks) {
|
|
|
74
78
|
result = Object.keys(result).filter((key) => !isInSkipList(key, skip)).reduce((acc, cur) => ({ ...acc, [cur]: result[cur] }), {});
|
|
75
79
|
return result;
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const IGNORED_MAPPING_PROPS = ["build", "platform", "chunks", "packageInfo"];
|
|
82
|
+
function normalizeMappingConfigs(configs, mappingVersion) {
|
|
83
|
+
return Object.entries(configs).reduce((acc, [pattern, cfg]) => {
|
|
84
|
+
const ignored = IGNORED_MAPPING_PROPS.filter((prop) => cfg[prop] !== void 0);
|
|
85
|
+
if (ignored.length > 0) {
|
|
86
|
+
logger.warn(
|
|
87
|
+
`Mapping '${pattern}' sets ${ignored.join(", ")}, which mapped paths do not honour (they all share one bundle). Ignored.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
acc[pattern] = normalizeMappingConfig(cfg, mappingVersion);
|
|
91
|
+
return acc;
|
|
92
|
+
}, {});
|
|
81
93
|
}
|
|
82
94
|
export {
|
|
83
95
|
withNativeFederation
|
|
@@ -7,6 +7,7 @@ import { nodeIo } from "../../utils/io/node-io-adapter.js";
|
|
|
7
7
|
import { AbortedError } from "../../utils/errors.js";
|
|
8
8
|
import { rewriteChunkImports } from "./rewrite-chunk-imports.js";
|
|
9
9
|
import { getBuildAdapter } from "./build-adapter.js";
|
|
10
|
+
import { resolveMappingConfig } from "../../config/mapping-utils.js";
|
|
10
11
|
async function bundleExposedAndMappings(config, fedOptions, externals, modifiedFiles, signal) {
|
|
11
12
|
return bundleExposedAndMappingsCore(
|
|
12
13
|
{ adapter: getBuildAdapter() },
|
|
@@ -78,7 +79,13 @@ async function bundleExposedAndMappingsCore(deps, config, fedOptions, externals,
|
|
|
78
79
|
for (const item of shared) {
|
|
79
80
|
const distEntryFile = popFromResultMap(resultMap, item.outName);
|
|
80
81
|
sharedResult.push(
|
|
81
|
-
toSharedMappingInfo(
|
|
82
|
+
toSharedMappingInfo(
|
|
83
|
+
item.fileName,
|
|
84
|
+
item.key,
|
|
85
|
+
path.basename(distEntryFile),
|
|
86
|
+
config,
|
|
87
|
+
fedOptions
|
|
88
|
+
)
|
|
82
89
|
);
|
|
83
90
|
entryFiles.push(distEntryFile);
|
|
84
91
|
}
|
|
@@ -112,9 +119,7 @@ function describeExposed(config, options) {
|
|
|
112
119
|
const result = [];
|
|
113
120
|
for (const key in config.exposes) {
|
|
114
121
|
const expose = config.exposes[key];
|
|
115
|
-
const localPath = normalize(
|
|
116
|
-
path.normalize(path.join(options.workspaceRoot, expose.file))
|
|
117
|
-
);
|
|
122
|
+
const localPath = normalize(path.normalize(path.join(options.workspaceRoot, expose.file)));
|
|
118
123
|
result.push({
|
|
119
124
|
key,
|
|
120
125
|
outFileName: "",
|
|
@@ -135,13 +140,21 @@ function describeSharedMappings(config, fedOptions) {
|
|
|
135
140
|
}
|
|
136
141
|
function toSharedMappingInfo(mappedPath, mappedImport, outFileName, config, fedOptions) {
|
|
137
142
|
const mappingVersion = config.features.mappingVersion ? getMappingVersion(mappedPath, fedOptions.workspaceRoot) : "";
|
|
143
|
+
const mappingConfig = resolveMappingConfig(
|
|
144
|
+
mappedImport,
|
|
145
|
+
config.sharedMappingsConfig,
|
|
146
|
+
config.features.mappingVersion
|
|
147
|
+
);
|
|
148
|
+
const version = mappingConfig.version ?? mappingVersion;
|
|
138
149
|
return {
|
|
139
150
|
packageName: mappedImport,
|
|
140
151
|
outFileName,
|
|
141
|
-
requiredVersion:
|
|
142
|
-
singleton:
|
|
143
|
-
strictVersion:
|
|
144
|
-
version
|
|
152
|
+
requiredVersion: mappingConfig.requiredVersion ?? (version.length > 0 ? "~" + version : ""),
|
|
153
|
+
singleton: mappingConfig.singleton,
|
|
154
|
+
strictVersion: mappingConfig.strictVersion,
|
|
155
|
+
version,
|
|
156
|
+
...mappingConfig.shareScope && { shareScope: mappingConfig.shareScope },
|
|
157
|
+
...mappingConfig.pool && { pool: mappingConfig.pool },
|
|
145
158
|
dev: !fedOptions.dev ? void 0 : {
|
|
146
159
|
entryPoint: normalize(path.normalize(mappedPath))
|
|
147
160
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import {
|
|
3
3
|
sharedPackageJsonRepository,
|
|
4
|
-
getPackageInfo
|
|
4
|
+
getPackageInfo,
|
|
5
|
+
installedVersions
|
|
5
6
|
} from "../../utils/package/package-info.js";
|
|
6
7
|
import { logger } from "../../utils/logger.js";
|
|
7
8
|
import { nodeIo } from "../../utils/io/node-io-adapter.js";
|
|
@@ -41,13 +42,18 @@ async function bundleSharedCore(deps, sharedBundles, config, fedOptions, externa
|
|
|
41
42
|
deps.io,
|
|
42
43
|
deps.repo
|
|
43
44
|
);
|
|
45
|
+
const resolvedVersions = installedVersions(Object.keys(sharedBundles), folder, deps.repo);
|
|
46
|
+
for (const [key, cfg] of Object.entries(sharedBundles)) {
|
|
47
|
+
if (cfg.packageInfo) resolvedVersions[key] = cfg.packageInfo.version ?? "";
|
|
48
|
+
}
|
|
44
49
|
const checksum = getChecksumCore(
|
|
45
50
|
deps.io,
|
|
46
51
|
sharedBundles,
|
|
47
52
|
fedOptions.dev ? "1" : "0",
|
|
48
53
|
builderVersion,
|
|
49
|
-
config.features
|
|
50
|
-
contentSignals
|
|
54
|
+
config.features,
|
|
55
|
+
contentSignals,
|
|
56
|
+
resolvedVersions
|
|
51
57
|
);
|
|
52
58
|
const bundleCache = cacheEntryCore(
|
|
53
59
|
deps.io,
|
|
@@ -8,6 +8,22 @@ export declare function resolveSharedPackageDirs(config: NormalizedFederationCon
|
|
|
8
8
|
/** Realpath'd dirs of symlinked shared packages — the bounded watch set.
|
|
9
9
|
* Deduped, since secondaries share a package dir. */
|
|
10
10
|
export declare function linkedSharedDirs(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Source directories of the workspace libs in `config.sharedMappings` — the watch set
|
|
13
|
+
* for mappings, derived from config alone rather than from a bundler cache.
|
|
14
|
+
*
|
|
15
|
+
* Coarser than the inputs a build actually compiled, and deliberately so: it covers
|
|
16
|
+
* files added to a lib after the last build, which a compiled-inputs watch set cannot
|
|
17
|
+
* know about yet. It does not follow imports out of the lib, so an adapter that can
|
|
18
|
+
* enumerate its build inputs should watch both. See angular-adapter#94.
|
|
19
|
+
*
|
|
20
|
+
* How coarse is the caller's to bound: an entry point that is not a lib barrel widens the
|
|
21
|
+
* watch to whatever directory it sits in (`'@app/env': ['src/environments/environment.ts']`
|
|
22
|
+
* to that folder, `'@shared': ['src/index.ts']` to all of `src`), and with `sharedMappings`
|
|
23
|
+
* unset every tsconfig path becomes a mapping. Watch these natively rather than polled —
|
|
24
|
+
* they are source trees, not the dist output `linkedSharedDirs` exists for.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sharedMappingDirs(config: NormalizedFederationConfig): string[];
|
|
11
27
|
/** Per-key content signal (max mtime of the resolved dir) for symlinked deps only.
|
|
12
28
|
* Registry deps get no signal, keeping their checksum version-only. (Every key is
|
|
13
29
|
* still resolved: detecting the symlink requires the realpath + lstat.) */
|
|
@@ -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 { toPosix } from "../../utils/path-patterns.js";
|
|
4
|
+
import { 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 = [];
|
|
@@ -25,6 +25,10 @@ function linkedSharedDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJ
|
|
|
25
25
|
const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
|
|
26
26
|
return [...new Set(entries.filter((e) => e.isSymlink).map((e) => e.realDir))];
|
|
27
27
|
}
|
|
28
|
+
function sharedMappingDirs(config) {
|
|
29
|
+
const dirs = Object.keys(config.sharedMappings).map((entryPoint) => toPosix(path.dirname(entryPoint))).filter((dir) => !dir.includes("node_modules"));
|
|
30
|
+
return [...new Set(dirs)];
|
|
31
|
+
}
|
|
28
32
|
function maxMtime(io, dir) {
|
|
29
33
|
let max = 0;
|
|
30
34
|
const walk = (d) => {
|
|
@@ -53,8 +57,7 @@ function affectedSharedKeys(modifiedFiles, dirs, io = nodeIo) {
|
|
|
53
57
|
if (modifiedFiles.length === 0 || dirs.size === 0) return affected;
|
|
54
58
|
const realFiles = modifiedFiles.map((f) => toPosix(io.realpath(f)));
|
|
55
59
|
for (const [key, dir] of dirs) {
|
|
56
|
-
|
|
57
|
-
if (realFiles.some((f) => f === dir || f.startsWith(prefix))) affected.add(key);
|
|
60
|
+
if (realFiles.some((f) => isUnderDir(f, dir))) affected.add(key);
|
|
58
61
|
}
|
|
59
62
|
return affected;
|
|
60
63
|
}
|
|
@@ -62,5 +65,6 @@ export {
|
|
|
62
65
|
affectedSharedKeys,
|
|
63
66
|
linkedContentSignals,
|
|
64
67
|
linkedSharedDirs,
|
|
65
|
-
resolveSharedPackageDirs
|
|
68
|
+
resolveSharedPackageDirs,
|
|
69
|
+
sharedMappingDirs
|
|
66
70
|
};
|
|
@@ -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,
|
|
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,15 +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 = "",
|
|
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
|
-
const
|
|
24
|
+
const installed = resolvedVersions[external];
|
|
25
|
+
const declared = shared[external].version;
|
|
26
|
+
const version = installed ? `~${installed}` : declared ? `@${declared}` : "";
|
|
13
27
|
const signal = contentSignals[external] ? `#${contentSignals[external]}` : "";
|
|
14
|
-
return clean + ":" + external + version + signal;
|
|
28
|
+
return clean + ":" + external + version + sharedInfoState(shared[external]) + signal;
|
|
15
29
|
}, "deps");
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
return hash.hash(
|
|
31
|
+
"sha256",
|
|
32
|
+
denseExternals + `:dev=${dev}:builder=${builderVersion}:features=${featureState(features)}`
|
|
33
|
+
).hex();
|
|
18
34
|
};
|
|
19
35
|
const cacheEntryCore = (io, pathToCache, fileName) => {
|
|
20
36
|
const metadataFile = path.join(pathToCache, fileName);
|
|
@@ -36,8 +52,11 @@ const cacheEntryCore = (io, pathToCache, fileName) => {
|
|
|
36
52
|
io.mkdirp(fullOutputPath);
|
|
37
53
|
cachedResult.files.forEach((file) => {
|
|
38
54
|
const cachedFile = path.join(pathToCache, file);
|
|
39
|
-
|
|
40
|
-
|
|
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));
|
|
41
60
|
});
|
|
42
61
|
},
|
|
43
62
|
clear: () => {
|
|
@@ -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) {
|
|
@@ -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[];
|
|
@@ -30,6 +54,7 @@ export interface NormalizedFederationConfig {
|
|
|
30
54
|
exposes: Record<string, ExposeEntry>;
|
|
31
55
|
shared: NormalizedSharedExternalsConfig;
|
|
32
56
|
sharedMappings: PathToImport;
|
|
57
|
+
sharedMappingsConfig: NormalizedSharedMappingConfigs;
|
|
33
58
|
skip: PreparedSkipList;
|
|
34
59
|
chunks: boolean;
|
|
35
60
|
externals: string[];
|
|
@@ -2,6 +2,18 @@ export interface NfFileWatcherOptions {
|
|
|
2
2
|
onChange?: (path: string) => void;
|
|
3
3
|
pollIntervalMs?: number;
|
|
4
4
|
debounceMs?: number;
|
|
5
|
+
/** Drop events whose mtime is unchanged since the last one seen for that path.
|
|
6
|
+
* macOS FSEvents re-delivers 'changed' for recently edited files roughly every
|
|
7
|
+
* 30s; with a watch list of a few thousand sources that replay alone keeps a
|
|
8
|
+
* rebuild loop awake forever. Default: true. */
|
|
9
|
+
dedupeReplays?: boolean;
|
|
10
|
+
/** Grace period after a path's recorded mtime during which an event whose mtime
|
|
11
|
+
* and byte length both match still passes, covering a second save inside one
|
|
12
|
+
* mtime tick. The 2000 default is twice the coarsest mtime granularity in play
|
|
13
|
+
* (1s on gRPC-FUSE/NFS/WSL2 drvfs/HFS+); raise it for a filesystem coarser than
|
|
14
|
+
* that, or for a network mount whose server clock runs behind the client.
|
|
15
|
+
* See AGENTS.md "Replay dedupe". Default: 2000. */
|
|
16
|
+
replayGraceMs?: number;
|
|
5
17
|
}
|
|
6
18
|
interface AddPathsOptions {
|
|
7
19
|
poll?: boolean;
|
|
@@ -5,6 +5,8 @@ export interface Digest {
|
|
|
5
5
|
}
|
|
6
6
|
export interface StatInfo {
|
|
7
7
|
mtimeMs: number;
|
|
8
|
+
/** Byte length; 0 for directories. */
|
|
9
|
+
size: number;
|
|
8
10
|
isSymbolicLink: boolean;
|
|
9
11
|
}
|
|
10
12
|
export interface FileReaderPort {
|
|
@@ -29,6 +31,7 @@ export interface FileWriterPort {
|
|
|
29
31
|
export interface GlobPort {
|
|
30
32
|
globFiles(pattern: string, opts: {
|
|
31
33
|
cwd: string;
|
|
34
|
+
ignore?: string[];
|
|
32
35
|
}): string[];
|
|
33
36
|
}
|
|
34
37
|
export interface HashPort {
|
|
@@ -45,9 +48,9 @@ interface WatchOptions {
|
|
|
45
48
|
}
|
|
46
49
|
export interface WatchPort {
|
|
47
50
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
+
* `onEvent` receives the changed entry's filename relative to `path` (null when
|
|
52
|
+
* the platform omits it). Watching a file reports that file's own basename, so a
|
|
53
|
+
* caller that registered a file can ignore the argument.
|
|
51
54
|
*/
|
|
52
55
|
watch(path: string, opts: WatchOptions, onEvent: (filename: string | null) => void): WatchHandle;
|
|
53
56
|
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { WatchPort, FileReaderPort } from '../domain/utils/io-port.contract.js';
|
|
2
2
|
import type { NfFileWatcher, NfFileWatcherOptions } from '../domain/utils/file-watcher.contract.js';
|
|
3
3
|
export declare function createNfWatcher(options?: NfFileWatcherOptions): NfFileWatcher;
|
|
4
|
-
export declare function createNfWatcherCore(io: WatchPort & FileReaderPort, options?: NfFileWatcherOptions): NfFileWatcher;
|
|
5
|
-
|
|
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,29 +1,82 @@
|
|
|
1
|
-
import { join } from "path";
|
|
1
|
+
import { dirname, join } from "path";
|
|
2
2
|
import { nodeIo } from "./io/node-io-adapter.js";
|
|
3
3
|
import { logger } from "./logger.js";
|
|
4
|
-
import { toPosix } from "./path-patterns.js";
|
|
4
|
+
import { isUnderDir, toPosix } from "./path-patterns.js";
|
|
5
5
|
function createNfWatcher(options = {}) {
|
|
6
6
|
return createNfWatcherCore(nodeIo, options);
|
|
7
7
|
}
|
|
8
|
-
function createNfWatcherCore(io, options = {}) {
|
|
8
|
+
function createNfWatcherCore(io, options = {}, now = Date.now) {
|
|
9
9
|
const { onChange } = options;
|
|
10
10
|
const pollIntervalMs = options.pollIntervalMs ?? 300;
|
|
11
11
|
const debounceMs = options.debounceMs ?? 0;
|
|
12
|
+
const dedupeReplays = options.dedupeReplays ?? true;
|
|
13
|
+
const replayGraceMs = options.replayGraceMs ?? 2e3;
|
|
12
14
|
const watchers = /* @__PURE__ */ new Map();
|
|
13
15
|
const dirtyPaths = /* @__PURE__ */ new Set();
|
|
14
|
-
const
|
|
16
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
17
|
+
const trackedFiles = /* @__PURE__ */ new Set();
|
|
18
|
+
const fileDirWatchers = /* @__PURE__ */ new Map();
|
|
19
|
+
const dirKey = (p) => {
|
|
20
|
+
const posix = toPosix(p);
|
|
21
|
+
return posix.length > 1 ? posix.replace(/\/+$/, "") : posix;
|
|
22
|
+
};
|
|
23
|
+
const covers = (path, poll) => {
|
|
24
|
+
for (const [dir, watch] of watchers) {
|
|
25
|
+
if ((watch.poll || !poll) && isUnderDir(path, dir)) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
const supersede = (dir, poll) => {
|
|
30
|
+
for (const map of [watchers, fileDirWatchers]) {
|
|
31
|
+
for (const [key, watch] of map) {
|
|
32
|
+
if (watch.poll && !poll) continue;
|
|
33
|
+
if (map === watchers && key === dir) continue;
|
|
34
|
+
if (!isUnderDir(key, dir)) continue;
|
|
35
|
+
watch.handle.close();
|
|
36
|
+
map.delete(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const identityOf = (path) => {
|
|
41
|
+
let stat = io.stat(path);
|
|
42
|
+
if (stat?.isSymbolicLink) stat = io.stat(io.realpath(path));
|
|
43
|
+
return stat ? { mtimeMs: stat.mtimeMs, size: stat.size } : null;
|
|
44
|
+
};
|
|
45
|
+
const isReplay = (path, at) => {
|
|
46
|
+
const current = identityOf(path);
|
|
47
|
+
if (!current) {
|
|
48
|
+
lastSeen.delete(path);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const previous = lastSeen.get(path);
|
|
52
|
+
lastSeen.set(path, current);
|
|
53
|
+
if (!previous || previous.mtimeMs !== current.mtimeMs || previous.size !== current.size) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return at - current.mtimeMs >= replayGraceMs;
|
|
57
|
+
};
|
|
58
|
+
let watchFailures = 0;
|
|
59
|
+
const watchFailed = (path) => {
|
|
60
|
+
if (++watchFailures > 1) return logger.debug(`Could not watch path '${path}'.`);
|
|
61
|
+
logger.warn(
|
|
62
|
+
`Could not watch '${path}'. Changes there will not trigger a rebuild; run with verbose logging to see any further watch failures.`
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
const deliver = (path, at) => {
|
|
66
|
+
if (dedupeReplays && isReplay(path, at)) return;
|
|
67
|
+
dirtyPaths.add(path);
|
|
15
68
|
if (onChange) onChange(path);
|
|
16
|
-
else dirtyPaths.add(path);
|
|
17
69
|
};
|
|
18
|
-
const pending = /* @__PURE__ */ new
|
|
70
|
+
const pending = /* @__PURE__ */ new Map();
|
|
19
71
|
let flushTimer;
|
|
20
72
|
const flush = () => {
|
|
21
|
-
for (const p of pending) deliver(p);
|
|
73
|
+
for (const [p, at] of pending) deliver(p, at);
|
|
22
74
|
pending.clear();
|
|
23
75
|
};
|
|
24
76
|
const notify = (path) => {
|
|
25
|
-
|
|
26
|
-
|
|
77
|
+
const at = now();
|
|
78
|
+
if (debounceMs <= 0) return deliver(path, at);
|
|
79
|
+
if (!pending.has(path)) pending.set(path, at);
|
|
27
80
|
if (flushTimer) clearTimeout(flushTimer);
|
|
28
81
|
flushTimer = setTimeout(flush, debounceMs);
|
|
29
82
|
flushTimer.unref?.();
|
|
@@ -31,16 +84,47 @@ function createNfWatcherCore(io, options = {}) {
|
|
|
31
84
|
return {
|
|
32
85
|
addPaths(paths, opts) {
|
|
33
86
|
const list = typeof paths === "string" ? [paths] : [...paths];
|
|
34
|
-
const
|
|
87
|
+
const shouldPoll = !!opts?.poll;
|
|
88
|
+
const poll = shouldPoll ? { intervalMs: pollIntervalMs } : void 0;
|
|
35
89
|
for (const p of list) {
|
|
36
|
-
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;
|
|
37
117
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
118
|
+
fileDirWatchers.set(dir, {
|
|
119
|
+
handle: io.watch(dir, { recursive: false, poll }, (filename) => {
|
|
120
|
+
if (!filename) return;
|
|
121
|
+
const changed = toPosix(join(dir, filename));
|
|
122
|
+
if (trackedFiles.has(changed)) notify(changed);
|
|
123
|
+
}),
|
|
124
|
+
poll: shouldPoll
|
|
125
|
+
});
|
|
42
126
|
} catch {
|
|
43
|
-
|
|
127
|
+
watchFailed(dir);
|
|
44
128
|
}
|
|
45
129
|
}
|
|
46
130
|
},
|
|
@@ -49,15 +133,24 @@ function createNfWatcherCore(io, options = {}) {
|
|
|
49
133
|
mutate: (fn) => fn(dirtyPaths),
|
|
50
134
|
async close() {
|
|
51
135
|
if (flushTimer) clearTimeout(flushTimer);
|
|
52
|
-
for (const handle of watchers.values()) {
|
|
136
|
+
for (const { handle } of [...watchers.values(), ...fileDirWatchers.values()]) {
|
|
53
137
|
handle.close();
|
|
54
138
|
}
|
|
55
139
|
watchers.clear();
|
|
140
|
+
fileDirWatchers.clear();
|
|
141
|
+
trackedFiles.clear();
|
|
142
|
+
lastSeen.clear();
|
|
143
|
+
watchFailures = 0;
|
|
56
144
|
}
|
|
57
145
|
};
|
|
58
146
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
147
|
+
function toPaths(sources) {
|
|
148
|
+
if (Array.isArray(sources)) return sources;
|
|
149
|
+
const cache = sources;
|
|
150
|
+
return typeof cache.keys === "function" ? cache.keys() : sources;
|
|
151
|
+
}
|
|
152
|
+
function syncNfFileWatcher(watcher, sources, linkedDirs = []) {
|
|
153
|
+
const files = [...toPaths(sources)].filter((k) => !k.includes("node_modules"));
|
|
61
154
|
if (files.length) watcher.addPaths(files);
|
|
62
155
|
if (linkedDirs.length) watcher.addPaths(linkedDirs, { poll: true });
|
|
63
156
|
}
|