Remove unnecessary dependencies, ensure everything is working (script and noscript).master
@@ -4,11 +4,6 @@ const nextConfig = { | |||||
experimental: { | experimental: { | ||||
optimizeCss: true, | optimizeCss: true, | ||||
}, | }, | ||||
webpack: (config) => { | |||||
config.resolve.fallback = { fs: false }; | |||||
return config; | |||||
}, | |||||
}; | }; | ||||
module.exports = nextConfig; | module.exports = nextConfig; |
@@ -73,9 +73,9 @@ const patchNote: NextApiHandler = async (req, res) => { | |||||
const updatedNote = { | const updatedNote = { | ||||
...note, | ...note, | ||||
title: title ?? note.title, | |||||
content: content ?? note.content, | |||||
image: image ?? note.image, | |||||
title: title || note.title, | |||||
content: content || note.content, | |||||
image: image ? `data:${image.type};base64,${image.toString('base64')}` : note.image, | |||||
}; | }; | ||||
data[noteIndex] = updatedNote; | data[noteIndex] = updatedNote; | ||||
@@ -225,7 +225,7 @@ const createNote = (params: NoteCollectionParams): NextApiHandler => async (req, | |||||
id: newId, | id: newId, | ||||
title, | title, | ||||
content, | content, | ||||
image, | |||||
image: `data:${image.type};base64,${image.toString('base64')}`, | |||||
}); | }); | ||||
}; | }; | ||||
@@ -68,9 +68,8 @@ | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@theoryofnekomata/formxtra": "^1.0.3", | "@theoryofnekomata/formxtra": "^1.0.3", | ||||
"@web-std/file": "^3.0.3", | |||||
"busboy": "^1.6.0", | "busboy": "^1.6.0", | ||||
"nookies": "^2.5.2", | |||||
"node-cache": "^5.1.2", | |||||
"seroval": "^0.10.2" | "seroval": "^0.10.2" | ||||
}, | }, | ||||
"exports": { | "exports": { | ||||
@@ -29,6 +29,7 @@ export interface FormProps extends Omit<React.HTMLProps<FormDerivedElement>, 'ac | |||||
responseEncType?: string; | responseEncType?: string; | ||||
serializerOptions?: SerializerOptions; | serializerOptions?: SerializerOptions; | ||||
disableFetch?: boolean; | disableFetch?: boolean; | ||||
methodFormKey?: string; | |||||
} | } | ||||
export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | ||||
@@ -46,6 +47,7 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
responseEncType = ENCTYPE_APPLICATION_JSON, | responseEncType = ENCTYPE_APPLICATION_JSON, | ||||
serializerOptions, | serializerOptions, | ||||
disableFetch = false, | disableFetch = false, | ||||
methodFormKey = METHOD_FORM_KEY, | |||||
...etcProps | ...etcProps | ||||
}, forwardedRef) => { | }, forwardedRef) => { | ||||
const { handleSubmit } = useFormFetch({ | const { handleSubmit } = useFormFetch({ | ||||
@@ -83,7 +85,7 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
encType={ENCTYPE_MULTIPART_FORM_DATA} | encType={ENCTYPE_MULTIPART_FORM_DATA} | ||||
> | > | ||||
{serverMethodOverride && ( | {serverMethodOverride && ( | ||||
<input type="hidden" name={METHOD_FORM_KEY} value={clientMethod} /> | |||||
<input type="hidden" name={methodFormKey} value={clientMethod} /> | |||||
)} | )} | ||||
{children} | {children} | ||||
</FormDerivedElementComponent> | </FormDerivedElementComponent> | ||||
@@ -104,4 +106,5 @@ Form.defaultProps = { | |||||
refresh: undefined, | refresh: undefined, | ||||
responseEncType: ENCTYPE_APPLICATION_JSON, | responseEncType: ENCTYPE_APPLICATION_JSON, | ||||
serializerOptions: undefined, | serializerOptions: undefined, | ||||
methodFormKey: METHOD_FORM_KEY, | |||||
}; | }; |
@@ -58,7 +58,7 @@ export const useFormFetch = ({ | |||||
form: event.currentTarget, | form: event.currentTarget, | ||||
encType, | encType, | ||||
serializers: encTypeSerializers, | serializers: encTypeSerializers, | ||||
options: { | |||||
serializerOptions: { | |||||
...serializerOptions, | ...serializerOptions, | ||||
submitter, | submitter, | ||||
}, | }, | ||||
@@ -1,3 +1,8 @@ | |||||
export const PREVENT_REDIRECT_FORM_KEY = '__iceform_prevent_redirect' as const; | export const PREVENT_REDIRECT_FORM_KEY = '__iceform_prevent_redirect' as const; | ||||
export const METHOD_FORM_KEY = '__iceform_method' as const; | export const METHOD_FORM_KEY = '__iceform_method' as const; | ||||
export const METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH', 'DELETE'] as const; | export const METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH', 'DELETE'] as const; | ||||
export const REQUEST_ID_FORM_KEY = '__iceform_request_id' as const; | |||||
export const CONTENT_TYPE_HEADER_KEY = 'content-type' as const; | |||||
export const LOCATION_HEADER_KEY = 'location' as const; | |||||
export const CONTENT_ENCODING_HEADER_KEY = 'content-encoding' as const; | |||||
export const DEFAULT_ENCODING = 'utf-8' as const; |
@@ -23,6 +23,9 @@ export interface ActionWrapperOptions { | |||||
fn: NextApiHandler, | fn: NextApiHandler, | ||||
onAction?: OnActionFunction, | onAction?: OnActionFunction, | ||||
deserializers?: EncTypeDeserializerMap, | deserializers?: EncTypeDeserializerMap, | ||||
methodFormKey?: string, | |||||
requestIdFormKey?: string, | |||||
preventRedirectFormKey?: string, | |||||
/** | /** | ||||
* Maps the Location header from the handler response to an accessible URL. | * Maps the Location header from the handler response to an accessible URL. | ||||
* @param url | * @param url | ||||
@@ -7,7 +7,11 @@ import { | |||||
deserializeFormObjectBody, | deserializeFormObjectBody, | ||||
EncTypeDeserializerMap, | EncTypeDeserializerMap, | ||||
} from '../../utils/serialization'; | } from '../../utils/serialization'; | ||||
import { METHOD_FORM_KEY, PREVENT_REDIRECT_FORM_KEY } from '../../common/constants'; | |||||
import { | |||||
METHOD_FORM_KEY, | |||||
PREVENT_REDIRECT_FORM_KEY, | |||||
REQUEST_ID_FORM_KEY, | |||||
} from '../../common/constants'; | |||||
import { ACTION_STATUS_CODE, DEFAULT_METHOD } from '../constants'; | import { ACTION_STATUS_CODE, DEFAULT_METHOD } from '../constants'; | ||||
import { getBody } from '../../utils/request'; | import { getBody } from '../../utils/request'; | ||||
import { | import { | ||||
@@ -16,25 +20,21 @@ import { | |||||
} from '../../common/enctypes'; | } from '../../common/enctypes'; | ||||
import { ActionWrapperOptions } from './common'; | import { ActionWrapperOptions } from './common'; | ||||
import { IceformNextServerResponse } from '../response'; | import { IceformNextServerResponse } from '../response'; | ||||
import { | |||||
REQUEST_ID_COOKIE_KEY, | |||||
CookieManager, | |||||
} from '../../utils/cookies'; | |||||
import { cacheResponse } from '../cache'; | import { cacheResponse } from '../cache'; | ||||
const getFormObjectMethodAndBody = async ( | const getFormObjectMethodAndBody = async ( | ||||
req: IncomingMessage, | req: IncomingMessage, | ||||
deserializers: EncTypeDeserializerMap, | deserializers: EncTypeDeserializerMap, | ||||
methodFormKey = METHOD_FORM_KEY as string, | |||||
) => { | ) => { | ||||
const deserialized = await deserializeFormObjectBody({ | const deserialized = await deserializeFormObjectBody({ | ||||
req, | req, | ||||
deserializers, | deserializers, | ||||
}); | }); | ||||
const { | const { | ||||
[METHOD_FORM_KEY]: method = req.method ?? DEFAULT_METHOD, | |||||
[methodFormKey]: method = req.method ?? DEFAULT_METHOD, | |||||
...body | ...body | ||||
} = deserialized as { | } = deserialized as { | ||||
[METHOD_FORM_KEY]?: string, | |||||
[key: string]: unknown, | [key: string]: unknown, | ||||
}; | }; | ||||
@@ -80,7 +80,11 @@ export const getServerSideProps = (options: ActionWrapperOptions): GetServerSide | |||||
? getFormObjectMethodAndBody | ? getFormObjectMethodAndBody | ||||
: getBinaryMethodAndBody | : getBinaryMethodAndBody | ||||
); | ); | ||||
const { body, method } = await methodAndBodyFn(ctx.req, deserializers); | |||||
const { body, method } = await methodAndBodyFn( | |||||
ctx.req, | |||||
deserializers, | |||||
options.methodFormKey ?? METHOD_FORM_KEY, | |||||
); | |||||
const req = { | const req = { | ||||
...ctx.req, | ...ctx.req, | ||||
body, | body, | ||||
@@ -93,9 +97,7 @@ export const getServerSideProps = (options: ActionWrapperOptions): GetServerSide | |||||
} as DefaultNextApiRequest; | } as DefaultNextApiRequest; | ||||
const res = new IceformNextServerResponse(ctx.req); | const res = new IceformNextServerResponse(ctx.req); | ||||
await options.fn(req, res); | await options.fn(req, res); | ||||
const requestId = crypto.randomUUID(); | |||||
const cookieManager = new CookieManager(ctx); | |||||
cookieManager.setCookie(REQUEST_ID_COOKIE_KEY, requestId); | |||||
const requestId = crypto.randomBytes(8).toString('base64url'); | |||||
await cacheResponse(requestId, res); | await cacheResponse(requestId, res); | ||||
if (typeof options.onAction === 'function') { | if (typeof options.onAction === 'function') { | ||||
@@ -112,9 +114,9 @@ export const getServerSideProps = (options: ActionWrapperOptions): GetServerSide | |||||
const preventRedirect = ( | const preventRedirect = ( | ||||
typeof req.body === 'object' | typeof req.body === 'object' | ||||
&& req.body !== null | && req.body !== null | ||||
&& PREVENT_REDIRECT_FORM_KEY in req.body | |||||
&& (options.preventRedirectFormKey ?? PREVENT_REDIRECT_FORM_KEY) in req.body | |||||
); | ); | ||||
const redirectDestination = ( | |||||
const redirectDestinationRaw = ( | |||||
res.location | res.location | ||||
&& typeof options.mapLocationToRedirectDestination === 'function' | && typeof options.mapLocationToRedirectDestination === 'function' | ||||
&& !preventRedirect | && !preventRedirect | ||||
@@ -122,9 +124,12 @@ export const getServerSideProps = (options: ActionWrapperOptions): GetServerSide | |||||
? options.mapLocationToRedirectDestination(referer, res.location) | ? options.mapLocationToRedirectDestination(referer, res.location) | ||||
: referer; | : referer; | ||||
const url = new URL(redirectDestinationRaw, 'http://example.com'); | |||||
url.searchParams.set(options.requestIdFormKey ?? REQUEST_ID_FORM_KEY, requestId); | |||||
return { | return { | ||||
redirect: { | redirect: { | ||||
destination: redirectDestination, | |||||
destination: `/${url.pathname}${url.search}`, | |||||
statusCode: ACTION_STATUS_CODE, | statusCode: ACTION_STATUS_CODE, | ||||
}, | }, | ||||
props: { | props: { | ||||
@@ -1,5 +1,6 @@ | |||||
import { createWriteStream } from 'fs'; | |||||
import { readFile, unlink } from 'fs/promises'; | |||||
import NodeCache from 'node-cache'; | |||||
import { STATUS_CODES } from 'http'; | |||||
import { DEFAULT_RESPONSE_STATUS_CODE } from './constants'; | |||||
interface CacheableResponse { | interface CacheableResponse { | ||||
statusCode: number; | statusCode: number; | ||||
@@ -8,41 +9,60 @@ interface CacheableResponse { | |||||
data?: unknown; | data?: unknown; | ||||
} | } | ||||
const getFilePathFromRequestId = (requestId: string) => `${requestId}`; | |||||
const cache = new NodeCache({ | |||||
deleteOnExpire: true, | |||||
stdTTL: 60, | |||||
}); | |||||
export const cacheResponse = async (requestId: string, res: CacheableResponse) => { | export const cacheResponse = async (requestId: string, res: CacheableResponse) => { | ||||
const filePath = getFilePathFromRequestId(requestId); | |||||
const cacheStream = createWriteStream(filePath, { encoding: 'utf-8' }); | |||||
cacheStream.write(`${res.statusCode.toString()} ${res.statusMessage || ''}\n`); | |||||
let cacheValue = ''; | |||||
cacheValue += `${res.statusCode.toString()} ${res.statusMessage || ''}\n`; | |||||
if (res.contentType) { | if (res.contentType) { | ||||
cacheStream.write(`Content-Type: ${res.contentType}\n`); | |||||
cacheValue += `Content-Type: ${res.contentType}\n`; | |||||
} | } | ||||
if (res.data) { | if (res.data) { | ||||
cacheStream.write('\n'); | |||||
cacheStream.write(res.data as string); | |||||
cacheValue += `\n${res.data as string}`; | |||||
} | } | ||||
return new Promise((resolve) => { | |||||
cacheStream.close(resolve); | |||||
return new Promise<void>((resolve) => { | |||||
cache.set(requestId, cacheValue); | |||||
resolve(); | |||||
}); | }); | ||||
}; | }; | ||||
export const retrieveCache = async (requestId: string) => { | |||||
const filePath = getFilePathFromRequestId(requestId); | |||||
const requestBuffer = await readFile(filePath, 'utf-8'); | |||||
await unlink(filePath); | |||||
const [statusLine, ...headersAndBody] = requestBuffer.split('\n'); | |||||
const parseCacheValue = (cacheValue: string) => { | |||||
const [statusLine, ...headersAndBody] = cacheValue.split('\n'); | |||||
const [statusCode, ...statusMessageWords] = statusLine.split(' '); | const [statusCode, ...statusMessageWords] = statusLine.split(' '); | ||||
const statusMessage = statusMessageWords.join(' '); | const statusMessage = statusMessageWords.join(' '); | ||||
const bodyStart = headersAndBody.findIndex((line) => line === ''); | const bodyStart = headersAndBody.findIndex((line) => line === ''); | ||||
const headers = headersAndBody.slice(0, bodyStart); | const headers = headersAndBody.slice(0, bodyStart); | ||||
const body = headersAndBody.slice(bodyStart + 1).join('\n'); | |||||
const data = headersAndBody.slice(bodyStart + 1).join('\n'); | |||||
const contentTypeHeader = headers.find((header) => header.toLowerCase().startsWith('content-type:')); | const contentTypeHeader = headers.find((header) => header.toLowerCase().startsWith('content-type:')); | ||||
const contentType = contentTypeHeader?.split(':')[1].trim(); | const contentType = contentTypeHeader?.split(':')[1].trim(); | ||||
return { | return { | ||||
statusCode: parseInt(statusCode, 10), | statusCode: parseInt(statusCode, 10), | ||||
statusMessage, | |||||
statusMessage: statusMessage || STATUS_CODES[statusCode], | |||||
contentType, | contentType, | ||||
body, | |||||
data, | |||||
}; | }; | ||||
}; | }; | ||||
export const retrieveCache = async (requestId: string) => new Promise< | |||||
CacheableResponse | |||||
>((resolve) => { | |||||
const requestBuffer = cache.get<string>(requestId); | |||||
if (!requestBuffer) { | |||||
resolve({ | |||||
statusCode: DEFAULT_RESPONSE_STATUS_CODE, | |||||
statusMessage: STATUS_CODES[DEFAULT_RESPONSE_STATUS_CODE], | |||||
contentType: undefined, | |||||
data: '', | |||||
}); | |||||
return; | |||||
} | |||||
cache.del(requestId); | |||||
const value = parseCacheValue(requestBuffer); | |||||
resolve(value); | |||||
}); |
@@ -1,6 +1,3 @@ | |||||
export const DEFAULT_METHOD = 'GET' as const; | export const DEFAULT_METHOD = 'GET' as const; | ||||
export const DEFAULT_ENCODING = 'utf-8' as const; | |||||
export const ACTION_STATUS_CODE = 307 as const; // temporary redirect | export const ACTION_STATUS_CODE = 307 as const; // temporary redirect | ||||
export const DEFAULT_RESPONSE_STATUS_CODE = 200 as const; // ok | export const DEFAULT_RESPONSE_STATUS_CODE = 200 as const; // ok | ||||
export const CONTENT_TYPE_HEADER_KEY = 'content-type' as const; | |||||
export const LOCATION_HEADER_KEY = 'location' as const; |
@@ -2,11 +2,10 @@ import { ParsedUrlQuery } from 'querystring'; | |||||
import { GetServerSideProps, GetServerSidePropsContext, PreviewData } from 'next'; | import { GetServerSideProps, GetServerSidePropsContext, PreviewData } from 'next'; | ||||
import { deserialize } from 'seroval'; | import { deserialize } from 'seroval'; | ||||
import { NextApiRequest, NextApiResponse } from '../common/types'; | import { NextApiRequest, NextApiResponse } from '../common/types'; | ||||
import { DEFAULT_ENCODING, DEFAULT_METHOD } from './constants'; | |||||
import { DEFAULT_METHOD } from './constants'; | |||||
import { getBody } from '../utils/request'; | import { getBody } from '../utils/request'; | ||||
import { REQUEST_ID_COOKIE_KEY, CookieManager } from '../utils/cookies'; | |||||
import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | ||||
import { METHODS_WITH_BODY } from '../common/constants'; | |||||
import { METHODS_WITH_BODY, REQUEST_ID_FORM_KEY, DEFAULT_ENCODING } from '../common/constants'; | |||||
import { retrieveCache } from './cache'; | import { retrieveCache } from './cache'; | ||||
export type DestinationGetServerSideProps< | export type DestinationGetServerSideProps< | ||||
@@ -21,6 +20,7 @@ export type DestinationGetServerSideProps< | |||||
export interface DestinationWrapperOptions { | export interface DestinationWrapperOptions { | ||||
fn?: DestinationGetServerSideProps; | fn?: DestinationGetServerSideProps; | ||||
requestIdKey?: string; | |||||
} | } | ||||
export const getServerSideProps = ( | export const getServerSideProps = ( | ||||
@@ -41,26 +41,31 @@ export const getServerSideProps = ( | |||||
} | } | ||||
const res: NextApiResponse = {}; | const res: NextApiResponse = {}; | ||||
const cookieManager = new CookieManager(ctx); | |||||
const resRequestId = cookieManager.getCookie(REQUEST_ID_COOKIE_KEY); | |||||
cookieManager.unsetCookie(REQUEST_ID_COOKIE_KEY); | |||||
const resRequestId = ctx.query[ | |||||
options.requestIdKey ?? REQUEST_ID_FORM_KEY | |||||
] as string | undefined; | |||||
if (resRequestId) { | if (resRequestId) { | ||||
const { | const { | ||||
statusCode, | statusCode, | ||||
statusMessage, | statusMessage, | ||||
contentType, | contentType, | ||||
body: resBody, | |||||
data: resBody, | |||||
} = await retrieveCache(resRequestId); | } = await retrieveCache(resRequestId); | ||||
ctx.res.statusCode = statusCode; | ctx.res.statusCode = statusCode; | ||||
ctx.res.statusMessage = statusMessage; | |||||
if (contentType === ENCTYPE_APPLICATION_JSON) { | |||||
res.body = deserialize(resBody); | |||||
} else if (contentType === ENCTYPE_APPLICATION_OCTET_STREAM) { | |||||
res.body = deserialize(resBody); | |||||
if (typeof statusMessage !== 'undefined') { | |||||
ctx.res.statusMessage = statusMessage; | |||||
} | |||||
if (resBody) { | |||||
if (contentType === ENCTYPE_APPLICATION_JSON) { | |||||
res.body = deserialize(resBody as string); | |||||
} else if (contentType === ENCTYPE_APPLICATION_OCTET_STREAM) { | |||||
res.body = deserialize(resBody as string); | |||||
} else { | |||||
const c = console; | |||||
c.warn('Could not parse response body, returning nothing'); | |||||
res.body = null; | |||||
} | |||||
} else { | } else { | ||||
const c = console; | |||||
c.warn('Could not parse response body, returning nothing'); | |||||
res.body = null; | res.body = null; | ||||
} | } | ||||
} | } | ||||
@@ -1,11 +1,12 @@ | |||||
import { ServerResponse } from 'http'; | import { ServerResponse } from 'http'; | ||||
import { NextApiResponse as DefaultNextApiResponse } from 'next/dist/shared/lib/utils'; | import { NextApiResponse as DefaultNextApiResponse } from 'next/dist/shared/lib/utils'; | ||||
import { serialize } from 'seroval'; | import { serialize } from 'seroval'; | ||||
import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | ||||
import { | import { | ||||
ACTION_STATUS_CODE, CONTENT_TYPE_HEADER_KEY, LOCATION_HEADER_KEY, DEFAULT_RESPONSE_STATUS_CODE, | |||||
ACTION_STATUS_CODE, | |||||
DEFAULT_RESPONSE_STATUS_CODE, | |||||
} from './constants'; | } from './constants'; | ||||
import { CONTENT_TYPE_HEADER_KEY, LOCATION_HEADER_KEY } from '../common/constants'; | |||||
// for client-side | // for client-side | ||||
class DummyServerResponse {} | class DummyServerResponse {} | ||||
@@ -1,56 +0,0 @@ | |||||
import { IncomingMessage, ServerResponse } from 'http'; | |||||
import * as nookies from 'nookies'; | |||||
import * as crypto from 'crypto'; | |||||
const COMMON_COOKIE_CONFIG = { | |||||
path: '/', | |||||
httpOnly: true, | |||||
}; | |||||
const COMMON_SET_COOKIE_CONFIG = { | |||||
...COMMON_COOKIE_CONFIG, | |||||
maxAge: 30 * 24 * 60 * 60, | |||||
}; | |||||
const cookieKeys: Record<string, string> = {}; | |||||
export const REQUEST_ID_COOKIE_KEY = 'b' as const; | |||||
export class CookieManager { | |||||
private readonly ctx: { req: IncomingMessage, res: ServerResponse<IncomingMessage> }; | |||||
constructor(ctx: { req: IncomingMessage, res: ServerResponse<IncomingMessage> }) { | |||||
// noop | |||||
this.ctx = ctx; | |||||
} | |||||
private static generateCookieKey(key: string) { | |||||
const random = crypto.randomBytes(16).toString('hex'); | |||||
return `if${key}${random}`; | |||||
} | |||||
setCookie(key: string, value: string) { | |||||
// cleanup previous cookie | |||||
this.unsetCookie(key); | |||||
cookieKeys[key] = CookieManager.generateCookieKey(key); | |||||
nookies.setCookie( | |||||
this.ctx, | |||||
cookieKeys[key], | |||||
value, | |||||
COMMON_SET_COOKIE_CONFIG, | |||||
); | |||||
} | |||||
unsetCookie(key: string) { | |||||
nookies.destroyCookie(this.ctx, cookieKeys[key], COMMON_COOKIE_CONFIG); | |||||
} | |||||
hasCookie(key: string) { | |||||
const cookies = nookies.parseCookies(this.ctx); | |||||
return cookieKeys[key] in cookies; | |||||
} | |||||
getCookie(key: string) { | |||||
const cookies = nookies.parseCookies(this.ctx); | |||||
return cookies[cookieKeys[key]]; | |||||
} | |||||
} |
@@ -37,6 +37,11 @@ export const parseMultipartFormData = async ( | |||||
}); | }); | ||||
file.on('close', () => { | file.on('close', () => { | ||||
// filename can be undefined somehow... | |||||
if (!filename && buffer.length <= 0) { | |||||
return; | |||||
} | |||||
const bufferMut = buffer as unknown as Record<string, unknown>; | const bufferMut = buffer as unknown as Record<string, unknown>; | ||||
bufferMut.name = filename; | bufferMut.name = filename; | ||||
bufferMut.type = mimeType; | bufferMut.type = mimeType; | ||||
@@ -6,8 +6,13 @@ import { | |||||
ENCTYPE_X_WWW_FORM_URLENCODED, | ENCTYPE_X_WWW_FORM_URLENCODED, | ||||
} from '../common/enctypes'; | } from '../common/enctypes'; | ||||
import { getBody, parseMultipartFormData } from './request'; | import { getBody, parseMultipartFormData } from './request'; | ||||
import { METHOD_FORM_KEY, PREVENT_REDIRECT_FORM_KEY } from '../common/constants'; | |||||
import { DEFAULT_ENCODING } from '../server/constants'; | |||||
import { | |||||
METHOD_FORM_KEY, | |||||
PREVENT_REDIRECT_FORM_KEY, | |||||
CONTENT_ENCODING_HEADER_KEY, | |||||
CONTENT_TYPE_HEADER_KEY, | |||||
DEFAULT_ENCODING, | |||||
} from '../common/constants'; | |||||
export type EncTypeSerializer = (data: unknown) => string; | export type EncTypeSerializer = (data: unknown) => string; | ||||
@@ -19,7 +24,9 @@ export interface SerializeBodyParams { | |||||
form: HTMLFormElement, | form: HTMLFormElement, | ||||
encType: string, | encType: string, | ||||
serializers?: EncTypeSerializerMap, | serializers?: EncTypeSerializerMap, | ||||
options?: SerializerOptions, | |||||
serializerOptions?: SerializerOptions, | |||||
methodFormKey?: string, | |||||
preventRedirectFormKey?: string, | |||||
} | } | ||||
export const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { | export const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { | ||||
@@ -31,13 +38,15 @@ export const serializeBody = (params: SerializeBodyParams) => { | |||||
form, | form, | ||||
encType, | encType, | ||||
serializers = DEFAULT_ENCTYPE_SERIALIZERS, | serializers = DEFAULT_ENCTYPE_SERIALIZERS, | ||||
options, | |||||
serializerOptions, | |||||
methodFormKey = METHOD_FORM_KEY, | |||||
preventRedirectFormKey = PREVENT_REDIRECT_FORM_KEY, | |||||
} = params; | } = params; | ||||
if (encType === ENCTYPE_X_WWW_FORM_URLENCODED) { | if (encType === ENCTYPE_X_WWW_FORM_URLENCODED) { | ||||
const searchParams = new URLSearchParams(form); | const searchParams = new URLSearchParams(form); | ||||
searchParams.delete(METHOD_FORM_KEY); | |||||
searchParams.delete(PREVENT_REDIRECT_FORM_KEY); | |||||
searchParams.delete(methodFormKey); | |||||
searchParams.delete(preventRedirectFormKey); | |||||
return searchParams; | return searchParams; | ||||
} | } | ||||
@@ -48,23 +57,25 @@ export const serializeBody = (params: SerializeBodyParams) => { | |||||
entries: () => IterableIterator<[string, unknown]>; | entries: () => IterableIterator<[string, unknown]>; | ||||
}; | }; | ||||
}; | }; | ||||
const formData = new FormDataUnknown(form, options?.submitter); | |||||
const formData = new FormDataUnknown(form, serializerOptions?.submitter); | |||||
const emptyFiles = Array.from(formData.entries()) | const emptyFiles = Array.from(formData.entries()) | ||||
.filter(([, value]) => ( | .filter(([, value]) => ( | ||||
value instanceof File && value.size === 0 && value.name === '' | value instanceof File && value.size === 0 && value.name === '' | ||||
)); | )); | ||||
emptyFiles.forEach(([key]) => formData.delete(key)); | emptyFiles.forEach(([key]) => formData.delete(key)); | ||||
formData.delete(METHOD_FORM_KEY); | |||||
formData.delete(PREVENT_REDIRECT_FORM_KEY); | |||||
formData.delete(methodFormKey); | |||||
formData.delete(preventRedirectFormKey); | |||||
return formData; | return formData; | ||||
} | } | ||||
if (typeof serializers[encType] === 'function') { | if (typeof serializers[encType] === 'function') { | ||||
const { | const { | ||||
[METHOD_FORM_KEY]: _method, | |||||
[PREVENT_REDIRECT_FORM_KEY]: _preventRedirect, | |||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |||||
[methodFormKey]: _method, | |||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |||||
[preventRedirectFormKey]: _preventRedirect, | |||||
...formValues | ...formValues | ||||
} = getFormValues(form, options); | |||||
} = getFormValues(form, serializerOptions); | |||||
return serializers[encType](formValues); | return serializers[encType](formValues); | ||||
} | } | ||||
@@ -90,7 +101,7 @@ export const DEFAULT_ENCTYPE_DESERIALIZERS: EncTypeDeserializerMap = { | |||||
export const deserializeFormObjectBody = async (params: DeserializeBodyParams) => { | export const deserializeFormObjectBody = async (params: DeserializeBodyParams) => { | ||||
const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; | const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; | ||||
const contentType = req.headers['content-type'] ?? ENCTYPE_APPLICATION_OCTET_STREAM; | |||||
const contentType = req.headers[CONTENT_TYPE_HEADER_KEY] ?? ENCTYPE_APPLICATION_OCTET_STREAM; | |||||
if (contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`)) { | if (contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`)) { | ||||
return parseMultipartFormData(req); | return parseMultipartFormData(req); | ||||
@@ -98,7 +109,7 @@ export const deserializeFormObjectBody = async (params: DeserializeBodyParams) = | |||||
const bodyRaw = await getBody(req); | const bodyRaw = await getBody(req); | ||||
const encoding = ( | const encoding = ( | ||||
req.headers['content-encoding'] ?? DEFAULT_ENCODING | |||||
req.headers[CONTENT_ENCODING_HEADER_KEY] ?? DEFAULT_ENCODING | |||||
) as BufferEncoding; | ) as BufferEncoding; | ||||
const { [contentType]: theDeserializer } = deserializers; | const { [contentType]: theDeserializer } = deserializers; | ||||
@@ -11,15 +11,12 @@ importers: | |||||
'@theoryofnekomata/formxtra': | '@theoryofnekomata/formxtra': | ||||
specifier: ^1.0.3 | specifier: ^1.0.3 | ||||
version: 1.0.3 | version: 1.0.3 | ||||
'@web-std/file': | |||||
specifier: ^3.0.3 | |||||
version: 3.0.3 | |||||
busboy: | busboy: | ||||
specifier: ^1.6.0 | specifier: ^1.6.0 | ||||
version: 1.6.0 | version: 1.6.0 | ||||
nookies: | |||||
specifier: ^2.5.2 | |||||
version: 2.5.2 | |||||
node-cache: | |||||
specifier: ^5.1.2 | |||||
version: 5.1.2 | |||||
seroval: | seroval: | ||||
specifier: ^0.10.2 | specifier: ^0.10.2 | ||||
version: 0.10.2 | version: 0.10.2 | ||||
@@ -1515,31 +1512,6 @@ packages: | |||||
pretty-format: 29.7.0 | pretty-format: 29.7.0 | ||||
dev: true | dev: true | ||||
/@web-std/blob@3.0.5: | |||||
resolution: {integrity: sha512-Lm03qr0eT3PoLBuhkvFBLf0EFkAsNz/G/AYCzpOdi483aFaVX86b4iQs0OHhzHJfN5C15q17UtDbyABjlzM96A==} | |||||
dependencies: | |||||
'@web-std/stream': 1.0.0 | |||||
web-encoding: 1.1.5 | |||||
dev: false | |||||
/@web-std/file@3.0.3: | |||||
resolution: {integrity: sha512-X7YYyvEERBbaDfJeC9lBKC5Q5lIEWYCP1SNftJNwNH/VbFhdHm+3neKOQP+kWEYJmosbDFq+NEUG7+XIvet/Jw==} | |||||
dependencies: | |||||
'@web-std/blob': 3.0.5 | |||||
dev: false | |||||
/@web-std/stream@1.0.0: | |||||
resolution: {integrity: sha512-jyIbdVl+0ZJyKGTV0Ohb9E6UnxP+t7ZzX4Do3AHjZKxUXKMs9EmqnBDQgHF7bEw0EzbQygOjtt/7gvtmi//iCQ==} | |||||
dependencies: | |||||
web-streams-polyfill: 3.2.1 | |||||
dev: false | |||||
/@zxing/text-encoding@0.9.0: | |||||
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} | |||||
requiresBuild: true | |||||
dev: false | |||||
optional: true | |||||
/abab@2.0.6: | /abab@2.0.6: | ||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} | resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} | ||||
dev: true | dev: true | ||||
@@ -1987,6 +1959,11 @@ packages: | |||||
engines: {node: '>=0.8'} | engines: {node: '>=0.8'} | ||||
dev: true | dev: true | ||||
/clone@2.1.2: | |||||
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} | |||||
engines: {node: '>=0.8'} | |||||
dev: false | |||||
/color-convert@1.9.3: | /color-convert@1.9.3: | ||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} | ||||
dependencies: | dependencies: | ||||
@@ -3578,6 +3555,7 @@ packages: | |||||
dependencies: | dependencies: | ||||
call-bind: 1.0.2 | call-bind: 1.0.2 | ||||
has-tostringtag: 1.0.0 | has-tostringtag: 1.0.0 | ||||
dev: true | |||||
/is-array-buffer@3.0.2: | /is-array-buffer@3.0.2: | ||||
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} | resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} | ||||
@@ -4224,6 +4202,13 @@ packages: | |||||
- '@babel/core' | - '@babel/core' | ||||
- babel-plugin-macros | - babel-plugin-macros | ||||
/node-cache@5.1.2: | |||||
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} | |||||
engines: {node: '>= 8.0.0'} | |||||
dependencies: | |||||
clone: 2.1.2 | |||||
dev: false | |||||
/node-releases@2.0.13: | /node-releases@2.0.13: | ||||
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} | resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} | ||||
@@ -5457,16 +5442,6 @@ packages: | |||||
/util-deprecate@1.0.2: | /util-deprecate@1.0.2: | ||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} | ||||
/util@0.12.5: | |||||
resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} | |||||
dependencies: | |||||
inherits: 2.0.4 | |||||
is-arguments: 1.1.1 | |||||
is-generator-function: 1.0.10 | |||||
is-typed-array: 1.1.12 | |||||
which-typed-array: 1.1.11 | |||||
dev: false | |||||
/utils-merge@1.0.1: | /utils-merge@1.0.1: | ||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} | ||||
engines: {node: '>= 0.4.0'} | engines: {node: '>= 0.4.0'} | ||||
@@ -5648,19 +5623,6 @@ packages: | |||||
defaults: 1.0.4 | defaults: 1.0.4 | ||||
dev: true | dev: true | ||||
/web-encoding@1.1.5: | |||||
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} | |||||
dependencies: | |||||
util: 0.12.5 | |||||
optionalDependencies: | |||||
'@zxing/text-encoding': 0.9.0 | |||||
dev: false | |||||
/web-streams-polyfill@3.2.1: | |||||
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} | |||||
engines: {node: '>= 8'} | |||||
dev: false | |||||
/webidl-conversions@7.0.0: | /webidl-conversions@7.0.0: | ||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} | resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} | ||||
engines: {node: '>=12'} | engines: {node: '>=12'} | ||||