Skip to content

Commit dd87269

Browse files
authored
Merge pull request #981 from Aukevanoost/f/performance-caching-II
feat(nf): Performance boost - cache external artifacts with checksum
2 parents 5983b6c + c4c5d64 commit dd87269

File tree

6 files changed

+187
-110
lines changed

6 files changed

+187
-110
lines changed

libs/native-federation-core/src/lib/core/build-for-federation.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { FederationOptions } from './federation-options';
1414
import { writeFederationInfo } from './write-federation-info';
1515
import { writeImportMap } from './write-import-map';
1616
import { logger } from '../utils/logger';
17+
import { getCachePath } from './bundle-caching';
18+
import { normalizePackageName } from '../utils/normalize';
1719
import { AbortedError } from '../utils/errors';
1820

1921
export interface BuildParams {
@@ -27,9 +29,7 @@ export const defaultBuildParams: BuildParams = {
2729
skipShared: false,
2830
};
2931

30-
// Externals cache
3132
const sharedPackageInfoCache: SharedInfo[] = [];
32-
const cachedSharedPackages = new Set<string>();
3333

3434
export async function buildForFederation(
3535
config: NormalizedFederationConfig,
@@ -64,7 +64,23 @@ export async function buildForFederation(
6464
? describeExposed(config, fedOptions)
6565
: artefactInfo.exposes;
6666

67-
if (!buildParams.skipShared) {
67+
const cacheProjectFolder = normalizePackageName(config.name);
68+
if (cacheProjectFolder.length < 1) {
69+
logger.warn(
70+
"Project name in 'federation.config.js' is empty, defaulting to root cache folder (could collide with other projects in the workspace).",
71+
);
72+
}
73+
74+
const pathToCache = getCachePath(
75+
fedOptions.workspaceRoot,
76+
cacheProjectFolder,
77+
);
78+
79+
if (!buildParams.skipShared && sharedPackageInfoCache.length > 0) {
80+
logger.info('Checksum matched, re-using cached externals.');
81+
}
82+
83+
if (!buildParams.skipShared && sharedPackageInfoCache.length === 0) {
6884
const { sharedBrowser, sharedServer, separateBrowser, separateServer } =
6985
splitShared(config.shared);
7086

@@ -76,6 +92,7 @@ export async function buildForFederation(
7692
fedOptions,
7793
externals,
7894
'browser',
95+
{ pathToCache, bundleName: 'browser-shared' },
7996
);
8097

8198
logger.measure(
@@ -84,9 +101,7 @@ export async function buildForFederation(
84101
);
85102

86103
sharedPackageInfoCache.push(...sharedPackageInfoBrowser);
87-
Object.keys(sharedBrowser).forEach((packageName) =>
88-
cachedSharedPackages.add(packageName),
89-
);
104+
90105
if (signal?.aborted)
91106
throw new AbortedError(
92107
'[buildForFederation] After shared-browser bundle',
@@ -101,15 +116,14 @@ export async function buildForFederation(
101116
fedOptions,
102117
externals,
103118
'node',
119+
{ pathToCache, bundleName: 'node-shared' },
104120
);
105121
logger.measure(
106122
start,
107123
'[build artifacts] - To bundle all shared node externals',
108124
);
109125
sharedPackageInfoCache.push(...sharedPackageInfoServer);
110-
Object.keys(sharedServer).forEach((packageName) =>
111-
cachedSharedPackages.add(packageName),
112-
);
126+
113127
if (signal?.aborted)
114128
throw new AbortedError('[buildForFederation] After shared-node bundle');
115129
}
@@ -122,15 +136,14 @@ export async function buildForFederation(
122136
config,
123137
fedOptions,
124138
'browser',
139+
pathToCache,
125140
);
126141
logger.measure(
127142
start,
128143
'[build artifacts] - To bundle all separate browser externals',
129144
);
130145
sharedPackageInfoCache.push(...separatePackageInfoBrowser);
131-
Object.keys(separateBrowser).forEach((packageName) =>
132-
cachedSharedPackages.add(packageName),
133-
);
146+
134147
if (signal?.aborted)
135148
throw new AbortedError(
136149
'[buildForFederation] After separate-browser bundle',
@@ -145,15 +158,13 @@ export async function buildForFederation(
145158
config,
146159
fedOptions,
147160
'node',
161+
pathToCache,
148162
);
149163
logger.measure(
150164
start,
151165
'[build artifacts] - To bundle all separate node externals',
152166
);
153167
sharedPackageInfoCache.push(...separatePackageInfoServer);
154-
Object.keys(separateServer).forEach((packageName) =>
155-
cachedSharedPackages.add(packageName),
156-
);
157168
}
158169

159170
if (signal?.aborted)
@@ -203,6 +214,7 @@ async function bundleSeparate(
203214
config: NormalizedFederationConfig,
204215
fedOptions: FederationOptions,
205216
platform: 'node' | 'browser',
217+
pathToCache: string,
206218
) {
207219
const bundlePromises = Object.entries(separateBrowser).map(
208220
async ([key, shared]) => {
@@ -216,6 +228,10 @@ async function bundleSeparate(
216228
fedOptions,
217229
filteredExternals,
218230
platform,
231+
{
232+
pathToCache,
233+
bundleName: `${platform}-${normalizePackageName(key)}`,
234+
},
219235
);
220236
},
221237
);
@@ -233,7 +249,6 @@ function splitShared(
233249
const separateServer: Record<string, NormalizedSharedConfig> = {};
234250

235251
for (const key in shared) {
236-
if (cachedSharedPackages.has(key)) continue;
237252
const obj = shared[key];
238253
if (obj.platform === 'node' && obj.build === 'default') {
239254
sharedServer[key] = obj;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import path from 'path';
2+
import fs from 'fs';
3+
import crypto from 'crypto';
4+
import { NormalizedSharedConfig } from '../config/federation-config';
5+
import { SharedInfo } from '@softarc/native-federation-runtime';
6+
import { logger } from '../utils/logger';
7+
8+
export const getCachePath = (workspaceRoot: string, project: string) =>
9+
path.join(workspaceRoot, 'node_modules/.cache/native-federation', project);
10+
11+
export const getFilename = (title: string) => {
12+
return `${title}.meta.json`;
13+
};
14+
15+
export const getChecksum = (
16+
shared: Record<string, NormalizedSharedConfig>,
17+
): string => {
18+
const denseExternals = Object.keys(shared)
19+
.sort()
20+
.reduce((clean, external) => {
21+
return (
22+
clean +
23+
':' +
24+
external +
25+
(shared[external].version ? `@${shared[external].version}` : '')
26+
);
27+
}, 'deps');
28+
29+
return crypto.createHash('sha256').update(denseExternals).digest('hex');
30+
};
31+
32+
export const cacheEntry = (pathToCache: string, fileName: string) => ({
33+
getMetadata: (
34+
checksum: string,
35+
):
36+
| {
37+
checksum: string;
38+
externals: SharedInfo[];
39+
files: string[];
40+
}
41+
| undefined => {
42+
const metadataFile = path.join(pathToCache, fileName);
43+
if (!fs.existsSync(pathToCache) || !fs.existsSync(metadataFile))
44+
return undefined;
45+
46+
const cachedResult: {
47+
checksum: string;
48+
externals: SharedInfo[];
49+
files: string[];
50+
} = JSON.parse(fs.readFileSync(metadataFile, 'utf-8'));
51+
if (cachedResult.checksum !== checksum) return undefined;
52+
return cachedResult;
53+
},
54+
persist: (payload: {
55+
checksum: string;
56+
externals: SharedInfo[];
57+
files: string[];
58+
}) => {
59+
fs.writeFileSync(
60+
path.join(pathToCache, fileName),
61+
JSON.stringify(payload),
62+
'utf-8',
63+
);
64+
},
65+
copyFiles: (fullOutputPath: string) => {
66+
const metadataFile = path.join(pathToCache, fileName);
67+
if (!fs.existsSync(metadataFile))
68+
throw new Error(
69+
'Error copying artifacts to dist, metadata file could not be found.',
70+
);
71+
72+
const cachedResult: {
73+
externals: SharedInfo[];
74+
files: string[];
75+
} = JSON.parse(fs.readFileSync(metadataFile, 'utf-8'));
76+
77+
fs.mkdirSync(path.dirname(fullOutputPath), { recursive: true });
78+
79+
cachedResult.files.forEach((file) => {
80+
const cachedFile = path.join(pathToCache, file);
81+
const distFileName = path.join(fullOutputPath, file);
82+
83+
if (fs.existsSync(cachedFile)) {
84+
fs.copyFileSync(cachedFile, distFileName);
85+
}
86+
});
87+
},
88+
clear: () => {
89+
const metadataFile = path.join(pathToCache, fileName);
90+
if (!fs.existsSync(pathToCache)) {
91+
fs.mkdirSync(pathToCache, { recursive: true });
92+
logger.debug(`Creating cache folder '${pathToCache}' for '${fileName}'.`);
93+
return;
94+
}
95+
if (!fs.existsSync(metadataFile)) {
96+
logger.debug(
97+
`Could not purge cached bundle, metadata file '${metadataFile}' does not exist.`,
98+
);
99+
return;
100+
}
101+
102+
const cachedResult: {
103+
checksum: string;
104+
externals: SharedInfo[];
105+
files: string[];
106+
} = JSON.parse(fs.readFileSync(metadataFile, 'utf-8'));
107+
108+
cachedResult.files.forEach((file) => {
109+
const cachedFile = path.join(pathToCache, file);
110+
if (fs.existsSync(cachedFile)) fs.unlinkSync(cachedFile);
111+
});
112+
113+
fs.unlinkSync(metadataFile);
114+
},
115+
});

0 commit comments

Comments
 (0)