Skip to content

Commit 6a6538c

Browse files
authored
fix: correctly invalidate static analysis cache of child nodes during dev (#13793)
* don't cache during dev * changeset * ok we can cache * Update .changeset/big-eels-watch.md * fix universal file check * add correct test * format * cleanup * fix * ensure nodes are analysed sequentially * correctly invalidate child nodes when universal node is updated * windows path separators :)))) * we'll come back to this later * lint
1 parent e7b57e7 commit 6a6538c

File tree

10 files changed

+118
-69
lines changed

10 files changed

+118
-69
lines changed

.changeset/big-eels-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: correctly invalidate static analysis cache of child nodes when modifying a universal `+layout` file during dev

packages/kit/src/core/postbuild/analyse.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ async function analyse({
6363
internal.set_manifest(manifest);
6464
internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));
6565

66+
/** @type {Map<string, { page_options: Record<string, any> | null, children: string[] }>} */
6667
const static_exports = new Map();
6768

6869
// first, build server nodes without the client manifest so we can analyse it

packages/kit/src/exports/vite/build/build_server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { create_node_analyser } from '../static_analysis/index.js';
1515
* @param {import('vite').Manifest | null} client_manifest
1616
* @param {import('vite').Rollup.OutputAsset[] | null} css
1717
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
18-
* @param {Map<string, Record<string, any> | null>} static_exports
18+
* @param {Map<string, { page_options: Record<string, any> | null, children: string[] }>} static_exports
1919
*/
2020
export async function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css, output_config, static_exports) {
2121
mkdirp(`${out}/server/nodes`);

packages/kit/src/exports/vite/dev/index.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,18 @@ export async function dev(vite, vite_config, svelte_config) {
355355

356356
// Debounce add/unlink events because in case of folder deletion or moves
357357
// they fire in rapid succession, causing needless invocations.
358+
// These watchers only run for routes, param matchers, and client hooks.
358359
watch('add', () => debounce(update_manifest));
359360
watch('unlink', () => debounce(update_manifest));
360361
watch('change', (file) => {
361362
// Don't run for a single file if the whole manifest is about to get updated
362363
if (timeout || restarting) return;
363364

365+
if (/\+(page|layout).*$/.test(file)) {
366+
invalidate_page_options(path.relative(cwd, file));
367+
}
368+
364369
sync.update(svelte_config, manifest_data, file);
365-
invalidate_page_options(path.relative(cwd, file));
366370
});
367371

368372
const { appTemplate, errorTemplate, serviceWorker, hooks } = svelte_config.kit.files;
@@ -389,10 +393,10 @@ export async function dev(vite, vite_config, svelte_config) {
389393
}
390394
});
391395

392-
// changing the svelte config requires restarting the dev server
393-
// the config is only read on start and passed on to vite-plugin-svelte
394-
// which needs up-to-date values to operate correctly
395396
vite.watcher.on('change', async (file) => {
397+
// changing the svelte config requires restarting the dev server
398+
// the config is only read on start and passed on to vite-plugin-svelte
399+
// which needs up-to-date values to operate correctly
396400
if (path.basename(file) === 'svelte.config.js') {
397401
console.log(`svelte config changed, restarting vite dev-server. changed file: ${file}`);
398402
restarting = true;

packages/kit/src/exports/vite/static_analysis/index.js

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,22 @@ import { read } from '../../../utils/filesystem.js';
44

55
const inheritable_page_options = new Set(['ssr', 'prerender', 'csr', 'trailingSlash', 'config']);
66

7-
const page_options = new Set([...inheritable_page_options, 'entries']);
7+
const valid_page_options = new Set([...inheritable_page_options, 'entries']);
88

99
const skip_parsing_regex = new RegExp(
10-
`${Array.from(page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)`
10+
`${Array.from(valid_page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)`
1111
);
1212

1313
const parser = Parser.extend(tsPlugin());
1414

1515
/**
16-
* Collects exported page options from a +page.js/+layout.js file.
17-
* We ignore reassignments and use the declared value.
18-
* Returns `null` if any export is too difficult to analyse.
19-
* @param {string} filename
16+
* Collects page options from a +page.js/+layout.js file, ignoring reassignments
17+
* and using the declared value. Returns `null` if any export is too difficult to analyse.
18+
* @param {string} filename The name of the file to report when an error occurs
2019
* @param {string} input
2120
* @returns {Record<string, any> | null}
2221
*/
23-
export function statically_analyse_exports(filename, input) {
22+
export function statically_analyse_page_options(filename, input) {
2423
// if there's a chance there are no page exports or export all declaration,
2524
// then we can skip the AST parsing which is expensive
2625
if (!skip_parsing_regex.test(input)) {
@@ -34,14 +33,14 @@ export function statically_analyse_exports(filename, input) {
3433
});
3534

3635
/** @type {Map<string, import('acorn').Literal['value']>} */
37-
const static_exports = new Map();
36+
const page_options = new Map();
3837

3938
for (const statement of source.body) {
4039
// ignore export all declarations with aliases that are not page options
4140
if (
4241
statement.type === 'ExportAllDeclaration' &&
4342
statement.exported &&
44-
!page_options.has(get_name(statement.exported))
43+
!valid_page_options.has(get_name(statement.exported))
4544
) {
4645
continue;
4746
}
@@ -60,7 +59,7 @@ export function statically_analyse_exports(filename, input) {
6059
const export_specifiers = new Map();
6160
for (const specifier of statement.specifiers) {
6261
const exported_name = get_name(specifier.exported);
63-
if (!page_options.has(exported_name)) {
62+
if (!valid_page_options.has(exported_name)) {
6463
continue;
6564
}
6665

@@ -109,7 +108,7 @@ export function statically_analyse_exports(filename, input) {
109108
}
110109

111110
if (variable_declarator.init?.type === 'Literal') {
112-
static_exports.set(
111+
page_options.set(
113112
/** @type {string} */ (export_specifiers.get(variable_declarator.id.name)),
114113
variable_declarator.init.value
115114
);
@@ -138,7 +137,7 @@ export function statically_analyse_exports(filename, input) {
138137

139138
// class and function declarations
140139
if (statement.declaration.type !== 'VariableDeclaration') {
141-
if (page_options.has(statement.declaration.id.name)) {
140+
if (valid_page_options.has(statement.declaration.id.name)) {
142141
return null;
143142
}
144143
continue;
@@ -149,12 +148,12 @@ export function statically_analyse_exports(filename, input) {
149148
return null;
150149
}
151150

152-
if (!page_options.has(declaration.id.name)) {
151+
if (!valid_page_options.has(declaration.id.name)) {
153152
continue;
154153
}
155154

156155
if (declaration.init?.type === 'Literal') {
157-
static_exports.set(declaration.id.name, declaration.init.value);
156+
page_options.set(declaration.id.name, declaration.init.value);
158157
continue;
159158
}
160159

@@ -163,10 +162,10 @@ export function statically_analyse_exports(filename, input) {
163162
}
164163
}
165164

166-
return Object.fromEntries(static_exports);
165+
return Object.fromEntries(page_options);
167166
} catch (error) {
168167
if (error instanceof Error) {
169-
error.message = `Failed to statically analyse ${filename}. ${error.message}`;
168+
error.message = `Failed to statically analyse page options for ${filename}. ${error.message}`;
170169
}
171170
throw error;
172171
}
@@ -183,77 +182,82 @@ export function get_name(node) {
183182
/**
184183
* @param {{
185184
* resolve: (file: string) => Promise<Record<string, any>>;
186-
* static_exports?: Map<string, Record<string, any> | null>;
185+
* static_exports?: Map<string, { page_options: Record<string, any> | null, children: string[] }>;
187186
* }} opts
188187
*/
189188
export function create_node_analyser({ resolve, static_exports = new Map() }) {
190189
/**
191190
* Computes the final page options for a node (if possible). Otherwise, returns `null`.
192191
* @param {import('types').PageNode} node
193-
* @returns {Promise<import('types').UniversalNode | null>}
192+
* @returns {Promise<Record<string, any> | null>}
194193
*/
195194
const get_page_options = async (node) => {
196-
if (node.universal && static_exports.has(node.universal)) {
197-
return /** @type {import('types').UniversalNode | null} */ (
198-
static_exports.get(node.universal)
199-
);
195+
const key = node.universal || node.server;
196+
if (key && static_exports.has(key)) {
197+
return { ...static_exports.get(key)?.page_options };
200198
}
201199

202-
/** @type {Record<string, any> | null} */
200+
/** @type {Record<string, any>} */
203201
let page_options = {};
204202

205-
if (node.server) {
206-
const module = await resolve(node.server);
207-
for (const key in inheritable_page_options) {
208-
if (key in module) {
209-
page_options[key] = module[key];
210-
}
211-
}
212-
}
203+
if (node.parent) {
204+
const parent_options = await get_page_options(node.parent);
213205

214-
if (node.universal) {
215-
let universal_exports = static_exports.get(node.universal);
216-
if (universal_exports === undefined) {
217-
const input = read(node.universal);
218-
universal_exports = statically_analyse_exports(node.universal, input);
206+
const parent_key = node.parent.universal || node.parent.server;
207+
if (key && parent_key) {
208+
static_exports.get(parent_key)?.children.push(key);
219209
}
220210

221-
if (universal_exports === null) {
222-
static_exports.set(node.universal, null);
211+
if (parent_options === null) {
212+
// if the parent cannot be analysed, we can't know what page options
213+
// the child node inherits, so we also mark it as unanalysable
214+
if (key) {
215+
static_exports.set(key, { page_options: null, children: [] });
216+
}
223217
return null;
224218
}
225219

226-
page_options = { ...page_options, ...universal_exports };
220+
page_options = { ...parent_options };
227221
}
228222

229-
if (node.parent) {
230-
const parent_options = await get_page_options(node.parent);
231-
if (parent_options === null) {
232-
// if the parent cannot be statically analysed, we can't know what
233-
// page options the current node inherits, so we invalidate it too
234-
if (node.universal) {
235-
static_exports.set(node.universal, null);
223+
if (node.server) {
224+
const module = await resolve(node.server);
225+
for (const page_option in inheritable_page_options) {
226+
if (page_option in module) {
227+
page_options[page_option] = module[page_option];
236228
}
229+
}
230+
}
231+
232+
if (node.universal) {
233+
const input = read(node.universal);
234+
const universal_page_options = statically_analyse_page_options(node.universal, input);
235+
236+
if (universal_page_options === null) {
237+
static_exports.set(node.universal, { page_options: null, children: [] });
237238
return null;
238239
}
239240

240-
page_options = { ...parent_options, ...page_options };
241+
page_options = { ...page_options, ...universal_page_options };
241242
}
242243

243-
if (node.universal) {
244-
static_exports.set(node.universal, page_options);
244+
if (key) {
245+
static_exports.set(key, { page_options, children: [] });
245246
}
246247

247248
return page_options;
248249
};
249250

250251
/**
251252
* @param {string} file
252-
* @returns {void}
253253
*/
254254
const invalidate_page_options = (file) => {
255+
static_exports.get(file)?.children.forEach((child) => static_exports.delete(child));
255256
static_exports.delete(file);
256257
};
257258

258-
return { get_page_options, invalidate_page_options };
259+
return {
260+
get_page_options,
261+
invalidate_page_options
262+
};
259263
}

packages/kit/src/exports/vite/static_analysis/index.spec.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from 'vitest';
2-
import { statically_analyse_exports } from './index.js';
2+
import { statically_analyse_page_options } from './index.js';
33

44
test.each([
55
[
@@ -18,7 +18,7 @@ test.each([
1818
`
1919
]
2020
])('page option is assigned a literal value: %s', (_, input) => {
21-
const exports = statically_analyse_exports('', input);
21+
const exports = statically_analyse_page_options('', input);
2222
expect(exports).toEqual({ ssr: false, csr: true, prerender: 'auto', trailingSlash: 'always' });
2323
});
2424

@@ -52,7 +52,7 @@ test.each([
5252
],
5353
['export all declaration alias', "export * as ssr from './foo'"]
5454
])('fails when page option is assigned a dynamic value: %s', (_, input) => {
55-
const exports = statically_analyse_exports('', input);
55+
const exports = statically_analyse_page_options('', input);
5656
expect(exports).toEqual(null);
5757
});
5858

@@ -62,7 +62,7 @@ test.each([
6262
['export all declaration alias', 'export * as bar from "./foo"'],
6363
['non-page option export', "export const foo = 'bar'"]
6464
])('ignores %s', (_, input) => {
65-
const exports = statically_analyse_exports('', input);
65+
const exports = statically_analyse_page_options('', input);
6666
expect(exports).toEqual({});
6767
});
6868

@@ -72,7 +72,7 @@ test.each([
7272
['whitespace', 'export * from "./foo";'],
7373
['multiple lines and whitespace', "export \n *\n from 'abc'; "]
7474
])('fails when export all declaration is used: %s', (_, input) => {
75-
const exports = statically_analyse_exports('', input);
75+
const exports = statically_analyse_page_options('', input);
7676
expect(exports).toEqual(null);
7777
});
7878

@@ -131,7 +131,7 @@ test.each([
131131
`
132132
]
133133
])('non-reassigned page options: %s', (_, input) => {
134-
const exports = statically_analyse_exports('', input);
134+
const exports = statically_analyse_page_options('', input);
135135
expect(exports).toEqual({ ssr: true, prerender: true });
136136
});
137137

@@ -151,7 +151,7 @@ test.each([
151151
`
152152
]
153153
])('export specifier references: %s', (_, input) => {
154-
const exports = statically_analyse_exports('', input);
154+
const exports = statically_analyse_page_options('', input);
155155
expect(exports).toEqual({ ssr: false });
156156
});
157157

@@ -185,6 +185,6 @@ test.each([
185185
`
186186
]
187187
])('fails when export specifier references: %s', (_, input) => {
188-
const exports = statically_analyse_exports('', input);
188+
const exports = statically_analyse_page_options('', input);
189189
expect(exports).toEqual(null);
190190
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const ssr = false;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
document.body.style.backgroundColor = 'red';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>hello world</p>

0 commit comments

Comments
 (0)