Skip to content

Commit

Permalink
feat(xhr): supports FormData
Browse files Browse the repository at this point in the history
  • Loading branch information
ykzts committed Feb 14, 2023
1 parent eae75ae commit c760d47
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 19 deletions.
58 changes: 58 additions & 0 deletions src/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @license
* Copyright (c) 2013 Yamagishi Kazutoshi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

import { Blob, type BlobOptions } from 'buffer';
import { type BinaryLike } from 'crypto';

/**
* @see {@link https://w3c.github.io/FileAPI/#typedefdef-blobpart File API - typedef (BufferSource or Blob or USVString) BlobPart}
*/
export type BlobPart = BinaryLike | Blob;

/**
* @see {@link https://w3c.github.io/FileAPI/#dfn-FilePropertyBag File API - interface FilePropertyBag}
*/
export interface FilePropertyBag extends BlobOptions {
lastModified: number;
}

/**
* @see {@link https://w3c.github.io/FileAPI/#dfn-file File API - interface File}
*/
export default class File extends Blob {
readonly name: string;
readonly lastModifiled: number;

constructor(
fileBits: BlobPart[],
fileName: string,
options?: FilePropertyBag
) {
const { lastModified = Date.now(), ...blobPropertyBag } = options ?? {};

super(fileBits, blobPropertyBag);

this.name = fileName;
this.lastModifiled = lastModified;
}
}
42 changes: 38 additions & 4 deletions src/formdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,48 @@
* THE SOFTWARE.
*/

import File from './file';

/* eslint-disable @typescript-eslint/no-empty-function */

/**
* @see {@link https://xhr.spec.whatwg.org/#formdataentryvalue XMLHttpRequest Standard - typedef (File or USVString) FormDataEntryValue}
*/
export type FormDataEntryValue = File | string;

/**
* @see {@link https://xhr.spec.whatwg.org/#interface-formdata XMLHttpRequest Standard - 5. Interface FormData}
*/
export default class FormData {
/**
* @todo Implement this function.
* @see {@link https://xhr.spec.whatwg.org/#dom-formdata-append XMLHttpRequest Standard - The append(name, value) and append(name, blobValue, filename) method}
*/
append(name: string, value: string): void {}

/**
* @see {@link https://xhr.spec.whatwg.org/#dom-formdata-delete XMLHttpRequest Standard - The delete(name) method}
*/
delete(name: string): void {}

/**
* @see {@link https://xhr.spec.whatwg.org/#dom-formdata-get XMLHttpRequest Standard - The get(name) method}
*/
append(/* name, value, filename */): void {
// wip
}
get(name: string): FormDataEntryValue {}

/**
* @see {@link https://xhr.spec.whatwg.org/#dom-get-getall XMLHttpRequest Standard - The getAll(name) method}
*/
getAll(name: string): FormDataEntryValue[] {}

/**
* @see {@link https://xhr.spec.whatwg.org/#dom-formdata-has XMLHttpRequest Standard - The has(name) method}
*/
has(name: string): boolean {}

/**
* @see {@link https://xhr.spec.whatwg.org/#dom-formdata-set XMLHttpRequest Standard - The set(name, value) and set(name, blobValue, filename) method}
*/
set(name: string, value: string): void {}

*[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]> {}
}
53 changes: 38 additions & 15 deletions src/xmlhttprequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* THE SOFTWARE.
*/

import { Blob } from 'buffer';
import * as http from 'http';
import * as https from 'https';
import FormData from './formdata';
Expand Down Expand Up @@ -77,12 +78,11 @@ const FORBIDDEN_RESPONSE_HEADERS = ['set-cookie', 'set-cookie2'];
*/
const HTTP_HEADER_FIELD_NAME_REGEXP = /[!#$%&'*+-.^_`|~a-z0-9]+/;

export type BodyInit =
| ArrayBuffer
| Buffer
export type XMLHttpRequestBodyInit =
| Blob
| BufferSource
| FormData
| URLSearchParams
| Uint8Array
| string;

export type XMLHttpRequestResponseType =
Expand Down Expand Up @@ -478,24 +478,47 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
/**
* @see {@link https://xhr.spec.whatwg.org/#the-send()-method XMLHttpRequest Standard - 4.5.6. The send() method}
*/
send(body: BodyInit | null = null): void {
send(body: XMLHttpRequestBodyInit | null = null): void {
if (this.readyState !== XMLHttpRequest.OPENED || !this.#client) {
// TODO: Add human readable message.
throw new DOMException('', 'InvalidStateError');
}

if (body) {
const bodyInit =
body instanceof ArrayBuffer || body instanceof Uint8Array
? Buffer.from(body)
: body;

if (typeof bodyInit === 'string' || bodyInit instanceof Buffer) {
const length = Buffer.isBuffer(bodyInit)
? bodyInit.length
: Buffer.byteLength(bodyInit);
if (body instanceof Blob) {
this.setRequestHeader('Content-Length', body.size.toString());
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
this.setRequestHeader('Content-Length', body.byteLength.toString());
} else if (body instanceof URLSearchParams) {
const value = body.toString();

this.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded; charset=UTF-8'
);
this.setRequestHeader('Content-Length', value.length.toString());
} else if (body instanceof FormData) {
const boundary = '------xxxxx';

let chunk = '';
for (const [name, value] of body) {
if (typeof value === 'string') {
chunk += [
boundary,
`Content-Disposition: form-data; name="${name}"`,
'',
value
].join('\r\n');
}
}

this.#client.setHeader('Content-Length', length);
this.setRequestHeader(
'Content-Type',
`multipart/form-data; boundary=${boundary}`
);
} else {
this.setRequestHeader('Content-Type', 'text/plain');
this.setRequestHeader('Content-Length', body.length.toString());
}

this.#client.addListener('socket', (socket) => {
Expand Down

0 comments on commit c760d47

Please sign in to comment.