@softarc/native-federation 4.3.0 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/internal/browser.d.ts +6 -0
- package/dist/internal/browser.js +9 -0
- package/dist/internal.d.ts +9 -6
- package/dist/internal.js +21 -6
- package/dist/lib/config/with-native-federation.js +2 -1
- package/dist/lib/core/build/build-for-federation.d.ts +7 -0
- package/dist/lib/core/build/build-for-federation.js +32 -134
- package/dist/lib/core/build/bundle-shared.d.ts +7 -0
- package/dist/lib/core/build/bundle-shared.js +38 -7
- package/dist/lib/core/build/rebuild-for-federation.d.ts +8 -0
- package/dist/lib/core/build/rebuild-for-federation.js +26 -0
- package/dist/lib/core/build/resolve-shared-dirs.d.ts +16 -0
- package/dist/lib/core/build/resolve-shared-dirs.js +66 -0
- package/dist/lib/core/build/shared-bundle-plan.d.ts +22 -0
- package/dist/lib/core/build/shared-bundle-plan.js +70 -0
- package/dist/lib/core/build/synthesize-cjs-exports.d.ts +12 -0
- package/dist/lib/core/build/synthesize-cjs-exports.js +58 -0
- package/dist/lib/core/cache/cache-persistence.d.ts +2 -2
- package/dist/lib/core/cache/cache-persistence.js +7 -4
- package/dist/lib/core/federation-builder.d.ts +2 -1
- package/dist/lib/core/federation-builder.js +3 -0
- package/dist/lib/core/output/densify-externals.d.ts +1 -0
- package/dist/lib/core/output/densify-externals.js +21 -4
- package/dist/lib/core/output/write-federation-info.js +1 -1
- package/dist/lib/domain/config/federation-config.contract.d.ts +2 -0
- package/dist/lib/domain/core/federation-info.contract.d.ts +1 -0
- package/dist/lib/domain/utils/file-watcher.contract.d.ts +7 -1
- package/dist/lib/domain/utils/io-port.contract.d.ts +14 -3
- package/dist/lib/utils/file-watcher.d.ts +1 -1
- package/dist/lib/utils/file-watcher.js +23 -6
- package/dist/lib/utils/io/node-io-adapter.js +77 -21
- package/dist/lib/utils/package/cjs-named-exports.d.ts +21 -0
- package/dist/lib/utils/package/cjs-named-exports.js +36 -0
- package/dist/lib/utils/package/esm-detection.d.ts +13 -0
- package/dist/lib/utils/package/esm-detection.js +29 -0
- package/package.json +9 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { inferPackageFromSecondary, normalizePackageName } from "../../utils/normalize.js";
|
|
2
|
+
function splitShared(shared) {
|
|
3
|
+
const sharedServer = {};
|
|
4
|
+
const sharedBrowser = {};
|
|
5
|
+
const separateBrowser = {};
|
|
6
|
+
const separateServer = {};
|
|
7
|
+
for (const key in shared) {
|
|
8
|
+
const obj = shared[key];
|
|
9
|
+
if (obj?.platform === "node") {
|
|
10
|
+
if (obj.build === "default") sharedServer[key] = obj;
|
|
11
|
+
else separateServer[key] = obj;
|
|
12
|
+
} else if (obj?.platform === "browser") {
|
|
13
|
+
if (obj.build === "default") sharedBrowser[key] = obj;
|
|
14
|
+
else separateBrowser[key] = obj;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { sharedBrowser, sharedServer, separateBrowser, separateServer };
|
|
18
|
+
}
|
|
19
|
+
function planSeparate(separate, platform, externals) {
|
|
20
|
+
const grouped = {};
|
|
21
|
+
for (const [key, shared] of Object.entries(separate)) {
|
|
22
|
+
const packageName = shared.build === "separate" ? key : inferPackageFromSecondary(key);
|
|
23
|
+
if (!grouped[packageName]) grouped[packageName] = { chunks: shared.chunks, entries: {} };
|
|
24
|
+
grouped[packageName].entries[key] = shared;
|
|
25
|
+
}
|
|
26
|
+
return Object.entries(grouped).map(([packageName, group]) => ({
|
|
27
|
+
bundleName: `${platform}-${normalizePackageName(packageName)}`,
|
|
28
|
+
platform,
|
|
29
|
+
chunks: group.chunks,
|
|
30
|
+
entries: group.entries,
|
|
31
|
+
externals: externals.filter((e) => !e.startsWith(packageName)),
|
|
32
|
+
keys: Object.keys(group.entries),
|
|
33
|
+
kind: "separate"
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
function planSharedBundles(config, externals) {
|
|
37
|
+
const { sharedBrowser, sharedServer, separateBrowser, separateServer } = splitShared(
|
|
38
|
+
config.shared
|
|
39
|
+
);
|
|
40
|
+
const plans = [];
|
|
41
|
+
if (Object.keys(sharedBrowser).length > 0) {
|
|
42
|
+
plans.push({
|
|
43
|
+
bundleName: "browser-shared",
|
|
44
|
+
platform: "browser",
|
|
45
|
+
chunks: config.chunks,
|
|
46
|
+
entries: sharedBrowser,
|
|
47
|
+
externals,
|
|
48
|
+
keys: Object.keys(sharedBrowser),
|
|
49
|
+
kind: "shared"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (Object.keys(sharedServer).length > 0) {
|
|
53
|
+
plans.push({
|
|
54
|
+
bundleName: "node-shared",
|
|
55
|
+
platform: "node",
|
|
56
|
+
chunks: config.chunks,
|
|
57
|
+
entries: sharedServer,
|
|
58
|
+
externals,
|
|
59
|
+
keys: Object.keys(sharedServer),
|
|
60
|
+
kind: "shared"
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
plans.push(...planSeparate(separateBrowser, "browser", externals));
|
|
64
|
+
plans.push(...planSeparate(separateServer, "node", externals));
|
|
65
|
+
return plans;
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
planSharedBundles,
|
|
69
|
+
splitShared
|
|
70
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IoPort } from '../../domain/utils/io-port.contract.js';
|
|
2
|
+
import type { PackageInfo } from '../../domain/utils/package-json.contract.js';
|
|
3
|
+
/** Evaluates a module at build time (Node `require`) and returns its value, or throws. */
|
|
4
|
+
export type ModuleEvaluator = (absPath: string) => unknown;
|
|
5
|
+
/**
|
|
6
|
+
* For a CommonJS shared external, `require()`s it, enumerates its runtime named
|
|
7
|
+
* exports, and writes a synthetic ESM entry re-exporting them.
|
|
8
|
+
*
|
|
9
|
+
* Returns the synthetic entry path, or `null` to keep the original entry (not a CJS
|
|
10
|
+
* candidate, no names to wrap, or a `require()` failure → default-only fallback).
|
|
11
|
+
*/
|
|
12
|
+
export declare function synthesizeCjsNamedExportsEntry(io: IoPort, evaluateModule: ModuleEvaluator, pi: PackageInfo, cachePath: string, outName: string): string | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { logger } from "../../utils/logger.js";
|
|
3
|
+
import { isCjsCandidate } from "../../utils/package/esm-detection.js";
|
|
4
|
+
import {
|
|
5
|
+
planCjsWrap,
|
|
6
|
+
buildSyntheticCjsEntry,
|
|
7
|
+
isEsmInteropError
|
|
8
|
+
} from "../../utils/package/cjs-named-exports.js";
|
|
9
|
+
const SYNTHETIC_DIR = ".nf-cjs-entries";
|
|
10
|
+
const warned = /* @__PURE__ */ new Set();
|
|
11
|
+
function nearestPackageType(io, fromDir) {
|
|
12
|
+
let dir = fromDir;
|
|
13
|
+
for (; ; ) {
|
|
14
|
+
const pkg = path.join(dir, "package.json");
|
|
15
|
+
if (io.exists(pkg)) {
|
|
16
|
+
try {
|
|
17
|
+
const type = JSON.parse(io.readText(pkg))?.type;
|
|
18
|
+
return type === "module" || type === "commonjs" ? type : void 0;
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) return void 0;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function synthesizeCjsNamedExportsEntry(io, evaluateModule, pi, cachePath, outName) {
|
|
28
|
+
const entryPoint = pi.entryPoint;
|
|
29
|
+
const candidate = isCjsCandidate({
|
|
30
|
+
esm: pi.esm,
|
|
31
|
+
entryPoint,
|
|
32
|
+
packageType: nearestPackageType(io, path.dirname(entryPoint)),
|
|
33
|
+
readSource: () => io.readText(entryPoint)
|
|
34
|
+
});
|
|
35
|
+
if (!candidate) return null;
|
|
36
|
+
let plan;
|
|
37
|
+
try {
|
|
38
|
+
plan = planCjsWrap(evaluateModule(entryPoint));
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (!isEsmInteropError(err) && !warned.has(entryPoint)) {
|
|
41
|
+
warned.add(entryPoint);
|
|
42
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
43
|
+
logger.warn(
|
|
44
|
+
`[native-federation] Could not enumerate named exports of "${entryPoint}" at build time (${detail}); falling back to default-only. Named imports from this package may fail across the module boundary.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!plan.wrap) return null;
|
|
50
|
+
const dir = path.join(cachePath, SYNTHETIC_DIR);
|
|
51
|
+
io.mkdirp(dir);
|
|
52
|
+
const syntheticPath = path.join(dir, outName);
|
|
53
|
+
io.writeText(syntheticPath, buildSyntheticCjsEntry(entryPoint, plan.keys));
|
|
54
|
+
return syntheticPath;
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
synthesizeCjsNamedExportsEntry
|
|
58
|
+
};
|
|
@@ -3,8 +3,8 @@ import type { ChunkInfo, IntegrityMap, SharedInfo } from '../../domain/core/fede
|
|
|
3
3
|
import type { FileReaderPort, FileWriterPort, HashPort } from '../../domain/utils/io-port.contract.js';
|
|
4
4
|
export declare const getDefaultCachePath: (workspaceRoot: string) => string;
|
|
5
5
|
export declare const getFilename: (title: string, dev?: boolean) => string;
|
|
6
|
-
export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string) => string;
|
|
7
|
-
export declare const getChecksumCore: (hash: HashPort, shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string) => string;
|
|
6
|
+
export declare const getChecksum: (shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, synthesizeCjsExports?: boolean, contentSignals?: Record<string, string>) => string;
|
|
7
|
+
export declare const getChecksumCore: (hash: HashPort, shared: Record<string, NormalizedExternalConfig>, dev: "1" | "0", builderVersion?: string, synthesizeCjsExports?: boolean, contentSignals?: Record<string, string>) => string;
|
|
8
8
|
export type CacheMetadata = {
|
|
9
9
|
checksum: string;
|
|
10
10
|
externals: SharedInfo[];
|
|
@@ -6,12 +6,15 @@ const getFilename = (title, dev) => {
|
|
|
6
6
|
const devSuffix = dev ? "-dev" : "";
|
|
7
7
|
return `${title}${devSuffix}.meta.json`;
|
|
8
8
|
};
|
|
9
|
-
const getChecksum = (shared, dev, builderVersion = "") => getChecksumCore(nodeIo, shared, dev, builderVersion);
|
|
10
|
-
const getChecksumCore = (hash, shared, dev, builderVersion = "") => {
|
|
9
|
+
const getChecksum = (shared, dev, builderVersion = "", synthesizeCjsExports = true, contentSignals = {}) => getChecksumCore(nodeIo, shared, dev, builderVersion, synthesizeCjsExports, contentSignals);
|
|
10
|
+
const getChecksumCore = (hash, shared, dev, builderVersion = "", synthesizeCjsExports = true, contentSignals = {}) => {
|
|
11
11
|
const denseExternals = Object.keys(shared).sort().reduce((clean, external) => {
|
|
12
|
-
|
|
12
|
+
const version = shared[external].version ? `@${shared[external].version}` : "";
|
|
13
|
+
const signal = contentSignals[external] ? `#${contentSignals[external]}` : "";
|
|
14
|
+
return clean + ":" + external + version + signal;
|
|
13
15
|
}, "deps");
|
|
14
|
-
|
|
16
|
+
const cjs = synthesizeCjsExports ? "1" : "0";
|
|
17
|
+
return hash.hash("sha256", denseExternals + `:dev=${dev}:builder=${builderVersion}:cjs=${cjs}`).hex();
|
|
15
18
|
};
|
|
16
19
|
const cacheEntryCore = (io, pathToCache, fileName) => {
|
|
17
20
|
const metadataFile = path.join(pathToCache, fileName);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FederationInfo } from '../domain/core/federation-info.contract.js';
|
|
2
2
|
import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
|
|
3
|
-
import { type FederationOptions } from '../domain/core/federation-options.contract.js';
|
|
3
|
+
import { type FederationOptions, type NormalizedFederationOptions } from '../domain/core/federation-options.contract.js';
|
|
4
4
|
import type { NFBuildAdapter } from '../domain/core/build-adapter.contract.js';
|
|
5
5
|
export interface BuildHelperParams {
|
|
6
6
|
options: FederationOptions;
|
|
@@ -19,5 +19,6 @@ export declare const federationBuilder: {
|
|
|
19
19
|
readonly federationInfo: FederationInfo;
|
|
20
20
|
readonly externals: string[];
|
|
21
21
|
readonly config: NormalizedFederationConfig;
|
|
22
|
+
readonly options: NormalizedFederationOptions;
|
|
22
23
|
};
|
|
23
24
|
export {};
|
|
@@ -6,3 +6,4 @@ import type { SharedInfo, DenseSharedInfo } from '../../domain/core/federation-i
|
|
|
6
6
|
* already-dense entries pass through unchanged.
|
|
7
7
|
*/
|
|
8
8
|
export declare function densifyExternals(shared: Array<SharedInfo | DenseSharedInfo>): Array<SharedInfo | DenseSharedInfo>;
|
|
9
|
+
export declare function toDenseSharedInfoFormat(shared: Array<SharedInfo | DenseSharedInfo>): DenseSharedInfo[];
|
|
@@ -3,17 +3,22 @@ import { inferPackageFromSecondary } from "../../utils/normalize.js";
|
|
|
3
3
|
function isDense(entry) {
|
|
4
4
|
return "entries" in entry;
|
|
5
5
|
}
|
|
6
|
-
function
|
|
7
|
-
return entry.packageName.startsWith(CHUNK_PREFIX
|
|
6
|
+
function isFlatChunk(entry) {
|
|
7
|
+
return entry.packageName.startsWith(CHUNK_PREFIX);
|
|
8
8
|
}
|
|
9
9
|
function densifyExternals(shared) {
|
|
10
10
|
const result = [];
|
|
11
11
|
const groupIndex = /* @__PURE__ */ new Map();
|
|
12
12
|
for (const entry of shared) {
|
|
13
|
-
if (isDense(entry)
|
|
13
|
+
if (isDense(entry)) {
|
|
14
14
|
result.push(entry);
|
|
15
15
|
continue;
|
|
16
16
|
}
|
|
17
|
+
if (isFlatChunk(entry)) {
|
|
18
|
+
const { outFileName, ...rest } = entry;
|
|
19
|
+
result.push({ ...rest, entries: { [entry.packageName]: outFileName } });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
17
22
|
const parent = inferPackageFromSecondary(entry.packageName);
|
|
18
23
|
const sig = JSON.stringify({
|
|
19
24
|
singleton: entry.singleton,
|
|
@@ -35,6 +40,7 @@ function densifyExternals(shared) {
|
|
|
35
40
|
if (entry.version !== void 0) dense.version = entry.version;
|
|
36
41
|
if (entry.shareScope !== void 0) dense.shareScope = entry.shareScope;
|
|
37
42
|
if (entry.bundle !== void 0) dense.bundle = entry.bundle;
|
|
43
|
+
if (entry.pool !== void 0) dense.pool = entry.pool;
|
|
38
44
|
if (entry.dev !== void 0) dense.dev = entry.dev;
|
|
39
45
|
groupIndex.set(key, result.length);
|
|
40
46
|
result.push(dense);
|
|
@@ -44,6 +50,17 @@ function densifyExternals(shared) {
|
|
|
44
50
|
}
|
|
45
51
|
return result;
|
|
46
52
|
}
|
|
53
|
+
function toDenseSharedInfoFormat(shared) {
|
|
54
|
+
return shared.map((external) => {
|
|
55
|
+
if ("entries" in external) return external;
|
|
56
|
+
const { outFileName, ...baseSharedInfoProps } = external;
|
|
57
|
+
return {
|
|
58
|
+
...baseSharedInfoProps,
|
|
59
|
+
entries: { [external.packageName]: outFileName }
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
47
63
|
export {
|
|
48
|
-
densifyExternals
|
|
64
|
+
densifyExternals,
|
|
65
|
+
toDenseSharedInfoFormat
|
|
49
66
|
};
|
|
@@ -6,7 +6,7 @@ function writeFederationInfoCore(io, federationInfo, fedOptions) {
|
|
|
6
6
|
fedOptions.outputPath,
|
|
7
7
|
"remoteEntry.json"
|
|
8
8
|
);
|
|
9
|
-
io.writeText(metaDataPath, JSON.stringify(federationInfo, null, 2));
|
|
9
|
+
io.writeText(metaDataPath, JSON.stringify({ $version: "v4", ...federationInfo }, null, 2));
|
|
10
10
|
}
|
|
11
11
|
function writeFederationInfo(federationInfo, fedOptions) {
|
|
12
12
|
writeFederationInfoCore(nodeIo, federationInfo, fedOptions);
|
|
@@ -21,6 +21,7 @@ export interface FederationConfig {
|
|
|
21
21
|
denseChunking?: boolean;
|
|
22
22
|
denseExternals?: boolean;
|
|
23
23
|
integrityHashes?: boolean;
|
|
24
|
+
synthesizeCjsExports?: boolean;
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
export interface NormalizedFederationConfig {
|
|
@@ -39,5 +40,6 @@ export interface NormalizedFederationConfig {
|
|
|
39
40
|
denseChunking: boolean;
|
|
40
41
|
denseExternals: boolean;
|
|
41
42
|
integrityHashes: boolean;
|
|
43
|
+
synthesizeCjsExports: boolean;
|
|
42
44
|
};
|
|
43
45
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export interface NfFileWatcherOptions {
|
|
2
2
|
onChange?: (path: string) => void;
|
|
3
|
+
pollIntervalMs?: number;
|
|
4
|
+
debounceMs?: number;
|
|
5
|
+
}
|
|
6
|
+
interface AddPathsOptions {
|
|
7
|
+
poll?: boolean;
|
|
3
8
|
}
|
|
4
9
|
export interface NfFileWatcher {
|
|
5
|
-
addPaths(paths: string | readonly string[]): void;
|
|
10
|
+
addPaths(paths: string | readonly string[], opts?: AddPathsOptions): void;
|
|
6
11
|
close(): Promise<void>;
|
|
7
12
|
get(): ReadonlySet<string>;
|
|
8
13
|
clear(): void;
|
|
9
14
|
mutate(fn: (dirtyPaths: Set<string>) => void): void;
|
|
10
15
|
}
|
|
16
|
+
export {};
|
|
@@ -3,6 +3,10 @@ export interface Digest {
|
|
|
3
3
|
hex(): string;
|
|
4
4
|
base64(): string;
|
|
5
5
|
}
|
|
6
|
+
export interface StatInfo {
|
|
7
|
+
mtimeMs: number;
|
|
8
|
+
isSymbolicLink: boolean;
|
|
9
|
+
}
|
|
6
10
|
export interface FileReaderPort {
|
|
7
11
|
readText(path: string): string;
|
|
8
12
|
readBytes(path: string): Uint8Array;
|
|
@@ -13,6 +17,8 @@ export interface FileReaderPort {
|
|
|
13
17
|
isDirectory(path: string): boolean;
|
|
14
18
|
/** Immediate child entry names (not full paths). Empty array on ENOENT, never throws. */
|
|
15
19
|
readDir(path: string): string[];
|
|
20
|
+
realpath(path: string): string;
|
|
21
|
+
stat(path: string): StatInfo | null;
|
|
16
22
|
}
|
|
17
23
|
export interface FileWriterPort {
|
|
18
24
|
writeText(path: string, data: string): void;
|
|
@@ -31,15 +37,20 @@ export interface HashPort {
|
|
|
31
37
|
export interface WatchHandle {
|
|
32
38
|
close(): void;
|
|
33
39
|
}
|
|
40
|
+
interface WatchOptions {
|
|
41
|
+
recursive: boolean;
|
|
42
|
+
poll?: {
|
|
43
|
+
intervalMs: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
34
46
|
export interface WatchPort {
|
|
35
47
|
/**
|
|
36
48
|
* For a recursive directory watch `onEvent` receives the changed entry's
|
|
37
49
|
* filename relative to `path`; for a file it receives the path itself (or
|
|
38
50
|
* null when the platform omits it).
|
|
39
51
|
*/
|
|
40
|
-
watch(path: string, opts:
|
|
41
|
-
recursive: boolean;
|
|
42
|
-
}, onEvent: (filename: string | null) => void): WatchHandle;
|
|
52
|
+
watch(path: string, opts: WatchOptions, onEvent: (filename: string | null) => void): WatchHandle;
|
|
43
53
|
}
|
|
44
54
|
export interface IoPort extends FileReaderPort, FileWriterPort, GlobPort, HashPort, WatchPort {
|
|
45
55
|
}
|
|
56
|
+
export {};
|
|
@@ -4,4 +4,4 @@ export declare function createNfWatcher(options?: NfFileWatcherOptions): NfFileW
|
|
|
4
4
|
export declare function createNfWatcherCore(io: WatchPort & FileReaderPort, options?: NfFileWatcherOptions): NfFileWatcher;
|
|
5
5
|
export declare function syncNfFileWatcher(watcher: NfFileWatcher, bundlerCache: {
|
|
6
6
|
keys(): IterableIterator<string>;
|
|
7
|
-
}): void;
|
|
7
|
+
}, linkedDirs?: readonly string[]): void;
|
|
@@ -7,22 +7,37 @@ function createNfWatcher(options = {}) {
|
|
|
7
7
|
}
|
|
8
8
|
function createNfWatcherCore(io, options = {}) {
|
|
9
9
|
const { onChange } = options;
|
|
10
|
+
const pollIntervalMs = options.pollIntervalMs ?? 300;
|
|
11
|
+
const debounceMs = options.debounceMs ?? 0;
|
|
10
12
|
const watchers = /* @__PURE__ */ new Map();
|
|
11
13
|
const dirtyPaths = /* @__PURE__ */ new Set();
|
|
12
|
-
const
|
|
14
|
+
const deliver = (path) => {
|
|
13
15
|
if (onChange) onChange(path);
|
|
14
16
|
else dirtyPaths.add(path);
|
|
15
17
|
};
|
|
18
|
+
const pending = /* @__PURE__ */ new Set();
|
|
19
|
+
let flushTimer;
|
|
20
|
+
const flush = () => {
|
|
21
|
+
for (const p of pending) deliver(p);
|
|
22
|
+
pending.clear();
|
|
23
|
+
};
|
|
24
|
+
const notify = (path) => {
|
|
25
|
+
if (debounceMs <= 0) return deliver(path);
|
|
26
|
+
pending.add(path);
|
|
27
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
28
|
+
flushTimer = setTimeout(flush, debounceMs);
|
|
29
|
+
flushTimer.unref?.();
|
|
30
|
+
};
|
|
16
31
|
return {
|
|
17
|
-
addPaths(paths) {
|
|
32
|
+
addPaths(paths, opts) {
|
|
18
33
|
const list = typeof paths === "string" ? [paths] : [...paths];
|
|
34
|
+
const poll = opts?.poll ? { intervalMs: pollIntervalMs } : void 0;
|
|
19
35
|
for (const p of list) {
|
|
20
36
|
if (watchers.has(p)) continue;
|
|
21
37
|
try {
|
|
22
|
-
const
|
|
23
|
-
const handle = isDir ? io.watch(p, { recursive: true }, (filename) => {
|
|
38
|
+
const handle = io.isDirectory(p) ? io.watch(p, { recursive: true, poll }, (filename) => {
|
|
24
39
|
if (filename) notify(toPosix(join(p, filename)));
|
|
25
|
-
}) : io.watch(p, { recursive: false }, () => notify(toPosix(p)));
|
|
40
|
+
}) : io.watch(p, { recursive: false, poll }, () => notify(toPosix(p)));
|
|
26
41
|
watchers.set(p, handle);
|
|
27
42
|
} catch {
|
|
28
43
|
logger.debug(`Could not watch path '${p}'.`);
|
|
@@ -33,6 +48,7 @@ function createNfWatcherCore(io, options = {}) {
|
|
|
33
48
|
clear: () => dirtyPaths.clear(),
|
|
34
49
|
mutate: (fn) => fn(dirtyPaths),
|
|
35
50
|
async close() {
|
|
51
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
36
52
|
for (const handle of watchers.values()) {
|
|
37
53
|
handle.close();
|
|
38
54
|
}
|
|
@@ -40,9 +56,10 @@ function createNfWatcherCore(io, options = {}) {
|
|
|
40
56
|
}
|
|
41
57
|
};
|
|
42
58
|
}
|
|
43
|
-
function syncNfFileWatcher(watcher, bundlerCache) {
|
|
59
|
+
function syncNfFileWatcher(watcher, bundlerCache, linkedDirs = []) {
|
|
44
60
|
const files = [...bundlerCache.keys()].filter((k) => !k.includes("node_modules"));
|
|
45
61
|
if (files.length) watcher.addPaths(files);
|
|
62
|
+
if (linkedDirs.length) watcher.addPaths(linkedDirs, { poll: true });
|
|
46
63
|
}
|
|
47
64
|
export {
|
|
48
65
|
createNfWatcher,
|
|
@@ -1,48 +1,64 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
2
3
|
import * as crypto from "crypto";
|
|
3
4
|
import fg from "fast-glob";
|
|
4
5
|
const nodeIo = {
|
|
5
|
-
readText(
|
|
6
|
-
return fs.readFileSync(
|
|
6
|
+
readText(path2) {
|
|
7
|
+
return fs.readFileSync(path2, "utf-8");
|
|
7
8
|
},
|
|
8
|
-
readBytes(
|
|
9
|
-
return fs.readFileSync(
|
|
9
|
+
readBytes(path2) {
|
|
10
|
+
return fs.readFileSync(path2);
|
|
10
11
|
},
|
|
11
|
-
exists(
|
|
12
|
-
return fs.existsSync(
|
|
12
|
+
exists(path2) {
|
|
13
|
+
return fs.existsSync(path2);
|
|
13
14
|
},
|
|
14
|
-
isFile(
|
|
15
|
+
isFile(path2) {
|
|
15
16
|
try {
|
|
16
|
-
return fs.statSync(
|
|
17
|
+
return fs.statSync(path2).isFile();
|
|
17
18
|
} catch {
|
|
18
19
|
return false;
|
|
19
20
|
}
|
|
20
21
|
},
|
|
21
|
-
isDirectory(
|
|
22
|
+
isDirectory(path2) {
|
|
22
23
|
try {
|
|
23
|
-
return fs.statSync(
|
|
24
|
+
return fs.statSync(path2).isDirectory();
|
|
24
25
|
} catch {
|
|
25
26
|
return false;
|
|
26
27
|
}
|
|
27
28
|
},
|
|
28
|
-
readDir(
|
|
29
|
+
readDir(path2) {
|
|
29
30
|
try {
|
|
30
|
-
return fs.readdirSync(
|
|
31
|
+
return fs.readdirSync(path2);
|
|
31
32
|
} catch {
|
|
32
33
|
return [];
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
realpath(path2) {
|
|
37
|
+
try {
|
|
38
|
+
return fs.realpathSync(path2);
|
|
39
|
+
} catch {
|
|
40
|
+
return path2;
|
|
41
|
+
}
|
|
37
42
|
},
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
stat(path2) {
|
|
44
|
+
try {
|
|
45
|
+
const s = fs.lstatSync(path2);
|
|
46
|
+
return { mtimeMs: s.mtimeMs, isSymbolicLink: s.isSymbolicLink() };
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
writeText(path2, data) {
|
|
52
|
+
fs.writeFileSync(path2, data, "utf-8");
|
|
53
|
+
},
|
|
54
|
+
mkdirp(path2) {
|
|
55
|
+
fs.mkdirSync(path2, { recursive: true });
|
|
40
56
|
},
|
|
41
57
|
copyFile(from, to) {
|
|
42
58
|
fs.copyFileSync(from, to);
|
|
43
59
|
},
|
|
44
|
-
remove(
|
|
45
|
-
fs.unlinkSync(
|
|
60
|
+
remove(path2) {
|
|
61
|
+
fs.unlinkSync(path2);
|
|
46
62
|
},
|
|
47
63
|
globFiles(pattern, opts) {
|
|
48
64
|
return fg.sync(pattern, { cwd: opts.cwd, onlyFiles: true, deep: Infinity });
|
|
@@ -54,15 +70,55 @@ const nodeIo = {
|
|
|
54
70
|
base64: () => sum.digest("base64")
|
|
55
71
|
};
|
|
56
72
|
},
|
|
57
|
-
watch(
|
|
73
|
+
watch(watchPath, opts, onEvent) {
|
|
74
|
+
if (opts.poll) return pollWatch(watchPath, opts.recursive, opts.poll.intervalMs, onEvent);
|
|
58
75
|
const watcher = opts.recursive ? fs.watch(
|
|
59
|
-
|
|
76
|
+
watchPath,
|
|
60
77
|
{ recursive: true },
|
|
61
78
|
(_event, filename) => onEvent(filename ? filename.toString() : null)
|
|
62
|
-
) : fs.watch(
|
|
79
|
+
) : fs.watch(watchPath, () => onEvent(watchPath));
|
|
63
80
|
return { close: () => watcher.close() };
|
|
64
81
|
}
|
|
65
82
|
};
|
|
83
|
+
function pollWatch(root, recursive, intervalMs, onEvent) {
|
|
84
|
+
const snapshot = () => {
|
|
85
|
+
const out = /* @__PURE__ */ new Map();
|
|
86
|
+
const walk = (dir) => {
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const full = path.join(dir, entry.name);
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
if (recursive) walk(full);
|
|
97
|
+
} else {
|
|
98
|
+
try {
|
|
99
|
+
out.set(path.relative(root, full), fs.statSync(full).mtimeMs);
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
walk(root);
|
|
106
|
+
return out;
|
|
107
|
+
};
|
|
108
|
+
let prev = snapshot();
|
|
109
|
+
const timer = setInterval(() => {
|
|
110
|
+
const next = snapshot();
|
|
111
|
+
for (const [rel, mtime] of next) {
|
|
112
|
+
if (prev.get(rel) !== mtime) onEvent(rel);
|
|
113
|
+
}
|
|
114
|
+
for (const rel of prev.keys()) {
|
|
115
|
+
if (!next.has(rel)) onEvent(rel);
|
|
116
|
+
}
|
|
117
|
+
prev = next;
|
|
118
|
+
}, intervalMs);
|
|
119
|
+
timer.unref?.();
|
|
120
|
+
return { close: () => clearInterval(timer) };
|
|
121
|
+
}
|
|
66
122
|
export {
|
|
67
123
|
nodeIo
|
|
68
124
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A key is re-exportable only if it is a syntactically valid identifier name. */
|
|
2
|
+
export declare const isIdentifierName: (name: string) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Decides whether a `require()`d CommonJS value needs a synthetic named-export
|
|
5
|
+
* wrapper, and which names to re-export. UMD/CJS packages (e.g. dayjs) assign
|
|
6
|
+
* named exports dynamically, invisible to static lexers; enumerating the value
|
|
7
|
+
* recovers them. ES-module namespaces and `__esModule` interop objects are
|
|
8
|
+
* skipped — a bundler already exposes those statically.
|
|
9
|
+
*/
|
|
10
|
+
export declare const planCjsWrap: (mod: unknown) => {
|
|
11
|
+
wrap: boolean;
|
|
12
|
+
keys: string[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Synthetic ESM entry re-exporting the CommonJS default plus each discovered name.
|
|
16
|
+
* The `export { local as key }` clause lets reserved-word keys (`class`, `default`)
|
|
17
|
+
* pass without escaping.
|
|
18
|
+
*/
|
|
19
|
+
export declare const buildSyntheticCjsEntry: (importPath: string, keys: string[]) => string;
|
|
20
|
+
/** True when a `require()` failure just means the file was ESM after all. */
|
|
21
|
+
export declare const isEsmInteropError: (err: unknown) => boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const isIdentifierName = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
|
2
|
+
const planCjsWrap = (mod) => {
|
|
3
|
+
if (mod === null || typeof mod !== "object" && typeof mod !== "function") {
|
|
4
|
+
return { wrap: false, keys: [] };
|
|
5
|
+
}
|
|
6
|
+
if (mod[Symbol.toStringTag] === "Module") {
|
|
7
|
+
return { wrap: false, keys: [] };
|
|
8
|
+
}
|
|
9
|
+
if (mod["__esModule"]) {
|
|
10
|
+
return { wrap: false, keys: [] };
|
|
11
|
+
}
|
|
12
|
+
const keys = Object.keys(mod).filter(
|
|
13
|
+
(key) => key !== "default" && key !== "__esModule" && isIdentifierName(key)
|
|
14
|
+
);
|
|
15
|
+
return { wrap: keys.length > 0, keys };
|
|
16
|
+
};
|
|
17
|
+
const buildSyntheticCjsEntry = (importPath, keys) => {
|
|
18
|
+
const spec = JSON.stringify(importPath);
|
|
19
|
+
const lines = [`import _nfDefault from ${spec};`, `export default _nfDefault;`];
|
|
20
|
+
if (keys.length > 0) {
|
|
21
|
+
keys.forEach((key, i) => lines.push(`const _nf${i} = _nfDefault[${JSON.stringify(key)}];`));
|
|
22
|
+
lines.push(`export { ${keys.map((key, i) => `_nf${i} as ${key}`).join(", ")} };`);
|
|
23
|
+
}
|
|
24
|
+
return lines.join("\n") + "\n";
|
|
25
|
+
};
|
|
26
|
+
const isEsmInteropError = (err) => {
|
|
27
|
+
const code = err?.code;
|
|
28
|
+
if (code === "ERR_REQUIRE_ESM" || code === "ERR_REQUIRE_ASYNC_MODULE") return true;
|
|
29
|
+
return err instanceof SyntaxError && /Unexpected token 'export'|Cannot use import statement|export|import statement/.test(err.message);
|
|
30
|
+
};
|
|
31
|
+
export {
|
|
32
|
+
buildSyntheticCjsEntry,
|
|
33
|
+
isEsmInteropError,
|
|
34
|
+
isIdentifierName,
|
|
35
|
+
planCjsWrap
|
|
36
|
+
};
|
|
@@ -3,3 +3,16 @@
|
|
|
3
3
|
* or ambiguous (`undefined`).
|
|
4
4
|
*/
|
|
5
5
|
export declare const isESMExport: (e: string) => boolean | undefined;
|
|
6
|
+
export type ModuleFormat = 'esm' | 'cjs' | 'unknown';
|
|
7
|
+
/** Node's format rule keyed on extension alone; `.js` stays ambiguous. */
|
|
8
|
+
export declare const classifyByExtension: (entryPoint: string) => ModuleFormat;
|
|
9
|
+
/** Does the source contain top-level ESM `import`/`export`? Dynamic `import()` is excluded (legal in CJS). */
|
|
10
|
+
export declare const hasEsmSyntax: (source: string) => boolean;
|
|
11
|
+
export declare const isCjsCandidate: (input: {
|
|
12
|
+
esm?: boolean;
|
|
13
|
+
entryPoint: string;
|
|
14
|
+
/** Nearest package.json `type` for an ambiguous `.js`. */
|
|
15
|
+
packageType?: "module" | "commonjs";
|
|
16
|
+
/** Lazy source for fallback content sniff; only read for an ambiguous `.js`. */
|
|
17
|
+
readSource?: () => string;
|
|
18
|
+
}) => boolean;
|