Skip to content

Commit

Permalink
Node polyfills (#4934)
Browse files Browse the repository at this point in the history
* 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 <322311+benmccann@users.noreply.github.com>

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
  • Loading branch information
Rich-Harris and benmccann committed May 23, 2022
1 parent 00a6c56 commit c660143
Show file tree
Hide file tree
Showing 21 changed files with 105 additions and 91 deletions.
8 changes: 8 additions & 0 deletions .changeset/plenty-mirrors-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sveltejs/adapter-netlify': patch
'@sveltejs/adapter-node': patch
'@sveltejs/adapter-vercel': patch
'@sveltejs/kit': patch
---

[breaking] replace @sveltejs/kit/install-fetch with @sveltejs/kit/node/polyfills
8 changes: 8 additions & 0 deletions documentation/docs/01-web-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ export {};
// ---cut---
const foo = url.searchParams.get('foo');
```

### Web Crypto

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:

```js
const uuid = crypto.randomUUID();
```
2 changes: 2 additions & 0 deletions packages/adapter-netlify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
"dependencies": {
"@iarna/toml": "^2.2.5",
"esbuild": "^0.14.29",
"set-cookie-parser": "^2.4.8",
"tiny-glob": "^0.2.9"
},
"devDependencies": {
"@types/set-cookie-parser": "^2.4.2",
"@netlify/functions": "^1.0.0",
"@sveltejs/kit": "workspace:*"
}
Expand Down
5 changes: 3 additions & 2 deletions packages/adapter-netlify/src/headers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as set_cookie_parser from 'set-cookie-parser';

/**
* Splits headers into two categories: single value and multi value
* @param {Headers} headers
Expand All @@ -15,8 +17,7 @@ export function split_headers(headers) {

headers.forEach((value, key) => {
if (key === 'set-cookie') {
// @ts-expect-error (headers.raw() is non-standard)
m[key] = headers.raw()[key];
m[key] = set_cookie_parser.splitCookiesString(value);
} else {
h[key] = value;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-netlify/src/headers.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '../src/shims.js';
import './shims.js';
import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { split_headers } from '../src/headers.js';
import { split_headers } from './headers.js';

test('empty headers', () => {
const headers = new Headers();
Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-netlify/src/shims.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { installFetch } from '@sveltejs/kit/install-fetch';
installFetch();
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
installPolyfills();
4 changes: 2 additions & 2 deletions packages/adapter-node/src/shims.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { installFetch } from '@sveltejs/kit/install-fetch';
installFetch();
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
installPolyfills();
4 changes: 2 additions & 2 deletions packages/adapter-vercel/files/serverless.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { installFetch } from '@sveltejs/kit/install-fetch';
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
import { getRequest, setResponse } from '@sveltejs/kit/node';
import { Server } from 'SERVER';
import { manifest } from 'MANIFEST';

installFetch();
installPolyfills();

const server = new Server(manifest);

Expand Down
6 changes: 3 additions & 3 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@
"./node": {
"import": "./dist/node.js"
},
"./node/polyfills": {
"import": "./dist/node/polyfills.js"
},
"./hooks": {
"import": "./dist/hooks.js"
},
"./install-fetch": {
"import": "./dist/install-fetch.js"
}
},
"types": "types/index.d.ts",
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export default [
{
input: {
cli: 'src/cli.js',
node: 'src/node.js',
hooks: 'src/hooks.js',
'install-fetch': 'src/install-fetch.js'
node: 'src/node/index.js',
'node/polyfills': 'src/node/polyfills.js',
hooks: 'src/hooks.js'
},
output: {
dir: 'dist',
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/core/build/prerender/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { pathToFileURL, URL } from 'url';
import { mkdirp } from '../../../utils/filesystem.js';
import { installFetch } from '../../../install-fetch.js';
import { installPolyfills } from '../../../node/polyfills.js';
import { is_root_relative, normalize_path, resolve } from '../../../utils/url.js';
import { queue } from './queue.js';
import { crawl } from './crawl.js';
Expand Down Expand Up @@ -59,7 +59,7 @@ export async function prerender({ config, entries, files, log }) {
return prerendered;
}

installFetch();
installPolyfills();

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

Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/core/dev/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import path from 'path';
import { URL } from 'url';
import colors from 'kleur';
import sirv from 'sirv';
import { installFetch } from '../../install-fetch.js';
import { installPolyfills } from '../../node/polyfills.js';
import * as sync from '../sync/sync.js';
import { getRequest, setResponse } from '../../node.js';
import { getRequest, setResponse } from '../../node/index.js';
import { SVELTE_KIT_ASSETS } from '../constants.js';
import { get_mime_lookup, get_runtime_path, resolve_entry } from '../utils.js';
import { coalesce_to_error } from '../../utils/error.js';
Expand Down Expand Up @@ -34,7 +34,7 @@ export async function create_plugin(config, cwd) {
name: 'vite-plugin-svelte-kit',

configureServer(vite) {
installFetch();
installPolyfills();

/** @type {import('types').SSRManifest} */
let manifest;
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/core/preview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import https from 'https';
import { join } from 'path';
import sirv from 'sirv';
import { pathToFileURL } from 'url';
import { getRequest, setResponse } from '../../node.js';
import { installFetch } from '../../install-fetch.js';
import { getRequest, setResponse } from '../../node/index.js';
import { installPolyfills } from '../../node/polyfills.js';
import { SVELTE_KIT_ASSETS } from '../constants.js';

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

const { paths } = config.kit;
const base = paths.base;
Expand Down
27 changes: 0 additions & 27 deletions packages/kit/src/install-fetch.js

This file was deleted.

11 changes: 8 additions & 3 deletions packages/kit/src/node.js → packages/kit/src/node/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Readable } from 'stream';
import * as set_cookie_parser from 'set-cookie-parser';

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

if (response.headers.has('set-cookie')) {
// @ts-expect-error (headers.raw() is non-standard)
headers['set-cookie'] = response.headers.raw()['set-cookie'];
const header = /** @type {string} */ (response.headers.get('set-cookie'));
const split = set_cookie_parser.splitCookiesString(header);

// @ts-expect-error
headers['set-cookie'] = split;
}

res.writeHead(response.status, headers);
Expand All @@ -84,7 +89,7 @@ export async function setResponse(res, response) {
response.body.pipe(res);
} else {
if (response.body) {
res.write(await response.arrayBuffer());
res.write(new Uint8Array(await response.arrayBuffer()));
}

res.end();
Expand Down
23 changes: 23 additions & 0 deletions packages/kit/src/node/polyfills.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fetch, { Response, Request, Headers } from 'node-fetch';
import { webcrypto as crypto } from 'crypto';

/** @type {Record<string, any>} */
const globals = {
crypto,
fetch,
Response,
Request,
Headers
};

// exported for dev/preview and node environments
export function installPolyfills() {
for (const name in globals) {
// TODO use built-in fetch once https://github.com/nodejs/undici/issues/1262 is resolved
Object.defineProperty(globalThis, name, {
enumerable: true,
configurable: true,
value: globals[name]
});
}
}
12 changes: 8 additions & 4 deletions packages/kit/src/runtime/server/page/crypto.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'uvu';
import * as assert from 'uvu/assert';
import crypto from 'crypto';
import { webcrypto } from 'crypto';
import { sha256 } from './crypto.js';

const inputs = [
Expand All @@ -12,9 +12,13 @@ const inputs = [
].slice(0);

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

const actual = sha256(input);
assert.equal(actual, expected);
Expand Down
36 changes: 7 additions & 29 deletions packages/kit/src/runtime/server/page/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,11 @@ import { sha256, base64 } from './crypto.js';
/** @type {Promise<void>} */
export let csp_ready;

/** @type {() => string} */
let generate_nonce;

/** @type {(input: string) => string} */
let generate_hash;

if (typeof crypto !== 'undefined') {
const array = new Uint8Array(16);

generate_nonce = () => {
crypto.getRandomValues(array);
return base64(array);
};

generate_hash = sha256;
} else {
// TODO: remove this in favor of web crypto API once we no longer support Node 14
const name = 'crypto'; // store in a variable to fool esbuild when adapters bundle kit
csp_ready = import(name).then((crypto) => {
generate_nonce = () => {
return crypto.randomBytes(16).toString('base64');
};

generate_hash = (input) => {
return crypto.createHash('sha256').update(input, 'utf-8').digest().toString('base64');
};
});
const array = new Uint8Array(16);

function generate_nonce() {
crypto.getRandomValues(array);
return base64(array);
}

const quoted = new Set([
Expand Down Expand Up @@ -133,7 +111,7 @@ export class Csp {
add_script(content) {
if (this.#script_needs_csp) {
if (this.#use_hashes) {
this.#script_src.push(`sha256-${generate_hash(content)}`);
this.#script_src.push(`sha256-${sha256(content)}`);
} else if (this.#script_src.length === 0) {
this.#script_src.push(`nonce-${this.nonce}`);
}
Expand All @@ -144,7 +122,7 @@ export class Csp {
add_style(content) {
if (this.#style_needs_csp) {
if (this.#use_hashes) {
this.#style_src.push(`sha256-${generate_hash(content)}`);
this.#style_src.push(`sha256-${sha256(content)}`);
} else if (this.#style_src.length === 0) {
this.#style_src.push(`nonce-${this.nonce}`);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/runtime/server/page/csp.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { webcrypto } from 'crypto';
import { Csp } from './csp.js';

// @ts-expect-error
globalThis.crypto = webcrypto;

test('generates blank CSP header', () => {
const csp = new Csp(
{
Expand Down
11 changes: 8 additions & 3 deletions packages/kit/types/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,16 @@ declare module '@sveltejs/kit/hooks' {
/**
* A polyfill for `fetch` and its related interfaces, used by adapters for environments that don't provide a native implementation.
*/
declare module '@sveltejs/kit/install-fetch' {
declare module '@sveltejs/kit/node/polyfills' {
/**
* Make `fetch`, `Headers`, `Request` and `Response` available as globals, via `node-fetch`
* Make various web APIs available as globals:
* - `crypto`
* - `fetch`
* - `Headers`
* - `Request`
* - `Response`
*/
export function installFetch(): void;
export function installPolyfills(): void;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c660143

Please sign in to comment.