diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index a1d65f277019..468cdb73b3f2 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -1,5 +1,6 @@ import path from 'node:path'; import { platforms } from './platforms.js'; +import { get_options_message } from './utils.js'; /** @type {import('./index.js').default} */ export default function (options) { @@ -17,36 +18,13 @@ export default function (options) { ${dynamic_routes.map((route) => ` - ${path.posix.join(prefix, route.id)}`).join('\n')}\n` ); - const options = [ - 'set the `fallback` option — see https://svelte.dev/docs/kit/single-page-apps#usage for more info.', - 'add `export const prerender = true` to your root `+layout.js/.ts` or `+layout.server.js/.ts` file. This will try to prerender all pages.', - 'add `export const prerender = true` to any `+server.js/ts` files that are not fetched by page `load` functions.' - ]; - - if ( - has_param_routes || - JSON.stringify(builder.config.kit.prerender.entries) !== '["*"]' - ) { - let option = 'adjust the `prerender.entries` config option'; - if (has_param_routes) - option += ' (routes with parameters are not part of entry points by default)'; - options.push(option); - } - - options.push( - "pass `strict: false` to `adapter-static` to ignore this error. Only do this if you are sure you don't need the routes in question in your final app, as they will be unavailable. See https://github.com/sveltejs/kit/tree/main/packages/adapter-static#strict for more info." - ); - builder.log( - `You have the following options:${options.map((o) => `\n - ${o}`).join('')}\n` + get_options_message( + has_param_routes, + JSON.stringify(builder.config.kit.prerender.entries) !== '["*"]' + ) ); - builder.log( - `If this doesn't help, you may need to use a different adapter. @sveltejs/adapter-static can only be used for sites that don't need a server for dynamic rendering, and can run on just a static file server.\n` - ); - - builder.log(`See https://svelte.dev/docs/kit/page-options#prerender for more details\n`); - const error = new Error('Encountered dynamic routes'); error.stack = ''; throw error; @@ -99,6 +77,33 @@ ${dynamic_routes.map((route) => ` - ${path.posix.join(prefix, route.id)}`).join } if (!options) platform?.done(builder); + }, + vite: { + plugins: [ + { + name: 'vite-plugin-sveltekit-adapter-static', + configEnvironment(name) { + if (name === 'ssr') { + return { + define: { + __SVELTEKIT_ADAPTER_STATIC_FALLBACK__: options?.fallback ? 'true' : 'false' + } + }; + } + }, + applyToEnvironment(environment) { + return environment.name === 'ssr'; + }, + resolveId: { + filter: { + id: /^sveltekit:server-entry$/ + }, + handler() { + return this.resolve(import.meta.resolve('./src/dev.js')); + } + } + } + ] } }; } diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json index 16efa568a11e..2d00004fa995 100644 --- a/packages/adapter-static/package.json +++ b/packages/adapter-static/package.json @@ -30,7 +30,8 @@ "files": [ "index.js", "index.d.ts", - "platforms.js" + "platforms.js", + "src/**/*.js" ], "scripts": { "lint": "prettier --check .", diff --git a/packages/adapter-static/src/dev.js b/packages/adapter-static/src/dev.js new file mode 100644 index 000000000000..9efc20327546 --- /dev/null +++ b/packages/adapter-static/src/dev.js @@ -0,0 +1,55 @@ +import { env } from 'sveltekit:env'; +import { Server } from 'sveltekit:server'; +import { manifest } from 'sveltekit:server-manifest'; +import { createReadableStream } from '@sveltejs/kit/node'; +import { styleText } from 'node:util'; +import { get_options_message } from '../utils.js'; +import { set_prerendering } from '$app/env/internal'; + +set_prerendering(true); + +/** @type {import('../../kit/src/types/internal.js').InternalServer} */ +const server = new Server(manifest); + +await server.init({ + env, + read: (file) => createReadableStream(file) +}); + +/** + * @param {Request} request + * @returns {Promise} + */ +export async function fetch(request) { + return await server.respond(request, { + // simulate prerendering during development to surface errors earlier + prerendering: { + dependencies: new Map(), + remote_responses: new Map(), + fallback: __SVELTEKIT_ADAPTER_STATIC_FALLBACK__ + }, + getClientAddress() { + throw new Error('Cannot read clientAddress on prerendered pages'); + }, + // @ts-expect-error this is only needed during actual prerendering + read: undefined, + before_handle: async (event, _config, prerender, handle) => { + if (!event.isSubRequest && !event.isDataRequest && !event.isRemoteRequest && !prerender) { + const error = new Error('Encountered dynamic routes'); + error.stack = ''; + + console.error( + styleText( + ['bold', 'red'], + `@sveltejs/adapter-static: all routes must be fully prerenderable, but the ${event.url.pathname} route is dynamic\n` + ) + ); + + console.log(get_options_message(!!Object.keys(event.params).length, false)); + + throw error; + } + return await handle(); + } + }); +} diff --git a/packages/adapter-static/src/global-private.d.ts b/packages/adapter-static/src/global-private.d.ts new file mode 100644 index 000000000000..5b9e62c964da --- /dev/null +++ b/packages/adapter-static/src/global-private.d.ts @@ -0,0 +1,5 @@ +declare global { + export const __SVELTEKIT_ADAPTER_STATIC_FALLBACK__: boolean; +} + +export {}; diff --git a/packages/adapter-static/test/apps/prerendered/jsconfig.json b/packages/adapter-static/test/apps/prerendered/jsconfig.json new file mode 100644 index 000000000000..681df462f63f --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/jsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "$app/tsconfig", + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/no-prerender/+page.js b/packages/adapter-static/test/apps/prerendered/src/routes/no-prerender/+page.js new file mode 100644 index 000000000000..d43d0cd2a55d --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/routes/no-prerender/+page.js @@ -0,0 +1 @@ +export const prerender = false; diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/no-prerender/+page.svelte b/packages/adapter-static/test/apps/prerendered/src/routes/no-prerender/+page.svelte new file mode 100644 index 000000000000..b1387648182b --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/routes/no-prerender/+page.svelte @@ -0,0 +1 @@ +

not prerendered

diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/query/+page.svelte b/packages/adapter-static/test/apps/prerendered/src/routes/query/+page.svelte new file mode 100644 index 000000000000..c7e550b0f10f --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/routes/query/+page.svelte @@ -0,0 +1,7 @@ + + +

{str}

diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/query/remote.js b/packages/adapter-static/test/apps/prerendered/src/routes/query/remote.js new file mode 100644 index 000000000000..1ff06ff4705c --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/routes/query/remote.js @@ -0,0 +1,5 @@ +import { query } from '$app/server'; + +export const not_allowed = query(() => { + return 'foo'; +}); diff --git a/packages/adapter-static/test/apps/prerendered/test/test.js b/packages/adapter-static/test/apps/prerendered/test/test.js index 6b16d2fed83b..aa97da9d2085 100644 --- a/packages/adapter-static/test/apps/prerendered/test/test.js +++ b/packages/adapter-static/test/apps/prerendered/test/test.js @@ -27,3 +27,7 @@ test('exposes public env vars to the client', async ({ page }) => { expect(await page.textContent('h1')).toEqual('The answer is 42'); expect(await page.textContent('h2')).toEqual('The dynamic answer is 42'); }); + +test.describe('dev', () => { + // TODO: add tests for prerender errors during development +}); diff --git a/packages/adapter-static/test/apps/prerendered/vite.config.js b/packages/adapter-static/test/apps/prerendered/vite.config.js index dc6532d9578d..486bb4f887dc 100644 --- a/packages/adapter-static/test/apps/prerendered/vite.config.js +++ b/packages/adapter-static/test/apps/prerendered/vite.config.js @@ -8,7 +8,15 @@ const config = { }, plugins: [ sveltekit({ - adapter: adapter() + adapter: adapter(), + experimental: { + remoteFunctions: true + }, + compilerOptions: { + experimental: { + async: true + } + } }) ] }; diff --git a/packages/adapter-static/tsconfig.json b/packages/adapter-static/tsconfig.json index 22ea3b1d6c08..1e7feddb95d5 100644 --- a/packages/adapter-static/tsconfig.json +++ b/packages/adapter-static/tsconfig.json @@ -11,5 +11,5 @@ }, "types": ["node"] }, - "include": ["index.js", "test/utils.js"] + "include": ["index.js", "utils.js", "src/*.js", "src/*.ts", "test/utils.js"] } diff --git a/packages/adapter-static/utils.js b/packages/adapter-static/utils.js new file mode 100644 index 000000000000..74f17766de72 --- /dev/null +++ b/packages/adapter-static/utils.js @@ -0,0 +1,31 @@ +/** + * Returns a message with a list of options to correctly configure the app. + * @param {boolean} has_param_routes + * @param {boolean} has_custom_entries + * @returns {string} + */ +export function get_options_message(has_param_routes, has_custom_entries) { + const options = [ + 'add `export const prerender = true` to your root `+layout.js/.ts` or `+layout.server.js/.ts` file. This will try to prerender all pages.', + 'add `export const prerender = true` to any `+server.js/ts` files that are not fetched by page `load` functions.' + ]; + + if (has_param_routes || has_custom_entries) { + let option = 'adjust the `prerender.entries` config option'; + if (has_param_routes) + option += ' (routes with parameters are not part of entry points by default)'; + options.push(option); + } + + options.push( + 'set the `fallback` option — see https://svelte.dev/docs/kit/single-page-apps#usage for more info.', + "pass `strict: false` to `adapter-static` to ignore this error. Only do this if you are sure you don't need the routes in question in your final app, as they will be unavailable. See https://github.com/sveltejs/kit/tree/main/packages/adapter-static#strict for more info." + ); + + let message = `You have the following options:${options.map((o) => `\n - ${o}`).join('')}`; + message += + "\n\nIf this doesn't help, you may need to use a different adapter. @sveltejs/adapter-static can only be used for sites that don't need a server for dynamic rendering, and can run on just a static file server."; + message += '\n\nSee https://svelte.dev/docs/kit/page-options#prerender for more details\n'; + + return message; +} diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index be9b86052bec..add4f13dd425 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -136,6 +136,7 @@ export async function internal_respond(request, options, manifest, state) { /** @type {boolean[] | undefined} */ let invalidated_data_nodes; + /** Skipped for query remote function requests */ let skip_route_resolution = false; if (is_route_resolution_request) { @@ -368,7 +369,7 @@ export async function internal_respond(request, options, manifest, state) { return text('Not found', { status: 404, headers }); } - if (!state.prerendering?.fallback && !skip_route_resolution) { + if ((!state.prerendering?.fallback && !skip_route_resolution) || is_data_request) { try { const matchers = await manifest._.matchers(); const result = find_route(resolved_path, manifest._.routes, matchers); @@ -435,7 +436,7 @@ export async function internal_respond(request, options, manifest, state) { prerender = node.prerender ?? prerender; } else if (page_nodes) { config = page_nodes.get_config() ?? config; - prerender = page_nodes.prerender(); + prerender = state.prerender_default = page_nodes.prerender(); } if (state.emulator?.platform) { @@ -614,7 +615,7 @@ export async function internal_respond(request, options, manifest, state) { }); } - if (options.hash_routing || state.prerendering?.fallback) { + if ((options.hash_routing || state.prerendering?.fallback) && !is_data_request) { return await render_response({ event, event_state,