Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/big-eels-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: correctly invalidate static analysis cache of child nodes when modifying a universal `+layout` file during dev
1 change: 1 addition & 0 deletions packages/kit/src/core/postbuild/analyse.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async function analyse({
internal.set_manifest(manifest);
internal.set_read_implementation((file) => createReadableStream(`${server_root}/server/${file}`));

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

// first, build server nodes without the client manifest so we can analyse it
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/exports/vite/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { create_node_analyser } from '../static_analysis/index.js';
* @param {import('vite').Manifest | null} client_manifest
* @param {import('vite').Rollup.OutputAsset[] | null} css
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
* @param {Map<string, Record<string, any> | null>} static_exports
* @param {Map<string, { page_options: Record<string, any> | null, children: string[] }>} static_exports
*/
export async function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css, output_config, static_exports) {
mkdirp(`${out}/server/nodes`);
Expand Down
12 changes: 8 additions & 4 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,14 +355,18 @@ export async function dev(vite, vite_config, svelte_config) {

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

if (/\+(page|layout).*$/.test(file)) {
invalidate_page_options(path.relative(cwd, file));
}

sync.update(svelte_config, manifest_data, file);
invalidate_page_options(path.relative(cwd, file));
});

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

// changing the svelte config requires restarting the dev server
// the config is only read on start and passed on to vite-plugin-svelte
// which needs up-to-date values to operate correctly
vite.watcher.on('change', async (file) => {
// changing the svelte config requires restarting the dev server
// the config is only read on start and passed on to vite-plugin-svelte
// which needs up-to-date values to operate correctly
if (path.basename(file) === 'svelte.config.js') {
console.log(`svelte config changed, restarting vite dev-server. changed file: ${file}`);
restarting = true;
Expand Down
106 changes: 55 additions & 51 deletions packages/kit/src/exports/vite/static_analysis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,22 @@ import { read } from '../../../utils/filesystem.js';

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

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

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

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

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

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

for (const statement of source.body) {
// ignore export all declarations with aliases that are not page options
if (
statement.type === 'ExportAllDeclaration' &&
statement.exported &&
!page_options.has(get_name(statement.exported))
!valid_page_options.has(get_name(statement.exported))
) {
continue;
}
Expand All @@ -60,7 +59,7 @@ export function statically_analyse_exports(filename, input) {
const export_specifiers = new Map();
for (const specifier of statement.specifiers) {
const exported_name = get_name(specifier.exported);
if (!page_options.has(exported_name)) {
if (!valid_page_options.has(exported_name)) {
continue;
}

Expand Down Expand Up @@ -109,7 +108,7 @@ export function statically_analyse_exports(filename, input) {
}

if (variable_declarator.init?.type === 'Literal') {
static_exports.set(
page_options.set(
/** @type {string} */ (export_specifiers.get(variable_declarator.id.name)),
variable_declarator.init.value
);
Expand Down Expand Up @@ -138,7 +137,7 @@ export function statically_analyse_exports(filename, input) {

// class and function declarations
if (statement.declaration.type !== 'VariableDeclaration') {
if (page_options.has(statement.declaration.id.name)) {
if (valid_page_options.has(statement.declaration.id.name)) {
return null;
}
continue;
Expand All @@ -149,12 +148,12 @@ export function statically_analyse_exports(filename, input) {
return null;
}

if (!page_options.has(declaration.id.name)) {
if (!valid_page_options.has(declaration.id.name)) {
continue;
}

if (declaration.init?.type === 'Literal') {
static_exports.set(declaration.id.name, declaration.init.value);
page_options.set(declaration.id.name, declaration.init.value);
continue;
}

Expand All @@ -163,10 +162,10 @@ export function statically_analyse_exports(filename, input) {
}
}

return Object.fromEntries(static_exports);
return Object.fromEntries(page_options);
} catch (error) {
if (error instanceof Error) {
error.message = `Failed to statically analyse ${filename}. ${error.message}`;
error.message = `Failed to statically analyse page options for ${filename}. ${error.message}`;
}
throw error;
}
Expand All @@ -183,77 +182,82 @@ export function get_name(node) {
/**
* @param {{
* resolve: (file: string) => Promise<Record<string, any>>;
* static_exports?: Map<string, Record<string, any> | null>;
* static_exports?: Map<string, { page_options: Record<string, any> | null, children: string[] }>;
* }} opts
*/
export function create_node_analyser({ resolve, static_exports = new Map() }) {
/**
* Computes the final page options for a node (if possible). Otherwise, returns `null`.
* @param {import('types').PageNode} node
* @returns {Promise<import('types').UniversalNode | null>}
* @returns {Promise<Record<string, any> | null>}
*/
const get_page_options = async (node) => {
if (node.universal && static_exports.has(node.universal)) {
return /** @type {import('types').UniversalNode | null} */ (
static_exports.get(node.universal)
);
const key = node.universal || node.server;
if (key && static_exports.has(key)) {
return { ...static_exports.get(key)?.page_options };
}

/** @type {Record<string, any> | null} */
/** @type {Record<string, any>} */
let page_options = {};

if (node.server) {
const module = await resolve(node.server);
for (const key in inheritable_page_options) {
if (key in module) {
page_options[key] = module[key];
}
}
}
if (node.parent) {
const parent_options = await get_page_options(node.parent);

if (node.universal) {
let universal_exports = static_exports.get(node.universal);
if (universal_exports === undefined) {
const input = read(node.universal);
universal_exports = statically_analyse_exports(node.universal, input);
const parent_key = node.parent.universal || node.parent.server;
if (key && parent_key) {
static_exports.get(parent_key)?.children.push(key);
}

if (universal_exports === null) {
static_exports.set(node.universal, null);
if (parent_options === null) {
// if the parent cannot be analysed, we can't know what page options
// the child node inherits, so we also mark it as unanalysable
if (key) {
static_exports.set(key, { page_options: null, children: [] });
}
return null;
}

page_options = { ...page_options, ...universal_exports };
page_options = { ...parent_options };
}

if (node.parent) {
const parent_options = await get_page_options(node.parent);
if (parent_options === null) {
// if the parent cannot be statically analysed, we can't know what
// page options the current node inherits, so we invalidate it too
if (node.universal) {
static_exports.set(node.universal, null);
if (node.server) {
const module = await resolve(node.server);
for (const page_option in inheritable_page_options) {
if (page_option in module) {
page_options[page_option] = module[page_option];
}
}
}

if (node.universal) {
const input = read(node.universal);
const universal_page_options = statically_analyse_page_options(node.universal, input);

if (universal_page_options === null) {
static_exports.set(node.universal, { page_options: null, children: [] });
return null;
}

page_options = { ...parent_options, ...page_options };
page_options = { ...page_options, ...universal_page_options };
}

if (node.universal) {
static_exports.set(node.universal, page_options);
if (key) {
static_exports.set(key, { page_options, children: [] });
}

return page_options;
};

/**
* @param {string} file
* @returns {void}
*/
const invalidate_page_options = (file) => {
static_exports.get(file)?.children.forEach((child) => static_exports.delete(child));
static_exports.delete(file);
};

return { get_page_options, invalidate_page_options };
return {
get_page_options,
invalidate_page_options
};
}
16 changes: 8 additions & 8 deletions packages/kit/src/exports/vite/static_analysis/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest';
import { statically_analyse_exports } from './index.js';
import { statically_analyse_page_options } from './index.js';

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

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

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

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

Expand Down Expand Up @@ -131,7 +131,7 @@ test.each([
`
]
])('non-reassigned page options: %s', (_, input) => {
const exports = statically_analyse_exports('', input);
const exports = statically_analyse_page_options('', input);
expect(exports).toEqual({ ssr: true, prerender: true });
});

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

Expand Down Expand Up @@ -185,6 +185,6 @@ test.each([
`
]
])('fails when export specifier references: %s', (_, input) => {
const exports = statically_analyse_exports('', input);
const exports = statically_analyse_page_options('', input);
expect(exports).toEqual(null);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ssr = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
document.body.style.backgroundColor = 'red';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>hello world</p>
Loading
Loading