Skip to content

Commit e403f69

Browse files
committed
Merge branch 'main' into version-3
2 parents c2d33b1 + 1527cf7 commit e403f69

14 files changed

Lines changed: 244 additions & 54 deletions

File tree

.changeset/beige-cameras-hide.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: use array type for select fields that accept multiple values

.changeset/quiet-devtools-mute.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: silently 404 Chrome DevTools workspaces request in dev and preview

.changeset/thin-turkeys-give.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: reimplement treeshaking non-dynamic prerendered remote functions

packages/kit/src/exports/public.d.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,28 +1858,35 @@ type InputElementProps<T extends keyof InputTypeMap> = T extends 'checkbox' | 'r
18581858
get files(): FileList | null;
18591859
set files(v: FileList | null);
18601860
}
1861-
: T extends 'select' | 'select multiple'
1861+
: T extends 'select'
18621862
? {
18631863
name: string;
1864-
multiple: T extends 'select' ? false : true;
18651864
'aria-invalid': boolean | 'false' | 'true' | undefined;
1866-
get value(): string | number;
1867-
set value(v: string | number);
1865+
get value(): string;
1866+
set value(v: string);
18681867
}
1869-
: T extends 'text'
1868+
: T extends 'select multiple'
18701869
? {
18711870
name: string;
1871+
multiple: true;
18721872
'aria-invalid': boolean | 'false' | 'true' | undefined;
1873-
get value(): string | number;
1874-
set value(v: string | number);
1873+
get value(): string[];
1874+
set value(v: string[]);
18751875
}
1876-
: {
1877-
name: string;
1878-
type: T;
1879-
'aria-invalid': boolean | 'false' | 'true' | undefined;
1880-
get value(): string | number;
1881-
set value(v: string | number);
1882-
};
1876+
: T extends 'text'
1877+
? {
1878+
name: string;
1879+
'aria-invalid': boolean | 'false' | 'true' | undefined;
1880+
get value(): string | number;
1881+
set value(v: string | number);
1882+
}
1883+
: {
1884+
name: string;
1885+
type: T;
1886+
'aria-invalid': boolean | 'false' | 'true' | undefined;
1887+
get value(): string | number;
1888+
set value(v: string | number);
1889+
};
18831890

18841891
type RemoteFormFieldMethods<T> = {
18851892
/** The values that will be submitted */
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/** @import { ServerMetadata } from 'types' */
2+
/** @import { OutputBundle } from 'rollup' */
3+
4+
import fs from 'node:fs';
5+
import path from 'node:path';
6+
import { Parser } from 'acorn';
7+
import MagicString from 'magic-string';
8+
import { posixify } from '../../../utils/filesystem.js';
9+
import { import_peer } from '../../../utils/import.js';
10+
11+
/**
12+
* @param {string} out
13+
* @param {Array<{ hash: string, file: string }>} remotes
14+
* @param {ServerMetadata} metadata
15+
* @param {string} cwd
16+
* @param {OutputBundle} server_bundle
17+
* @param {NonNullable<import('vitest/config').ViteUserConfig['build']>['sourcemap']} sourcemap
18+
*/
19+
export async function treeshake_prerendered_remotes(
20+
out,
21+
remotes,
22+
metadata,
23+
cwd,
24+
server_bundle,
25+
sourcemap
26+
) {
27+
if (remotes.length === 0) return;
28+
29+
const vite = /** @type {typeof import('vite')} */ (await import_peer('vite'));
30+
31+
for (const remote of remotes) {
32+
const exports_map = metadata.remotes.get(remote.hash);
33+
if (!exports_map) continue;
34+
35+
/** @type {string[]} */
36+
const dynamic = [];
37+
/** @type {string[]} */
38+
const prerendered = [];
39+
40+
for (const [name, value] of exports_map) {
41+
(value.dynamic ? dynamic : prerendered).push(name);
42+
}
43+
44+
if (prerendered.length === 0) continue; // nothing to treeshake
45+
46+
// remove file extension
47+
const remote_filename = path.basename(remote.file).split('.').slice(0, -1).join('.');
48+
49+
const remote_chunk = Object.values(server_bundle).find((chunk) => {
50+
return chunk.name === remote_filename;
51+
});
52+
53+
if (!remote_chunk) continue;
54+
55+
const chunk_path = posixify(path.relative(cwd, `${out}/server/${remote_chunk.fileName}`));
56+
57+
const code = fs.readFileSync(chunk_path, 'utf-8');
58+
const parsed = Parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' });
59+
const modified_code = new MagicString(code);
60+
61+
for (const fn of prerendered) {
62+
for (const node of parsed.body) {
63+
const declaration =
64+
node.type === 'ExportNamedDeclaration'
65+
? node.declaration
66+
: node.type === 'VariableDeclaration'
67+
? node
68+
: null;
69+
70+
if (!declaration || declaration.type !== 'VariableDeclaration') continue;
71+
72+
for (const declarator of declaration.declarations) {
73+
if (declarator.id.type === 'Identifier' && declarator.id.name === fn) {
74+
modified_code.overwrite(
75+
node.start,
76+
node.end,
77+
`const ${fn} = prerender('unchecked', () => { throw new Error('Unexpectedly called prerender function. Did you forget to set { dynamic: true } ?') });`
78+
);
79+
}
80+
}
81+
}
82+
}
83+
84+
for (const node of parsed.body) {
85+
if (node.type === 'ExportDefaultDeclaration') {
86+
modified_code.remove(node.start, node.end);
87+
}
88+
}
89+
90+
const stubbed = modified_code.toString();
91+
fs.writeFileSync(chunk_path, stubbed);
92+
93+
const bundle = /** @type {import('vite').Rollup.RollupOutput} */ (
94+
await vite.build({
95+
configFile: false,
96+
build: {
97+
write: false,
98+
ssr: true,
99+
target: 'esnext',
100+
sourcemap,
101+
rollupOptions: {
102+
// avoid resolving imports
103+
external: (id) => !id.endsWith(chunk_path),
104+
input: {
105+
treeshaken: chunk_path
106+
}
107+
}
108+
}
109+
})
110+
);
111+
112+
const chunk = bundle.output.find(
113+
(output) => output.type === 'chunk' && output.name === 'treeshaken'
114+
);
115+
if (chunk && chunk.type === 'chunk') {
116+
fs.writeFileSync(chunk_path, chunk.code);
117+
118+
const chunk_sourcemap = bundle.output.find(
119+
(output) => output.type === 'asset' && output.fileName === chunk.fileName + '.map'
120+
);
121+
if (chunk_sourcemap && chunk_sourcemap.type === 'asset') {
122+
fs.writeFileSync(chunk_path + '.map', chunk_sourcemap.source);
123+
}
124+
}
125+
}
126+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { SVELTE_KIT_ASSETS } from '../../../constants.js';
1313
import * as sync from '../../../core/sync/sync.js';
1414
import { get_mime_lookup, get_runtime_base } from '../../../core/utils.js';
1515
import { compact } from '../../../utils/array.js';
16-
import { not_found } from '../utils.js';
16+
import { is_chrome_devtools_request, not_found } from '../utils.js';
1717
import { SCHEME } from '../../../utils/url.js';
1818
import { check_feature } from '../../../utils/features.js';
1919
import { escape_html } from '../../../utils/escape.js';
@@ -467,6 +467,10 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root) {
467467
return;
468468
}
469469

470+
if (is_chrome_devtools_request(decoded, res)) {
471+
return;
472+
}
473+
470474
if (!decoded.startsWith(svelte_config.kit.paths.base)) {
471475
return not_found(req, res, svelte_config.kit.paths.base);
472476
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { import_peer } from '../../utils/import.js';
4343
import { compact } from '../../utils/array.js';
4444
import { should_ignore, has_children } from './static_analysis/utils.js';
4545
import { load_config } from '../../core/config/index.js';
46+
import { treeshake_prerendered_remotes } from './build/remote.js';
4647

4748
const cwd = process.cwd();
4849

@@ -1320,7 +1321,9 @@ function kit({ svelte_config }) {
13201321
}
13211322
mkdirp(out);
13221323

1323-
await builder.build(builder.environments.ssr);
1324+
const server_bundle = /** @type {import('rolldown').RolldownOutput} */ (
1325+
await builder.build(builder.environments.ssr)
1326+
);
13241327

13251328
const verbose = vite_config.logLevel === 'info';
13261329
const log = logger({ verbose });
@@ -1565,6 +1568,15 @@ function kit({ svelte_config }) {
15651568
});
15661569
prerendered = prerender_results.prerendered;
15671570

1571+
await treeshake_prerendered_remotes(
1572+
out,
1573+
remotes,
1574+
metadata,
1575+
cwd,
1576+
server_bundle,
1577+
vite_config.build.sourcemap
1578+
);
1579+
15681580
// generate a new manifest that doesn't include prerendered pages
15691581
fs.writeFileSync(
15701582
`${out}/server/manifest.js`,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import sirv from 'sirv';
66
import { loadEnv, normalizePath } from 'vite';
77
import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js';
88
import { SVELTE_KIT_ASSETS } from '../../../constants.js';
9-
import { not_found } from '../utils.js';
9+
import { is_chrome_devtools_request, not_found } from '../utils.js';
1010

1111
/** @typedef {import('http').IncomingMessage} Req */
1212
/** @typedef {import('http').ServerResponse} Res */
@@ -99,6 +99,10 @@ export async function preview(vite, vite_config, svelte_config) {
9999
return;
100100
}
101101

102+
if (is_chrome_devtools_request(pathname, res)) {
103+
return;
104+
}
105+
102106
if (pathname.startsWith(base)) {
103107
next();
104108
} else {

packages/kit/src/exports/vite/utils.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ export function get_env(env_config, mode) {
7777
};
7878
}
7979

80+
/**
81+
* Silently respond with 404 for Chrome DevTools workspaces request.
82+
* Chrome always requests this at the root, regardless of base path.
83+
* Users who want workspaces can install `vite-plugin-devtools-json`,
84+
* which takes precedence as Vite plugin middleware runs first.
85+
* @param {string} pathname
86+
* @param {import('http').ServerResponse} res
87+
* @returns {boolean} `true` if the request was handled
88+
*/
89+
export function is_chrome_devtools_request(pathname, res) {
90+
if (pathname === '/.well-known/appspecific/com.chrome.devtools.json') {
91+
res.writeHead(404);
92+
res.end('not found');
93+
return true;
94+
}
95+
return false;
96+
}
97+
8098
/**
8199
* @param {import('http').IncomingMessage} req
82100
* @param {import('http').ServerResponse} res

packages/kit/src/runtime/server/respond.js

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ const page_methods = new Set(['GET', 'HEAD', 'POST']);
5555

5656
const allowed_page_methods = new Set(['GET', 'HEAD', 'OPTIONS']);
5757

58-
let warned_on_devtools_json_request = false;
59-
6058
export const respond = propagate_context(internal_respond);
6159

6260
/**
@@ -676,21 +674,6 @@ export async function internal_respond(request, options, manifest, state) {
676674
// if this request came direct from the user, rather than
677675
// via our own `fetch`, render a 404 page
678676
if (state.depth === 0) {
679-
// In local development, Chrome requests this file for its 'automatic workspace folders' feature,
680-
// causing console spam. If users want to serve this file they can install
681-
// https://svelte.dev/docs/cli/devtools-json
682-
if (DEV && event.url.pathname === '/.well-known/appspecific/com.chrome.devtools.json') {
683-
if (!warned_on_devtools_json_request) {
684-
console.log(
685-
`\nGoogle Chrome is requesting ${event.url.pathname} to automatically configure devtools project settings. To learn why, and how to prevent this message, see https://svelte.dev/docs/cli/devtools-json\n`
686-
);
687-
688-
warned_on_devtools_json_request = true;
689-
}
690-
691-
return new Response(undefined, { status: 404 });
692-
}
693-
694677
return await respond_with_error({
695678
event,
696679
event_state,

0 commit comments

Comments
 (0)