summaryrefslogtreecommitdiff
path: root/packages/integrations/cloudflare/src/utils/non-server-chunk-detector.ts
blob: 16ba2aed9af5750b03ff2c5f450e59df8d7d3e7c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import type { OutputBundle } from 'rollup';
import type { PluginOption } from 'vite';

/**
 * A Vite bundle analyzer that identifies chunks that are not used for server rendering.
 *
 * The chunks injected by Astro for prerendering are flagged as non-server chunks.
 * Any chunks that is only used by a non-server chunk are also flagged as non-server chunks.
 * This continues transitively until all non-server chunks are found.
 */
export class NonServerChunkDetector {
	private nonServerChunks?: string[];

	public getPlugin(): PluginOption {
		return {
			name: 'non-server-chunk-detector',
			generateBundle: (_, bundle) => {
				// Skip if we bundle for client
				if (!bundle['index.js']) return;
				this.processBundle(bundle);
			},
		};
	}

	private processBundle(bundle: OutputBundle) {
		const chunkNamesToFiles = new Map<string, string>();

		const entryChunks: string[] = [];
		const chunkToDependencies = new Map<string, string[]>();

		for (const chunk of Object.values(bundle)) {
			if (chunk.type !== 'chunk') continue;

			// Construct a mapping from a chunk name to its file name
			chunkNamesToFiles.set(chunk.name, chunk.fileName);
			// Construct a mapping from a chunk file to all the modules it imports
			chunkToDependencies.set(chunk.fileName, [...chunk.imports, ...chunk.dynamicImports]);

			if (chunk.isEntry) {
				// Entry chunks should always be kept around since they are to be imported by the runtime
				entryChunks.push(chunk.fileName);
			}
		}

		const chunkDecisions = new Map<string, boolean>();

		for (const entry of entryChunks) {
			// Entry chunks are used on the server
			chunkDecisions.set(entry, true);
		}

		for (const chunk of ['prerender', 'prerender@_@astro']) {
			// Prerender chunks are not used on the server
			const fileName = chunkNamesToFiles.get(chunk);
			if (fileName) {
				chunkDecisions.set(fileName, false);
			}
		}

		// Start a stack of chunks that are used on the server
		const chunksToWalk = [...entryChunks];

		// Iterate over the chunks, traversing the transitive dependencies of the chunks used on the server
		for (let chunk = chunksToWalk.pop(); chunk; chunk = chunksToWalk.pop()) {
			for (const dep of chunkToDependencies.get(chunk) ?? []) {
				// Skip dependencies already flagged, dependencies may be repeated and/or circular
				if (chunkDecisions.has(dep)) continue;

				// A dependency of a module used on the server is also used on the server
				chunkDecisions.set(dep, true);
				// Add the dependency to the stack so its own dependencies are also flagged
				chunksToWalk.push(dep);
			}
		}

		// Any chunk not flagged as used on the server is a non-server chunk
		this.nonServerChunks = Array.from(chunkToDependencies.keys()).filter(
			(chunk) => !chunkDecisions.get(chunk),
		);
	}

	public getNonServerChunks(): string[] {
		return this.nonServerChunks ?? [];
	}
}