|
| 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