@softarc/native-federation 4.4.1 → 4.5.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 +8 -4
- 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 +15 -6
- 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/build-for-federation.js +2 -0
- package/dist/lib/core/build/resolve-shared-dirs.d.ts +9 -3
- package/dist/lib/core/build/resolve-shared-dirs.js +52 -12
- package/dist/lib/core/normalize-options.js +12 -6
- package/dist/lib/domain/config/external-config.contract.d.ts +14 -2
- package/dist/lib/domain/config/federation-config.contract.d.ts +3 -3
- 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 +2 -2
package/README.md
CHANGED
|
@@ -363,7 +363,7 @@ shared: share({
|
|
|
363
363
|
})
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
-
Finally, it's also possible to
|
|
366
|
+
Finally, it's also possible to exempt the secondary entry points of a specific external from the `ignoreUnusedDeps` feature, for example when sharing a whole suite of interconnected external dependencies like @angular/core. This can be handy when you want to avoid the chance of cross-version secondary entrypoints being used by the different micro frontends. E.g. mfe1 uses @angular/core v20.1.0 and mfe2 uses @angular/core/rxjs-interop v20.0.8, then you might want consistent use of v20.1.0 so rxjs-interop should be exported by mfe1. The `keepAll` prop allows you to enforce this:
|
|
367
367
|
|
|
368
368
|
```typescript
|
|
369
369
|
shared: share({
|
|
@@ -377,6 +377,10 @@ shared: share({
|
|
|
377
377
|
})
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
`keepAll` is read per **package family**, not per entry point: every entry point of @angular/core is published as long as _something_ still reaches @angular/core, but a package nothing imports at all is pruned anyway. That is what keeps the feature meaningful when `keepAll` is applied to every package at once — it exempts the secondaries from reachability, not the package itself. For a package with no secondary entry points the family is the package itself, so the flag changes nothing there. Use `ignoreUnusedDeps: false` to publish everything unconditionally.
|
|
381
|
+
|
|
382
|
+
Note that mapped paths read the same flag differently: there, `keepAll` opts the mapping out of reachability entirely (see [Keeping mappings that nothing imports](#keeping-mappings-that-nothing-imports)).
|
|
383
|
+
|
|
380
384
|
The API for configuring and using Native Federation is very similar to the one provided by our Module Federation plugin [@angular-architects/module-federation](https://www.npmjs.com/package/@angular-architects/module-federation). Hence, most of the articles on it are also valid for Native Federation.
|
|
381
385
|
|
|
382
386
|
### Sharing
|
|
@@ -487,7 +491,7 @@ module.exports = withNativeFederation({
|
|
|
487
491
|
});
|
|
488
492
|
```
|
|
489
493
|
|
|
490
|
-
- `keepAll` keeps the mapping even when nothing imports it
|
|
494
|
+
- `keepAll` keeps the mapping even when nothing imports it, and on a mapping a bare `includeSecondaries: true` means the same thing — a mapping has no secondary entry points, so the flag can only mean "exempt from reachability". A shared package reads it differently: `true` is the default there and only means "share the secondaries", so `{ keepAll: true }` is the only spelling that affects pruning — and even then the package itself still has to be reached.
|
|
491
495
|
- `resolveGlob` is additionally required for **wildcard** mappings. A wildcard is a pattern rather than an entry point, and normally only the reachability scan turns it into concrete files; `resolveGlob` expands it against the filesystem instead. Without it, a wildcard mapping is dropped with a warning.
|
|
492
496
|
|
|
493
497
|
An expanded wildcard is named by the same rule the reachability scan uses, so `libs/ui/*` matching `libs/ui/button/index.ts` is shared as `@my-org/ui/button`.
|
|
@@ -496,7 +500,7 @@ Expansion only accepts **entry points**. A glob cannot tell a library's public s
|
|
|
496
500
|
|
|
497
501
|
That restriction is not cosmetic: **only barrel imports can be shared as a mapped path.** A mapped path is advertised under its import specifier and marked external, so the specifier has to be one a browser import map can resolve, and a dot in the last segment does not resolve (see [vitejs/vite#21036](https://github.com/vitejs/vite/issues/21036)).
|
|
498
502
|
|
|
499
|
-
So the rule is simply
|
|
503
|
+
So the rule is simply _would this end up in `remoteEntry.json`?_ If it would, a non-barrel specifier fails the build:
|
|
500
504
|
|
|
501
505
|
```
|
|
502
506
|
Invalid 'shared mappings' config. Only barrel imports can be shared as a sharedMapping:
|
|
@@ -693,7 +697,7 @@ For a zero-build integration, declare your remotes in a manifest and include the
|
|
|
693
697
|
</script>
|
|
694
698
|
|
|
695
699
|
<!-- Include the orchestrator -->
|
|
696
|
-
<script src="https://unpkg.com/@softarc/native-federation-orchestrator@4.
|
|
700
|
+
<script src="https://unpkg.com/@softarc/native-federation-orchestrator@4.6.0/quickstart.mjs"></script>
|
|
697
701
|
```
|
|
698
702
|
|
|
699
703
|
The `mfe-loader-available` event signals that the orchestrator has fetched the
|
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
|
}
|
|
@@ -3,17 +3,26 @@ import {
|
|
|
3
3
|
expandWildcardMapping,
|
|
4
4
|
isWildcardMapping
|
|
5
5
|
} from "./expand-mappings.js";
|
|
6
|
+
import { inferPackageFromSecondary } from "../utils/normalize.js";
|
|
6
7
|
import { logger } from "../utils/logger.js";
|
|
7
8
|
function removeUnusedDeps(usedDependencies, config, ctx) {
|
|
8
|
-
const
|
|
9
|
+
const usedPackages = new Set([...usedDependencies.external].map(inferPackageFromSecondary));
|
|
10
|
+
const filteredDependencies = Object.entries(config.shared).filter(
|
|
11
|
+
([shared, meta]) => meta.includeSecondaries ? usedPackages.has(inferPackageFromSecondary(shared)) : usedDependencies.external.has(shared)
|
|
12
|
+
).reduce((acc, [shared, meta]) => ({ ...acc, [shared]: meta }), {});
|
|
13
|
+
const sharedMappings = withoutSkippedMappings(
|
|
14
|
+
{ ...keptMappings(config, ctx), ...usedDependencies.internal },
|
|
15
|
+
config.skip
|
|
16
|
+
);
|
|
17
|
+
if (Object.keys(config.sharedMappings).length > 0 && Object.keys(sharedMappings).length === 0) {
|
|
18
|
+
logger.warn(
|
|
19
|
+
"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."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
9
22
|
return {
|
|
10
23
|
...config,
|
|
11
24
|
shared: filteredDependencies,
|
|
12
|
-
|
|
13
|
-
sharedMappings: withoutSkippedMappings(
|
|
14
|
-
{ ...keptMappings(config, ctx), ...usedDependencies.internal },
|
|
15
|
-
config.skip
|
|
16
|
-
)
|
|
25
|
+
sharedMappings
|
|
17
26
|
};
|
|
18
27
|
}
|
|
19
28
|
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
|
|
@@ -6,9 +6,11 @@ import { logger } from "../../utils/logger.js";
|
|
|
6
6
|
import { AbortedError } from "../../utils/errors.js";
|
|
7
7
|
import { addExternalsToCache } from "../cache/federation-cache.js";
|
|
8
8
|
import { planSharedBundles } from "./shared-bundle-plan.js";
|
|
9
|
+
import { hintUnwatchedLinkedDeps } from "./resolve-shared-dirs.js";
|
|
9
10
|
async function buildForFederation(config, fedOptions, externals, signal) {
|
|
10
11
|
logger.info("Building federation artifacts");
|
|
11
12
|
logger.notice("Skip packages you don't want to share in your federation config");
|
|
13
|
+
hintUnwatchedLinkedDeps(config, fedOptions);
|
|
12
14
|
await executeSharedBundlePlans(planSharedBundles(config, externals), config, fedOptions, signal);
|
|
13
15
|
const start = process.hrtime();
|
|
14
16
|
const artifactInfo = await bundleExposedAndMappings(
|
|
@@ -6,8 +6,11 @@ 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
|
+
/** Names the npm-linked shared packages a watching build leaves unwatched -- see
|
|
12
|
+
* AGENTS.md "What to watch". Advisory only, so a resolution failure must not fail the build. */
|
|
13
|
+
export declare function hintUnwatchedLinkedDeps(config: NormalizedFederationConfig, fedOptions: NormalizedFederationOptions, io?: FileReaderPort, repo?: PackageJsonRepository): void;
|
|
11
14
|
/**
|
|
12
15
|
* Source directories of the workspace libs in `config.sharedMappings` — the watch set
|
|
13
16
|
* for mappings, derived from config alone rather than from a bundler cache.
|
|
@@ -24,9 +27,12 @@ export declare function linkedSharedDirs(config: NormalizedFederationConfig, fed
|
|
|
24
27
|
* they are source trees, not the dist output `linkedSharedDirs` exists for.
|
|
25
28
|
*/
|
|
26
29
|
export declare function sharedMappingDirs(config: NormalizedFederationConfig): string[];
|
|
27
|
-
/** Per-key content signal (max mtime of the resolved dir) for
|
|
30
|
+
/** Per-key content signal (max mtime of the resolved dir) for linked checkouts only.
|
|
28
31
|
* Registry deps get no signal, keeping their checksum version-only. (Every key is
|
|
29
|
-
* still resolved: detecting the symlink requires the realpath + lstat.)
|
|
32
|
+
* still resolved: detecting the symlink requires the realpath + lstat.)
|
|
33
|
+
*
|
|
34
|
+
* Deliberately not gated on `watchLinkedDeps`: that option decides whether an edit is
|
|
35
|
+
* noticed live, never whether the next build is correct. */
|
|
30
36
|
export declare function linkedContentSignals(keys: string[], folder: string, io?: FileReaderPort, repo?: PackageJsonRepository): Record<string, string>;
|
|
31
37
|
/** `config.shared` keys whose package directory contains at least one modified file. */
|
|
32
38
|
export declare function affectedSharedKeys(modifiedFiles: readonly string[], dirs: Map<string, string>, io?: FileReaderPort): Set<string>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import { nodeIo } from "../../utils/io/node-io-adapter.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import {
|
|
5
|
+
getPkgFolder,
|
|
6
|
+
sharedPackageJsonRepository
|
|
7
|
+
} from "../../utils/io/package-json-repository.js";
|
|
8
|
+
import { isOutsideNodeModules, isUnderDir, toPosix } from "../../utils/path-patterns.js";
|
|
5
9
|
const folderOf = (fedOptions) => fedOptions.packageJson ? path.dirname(fedOptions.packageJson) : fedOptions.workspaceRoot;
|
|
6
10
|
function resolveEntries(keys, folder, io, repo) {
|
|
7
11
|
const out = [];
|
|
@@ -9,10 +13,12 @@ function resolveEntries(keys, folder, io, repo) {
|
|
|
9
13
|
const pkgJsonPath = repo.findDepPackageJson(key, folder);
|
|
10
14
|
if (!pkgJsonPath) continue;
|
|
11
15
|
const pkgDir = path.dirname(pkgJsonPath);
|
|
16
|
+
const realDir = toPosix(io.realpath(pkgDir));
|
|
12
17
|
out.push({
|
|
13
18
|
key,
|
|
14
|
-
realDir
|
|
15
|
-
|
|
19
|
+
realDir,
|
|
20
|
+
// pnpm's default linker symlinks every dep, so the link alone proves nothing.
|
|
21
|
+
isLinkedCheckout: !!io.stat(pkgDir)?.isSymbolicLink && isOutsideNodeModules(realDir)
|
|
16
22
|
});
|
|
17
23
|
}
|
|
18
24
|
return out;
|
|
@@ -22,24 +28,50 @@ function resolveSharedPackageDirs(config, fedOptions, io = nodeIo, repo = shared
|
|
|
22
28
|
return new Map(entries.map((e) => [e.key, e.realDir]));
|
|
23
29
|
}
|
|
24
30
|
function linkedSharedDirs(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
|
|
31
|
+
if (!fedOptions.watchLinkedDeps) return [];
|
|
25
32
|
const entries = resolveEntries(Object.keys(config.shared), folderOf(fedOptions), io, repo);
|
|
26
|
-
return [...new Set(entries.filter((e) => e.
|
|
33
|
+
return [...new Set(entries.filter((e) => e.isLinkedCheckout).map((e) => e.realDir))];
|
|
34
|
+
}
|
|
35
|
+
function hintUnwatchedLinkedDeps(config, fedOptions, io = nodeIo, repo = sharedPackageJsonRepository) {
|
|
36
|
+
if (!fedOptions.watch || fedOptions.watchLinkedDeps) return;
|
|
37
|
+
try {
|
|
38
|
+
const names = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const entry of resolveEntries(
|
|
40
|
+
Object.keys(config.shared),
|
|
41
|
+
folderOf(fedOptions),
|
|
42
|
+
io,
|
|
43
|
+
repo
|
|
44
|
+
)) {
|
|
45
|
+
if (entry.isLinkedCheckout && !names.has(entry.realDir)) {
|
|
46
|
+
names.set(entry.realDir, getPkgFolder(entry.key));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (names.size === 0) return;
|
|
50
|
+
logger.notice(
|
|
51
|
+
`Detected npm-linked shared packages: ${[...names.values()].join(", ")}. Set 'watchLinkedDeps' to true to rebuild when they change.`
|
|
52
|
+
);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
27
55
|
}
|
|
28
56
|
function sharedMappingDirs(config) {
|
|
29
|
-
const dirs = Object.keys(config.sharedMappings).map((entryPoint) => toPosix(path.dirname(entryPoint))).filter(
|
|
57
|
+
const dirs = Object.keys(config.sharedMappings).map((entryPoint) => toPosix(path.dirname(entryPoint))).filter(isOutsideNodeModules);
|
|
30
58
|
return [...new Set(dirs)];
|
|
31
59
|
}
|
|
32
60
|
function maxMtime(io, dir) {
|
|
33
61
|
let max = 0;
|
|
62
|
+
const bump = (s) => {
|
|
63
|
+
if (s && s.mtimeMs > max) max = s.mtimeMs;
|
|
64
|
+
};
|
|
34
65
|
const walk = (d) => {
|
|
35
66
|
for (const name of io.readDir(d)) {
|
|
36
67
|
const full = path.join(d, name);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (s && s.mtimeMs > max) max = s.mtimeMs;
|
|
68
|
+
const entry = io.stat(full);
|
|
69
|
+
if (entry?.isSymbolicLink) {
|
|
70
|
+
if (!io.isDirectory(full)) bump(io.stat(io.realpath(full)));
|
|
71
|
+
continue;
|
|
42
72
|
}
|
|
73
|
+
if (io.isDirectory(full)) walk(full);
|
|
74
|
+
else bump(entry);
|
|
43
75
|
}
|
|
44
76
|
};
|
|
45
77
|
walk(dir);
|
|
@@ -47,8 +79,15 @@ function maxMtime(io, dir) {
|
|
|
47
79
|
}
|
|
48
80
|
function linkedContentSignals(keys, folder, io = nodeIo, repo = sharedPackageJsonRepository) {
|
|
49
81
|
const signals = {};
|
|
82
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
50
83
|
for (const entry of resolveEntries(keys, folder, io, repo)) {
|
|
51
|
-
if (entry.
|
|
84
|
+
if (!entry.isLinkedCheckout) continue;
|
|
85
|
+
let signal = byDir.get(entry.realDir);
|
|
86
|
+
if (signal === void 0) {
|
|
87
|
+
signal = String(maxMtime(io, entry.realDir));
|
|
88
|
+
byDir.set(entry.realDir, signal);
|
|
89
|
+
}
|
|
90
|
+
signals[entry.key] = signal;
|
|
52
91
|
}
|
|
53
92
|
return signals;
|
|
54
93
|
}
|
|
@@ -63,6 +102,7 @@ function affectedSharedKeys(modifiedFiles, dirs, io = nodeIo) {
|
|
|
63
102
|
}
|
|
64
103
|
export {
|
|
65
104
|
affectedSharedKeys,
|
|
105
|
+
hintUnwatchedLinkedDeps,
|
|
66
106
|
linkedContentSignals,
|
|
67
107
|
linkedSharedDirs,
|
|
68
108
|
resolveSharedPackageDirs,
|
|
@@ -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,14 +19,16 @@ 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
29
|
const projectName = resolveProjectName(options.projectName ?? config.name);
|
|
27
30
|
const suppliedCache = cache ?? createFederationCache(
|
|
28
|
-
getDefaultCachePath(
|
|
31
|
+
getDefaultCachePath(workspaceRoot)
|
|
29
32
|
);
|
|
30
33
|
const federationCache = {
|
|
31
34
|
...suppliedCache,
|
|
@@ -34,9 +37,12 @@ async function normalizeFederationOptionsCore(deps, options, cache) {
|
|
|
34
37
|
};
|
|
35
38
|
const normalizedOptions = {
|
|
36
39
|
...options,
|
|
40
|
+
workspaceRoot,
|
|
41
|
+
...packageJson && { packageJson },
|
|
37
42
|
entryPoints: options.entryPoints ?? Object.values(config.exposes ?? {}).map((e) => e.file),
|
|
38
43
|
projectName,
|
|
39
44
|
cacheExternalArtifacts: options.cacheExternalArtifacts ?? true,
|
|
45
|
+
watchLinkedDeps: options.watchLinkedDeps ?? false,
|
|
40
46
|
federationCache
|
|
41
47
|
};
|
|
42
48
|
const nothingShared = Object.keys(config.shared).length === 0 && Object.keys(config.sharedMappings).length === 0;
|
|
@@ -44,21 +50,21 @@ async function normalizeFederationOptionsCore(deps, options, cache) {
|
|
|
44
50
|
logger.debug("Nothing is shared, skipping the used dependency scan.");
|
|
45
51
|
} else if (config.features.ignoreUnusedDeps) {
|
|
46
52
|
const getUsedDeps = (deps.usedDependenciesFactory ?? getUsedDependenciesFactory)(
|
|
47
|
-
|
|
53
|
+
workspaceRoot,
|
|
48
54
|
options.entryPoints
|
|
49
55
|
);
|
|
50
56
|
config = removeUnusedDeps(getUsedDeps(config), config, {
|
|
51
57
|
io: deps.io,
|
|
52
|
-
workspaceRoot
|
|
58
|
+
workspaceRoot
|
|
53
59
|
});
|
|
54
60
|
logger.info("Removed unused dependencies.");
|
|
55
61
|
logger.debug(
|
|
56
|
-
'
|
|
62
|
+
'Keep everything with "ignoreUnusedDeps: false", or one mapping with "includeSecondaries: {keepAll: true}". On a shared package that flag only keeps the secondaries of a package something still imports.'
|
|
57
63
|
);
|
|
58
64
|
} else {
|
|
59
65
|
config.sharedMappings = expandOrDropWildcards(config, {
|
|
60
66
|
io: deps.io,
|
|
61
|
-
workspaceRoot
|
|
67
|
+
workspaceRoot
|
|
62
68
|
});
|
|
63
69
|
}
|
|
64
70
|
assertBarrelMappings(config.sharedMappings);
|
|
@@ -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
|
};
|
|
@@ -14,9 +14,9 @@ export type SharedMappingConfigs = Record<string, ExternalConfig>;
|
|
|
14
14
|
* have nothing to select. `requiredVersion` and `version` stay optional because their
|
|
15
15
|
* defaults are read from the mapped lib's package.json at build time.
|
|
16
16
|
*
|
|
17
|
-
* `includeSecondaries` collapses to a boolean
|
|
18
|
-
*
|
|
19
|
-
* because a mapping has no
|
|
17
|
+
* `includeSecondaries` collapses to a boolean meaning "exempt from `ignoreUnusedDeps`
|
|
18
|
+
* pruning" — stronger than on a shared external, where it only exempts the secondaries.
|
|
19
|
+
* `resolveGlob` is lifted out of it because a mapping has no secondaries to apply it to.
|
|
20
20
|
*/
|
|
21
21
|
export interface NormalizedMappingConfig {
|
|
22
22
|
singleton: boolean;
|
|
@@ -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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softarc/native-federation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"packageManager": "pnpm@11.18.0",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"eslint": "^10.4.1",
|
|
27
27
|
"globals": "^17.3.0",
|
|
28
28
|
"jiti": "^2.6.1",
|
|
29
|
-
"jsdom": "^
|
|
29
|
+
"jsdom": "^30.0.0",
|
|
30
30
|
"knip": "^6.26.0",
|
|
31
31
|
"prettier": "^3.9.4",
|
|
32
32
|
"tslib": "^2.3.0",
|