File size: 4,308 Bytes
bc20498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { log, logCompilerWarnings } from './utils/log.js';
import { toRollupError } from './utils/error.js';

/**
 * Vite-specific HMR handling
 *
 * @param {Function} compileSvelte
 * @param {import('vite').HmrContext} ctx
 * @param {import('./types/id.d.ts').SvelteRequest} svelteRequest
 * @param {import('./utils/vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache
 * @param {import('./types/options.d.ts').ResolvedOptions} options
 * @returns {Promise<import('vite').ModuleNode[] | void>}
 */
export async function handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options) {
	if (!cache.has(svelteRequest)) {
		// file hasn't been requested yet (e.g. async component)
		log.debug(
			`handleHotUpdate called before initial transform for ${svelteRequest.id}`,
			undefined,
			'hmr'
		);
		return;
	}
	const { read, server, modules } = ctx;

	const cachedJS = cache.getJS(svelteRequest);
	const cachedCss = cache.getCSS(svelteRequest);

	const content = await read();
	/** @type {import('./types/compile.d.ts').CompileData} */
	let compileData;
	try {
		compileData = await compileSvelte(svelteRequest, content, options);
		cache.update(compileData);
	} catch (e) {
		cache.setError(svelteRequest, e);
		throw toRollupError(e, options);
	}

	const affectedModules = [...modules];

	const cssIdx = modules.findIndex((m) => m.id === svelteRequest.cssId);
	if (cssIdx > -1) {
		const cssUpdated = cssChanged(cachedCss, compileData.compiled.css);
		if (!cssUpdated) {
			log.debug(`skipping unchanged css for ${svelteRequest.cssId}`, undefined, 'hmr');
			affectedModules.splice(cssIdx, 1);
		}
	}
	const jsIdx = modules.findIndex((m) => m.id === svelteRequest.id);
	if (jsIdx > -1) {
		const jsUpdated = jsChanged(cachedJS, compileData.compiled.js, svelteRequest.filename);
		if (!jsUpdated) {
			log.debug(`skipping unchanged js for ${svelteRequest.id}`, undefined, 'hmr');
			affectedModules.splice(jsIdx, 1);
			// transform won't be called, log warnings here
			logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options);
		}
	}

	// TODO is this enough? see also: https://github.com/vitejs/vite/issues/2274
	const ssrModulesToInvalidate = affectedModules.filter((m) => !!m.ssrTransformResult);
	if (ssrModulesToInvalidate.length > 0) {
		log.debug(
			`invalidating modules ${ssrModulesToInvalidate.map((m) => m.id).join(', ')}`,
			undefined,
			'hmr'
		);
		ssrModulesToInvalidate.forEach((moduleNode) => server.moduleGraph.invalidateModule(moduleNode));
	}
	if (affectedModules.length > 0) {
		log.debug(
			`handleHotUpdate for ${svelteRequest.id} result: ${affectedModules
				.map((m) => m.id)
				.join(', ')}`,
			undefined,
			'hmr'
		);
	}
	return affectedModules;
}

/**
 * @param {import('./types/compile.d.ts').Code} [prev]
 * @param {import('./types/compile.d.ts').Code} [next]
 * @returns {boolean}
 */
function cssChanged(prev, next) {
	return !isCodeEqual(prev?.code, next?.code);
}

/**
 * @param {import('./types/compile.d.ts').Code} [prev]
 * @param {import('./types/compile.d.ts').Code} [next]
 * @param {string} [filename]
 * @returns {boolean}
 */
function jsChanged(prev, next, filename) {
	const prevJs = prev?.code;
	const nextJs = next?.code;
	const isStrictEqual = isCodeEqual(prevJs, nextJs);
	if (isStrictEqual) {
		return false;
	}
	const isLooseEqual = isCodeEqual(normalizeJsCode(prevJs), normalizeJsCode(nextJs));
	if (!isStrictEqual && isLooseEqual) {
		log.debug(
			`ignoring compiler output js change for ${filename} as it is equal to previous output after normalization`,
			undefined,
			'hmr'
		);
	}
	return !isLooseEqual;
}

/**
 * @param {string} [prev]
 * @param {string} [next]
 * @returns {boolean}
 */
function isCodeEqual(prev, next) {
	if (!prev && !next) {
		return true;
	}
	if ((!prev && next) || (prev && !next)) {
		return false;
	}
	return prev === next;
}

/**
 * remove code that only changes metadata and does not require a js update for the component to keep working
 *
 * 1) add_location() calls. These add location metadata to elements, only used by some dev tools
 * 2) ... maybe more (or less) in the future
 *
 * @param {string} [code]
 * @returns {string | undefined}
 */
function normalizeJsCode(code) {
	if (!code) {
		return code;
	}
	return code.replace(/\s*\badd_location\s*\([^)]*\)\s*;?/g, '');
}