Skip to content

Commit c660143

Browse files
Node polyfills (#4934)
* move install-fetch to node/polyfills, swap node-fetch for undici * keep using node-fetch for now * polyfill crypto * remove logs * enable tests * use webcrypto API * lockfile * lint * fix some typechecking stuff * reset lockfile * undo unrelated changes * only polyfill missing APIs * update tests * more tweaks * use set-cookie-parser in adapter-netlify * gah * always polyfill, even on node 18 * include crypto in list of provided APIs * fix unrelated typechecking issue * changeset * Update packages/kit/src/node/index.js Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Ben McCann <[email protected]>
1 parent 00a6c56 commit c660143

File tree

21 files changed

+105
-91
lines changed

21 files changed

+105
-91
lines changed

.changeset/plenty-mirrors-clap.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@sveltejs/adapter-netlify': patch
3+
'@sveltejs/adapter-node': patch
4+
'@sveltejs/adapter-vercel': patch
5+
'@sveltejs/kit': patch
6+
---
7+
8+
[breaking] replace @sveltejs/kit/install-fetch with @sveltejs/kit/node/polyfills

documentation/docs/01-web-standards.md

+8
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,11 @@ export {};
6565
// ---cut---
6666
const foo = url.searchParams.get('foo');
6767
```
68+
69+
### Web Crypto
70+
71+
The [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is made available via the `crypto` global. It's used internally for [Content Security Policy](/docs/configuration#csp) headers, but you can also use it for things like generating UUIDs:
72+
73+
```js
74+
const uuid = crypto.randomUUID();
75+
```

packages/adapter-netlify/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
"dependencies": {
3535
"@iarna/toml": "^2.2.5",
3636
"esbuild": "^0.14.29",
37+
"set-cookie-parser": "^2.4.8",
3738
"tiny-glob": "^0.2.9"
3839
},
3940
"devDependencies": {
41+
"@types/set-cookie-parser": "^2.4.2",
4042
"@netlify/functions": "^1.0.0",
4143
"@sveltejs/kit": "workspace:*"
4244
}

packages/adapter-netlify/src/headers.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as set_cookie_parser from 'set-cookie-parser';
2+
13
/**
24
* Splits headers into two categories: single value and multi value
35
* @param {Headers} headers
@@ -15,8 +17,7 @@ export function split_headers(headers) {
1517

1618
headers.forEach((value, key) => {
1719
if (key === 'set-cookie') {
18-
// @ts-expect-error (headers.raw() is non-standard)
19-
m[key] = headers.raw()[key];
20+
m[key] = set_cookie_parser.splitCookiesString(value);
2021
} else {
2122
h[key] = value;
2223
}

packages/adapter-netlify/src/headers.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import '../src/shims.js';
1+
import './shims.js';
22
import { test } from 'uvu';
33
import * as assert from 'uvu/assert';
4-
import { split_headers } from '../src/headers.js';
4+
import { split_headers } from './headers.js';
55

66
test('empty headers', () => {
77
const headers = new Headers();

packages/adapter-netlify/src/shims.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
import { installFetch } from '@sveltejs/kit/install-fetch';
2-
installFetch();
1+
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
2+
installPolyfills();

packages/adapter-node/src/shims.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
import { installFetch } from '@sveltejs/kit/install-fetch';
2-
installFetch();
1+
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
2+
installPolyfills();

packages/adapter-vercel/files/serverless.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { installFetch } from '@sveltejs/kit/install-fetch';
1+
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
22
import { getRequest, setResponse } from '@sveltejs/kit/node';
33
import { Server } from 'SERVER';
44
import { manifest } from 'MANIFEST';
55

6-
installFetch();
6+
installPolyfills();
77

88
const server = new Server(manifest);
99

packages/kit/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@
6868
"./node": {
6969
"import": "./dist/node.js"
7070
},
71+
"./node/polyfills": {
72+
"import": "./dist/node/polyfills.js"
73+
},
7174
"./hooks": {
7275
"import": "./dist/hooks.js"
73-
},
74-
"./install-fetch": {
75-
"import": "./dist/install-fetch.js"
7676
}
7777
},
7878
"types": "types/index.d.ts",

packages/kit/rollup.config.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ export default [
6161
{
6262
input: {
6363
cli: 'src/cli.js',
64-
node: 'src/node.js',
65-
hooks: 'src/hooks.js',
66-
'install-fetch': 'src/install-fetch.js'
64+
node: 'src/node/index.js',
65+
'node/polyfills': 'src/node/polyfills.js',
66+
hooks: 'src/hooks.js'
6767
},
6868
output: {
6969
dir: 'dist',

packages/kit/src/core/build/prerender/prerender.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'fs';
22
import { dirname, join } from 'path';
33
import { pathToFileURL, URL } from 'url';
44
import { mkdirp } from '../../../utils/filesystem.js';
5-
import { installFetch } from '../../../install-fetch.js';
5+
import { installPolyfills } from '../../../node/polyfills.js';
66
import { is_root_relative, normalize_path, resolve } from '../../../utils/url.js';
77
import { queue } from './queue.js';
88
import { crawl } from './crawl.js';
@@ -59,7 +59,7 @@ export async function prerender({ config, entries, files, log }) {
5959
return prerendered;
6060
}
6161

62-
installFetch();
62+
installPolyfills();
6363

6464
const server_root = join(config.kit.outDir, 'output');
6565

packages/kit/src/core/dev/plugin.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import path from 'path';
33
import { URL } from 'url';
44
import colors from 'kleur';
55
import sirv from 'sirv';
6-
import { installFetch } from '../../install-fetch.js';
6+
import { installPolyfills } from '../../node/polyfills.js';
77
import * as sync from '../sync/sync.js';
8-
import { getRequest, setResponse } from '../../node.js';
8+
import { getRequest, setResponse } from '../../node/index.js';
99
import { SVELTE_KIT_ASSETS } from '../constants.js';
1010
import { get_mime_lookup, get_runtime_path, resolve_entry } from '../utils.js';
1111
import { coalesce_to_error } from '../../utils/error.js';
@@ -34,7 +34,7 @@ export async function create_plugin(config, cwd) {
3434
name: 'vite-plugin-svelte-kit',
3535

3636
configureServer(vite) {
37-
installFetch();
37+
installPolyfills();
3838

3939
/** @type {import('types').SSRManifest} */
4040
let manifest;

packages/kit/src/core/preview/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import https from 'https';
44
import { join } from 'path';
55
import sirv from 'sirv';
66
import { pathToFileURL } from 'url';
7-
import { getRequest, setResponse } from '../../node.js';
8-
import { installFetch } from '../../install-fetch.js';
7+
import { getRequest, setResponse } from '../../node/index.js';
8+
import { installPolyfills } from '../../node/polyfills.js';
99
import { SVELTE_KIT_ASSETS } from '../constants.js';
1010

1111
/** @typedef {import('http').IncomingMessage} Req */
@@ -34,7 +34,7 @@ const mutable = (dir) =>
3434
* }} opts
3535
*/
3636
export async function preview({ port, host, config, https: use_https = false }) {
37-
installFetch();
37+
installPolyfills();
3838

3939
const { paths } = config.kit;
4040
const base = paths.base;

packages/kit/src/install-fetch.js

-27
This file was deleted.

packages/kit/src/node.js renamed to packages/kit/src/node/index.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Readable } from 'stream';
2+
import * as set_cookie_parser from 'set-cookie-parser';
23

34
/** @param {import('http').IncomingMessage} req */
45
function get_raw_body(req) {
@@ -56,6 +57,7 @@ export async function getRequest(base, req) {
5657
if (req.httpVersionMajor === 2) {
5758
// we need to strip out the HTTP/2 pseudo-headers because node-fetch's
5859
// Request implementation doesn't like them
60+
// TODO is this still true with Node 18
5961
headers = Object.assign({}, headers);
6062
delete headers[':method'];
6163
delete headers[':path'];
@@ -74,8 +76,11 @@ export async function setResponse(res, response) {
7476
const headers = Object.fromEntries(response.headers);
7577

7678
if (response.headers.has('set-cookie')) {
77-
// @ts-expect-error (headers.raw() is non-standard)
78-
headers['set-cookie'] = response.headers.raw()['set-cookie'];
79+
const header = /** @type {string} */ (response.headers.get('set-cookie'));
80+
const split = set_cookie_parser.splitCookiesString(header);
81+
82+
// @ts-expect-error
83+
headers['set-cookie'] = split;
7984
}
8085

8186
res.writeHead(response.status, headers);
@@ -84,7 +89,7 @@ export async function setResponse(res, response) {
8489
response.body.pipe(res);
8590
} else {
8691
if (response.body) {
87-
res.write(await response.arrayBuffer());
92+
res.write(new Uint8Array(await response.arrayBuffer()));
8893
}
8994

9095
res.end();

packages/kit/src/node/polyfills.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import fetch, { Response, Request, Headers } from 'node-fetch';
2+
import { webcrypto as crypto } from 'crypto';
3+
4+
/** @type {Record<string, any>} */
5+
const globals = {
6+
crypto,
7+
fetch,
8+
Response,
9+
Request,
10+
Headers
11+
};
12+
13+
// exported for dev/preview and node environments
14+
export function installPolyfills() {
15+
for (const name in globals) {
16+
// TODO use built-in fetch once https://github.com/nodejs/undici/issues/1262 is resolved
17+
Object.defineProperty(globalThis, name, {
18+
enumerable: true,
19+
configurable: true,
20+
value: globals[name]
21+
});
22+
}
23+
}

packages/kit/src/runtime/server/page/crypto.spec.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from 'uvu';
22
import * as assert from 'uvu/assert';
3-
import crypto from 'crypto';
3+
import { webcrypto } from 'crypto';
44
import { sha256 } from './crypto.js';
55

66
const inputs = [
@@ -12,9 +12,13 @@ const inputs = [
1212
].slice(0);
1313

1414
inputs.forEach((input) => {
15-
test(input, () => {
16-
const expected_bytes = crypto.createHash('sha256').update(input, 'utf-8').digest();
17-
const expected = expected_bytes.toString('base64');
15+
test(input, async () => {
16+
// @ts-expect-error typescript what are you doing you lunatic
17+
const expected_bytes = await webcrypto.subtle.digest(
18+
'SHA-256',
19+
new TextEncoder().encode(input)
20+
);
21+
const expected = Buffer.from(expected_bytes).toString('base64');
1822

1923
const actual = sha256(input);
2024
assert.equal(actual, expected);

packages/kit/src/runtime/server/page/csp.js

+7-29
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,11 @@ import { sha256, base64 } from './crypto.js';
44
/** @type {Promise<void>} */
55
export let csp_ready;
66

7-
/** @type {() => string} */
8-
let generate_nonce;
9-
10-
/** @type {(input: string) => string} */
11-
let generate_hash;
12-
13-
if (typeof crypto !== 'undefined') {
14-
const array = new Uint8Array(16);
15-
16-
generate_nonce = () => {
17-
crypto.getRandomValues(array);
18-
return base64(array);
19-
};
20-
21-
generate_hash = sha256;
22-
} else {
23-
// TODO: remove this in favor of web crypto API once we no longer support Node 14
24-
const name = 'crypto'; // store in a variable to fool esbuild when adapters bundle kit
25-
csp_ready = import(name).then((crypto) => {
26-
generate_nonce = () => {
27-
return crypto.randomBytes(16).toString('base64');
28-
};
29-
30-
generate_hash = (input) => {
31-
return crypto.createHash('sha256').update(input, 'utf-8').digest().toString('base64');
32-
};
33-
});
7+
const array = new Uint8Array(16);
8+
9+
function generate_nonce() {
10+
crypto.getRandomValues(array);
11+
return base64(array);
3412
}
3513

3614
const quoted = new Set([
@@ -133,7 +111,7 @@ export class Csp {
133111
add_script(content) {
134112
if (this.#script_needs_csp) {
135113
if (this.#use_hashes) {
136-
this.#script_src.push(`sha256-${generate_hash(content)}`);
114+
this.#script_src.push(`sha256-${sha256(content)}`);
137115
} else if (this.#script_src.length === 0) {
138116
this.#script_src.push(`nonce-${this.nonce}`);
139117
}
@@ -144,7 +122,7 @@ export class Csp {
144122
add_style(content) {
145123
if (this.#style_needs_csp) {
146124
if (this.#use_hashes) {
147-
this.#style_src.push(`sha256-${generate_hash(content)}`);
125+
this.#style_src.push(`sha256-${sha256(content)}`);
148126
} else if (this.#style_src.length === 0) {
149127
this.#style_src.push(`nonce-${this.nonce}`);
150128
}

packages/kit/src/runtime/server/page/csp.spec.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { test } from 'uvu';
22
import * as assert from 'uvu/assert';
3+
import { webcrypto } from 'crypto';
34
import { Csp } from './csp.js';
45

6+
// @ts-expect-error
7+
globalThis.crypto = webcrypto;
8+
59
test('generates blank CSP header', () => {
610
const csp = new Csp(
711
{

packages/kit/types/ambient.d.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,16 @@ declare module '@sveltejs/kit/hooks' {
283283
/**
284284
* A polyfill for `fetch` and its related interfaces, used by adapters for environments that don't provide a native implementation.
285285
*/
286-
declare module '@sveltejs/kit/install-fetch' {
286+
declare module '@sveltejs/kit/node/polyfills' {
287287
/**
288-
* Make `fetch`, `Headers`, `Request` and `Response` available as globals, via `node-fetch`
288+
* Make various web APIs available as globals:
289+
* - `crypto`
290+
* - `fetch`
291+
* - `Headers`
292+
* - `Request`
293+
* - `Response`
289294
*/
290-
export function installFetch(): void;
295+
export function installPolyfills(): void;
291296
}
292297

293298
/**

pnpm-lock.yaml

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)