Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node polyfills #4934

Merged
merged 23 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.