Skip to content

Commit 737e031

Browse files
committed
feat: support dynamic router preload
1 parent 8cecdf9 commit 737e031

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed

webpack/plugin/pageDeps.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
const { sources, Compilation } = require("webpack");
2+
3+
// https://github.com/artem-malko/react-ssr-template/blob/main/src/infrastructure/dependencyManager/webpack/plugin.ts
4+
5+
const RawSource = sources.RawSource;
6+
const pluginName = "dependency-manager-plugin";
7+
8+
// https://github.com/shellscape/webpack-manifest-plugin/blob/6a521600b0b7dd66db805bf8fb8afaa8c41290cb/src/index.ts#L48
9+
const hashKey = /([a-f0-9]{16,32}\.?)/gi;
10+
const transformExtensions = /^(gz|map)$/i;
11+
12+
/**
13+
* Returns an info about deps for every page's chunk
14+
*/
15+
class PageDependenciesManagerPlugin {
16+
constructor(p = {}) {
17+
this.fileName = p.fileName || "manifest-deps.json";
18+
}
19+
apply(compiler) {
20+
// Capture the compilation and then set up further hooks.
21+
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
22+
// Modern: `processAssets` is one of the last hooks before frozen assets.
23+
// I choose `PROCESS_ASSETS_STAGE_REPORT` which is the last possible
24+
// stage after which to emit.
25+
compilation.hooks.processAssets.tapPromise(
26+
{
27+
name: pluginName,
28+
stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
29+
},
30+
() => this.emitStats(compilation)
31+
);
32+
});
33+
}
34+
35+
emitStats(compilation) {
36+
// Get stats.
37+
const statsChunks = compilation.getStats().toJson().chunks;
38+
39+
if (!statsChunks) {
40+
throw new Error("NO CHUNKS IN STATS");
41+
}
42+
43+
return Promise.resolve()
44+
.then(() => {
45+
const reducedStats = statsChunks.reduce(
46+
(mutableAcc, statsChunk) => {
47+
if (!statsChunk.id) {
48+
return mutableAcc;
49+
}
50+
51+
/**
52+
* It's possible, when chunk doesn't have its own name.
53+
* In that case its id will be used as a name
54+
*/
55+
56+
const files = Array.from(statsChunk.files || []).map((fileName) => {
57+
const replaced = fileName.replace(/\?.*/, "");
58+
const split = replaced.split(".");
59+
const extension = split.pop();
60+
const finalExtension = transformExtensions.test(extension) ? `${split.pop()}.${extension}` : extension;
61+
const name = statsChunk.names?.[0] ? statsChunk.names?.[0] + "." + finalExtension : fileName;
62+
// look like there have a difference for webpack-manifest-plugin
63+
return name.replace(hashKey, "");
64+
});
65+
66+
mutableAcc.chunkIdToChunkName[statsChunk.id] = {
67+
id: statsChunk.id,
68+
name: files[0],
69+
locName: statsChunk.origins?.[0]?.request,
70+
};
71+
72+
if (files[0]) {
73+
mutableAcc.chunkIdToFileNameMap[statsChunk.id] = files;
74+
}
75+
76+
if (statsChunk.children) {
77+
mutableAcc.chunkIdToChildrenIds[statsChunk.id] = statsChunk.children.filter((childId) => {
78+
/**
79+
* It's strange, but sometimes it is possible, that current chunk can have one dep
80+
* in parents and in children.
81+
* To prevent recursion in the next steps, we filter that ids out
82+
*/
83+
return !statsChunk.parents?.includes(childId);
84+
});
85+
}
86+
87+
return mutableAcc;
88+
},
89+
{
90+
chunkIdToFileNameMap: {},
91+
chunkIdToChunkName: {},
92+
chunkIdToChildrenIds: {},
93+
}
94+
);
95+
96+
return Object.keys(reducedStats.chunkIdToChunkName).reduce((mutableAcc, chunkId) => {
97+
const { name: chunkName, locName } = reducedStats.chunkIdToChunkName[chunkId];
98+
99+
// We do not collect deps for not page's chunks
100+
if (!chunkName || !/page/i.test(chunkName)) {
101+
return mutableAcc;
102+
}
103+
104+
const childrenIds = reducedStats.chunkIdToChildrenIds[chunkId];
105+
const files = getFiles(reducedStats.chunkIdToFileNameMap, reducedStats.chunkIdToChildrenIds, childrenIds);
106+
107+
mutableAcc[locName] = [chunkName, ...files];
108+
109+
return mutableAcc;
110+
}, {});
111+
})
112+
.then((result) => {
113+
const resultString = JSON.stringify(result, null, 2);
114+
const resultStringBuf = Buffer.from(resultString, "utf-8");
115+
const source = new RawSource(resultStringBuf);
116+
const filename = this.fileName;
117+
118+
const asset = compilation.getAsset(filename);
119+
120+
if (asset) {
121+
compilation.updateAsset(filename, source);
122+
} else {
123+
compilation.emitAsset(filename, source);
124+
}
125+
});
126+
}
127+
}
128+
129+
/**
130+
* This function has a recurtion inside, cause the first level children can have its own children
131+
*/
132+
const getFiles = (chunkIdToFileNameMap, chunkIdToChildrenIds, childrenIds) => {
133+
const mutableFoundFiles = [];
134+
135+
function innerFunc(chunkIdToFileNameMap, chunkIdToChildrenIds, childrenIds) {
136+
if (!childrenIds?.length) {
137+
return mutableFoundFiles;
138+
}
139+
140+
childrenIds.forEach((childId) => {
141+
const fileName = chunkIdToFileNameMap[childId];
142+
143+
if (chunkIdToChildrenIds[childId]?.length) {
144+
innerFunc(chunkIdToFileNameMap, chunkIdToChildrenIds, chunkIdToChildrenIds[childId]);
145+
}
146+
if (fileName && !mutableFoundFiles.includes(fileName)) {
147+
mutableFoundFiles.push(...fileName);
148+
}
149+
});
150+
}
151+
152+
innerFunc(chunkIdToFileNameMap, chunkIdToChildrenIds, childrenIds);
153+
154+
return mutableFoundFiles;
155+
};
156+
157+
module.exports.PageDependenciesManagerPlugin = PageDependenciesManagerPlugin;

0 commit comments

Comments
 (0)