diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index 06253e6919e4..5f90c2627c69 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -18,7 +18,6 @@ import { print_config_conflicts } from '../config/index.js'; import { create_app } from '../create_app/index.js'; import create_manifest_data from '../create_manifest_data/index.js'; import { getRawBody } from '../node/index.js'; -import { get_server } from '../server/index.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; import { copy_assets, resolve_entry } from '../utils.js'; @@ -106,14 +105,17 @@ class Watcher extends EventEmitter { } }; - /** @type {(req: import("http").IncomingMessage, res: import("http").ServerResponse) => void} */ - let handler = (req, res) => {}; - - this.server = await get_server(this.https, vite_config, (req, res) => handler(req, res)); - // don't warn on overriding defaults const [modified_vite_config] = deep_merge(default_config, vite_config); + const kit_plugin = await create_plugin(this.config, this.dir, this.cwd, () => { + if (!this.manifest) { + throw new Error('Manifest is not available'); + } + + return this.manifest; + }); + /** @type {[any, string[]]} */ const [merged_config, conflicts] = deep_merge(modified_vite_config, { configFile: false, @@ -138,14 +140,13 @@ class Watcher extends EventEmitter { compilerOptions: { hydratable: !!this.config.kit.hydrate } - }) + }), + kit_plugin ], publicDir: this.config.kit.files.assets, server: { - middlewareMode: true, - hmr: { - ...(this.https ? { server: this.server, port: this.port } : {}) - } + host: this.host, + https: this.https }, base: this.config.kit.paths.assets.startsWith('/') ? `${this.config.kit.paths.assets}/` : '/' }); @@ -154,17 +155,21 @@ class Watcher extends EventEmitter { this.vite = await vite.createServer(merged_config); - const get_manifest = () => { - if (!this.manifest) { - throw new Error('Manifest is not available'); + /** + * @type {import('connect').Server} + */ + const connect_server = this.vite.middlewares; + // TODO: replace index with name after https://github.com/vitejs/vite/pull/4908 is merged and released + const spa_fallback_index = 7; + const html_middlewares = ['viteIndexHtmlMiddleware', 'vite404Middleware']; + for (let i = connect_server.stack.length - 1; i > 0; i--) { + // @ts-expect-error using internals until https://github.com/vitejs/vite/pull/4640 is merged + if (html_middlewares.includes(connect_server.stack[i].name) || i === spa_fallback_index) { + connect_server.stack.splice(i, 1); } + } - return this.manifest; - }; - - handler = await create_handler(this.vite, this.config, this.dir, this.cwd, get_manifest); - - this.server.listen(this.port, this.host || '0.0.0.0'); + this.vite.listen(this.port); } update() { @@ -211,7 +216,7 @@ class Watcher extends EventEmitter { } close() { - if (!this.vite || !this.server || !this.cheapwatch) { + if (!this.vite || !this.cheapwatch) { throw new Error('Cannot close server before it is initialized'); } @@ -219,7 +224,6 @@ class Watcher extends EventEmitter { this.closed = true; this.vite.close(); - this.server.close(); this.cheapwatch.close(); } } @@ -248,13 +252,12 @@ function get_params(array) { } /** - * @param {vite.ViteDevServer} vite * @param {import('types/config').ValidatedConfig} config * @param {string} dir * @param {string} cwd * @param {() => import('types/internal').SSRManifest} get_manifest */ -async function create_handler(vite, config, dir, cwd, get_manifest) { +async function create_plugin(config, dir, cwd, get_manifest) { /** * @type {amp_validator.Validator?} */ @@ -273,172 +276,184 @@ async function create_handler(vite, config, dir, cwd, get_manifest) { } }; - /** - * @param {import('http').IncomingMessage} req - * @param {import('http').ServerResponse} res - */ - return (req, res) => { - vite.middlewares(req, res, async () => { - try { - if (!req.url || !req.method) throw new Error('Incomplete request'); - if (req.url === '/favicon.ico') return not_found(res); - - const parsed = new URL(req.url, 'http://localhost/'); - if (!parsed.pathname.startsWith(config.kit.paths.base)) return not_found(res); - - /** @type {Partial} */ - const user_hooks = resolve_entry(config.kit.files.hooks) - ? await vite.ssrLoadModule(`/${config.kit.files.hooks}`) - : {}; - - /** @type {import('types/internal').Hooks} */ - const hooks = { - getSession: user_hooks.getSession || (() => ({})), - handle: user_hooks.handle || (({ request, resolve }) => resolve(request)), - handleError: - user_hooks.handleError || - (({ /** @type {Error & { frame?: string }} */ error, request }) => { - console.error(colors.bold().red(error.message)); - if (error.frame) { - console.error(colors.gray(error.frame)); - } - if (error.stack) { - console.error(colors.gray(error.stack)); - } - }), - externalFetch: user_hooks.externalFetch || fetch - }; - - if (/** @type {any} */ (hooks).getContext) { - // TODO remove this for 1.0 - throw new Error( - 'The getContext hook has been removed. See https://kit.svelte.dev/docs#hooks' - ); - } - - if (/** @type {any} */ (hooks).serverFetch) { - // TODO remove this for 1.0 - throw new Error('The serverFetch hook has been renamed to externalFetch.'); - } - - const root = (await vite.ssrLoadModule(`/${dir}/generated/root.svelte`)).default; + return { + name: 'vite-plugin-svelte-kit', + /** + * + * @param {import('vite').ViteDevServer} vite + */ + configureServer(vite) { + return () => { + /** + * @type {import('connect').Server} + */ + const connect_server = vite.middlewares; + + /** + * Use a named function for debugging + * @type {import('connect').NextHandleFunction} + */ + async function svelteKitMiddleware(req, res, next) { + try { + if (!req.url || !req.method) throw new Error('Incomplete request'); + if (req.url === '/favicon.ico') return not_found(res); + + const parsed = new URL(req.url, 'http://localhost/'); + if (!parsed.pathname.startsWith(config.kit.paths.base)) return not_found(res); + + /** @type {Partial} */ + const user_hooks = resolve_entry(config.kit.files.hooks) + ? await vite.ssrLoadModule(`/${config.kit.files.hooks}`) + : {}; + + /** @type {import('types/internal').Hooks} */ + const hooks = { + getSession: user_hooks.getSession || (() => ({})), + handle: user_hooks.handle || (({ request, resolve }) => resolve(request)), + handleError: + user_hooks.handleError || + (({ /** @type {Error & { frame?: string }} */ error, request }) => { + console.error(colors.bold().red(error.message)); + if (error.frame) { + console.error(colors.gray(error.frame)); + } + if (error.stack) { + console.error(colors.gray(error.stack)); + } + }), + externalFetch: user_hooks.externalFetch || fetch + }; - const paths = await vite.ssrLoadModule(`/${SVELTE_KIT}/dev/runtime/paths.js`); + if (/** @type {any} */ (hooks).getContext) { + // TODO remove this for 1.0 + throw new Error( + 'The getContext hook has been removed. See https://kit.svelte.dev/docs#hooks' + ); + } - paths.set_paths({ - base: config.kit.paths.base, - assets: config.kit.paths.assets ? SVELTE_KIT_ASSETS : config.kit.paths.base - }); + if (/** @type {any} */ (hooks).serverFetch) { + // TODO remove this for 1.0 + throw new Error('The serverFetch hook has been renamed to externalFetch.'); + } - let body; + const root = (await vite.ssrLoadModule(`/${dir}/generated/root.svelte`)).default; - try { - body = await getRawBody(req); - } catch (err) { - res.statusCode = err.status || 400; - return res.end(err.reason || 'Invalid request body'); - } + const paths = await vite.ssrLoadModule(`/${SVELTE_KIT}/dev/runtime/paths.js`); - const host = /** @type {string} */ (config.kit.host || - req.headers[config.kit.hostHeader || 'host']); - - const rendered = await respond( - { - headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), - method: req.method, - host, - path: parsed.pathname.replace(config.kit.paths.base, ''), - query: parsed.searchParams, - rawBody: body - }, - { - amp: config.kit.amp, - dev: true, - entry: { - file: `/${SVELTE_KIT}/dev/runtime/internal/start.js`, - css: [], - js: [] - }, - floc: config.kit.floc, - get_stack: (error) => { - vite.ssrFixStacktrace(error); - return error.stack; - }, - handle_error: (error, request) => { - vite.ssrFixStacktrace(error); - hooks.handleError({ error, request }); - }, - hooks, - hydrate: config.kit.hydrate, - paths: { + paths.set_paths({ base: config.kit.paths.base, assets: config.kit.paths.assets ? SVELTE_KIT_ASSETS : config.kit.paths.base - }, - load_component: async (id) => { - const url = `/${id}`; - - const module = /** @type {SSRComponent} */ (await vite.ssrLoadModule(url)); - const node = await vite.moduleGraph.getModuleByUrl(url); - - if (!node) throw new Error(`Could not find node for ${url}`); - - const deps = new Set(); - find_deps(node, deps); - - const styles = new Set(); - - for (const dep of deps) { - const parsed = new URL(dep.url, 'http://localhost/'); - const query = parsed.searchParams; - - // TODO what about .scss files, etc? - if ( - dep.file.endsWith('.css') || - (query.has('svelte') && query.get('type') === 'style') - ) { - try { - const mod = await vite.ssrLoadModule(dep.url); - styles.add(mod.default); - } catch { - // this can happen with dynamically imported modules, I think - // because the Vite module graph doesn't distinguish between - // static and dynamic imports? TODO investigate, submit fix + }); + + let body; + + try { + body = await getRawBody(req); + } catch (err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } + + const host = /** @type {string} */ (config.kit.host || + req.headers[config.kit.hostHeader || 'host']); + + const rendered = await respond( + { + headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), + method: req.method, + host, + path: parsed.pathname.replace(config.kit.paths.base, ''), + query: parsed.searchParams, + rawBody: body + }, + { + amp: config.kit.amp, + dev: true, + entry: { + file: `/${SVELTE_KIT}/dev/runtime/internal/start.js`, + css: [], + js: [] + }, + floc: config.kit.floc, + get_stack: (error) => { + vite.ssrFixStacktrace(error); + return error.stack; + }, + handle_error: (error, request) => { + vite.ssrFixStacktrace(error); + hooks.handleError({ error, request }); + }, + hooks, + hydrate: config.kit.hydrate, + paths: { + base: config.kit.paths.base, + assets: config.kit.paths.assets ? SVELTE_KIT_ASSETS : config.kit.paths.base + }, + load_component: async (id) => { + const url = `/${id}`; + + const module = /** @type {SSRComponent} */ (await vite.ssrLoadModule(url)); + const node = await vite.moduleGraph.getModuleByUrl(url); + + if (!node) throw new Error(`Could not find node for ${url}`); + + const deps = new Set(); + find_deps(node, deps); + + const styles = new Set(); + + for (const dep of deps) { + const parsed = new URL(dep.url, 'http://localhost/'); + const query = parsed.searchParams; + + // TODO what about .scss files, etc? + if ( + dep.file.endsWith('.css') || + (query.has('svelte') && query.get('type') === 'style') + ) { + try { + const mod = await vite.ssrLoadModule(dep.url); + styles.add(mod.default); + } catch { + // this can happen with dynamically imported modules, I think + // because the Vite module graph doesn't distinguish between + // static and dynamic imports? TODO investigate, submit fix + } + } } - } - } - return { - module, - entry: url.endsWith('.svelte') ? url : url + '?import', - css: [], - js: [], - styles: Array.from(styles) - }; - }, - manifest: get_manifest(), - prerender: config.kit.prerender.enabled, - read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)), - root, - router: config.kit.router, - ssr: config.kit.ssr, - target: config.kit.target, - template: ({ head, body }) => { - let rendered = fs - .readFileSync(config.kit.files.template, 'utf8') - .replace('%svelte.head%', () => head) - .replace('%svelte.body%', () => body); - - if (config.kit.amp && validator) { - const result = validator.validateString(rendered); - - if (result.status !== 'PASS') { - const lines = rendered.split('\n'); - - /** @param {string} str */ - const escape = (str) => - str.replace(/&/g, '&').replace(//g, '>'); - - rendered = ` + return { + module, + entry: url.endsWith('.svelte') ? url : url + '?import', + css: [], + js: [], + styles: Array.from(styles) + }; + }, + manifest: get_manifest(), + prerender: config.kit.prerender.enabled, + read: (file) => fs.readFileSync(path.join(config.kit.files.assets, file)), + root, + router: config.kit.router, + ssr: config.kit.ssr, + target: config.kit.target, + template: ({ head, body }) => { + let rendered = fs + .readFileSync(config.kit.files.template, 'utf8') + .replace('%svelte.head%', () => head) + .replace('%svelte.body%', () => body); + + if (config.kit.amp && validator) { + const result = validator.validateString(rendered); + + if (result.status !== 'PASS') { + const lines = rendered.split('\n'); + + /** @param {string} str */ + const escape = (str) => + str.replace(/&/g, '&').replace(//g, '>'); + + rendered = ` @@ -469,28 +484,31 @@ async function create_handler(vite, config, dir, cwd, get_manifest) { ) .join('\n\n')} `; - } - } + } + } - return rendered; - }, - trailing_slash: config.kit.trailingSlash + return rendered; + }, + trailing_slash: config.kit.trailingSlash + } + ); + + if (rendered) { + res.writeHead(rendered.status, rendered.headers); + if (rendered.body) res.write(rendered.body); + res.end(); + } else { + not_found(res); + } + } catch (e) { + vite.ssrFixStacktrace(e); + res.statusCode = 500; + res.end(e.stack); } - ); - - if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) res.write(rendered.body); - res.end(); - } else { - not_found(res); } - } catch (e) { - vite.ssrFixStacktrace(e); - res.statusCode = 500; - res.end(e.stack); - } - }); + connect_server.use(svelteKitMiddleware); + }; + } }; }