Skip to content

Commit 5a0be3b

Browse files
committed
body parsing (#50)
1 parent c6d0883 commit 5a0be3b

File tree

23 files changed

+357
-47
lines changed

23 files changed

+357
-47
lines changed

packages/adapter-node/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs';
22
import http from 'http';
33
import { parse, URLSearchParams } from 'url';
44
import sirv from 'sirv';
5-
import { render } from '@sveltejs/app-utils';
5+
import { render, get_body } from '@sveltejs/app-utils';
66

77
const manifest = require('./manifest.js');
88
const client = require('./client.json');
@@ -39,6 +39,7 @@ const server = http.createServer((req, res) => {
3939
method: req.method,
4040
headers: req.headers,
4141
path: parsed.pathname,
42+
body: await get_body(req),
4243
query: new URLSearchParams(parsed.query)
4344
}, {
4445
static_dir: 'static',

packages/app-utils/rollup.config.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { nodeResolve } from '@rollup/plugin-node-resolve';
2-
import sucrase from '@rollup/plugin-sucrase';
2+
import typescript from '@rollup/plugin-typescript';
33
import pkg from './package.json';
44

55
export default {
@@ -18,9 +18,7 @@ export default {
1818
],
1919
plugins: [
2020
nodeResolve(),
21-
sucrase({
22-
transforms: ['typescript']
23-
})
21+
typescript()
2422
],
2523
external: [
2624
...require('module').builtinModules,
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { IncomingMessage } from 'http';
2+
import { read_only_form_data } from './read_only_form_data';
3+
4+
export function get_body(req: IncomingMessage) {
5+
const has_body = (
6+
req.headers['content-type'] !== undefined &&
7+
// https://github.com/jshttp/type-is/blob/c1f4388c71c8a01f79934e68f630ca4a15fffcd6/index.js#L81-L95
8+
(req.headers['transfer-encoding'] !== undefined || !isNaN(Number(req.headers['content-length'])))
9+
);
10+
11+
if (!has_body) return Promise.resolve(undefined);
12+
13+
const [type, ...directives] = req.headers['content-type'].split(/;\s*/);
14+
15+
switch (type) {
16+
case 'application/octet-stream':
17+
return get_buffer(req);
18+
19+
case 'text/plain':
20+
return get_text(req);
21+
22+
case 'application/json':
23+
return get_json(req);
24+
25+
case 'application/x-www-form-urlencoded':
26+
return get_urlencoded(req);
27+
28+
case 'multipart/form-data':
29+
const boundary = directives.find(directive => directive.startsWith('boundary='));
30+
if (!boundary) throw new Error(`Missing boundary`);
31+
return get_multipart(req, boundary.slice('boundary='.length));
32+
33+
default:
34+
throw new Error(`Invalid Content-Type ${type}`);
35+
}
36+
}
37+
38+
async function get_json(req: IncomingMessage) {
39+
return JSON.parse(await get_text(req));
40+
}
41+
42+
async function get_urlencoded(req: IncomingMessage) {
43+
const text = await get_text(req);
44+
45+
const { data, append } = read_only_form_data();
46+
47+
text.replace(/\+/g, ' ').split('&').forEach(str => {
48+
const [key, value] = str.split('=');
49+
append(decodeURIComponent(key), decodeURIComponent(value));
50+
});
51+
52+
return data;
53+
}
54+
55+
async function get_multipart(req: IncomingMessage, boundary: string) {
56+
const text = await get_text(req);
57+
const parts = text.split(`--${boundary}`);
58+
59+
const nope = () => {
60+
throw new Error('Malformed form data');
61+
}
62+
63+
if (parts[0] !== '' || parts[parts.length - 1].trim() !== '--') {
64+
nope();
65+
}
66+
67+
const { data, append } = read_only_form_data();
68+
69+
parts.slice(1, -1).forEach(part => {
70+
const match = /\s*([\s\S]+?)\r\n\r\n([\s\S]*)\s*/.exec(part);
71+
const raw_headers = match[1];
72+
const body = match[2].trim();
73+
74+
let key: string;
75+
76+
const headers = {};
77+
raw_headers.split('\r\n').forEach(str => {
78+
const [raw_header, ...raw_directives] = str.split('; ');
79+
let [name, value] = raw_header.split(': ');
80+
81+
name = name.toLowerCase();
82+
headers[name] = value;
83+
84+
const directives: Record<string, string> = {};
85+
raw_directives.forEach(raw_directive => {
86+
const [name, value] = raw_directive.split('=');
87+
directives[name] = JSON.parse(value); // TODO is this right?
88+
});
89+
90+
if (name === 'content-disposition') {
91+
if (value !== 'form-data') nope();
92+
93+
if (directives.filename) {
94+
// TODO we probably don't want to do this automatically
95+
throw new Error('File upload is not yet implemented');
96+
}
97+
98+
if (directives.name) {
99+
key = directives.name;
100+
}
101+
}
102+
});
103+
104+
if (!key) nope();
105+
106+
append(key, body);
107+
});
108+
109+
return data;
110+
}
111+
112+
function get_text(req: IncomingMessage): Promise<string> {
113+
return new Promise((fulfil, reject) => {
114+
let data = '';
115+
116+
req.on('error', reject);
117+
118+
req.on('data', chunk => {
119+
data += chunk;
120+
});
121+
122+
req.on('end', () => {
123+
fulfil(data);
124+
});
125+
});
126+
}
127+
128+
function get_buffer(req: IncomingMessage): Promise<ArrayBuffer> {
129+
return new Promise((fulfil, reject) => {
130+
let data = new Uint8Array(0);
131+
132+
req.on('error', reject);
133+
134+
req.on('data', chunk => {
135+
const new_data = new Uint8Array(data.length + chunk.length);
136+
137+
for (let i = 0; i < data.length; i += 1) {
138+
new_data[i] = data[i];
139+
}
140+
141+
for (let i = 0; i < chunk.length; i += 1) {
142+
new_data[i + data.length] = chunk[i];
143+
}
144+
145+
data = new_data;
146+
});
147+
148+
req.on('end', () => {
149+
fulfil(data.buffer);
150+
});
151+
});
152+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
type FormDataMap = Map<string, string[]>;
2+
3+
export function read_only_form_data() {
4+
const map: FormDataMap = new Map();
5+
6+
return {
7+
append(key: string, value: string) {
8+
if (map.has(key)) {
9+
map.get(key).push(value);
10+
} else {
11+
map.set(key, [value]);
12+
}
13+
},
14+
15+
data: new ReadOnlyFormData(map)
16+
};
17+
}
18+
19+
class ReadOnlyFormData {
20+
#map: FormDataMap;
21+
22+
constructor(map: FormDataMap) {
23+
this.#map = map;
24+
}
25+
26+
get(key: string) {
27+
return this.#map.get(key)?.[0];
28+
}
29+
30+
getAll(key: string) {
31+
return this.#map.get(key);
32+
}
33+
34+
has(key: string) {
35+
return this.#map.has(key);
36+
}
37+
38+
*[Symbol.iterator]() {
39+
for (const [key, value] of this.#map) {
40+
for (let i = 0; i < value.length; i += 1) {
41+
yield [key, value[i]];
42+
}
43+
}
44+
}
45+
46+
*entries() {
47+
for (const [key, value] of this.#map) {
48+
for (let i = 0; i < value.length; i += 1) {
49+
yield [key, value[i]];
50+
}
51+
}
52+
}
53+
54+
*keys() {
55+
for (const [key, value] of this.#map) {
56+
for (let i = 0; i < value.length; i += 1) {
57+
yield key;
58+
}
59+
}
60+
}
61+
62+
*values() {
63+
for (const [, value] of this.#map) {
64+
for (let i = 0; i < value.length; i += 1) {
65+
yield value;
66+
}
67+
}
68+
}
69+
}

packages/app-utils/src/http/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { get_body } from './get_body';

packages/app-utils/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { render } from './render';
22
export { prerender } from './prerender';
3-
export * from './files';
3+
export * from './files';
4+
export * from './http';

packages/app-utils/src/prerender/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { dirname, resolve as resolve_path } from 'path';
33
import { parse, resolve, URLSearchParams } from 'url';
44
import { mkdirp } from '../files';
55
import { render } from '../render';
6-
import { RouteManifest } from '../types';
6+
import { PageResponse, RouteManifest } from '../types';
77

88
function clean_html(html) {
99
return html
@@ -85,6 +85,7 @@ export async function prerender({
8585
method: 'GET',
8686
headers: {},
8787
path,
88+
body: null,
8889
query: new URLSearchParams()
8990
}, {
9091
only_prerender: !force,
@@ -130,9 +131,11 @@ export async function prerender({
130131
log.error(`${rendered.status} ${path}`);
131132
}
132133

133-
if (rendered.dependencies) {
134-
for (const path in rendered.dependencies) {
135-
const result = rendered.dependencies[path];
134+
const { dependencies } = rendered as PageResponse;
135+
136+
if (dependencies) {
137+
for (const path in dependencies) {
138+
const result = dependencies[path];
136139
const response_type = Math.floor(result.status / 100);
137140

138141
const is_html = result.headers['content-type'] === 'text/html';

packages/app-utils/src/render/page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export default async function render_page(
104104
method: opts.method || 'GET',
105105
headers: opts.headers || {}, // TODO inject credentials...
106106
path: resolved,
107+
body: opts.body,
107108
query: new URLSearchParams(parsed.query)
108109
}, options);
109110

packages/app-utils/src/render/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default function render_route(
2727
host: request.host,
2828
path: request.path,
2929
query: request.query,
30+
body: request.body,
3031
params
3132
}, context);
3233

packages/app-utils/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type IncomingRequest = {
88
host: string; // TODO is this actually necessary?
99
method: Method;
1010
headers: Headers;
11-
// TODO body
11+
body: any; // TODO
1212
path: string;
1313
query: URLSearchParams;
1414
};

packages/app-utils/tsconfig.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"ts-node": {
3+
"compilerOptions": {
4+
"module": "commonjs"
5+
}
6+
},
7+
"compilerOptions": {
8+
// target node v8+ (https://node.green/)
9+
// the only missing feature is Array.prototype.values
10+
"lib": ["es2020"],
11+
"target": "es2019",
12+
13+
// "declaration": true,
14+
// "declarationDir": "types",
15+
16+
"noEmitOnError": true,
17+
"noErrorTruncation": true,
18+
19+
// rollup takes care of these
20+
"module": "esnext",
21+
"moduleResolution": "node",
22+
"resolveJsonModule": true,
23+
"allowSyntheticDefaultImports": true,
24+
25+
// TODO: error all the things
26+
//"strict": true,
27+
"noImplicitThis": true,
28+
"noUnusedLocals": true,
29+
"noUnusedParameters": true
30+
}
31+
}

packages/kit/dummy/manifest.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const ErrorComponent = {};
2+
3+
export const components = [];
4+
5+
export const routes = [];

packages/kit/dummy/root.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function preload(_page: any, _session: any) {
2+
3+
}
4+
5+
export default class root {
6+
constructor(_opts: any) {
7+
8+
}
9+
10+
$set(_data: any) {
11+
12+
}
13+
}

0 commit comments

Comments
 (0)