Skip to content

Commit dc8573b

Browse files
committed
[devtools-snippet] add Next.js v13+ - Extract Chunk URLs.js
1 parent 3502139 commit dc8573b

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Next.js v13+ - Extract Chunk URLs.js
2+
// https://github.com/0xdevalias/userscripts/tree/main/devtools-snippets
3+
//
4+
// See also:
5+
// - https://gist.github.com/search?q=user:0xdevalias+webpack
6+
// - https://gist.github.com/0xdevalias/ac465fb2f7e6fded183c2a4273d21e61#react-server-components-nextjs-v13-and-webpack-notes-on-streaming-wire-format-__next_f-etc
7+
8+
const parseJSONFromEntry = entry => {
9+
const jsonPart = entry.substring(entry.indexOf('[') + 1, entry.lastIndexOf(']'));
10+
try {
11+
return JSON.parse(`[${jsonPart}]`);
12+
} catch (e) {
13+
console.error("Failed to parse JSON for entry: ", entry);
14+
return []; // Return an empty array or null as per error handling choice
15+
}
16+
};
17+
18+
// Function to transform dependencies into a simpler, directly accessible format
19+
function transformDependencies(dependencies) {
20+
return Object.values(dependencies).reduce((acc, currentDeps) => {
21+
Object.entries(currentDeps).forEach(([moduleId, path]) => {
22+
// If the paths match, skip to the next entry
23+
if (acc?.[moduleId] === path) return
24+
25+
if (!acc[moduleId]) {
26+
// If this module ID has not been encountered yet, initialize it with the current path
27+
acc[moduleId] = path;
28+
} else if (typeof acc[moduleId] === 'string' && acc[moduleId] !== path) {
29+
// If the current path for this module ID is different from the existing one,
30+
// and the existing one is a string, transform it into an array containing both paths.
31+
const oldPath = acc[moduleId];
32+
acc[moduleId] = [oldPath, path];
33+
} else if (Array.isArray(acc[moduleId]) && !acc[moduleId].includes(path)) {
34+
// If the existing entry for this module ID is an array and does not already include the current path,
35+
// add the current path to the array.
36+
acc[moduleId].push(path);
37+
} else {
38+
// Log any unhandled cases for further investigation. This could be used to catch any unexpected data structures or duplicates.
39+
console.error('Unhandled case', { acc, currentDeps, moduleId, path });
40+
}
41+
});
42+
return acc;
43+
}, {});
44+
}
45+
46+
// Get _next script urls
47+
const scriptTags = document.querySelectorAll('html script[src*="_next"]');
48+
const scriptUrls = Array.from(scriptTags).map(tag => tag.src).sort()
49+
// console.log(scriptUrls);
50+
51+
// Get imports/etc (v3)
52+
const moduleDependencies =
53+
self.__next_f
54+
.map(f => f?.[1])
55+
.filter(f => f?.includes('static/'))
56+
.flatMap(f => f.split('\n'))
57+
.map(parseJSONFromEntry)
58+
.filter(f => Array.isArray(f) ? f.length > 0 : !!f)
59+
.map(f => {
60+
if (!Array.isArray(f?.[1])) { return f } else {
61+
// Convert alternating key/value array to an object
62+
const keyValueArray = f[1];
63+
const keyValuePairs = [];
64+
for (let i = 0; i < keyValueArray.length; i += 2) {
65+
keyValuePairs.push([keyValueArray[i], keyValueArray[i + 1]]);
66+
}
67+
f[1] = Object.fromEntries(keyValuePairs);
68+
return f;
69+
}
70+
})
71+
.filter(f => Array.isArray(f) && f.length === 3 && typeof f?.[1] === 'object')
72+
.reduce((acc, [moduleId, dependencies, _]) => {
73+
acc[moduleId] = dependencies;
74+
return acc;
75+
}, {});
76+
77+
const chunkMappings = transformDependencies(moduleDependencies)
78+
79+
const uniqueChunkPaths = Array.from(new Set(Object.values(chunkMappings))).sort()
80+
81+
const dynamicChunkUrls = uniqueChunkPaths
82+
.map(path => `https://www.udio.com/_next/${path}`)
83+
.sort()
84+
85+
const chunkUrls = Array.from(new Set([...scriptUrls, ...dynamicChunkUrls])).sort()
86+
87+
const chunkUrlsJoined = chunkUrls.join('\n')
88+
89+
const buildId = self.__next_f
90+
.map(f => f?.[1])
91+
.filter(f => f?.includes('buildId'))
92+
.flatMap(f => f.trim().split('\n'))
93+
.flatMap(parseJSONFromEntry)
94+
.map(f => Array.isArray(f) ? f.flat() : f)
95+
.map(f => f?.[3]?.buildId)
96+
.filter(Boolean)?.[0]
97+
98+
const chunkUrlsJoinedWithBuildId = `${chunkUrlsJoined}\n${buildId}`
99+
100+
console.log({
101+
scriptUrls,
102+
moduleDependencies,
103+
chunkMappings,
104+
uniqueChunkPaths,
105+
dynamicChunkUrls,
106+
chunkUrls,
107+
buildId,
108+
})
109+
console.log(chunkUrlsJoinedWithBuildId)
110+
copy(chunkUrlsJoinedWithBuildId)
111+
console.log('Chunk URLs (joined) + BuildID copied to clipboard')

0 commit comments

Comments
 (0)