|
| 1 | +/** @import { ServerMetadata } from 'types' */ |
| 2 | +/** @import { OutputBundle } from 'rollup' */ |
| 3 | + |
| 4 | +import fs from 'node:fs'; |
| 5 | +import path from 'node:path'; |
| 6 | +import { Parser } from 'acorn'; |
| 7 | +import MagicString from 'magic-string'; |
| 8 | +import { posixify } from '../../../utils/filesystem.js'; |
| 9 | +import { import_peer } from '../../../utils/import.js'; |
| 10 | + |
| 11 | +/** |
| 12 | + * @param {string} out |
| 13 | + * @param {Array<{ hash: string, file: string }>} remotes |
| 14 | + * @param {ServerMetadata} metadata |
| 15 | + * @param {string} cwd |
| 16 | + * @param {OutputBundle} server_bundle |
| 17 | + * @param {NonNullable<import('vitest/config').ViteUserConfig['build']>['sourcemap']} sourcemap |
| 18 | + */ |
| 19 | +export async function treeshake_prerendered_remotes( |
| 20 | + out, |
| 21 | + remotes, |
| 22 | + metadata, |
| 23 | + cwd, |
| 24 | + server_bundle, |
| 25 | + sourcemap |
| 26 | +) { |
| 27 | + if (remotes.length === 0) return; |
| 28 | + |
| 29 | + const vite = /** @type {typeof import('vite')} */ (await import_peer('vite')); |
| 30 | + |
| 31 | + for (const remote of remotes) { |
| 32 | + const exports_map = metadata.remotes.get(remote.hash); |
| 33 | + if (!exports_map) continue; |
| 34 | + |
| 35 | + /** @type {string[]} */ |
| 36 | + const dynamic = []; |
| 37 | + /** @type {string[]} */ |
| 38 | + const prerendered = []; |
| 39 | + |
| 40 | + for (const [name, value] of exports_map) { |
| 41 | + (value.dynamic ? dynamic : prerendered).push(name); |
| 42 | + } |
| 43 | + |
| 44 | + if (prerendered.length === 0) continue; // nothing to treeshake |
| 45 | + |
| 46 | + // remove file extension |
| 47 | + const remote_filename = path.basename(remote.file).split('.').slice(0, -1).join('.'); |
| 48 | + |
| 49 | + const remote_chunk = Object.values(server_bundle).find((chunk) => { |
| 50 | + return chunk.name === remote_filename; |
| 51 | + }); |
| 52 | + |
| 53 | + if (!remote_chunk) continue; |
| 54 | + |
| 55 | + const chunk_path = posixify(path.relative(cwd, `${out}/server/${remote_chunk.fileName}`)); |
| 56 | + |
| 57 | + const code = fs.readFileSync(chunk_path, 'utf-8'); |
| 58 | + const parsed = Parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' }); |
| 59 | + const modified_code = new MagicString(code); |
| 60 | + |
| 61 | + for (const fn of prerendered) { |
| 62 | + for (const node of parsed.body) { |
| 63 | + const declaration = |
| 64 | + node.type === 'ExportNamedDeclaration' |
| 65 | + ? node.declaration |
| 66 | + : node.type === 'VariableDeclaration' |
| 67 | + ? node |
| 68 | + : null; |
| 69 | + |
| 70 | + if (!declaration || declaration.type !== 'VariableDeclaration') continue; |
| 71 | + |
| 72 | + for (const declarator of declaration.declarations) { |
| 73 | + if (declarator.id.type === 'Identifier' && declarator.id.name === fn) { |
| 74 | + modified_code.overwrite( |
| 75 | + node.start, |
| 76 | + node.end, |
| 77 | + `const ${fn} = prerender('unchecked', () => { throw new Error('Unexpectedly called prerender function. Did you forget to set { dynamic: true } ?') });` |
| 78 | + ); |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + for (const node of parsed.body) { |
| 85 | + if (node.type === 'ExportDefaultDeclaration') { |
| 86 | + modified_code.remove(node.start, node.end); |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + const stubbed = modified_code.toString(); |
| 91 | + fs.writeFileSync(chunk_path, stubbed); |
| 92 | + |
| 93 | + const bundle = /** @type {import('vite').Rollup.RollupOutput} */ ( |
| 94 | + await vite.build({ |
| 95 | + configFile: false, |
| 96 | + build: { |
| 97 | + write: false, |
| 98 | + ssr: true, |
| 99 | + target: 'esnext', |
| 100 | + sourcemap, |
| 101 | + rollupOptions: { |
| 102 | + // avoid resolving imports |
| 103 | + external: (id) => !id.endsWith(chunk_path), |
| 104 | + input: { |
| 105 | + treeshaken: chunk_path |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + }) |
| 110 | + ); |
| 111 | + |
| 112 | + const chunk = bundle.output.find( |
| 113 | + (output) => output.type === 'chunk' && output.name === 'treeshaken' |
| 114 | + ); |
| 115 | + if (chunk && chunk.type === 'chunk') { |
| 116 | + fs.writeFileSync(chunk_path, chunk.code); |
| 117 | + |
| 118 | + const chunk_sourcemap = bundle.output.find( |
| 119 | + (output) => output.type === 'asset' && output.fileName === chunk.fileName + '.map' |
| 120 | + ); |
| 121 | + if (chunk_sourcemap && chunk_sourcemap.type === 'asset') { |
| 122 | + fs.writeFileSync(chunk_path + '.map', chunk_sourcemap.source); |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | +} |
0 commit comments