From 65a80d34d8cc52d53c5620da7164b427f78c093f Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Sat, 26 Aug 2023 13:34:47 -0400 Subject: [PATCH] Feature/issue 1048 handle merging additional `Request` / `Response` properties (#1132) * support merging Response status property * support body on incoming requests * handle merging all custom response headers * lock down content-type headers in test cases * handle Response.statusText property * full response support clean and TODOs cleanup * update vercel adapter plugin specs for request and response handling * update netlify adapter plugin specs for request and response handling * add support for request.formData * add request.formData support to adapter plugins * variable name safe handler alias --- packages/cli/package.json | 1 + packages/cli/src/lib/api-route-worker.js | 23 +- packages/cli/src/lib/resource-utils.js | 48 +++- packages/cli/src/lifecycles/bundle.js | 5 - packages/cli/src/lifecycles/compile.js | 4 - packages/cli/src/lifecycles/serve.js | 103 ++++----- .../src/plugins/resource/plugin-api-routes.js | 54 +++-- .../plugins/resource/plugin-standard-font.js | 2 +- .../develop.default.hud-disabled.spec.js | 2 +- .../develop.default.hud.spec.js | 2 +- .../develop.default/develop.default.spec.js | 208 ++++++++++++++++-- .../cases/develop.default/src/api/fragment.js | 18 ++ .../cases/develop.default/src/api/missing.js | 3 + .../cases/develop.default/src/api/nothing.js | 6 +- .../src/api/submit-form-data.js | 11 + .../develop.default/src/api/submit-json.js | 12 + .../develop.default/src/components/card.js | 11 + .../develop.plugins.context.spec.js | 4 +- .../cases/develop.spa/develop.spa.spec.js | 10 +- .../cases/develop.ssr/develop.ssr.spec.js | 2 +- .../serve.config.static-router.spec.js | 2 +- .../serve.default.api.spec.js | 145 +++++++++++- .../serve.default.api/src/api/fragment.js | 8 +- .../serve.default.api/src/api/missing.js | 3 + .../serve.default.api/src/api/nothing.js | 4 +- .../src/api/submit-form-data.js | 11 + .../serve.default.api/src/api/submit-json.js | 12 + ...e.default.ssr-prerender-api-hybrid.spec.js | 4 +- .../serve.default.ssr-prerender.spec.js | 2 +- .../serve.default.ssr-static-export.spec.js | 2 +- .../serve.default.ssr.spec.js | 2 +- .../cases/serve.default/serve.default.spec.js | 12 +- .../test/cases/serve.spa/serve.spa.spec.js | 6 +- .../theme-pack/theme-pack.develop.spec.js | 4 +- .../develop.default/develop.default.spec.js | 2 +- packages/plugin-adapter-netlify/src/index.js | 30 ++- .../cases/build.default/build.default.spec.js | 91 +++++++- .../build.default/src/api/submit-form-data.js | 11 + .../build.default/src/api/submit-json.js | 14 ++ packages/plugin-adapter-vercel/src/index.js | 30 ++- .../cases/build.default/build.default.spec.js | 88 +++++++- .../build.default/src/api/submit-form-data.js | 11 + .../build.default/src/api/submit-json.js | 14 ++ .../develop.default/develop.default.spec.js | 6 +- .../qraphql-server/graphql-server.spec.js | 4 +- .../develop.default/develop.default.spec.js | 4 +- .../cases/serve.default/serve.default.spec.js | 2 +- .../develop.default/develop.default.spec.js | 2 +- yarn.lock | 88 +++++++- 49 files changed, 959 insertions(+), 184 deletions(-) create mode 100644 packages/cli/test/cases/develop.default/src/api/fragment.js create mode 100644 packages/cli/test/cases/develop.default/src/api/missing.js create mode 100644 packages/cli/test/cases/develop.default/src/api/submit-form-data.js create mode 100644 packages/cli/test/cases/develop.default/src/api/submit-json.js create mode 100644 packages/cli/test/cases/develop.default/src/components/card.js create mode 100644 packages/cli/test/cases/serve.default.api/src/api/missing.js create mode 100644 packages/cli/test/cases/serve.default.api/src/api/submit-form-data.js create mode 100644 packages/cli/test/cases/serve.default.api/src/api/submit-json.js create mode 100644 packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-form-data.js create mode 100644 packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-json.js create mode 100644 packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-form-data.js create mode 100644 packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-json.js diff --git a/packages/cli/package.json b/packages/cli/package.json index 5707f170f..8b49ce4db 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,6 +42,7 @@ "es-module-shims": "^1.2.0", "front-matter": "^4.0.2", "koa": "^2.13.0", + "koa-body": "^6.0.1", "livereload": "^0.9.1", "markdown-toc": "^1.2.0", "node-html-parser": "^1.2.21", diff --git a/packages/cli/src/lib/api-route-worker.js b/packages/cli/src/lib/api-route-worker.js index 5a87470ad..4af5fabc5 100644 --- a/packages/cli/src/lib/api-route-worker.js +++ b/packages/cli/src/lib/api-route-worker.js @@ -1,5 +1,6 @@ // https://github.com/nodejs/modules/issues/307#issuecomment-858729422 import { parentPort } from 'worker_threads'; +import { transformKoaRequestIntoStandardRequest } from './resource-utils.js'; // based on https://stackoverflow.com/questions/57447685/how-can-i-convert-a-request-object-into-a-stringifiable-object-in-javascript async function responseAsObject (response) { @@ -21,19 +22,31 @@ async function responseAsObject (response) { return filtered; } - // TODO handle full response - // https://github.com/ProjectEvergreen/greenwood/issues/1048 return { ...stringifiableObject(response), headers: Object.fromEntries(response.headers), - // signal: stringifiableObject(request.signal), body: await response.text() }; } async function executeRouteModule({ href, request }) { - const { handler } = await import(href); - const response = await handler(request); + const { body, headers = {}, method, url } = request; + const contentType = headers['content-type'] || ''; + const { handler } = await import(new URL(href)); + const format = contentType.startsWith('application/json') + ? JSON.parse(body) + : body; + + // handling of serialized FormData across Worker threads + if (contentType.startsWith('x-greenwood/www-form-urlencoded')) { + headers['content-type'] = 'application/x-www-form-urlencoded'; + } + + const response = await handler(transformKoaRequestIntoStandardRequest(new URL(url), { + method, + header: headers, + body: format + })); parentPort.postMessage(await responseAsObject(response)); } diff --git a/packages/cli/src/lib/resource-utils.js b/packages/cli/src/lib/resource-utils.js index 6f84e415e..410c13d60 100644 --- a/packages/cli/src/lib/resource-utils.js +++ b/packages/cli/src/lib/resource-utils.js @@ -36,6 +36,8 @@ async function modelResource(context, type, src = undefined, contents = undefine function mergeResponse(destination, source) { const headers = destination.headers || new Headers(); + const status = source.status || destination.status; + const statusText = source.statusText || destination.statusText; source.headers.forEach((value, key) => { // TODO better way to handle Response automatically setting content-type @@ -47,10 +49,10 @@ function mergeResponse(destination, source) { } }); - // TODO handle merging in state (aborted, type, status, etc) - // https://github.com/ProjectEvergreen/greenwood/issues/1048 return new Response(source.body, { - headers + headers, + status, + statusText }); } @@ -169,11 +171,49 @@ function isLocalLink(url = '') { return url !== '' && (url.indexOf('http') !== 0 && url.indexOf('//') !== 0); } +// TODO handle full request +// https://github.com/ProjectEvergreen/greenwood/discussions/1146 +function transformKoaRequestIntoStandardRequest(url, request) { + const { body, method, header } = request; + const headers = new Headers(header); + const contentType = headers.get('content-type') || ''; + let format; + + if (contentType.includes('application/x-www-form-urlencoded')) { + const formData = new FormData(); + + for (const key of Object.keys(body)) { + formData.append(key, body[key]); + } + + // when using FormData, let Request set the correct headers + // or else it will come out as multipart/form-data + // https://stackoverflow.com/a/43521052/417806 + headers.delete('content-type'); + + format = formData; + } else if (contentType.includes('application/json')) { + format = JSON.stringify(body); + } else { + format = body; + } + + // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters + return new Request(url, { + body: ['GET', 'HEAD'].includes(method.toUpperCase()) + ? null + : format, + method, + headers + }); +} + export { checkResourceExists, mergeResponse, modelResource, normalizePathnameForWindows, resolveForRelativeUrl, - trackResourcesForRoute + trackResourcesForRoute, + transformKoaRequestIntoStandardRequest }; \ No newline at end of file diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index aa483a7a2..a6fb8c971 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -12,8 +12,6 @@ async function emitResources(compilation) { const { resources, graph } = compilation; // https://stackoverflow.com/a/56150320/417806 - // TODO put into a util - // https://github.com/ProjectEvergreen/greenwood/issues/1008 await fs.writeFile(new URL('./resources.json', outputDir), JSON.stringify(resources, (key, value) => { if (value instanceof Map) { return { @@ -176,7 +174,6 @@ async function bundleApiRoutes(compilation) { async function bundleSsrPages(compilation) { // https://rollupjs.org/guide/en/#differences-to-the-javascript-api // TODO context plugins for SSR ? - // https://github.com/ProjectEvergreen/greenwood/issues/1008 // const contextPlugins = compilation.config.plugins.filter((plugin) => { // return plugin.type === 'context'; // }).map((plugin) => { @@ -207,8 +204,6 @@ async function bundleSsrPages(compilation) { staticHtml = await (await htmlOptimizer.optimize(new URL(`http://localhost:8080${route}`), new Response(staticHtml))).text(); // better way to write out this inline code? - // TODO flesh out response properties - // https://github.com/ProjectEvergreen/greenwood/issues/1048 await fs.writeFile(entryFileUrl, ` import { executeRouteModule } from '${normalizePathnameForWindows(executeModuleUrl)}'; diff --git a/packages/cli/src/lifecycles/compile.js b/packages/cli/src/lifecycles/compile.js index 5e0d2c977..8b09af2a7 100644 --- a/packages/cli/src/lifecycles/compile.js +++ b/packages/cli/src/lifecycles/compile.js @@ -13,7 +13,6 @@ const generateCompilation = () => { context: {}, config: {}, // TODO put resources into manifest - // https://github.com/ProjectEvergreen/greenwood/issues/1008 resources: new Map(), manifest: { apis: new Map() @@ -45,7 +44,6 @@ const generateCompilation = () => { if (await checkResourceExists(new URL('./manifest.json', outputDir))) { console.info('Loading manifest from build output...'); // TODO put reviver into a utility? - // https://github.com/ProjectEvergreen/greenwood/issues/1008 const manifest = JSON.parse(await fs.readFile(new URL('./manifest.json', outputDir)), function reviver(key, value) { if (typeof value === 'object' && value !== null) { if (value.dataType === 'Map') { @@ -61,7 +59,6 @@ const generateCompilation = () => { if (await checkResourceExists(new URL('./resources.json', outputDir))) { console.info('Loading resources from build output...'); // TODO put reviver into a utility? - // https://github.com/ProjectEvergreen/greenwood/issues/1008 const resources = JSON.parse(await fs.readFile(new URL('./resources.json', outputDir)), function reviver(key, value) { if (typeof value === 'object' && value !== null) { if (value.dataType === 'Map') { @@ -85,7 +82,6 @@ const generateCompilation = () => { // https://stackoverflow.com/a/56150320/417806 // TODO put reviver into a util? - // https://github.com/ProjectEvergreen/greenwood/issues/1008 await fs.writeFile(new URL('./manifest.json', scratchDir), JSON.stringify(compilation.manifest, (key, value) => { if (value instanceof Map) { return { diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index 8c3fc08f5..f15107daa 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -1,7 +1,8 @@ import fs from 'fs/promises'; import { hashString } from '../lib/hashing-utils.js'; import Koa from 'koa'; -import { checkResourceExists, mergeResponse } from '../lib/resource-utils.js'; +import { koaBody } from 'koa-body'; +import { checkResourceExists, mergeResponse, transformKoaRequestIntoStandardRequest } from '../lib/resource-utils.js'; import { Readable } from 'stream'; import { ResourceInterface } from '../lib/resource-interface.js'; @@ -30,14 +31,13 @@ async function getDevServer(compilation) { }) ]; + app.use(koaBody()); + // resolve urls to `file://` paths if applicable, otherwise default is `http://` app.use(async (ctx, next) => { try { const url = new URL(`http://localhost:${compilation.config.port}${ctx.url}`); - const initRequest = new Request(url, { - method: ctx.request.method, - headers: new Headers(ctx.request.header) - }); + const initRequest = transformKoaRequestIntoStandardRequest(url, ctx.request); const request = await resourcePlugins.reduce(async (requestPromise, plugin) => { const intermediateRequest = await requestPromise; return plugin.shouldResolve && await plugin.shouldResolve(url, intermediateRequest.clone()) @@ -58,9 +58,9 @@ async function getDevServer(compilation) { app.use(async (ctx, next) => { try { const url = new URL(ctx.url); - const { method, header } = ctx.request; const { status } = ctx.response; - const request = new Request(url.href, { method, headers: new Headers(header) }); + const request = transformKoaRequestIntoStandardRequest(url, ctx.request); + // intentionally ignore initial statusText to avoid false positives from 404s let response = new Response(null, { status }); for (const plugin of resourcePlugins) { @@ -74,14 +74,11 @@ async function getDevServer(compilation) { } ctx.body = response.body ? Readable.from(response.body) : ''; - ctx.type = response.headers.get('Content-Type'); ctx.status = response.status; - - // TODO automatically loop and apply all custom headers to Koa response, include Content-Type below - // https://github.com/ProjectEvergreen/greenwood/issues/1048 - if (response.headers.has('Content-Length')) { - ctx.set('Content-Length', response.headers.get('Content-Length')); - } + ctx.message = response.statusText; + response.headers.forEach((value, key) => { + ctx.set(key, value); + }); } catch (e) { ctx.status = 500; console.error(e); @@ -94,13 +91,12 @@ async function getDevServer(compilation) { app.use(async (ctx, next) => { try { const url = new URL(ctx.url); - const request = new Request(url, { - method: ctx.request.method, - headers: new Headers(ctx.request.header) - }); - const initResponse = new Response(ctx.body, { - status: ctx.response.status, - headers: new Headers(ctx.response.header) + const { header, status, message } = ctx.response; + const request = transformKoaRequestIntoStandardRequest(url, ctx.request); + const initResponse = new Response(status === 204 ? null : ctx.body, { + statusText: message, + status, + headers: new Headers(header) }); const response = await resourcePlugins.reduce(async (responsePromise, plugin) => { const intermediateResponse = await responsePromise; @@ -115,12 +111,10 @@ async function getDevServer(compilation) { }, Promise.resolve(initResponse.clone())); ctx.body = response.body ? Readable.from(response.body) : ''; - ctx.set('Content-Type', response.headers.get('Content-Type')); - // TODO automatically loop and apply all custom headers to Koa response, include Content-Type below - // https://github.com/ProjectEvergreen/greenwood/issues/1048 - if (response.headers.has('Content-Length')) { - ctx.set('Content-Length', response.headers.get('Content-Length')); - } + ctx.message = response.statusText; + response.headers.forEach((value, key) => { + ctx.set(key, value); + }); } catch (e) { ctx.status = 500; console.error(e); @@ -138,9 +132,11 @@ async function getDevServer(compilation) { // and only run in development if (process.env.__GWD_COMMAND__ === 'develop' && url.protocol === 'file:') { // eslint-disable-line no-underscore-dangle // TODO there's probably a better way to do this with tee-ing streams but this works for now + const { header, status, message } = ctx.response; const response = new Response(ctx.body, { - status: ctx.response.status, - headers: new Headers(ctx.response.header) + statusText: message, + status, + headers: new Headers(header) }).clone(); const splitResponse = response.clone(); const contents = await splitResponse.text(); @@ -157,14 +153,11 @@ async function getDevServer(compilation) { } else if (!inm || inm !== etagHash) { ctx.body = Readable.from(response.body); ctx.status = ctx.status; - ctx.set('Content-Type', ctx.response.header['content-type']); ctx.set('Etag', etagHash); - - // TODO automatically loop and apply all custom headers to Koa response, include Content-Type below - // https://github.com/ProjectEvergreen/greenwood/issues/1048 - if (response.headers.has('Content-Length')) { - ctx.set('Content-Length', response.headers.get('Content-Length')); - } + ctx.message = response.statusText; + response.headers.forEach((value, key) => { + ctx.set(key, value); + }); } } }); @@ -224,7 +217,10 @@ async function getStaticServer(compilation, composable) { const response = await proxyPlugin.serve(url, request); ctx.body = Readable.from(response.body); - ctx.set('Content-Type', response.headers.get('Content-Type')); + response.headers.forEach((value, key) => { + ctx.set(key, value); + }); + ctx.message = response.statusText; } } } catch (e) { @@ -261,14 +257,11 @@ async function getStaticServer(compilation, composable) { if (response.ok) { ctx.body = Readable.from(response.body); - ctx.type = response.headers.get('Content-Type'); ctx.status = response.status; - - // TODO automatically loop and apply all custom headers to Koa response, include Content-Type below - // https://github.com/ProjectEvergreen/greenwood/issues/1048 - if (response.headers.has('Content-Length')) { - ctx.set('Content-Length', response.headers.get('Content-Length')); - } + ctx.message = response.statusText; + response.headers.forEach((value, key) => { + ctx.set(key, value); + }); } } } catch (e) { @@ -289,38 +282,36 @@ async function getHybridServer(compilation) { const { outputDir } = context; const app = await getStaticServer(compilation, true); + app.use(koaBody()); + app.use(async (ctx) => { try { const url = new URL(`http://localhost:${config.port}${ctx.url}`); const matchingRoute = graph.find((node) => node.route === url.pathname) || { data: {} }; const isApiRoute = manifest.apis.has(url.pathname); - const request = new Request(url.href, { - method: ctx.request.method, - headers: ctx.request.header - }); + const request = transformKoaRequestIntoStandardRequest(url, ctx.request); if (!config.prerender && matchingRoute.isSSR && !matchingRoute.data.static) { const { handler } = await import(new URL(`./__${matchingRoute.filename}`, outputDir)); // TODO passing compilation this way too hacky? - // https://github.com/ProjectEvergreen/greenwood/issues/1008 const response = await handler(request, compilation); ctx.body = Readable.from(response.body); ctx.set('Content-Type', 'text/html'); - // TODO should use status from response - // https://github.com/ProjectEvergreen/greenwood/issues/1048 ctx.status = 200; } else if (isApiRoute) { const apiRoute = manifest.apis.get(url.pathname); const { handler } = await import(new URL(`.${apiRoute.path}`, outputDir)); const response = await handler(request); - const { body } = response; + const { body, status, headers, statusText } = response; - // TODO should use status from response - // https://github.com/ProjectEvergreen/greenwood/issues/1048 ctx.body = body ? Readable.from(body) : null; - ctx.status = 200; - ctx.set('Content-Type', response.headers.get('Content-Type')); + ctx.status = status; + ctx.message = statusText; + + headers.forEach((value, key) => { + ctx.set(key, value); + }); } } catch (e) { ctx.status = 500; diff --git a/packages/cli/src/plugins/resource/plugin-api-routes.js b/packages/cli/src/plugins/resource/plugin-api-routes.js index 66eef9d27..dcb023eb0 100644 --- a/packages/cli/src/plugins/resource/plugin-api-routes.js +++ b/packages/cli/src/plugins/resource/plugin-api-routes.js @@ -7,14 +7,18 @@ import { ResourceInterface } from '../../lib/resource-interface.js'; import { Worker } from 'worker_threads'; // https://stackoverflow.com/questions/57447685/how-can-i-convert-a-request-object-into-a-stringifiable-object-in-javascript -function requestAsObject (request) { - if (!request instanceof Request) { +async function requestAsObject (_request) { + if (!_request instanceof Request) { throw Object.assign( new Error(), { name: 'TypeError', message: 'Argument must be a Request object' } ); } - request = request.clone(); + + const request = _request.clone(); + const contentType = request.headers.get('content-type') || ''; + let headers = Object.fromEntries(request.headers); + let format; function stringifiableObject (obj) { const filtered = {}; @@ -26,13 +30,30 @@ function requestAsObject (request) { return filtered; } - // TODO handle full response - // https://github.com/ProjectEvergreen/greenwood/issues/1048 + if (contentType.includes('application/x-www-form-urlencoded')) { + const formData = await request.formData(); + const params = {}; + + for (const entry of formData.entries()) { + params[entry[0]] = entry[1]; + } + + // when using FormData, let Request set the correct headers + // or else it will come out as multipart/form-data + // for serialization between route workers, leave a special marker for Greenwood + // https://stackoverflow.com/a/43521052/417806 + headers['content-type'] = 'x-greenwood/www-form-urlencoded'; + format = JSON.stringify(params); + } else if (contentType.includes('application/json')) { + format = JSON.stringify(await request.json()); + } else { + format = await request.text(); + } + return { ...stringifiableObject(request), - headers: Object.fromEntries(request.headers), - signal: stringifiableObject(request.signal) - // bodyText: await request.text(), // requires function to be async + body: format, + headers }; } @@ -51,17 +72,13 @@ class ApiRoutesResource extends ResourceInterface { const api = this.compilation.manifest.apis.get(url.pathname); const apiUrl = new URL(`.${api.path}`, this.compilation.context.userWorkspace); const href = apiUrl.href; - const req = new Request(new URL(url), { - ...request - }); - // TODO does this ever run in anything but development mode? if (process.env.__GWD_COMMAND__ === 'develop') { // eslint-disable-line no-underscore-dangle const workerUrl = new URL('../../lib/api-route-worker.js', import.meta.url); + const req = await requestAsObject(request); - const response = await new Promise((resolve, reject) => { + const response = await new Promise(async (resolve, reject) => { const worker = new Worker(workerUrl); - const req = requestAsObject(request); worker.on('message', (result) => { resolve(result); @@ -75,14 +92,17 @@ class ApiRoutesResource extends ResourceInterface { worker.postMessage({ href, request: req }); }); + const { headers, body, status, statusText } = response; - return new Response(response.body, { - ...response + return new Response(status === 204 ? null : body, { + headers: new Headers(headers), + status, + statusText }); } else { const { handler } = await import(href); - return await handler(req); + return await handler(request); } } } diff --git a/packages/cli/src/plugins/resource/plugin-standard-font.js b/packages/cli/src/plugins/resource/plugin-standard-font.js index 23c319150..70d927b9c 100644 --- a/packages/cli/src/plugins/resource/plugin-standard-font.js +++ b/packages/cli/src/plugins/resource/plugin-standard-font.js @@ -21,7 +21,7 @@ class StandardFontResource extends ResourceInterface { async serve(url) { const extension = url.pathname.split('.').pop(); - const contentType = extension === 'eot' ? 'application/vnd.ms-fontobject' : extension; + const contentType = extension === 'eot' ? 'application/vnd.ms-fontobject' : `font/${extension}`; const body = await fs.readFile(url); return new Response(body, { diff --git a/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js b/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js index 649e63535..f05504d7f 100644 --- a/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js +++ b/packages/cli/test/cases/develop.default.hud-disabled/develop.default.hud-disabled.spec.js @@ -86,7 +86,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js b/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js index c611967d5..df5768c6b 100644 --- a/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js +++ b/packages/cli/test/cases/develop.default.hud/develop.default.hud.spec.js @@ -86,7 +86,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/develop.default/develop.default.spec.js b/packages/cli/test/cases/develop.default/develop.default.spec.js index 0136a1ce1..84837a0e7 100644 --- a/packages/cli/test/cases/develop.default/develop.default.spec.js +++ b/packages/cli/test/cases/develop.default/develop.default.spec.js @@ -18,8 +18,12 @@ * User Workspace * src/ * api/ + * fragment.js * greeting.js + * missing.js * nothing.js + * submit-form-data.js + * submit-json.js * assets/ * data.json * favicon.ico @@ -31,6 +35,7 @@ * splash-clip.mp4 * webcomponents.svg * components/ + * card.js * header.js * pages/ * index.html @@ -481,7 +486,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -574,7 +579,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -625,7 +630,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); @@ -659,7 +664,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/css'); + expect(response.headers['content-type']).to.equal('text/css'); done(); }); @@ -695,7 +700,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain(`image/${ext}`); + expect(response.headers['content-type']).to.equal(`image/${ext}`); done(); }); @@ -729,7 +734,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('image/x-icon'); + expect(response.headers['content-type']).to.equal('image/x-icon'); done(); }); @@ -763,7 +768,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('image/webp'); + expect(response.headers['content-type']).to.equal('image/webp'); done(); }); @@ -797,7 +802,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('image/avif'); + expect(response.headers['content-type']).to.equal('image/avif'); done(); }); @@ -832,7 +837,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain(`image/${ext}+xml`); + expect(response.headers['content-type']).to.equal(`image/${ext}+xml`); done(); }); @@ -867,7 +872,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain(ext); + expect(response.headers['content-type']).to.equal(`font/${ext}`); done(); }); @@ -904,7 +909,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type header', function(done) { - expect(response.headers['content-type']).to.contain(ext); + expect(response.headers['content-type']).to.equal(`video/${ext}`); done(); }); @@ -987,7 +992,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('audio/mpeg'); + expect(response.headers['content-type']).to.equal('audio/mpeg'); done(); }); @@ -1026,7 +1031,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json'); done(); }); @@ -1060,7 +1065,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json'); done(); }); @@ -1094,7 +1099,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); @@ -1130,7 +1135,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/css'); + expect(response.headers['content-type']).to.equal('text/css'); done(); }); @@ -1166,7 +1171,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); @@ -1201,7 +1206,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); @@ -1237,7 +1242,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); @@ -1272,7 +1277,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/plain'); + expect(response.headers['content-type']).to.equal('text/plain; charset=utf-8'); done(); }); @@ -1312,7 +1317,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json; charset=utf-8'); done(); }); @@ -1339,8 +1344,14 @@ describe('Develop Greenwood With: ', function() { done(); }); + it('should return a default status message', function(done) { + // OK appears to be a Koa default when statusText is an empty string + expect(response.statusText).to.equal('OK'); + done(); + }); + it('should return the correct content type', function(done) { - expect(response.headers.get('content-type')).to.equal('application/json; charset=utf-8'); + expect(response.headers.get('content-type')).to.equal('application/json'); done(); }); @@ -1350,6 +1361,73 @@ describe('Develop Greenwood With: ', function() { }); }); + describe('Develop command with API specific behaviors for an HTML ("fragment") API', function() { + const name = 'Greenwood'; + let response = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}:${port}/api/fragment?name=${name}`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + + resolve(); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return a custom status message', function(done) { + expect(response.statusMessage).to.equal('SUCCESS!!!'); + done(); + }); + + it('should return the correct content type', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + + it('should return the correct response body', function(done) { + expect(response.body).to.contain(`

Hello ${name}!!!

`); + done(); + }); + }); + + describe('Develop command with API specific behaviors with a custom response', function() { + let response = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}:${port}/api/missing`, (err, res) => { + if (err) { + reject(); + } + + response = res; + resolve(); + }); + }); + }); + + it('should return a 404 status', function(done) { + expect(response.statusCode).to.equal(404); + done(); + }); + + it('should return a body of not found', function(done) { + expect(response.body).to.equal('Not Found'); + done(); + }); + }); + describe('Develop command with API specific behaviors with a minimal response', function() { let response = {}; @@ -1366,10 +1444,96 @@ describe('Develop Greenwood With: ', function() { }); }); + it('should return a custom status code', function(done) { + expect(response.statusCode).to.equal(204); + done(); + }); + }); + + describe('Develop command with POST API specific behaviors for JSON', function() { + const param = 'Greenwood'; + let response = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.post({ + url: `${hostname}:${port}/api/submit-json`, + json: true, + body: { name: param } + }, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + + resolve(response); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the expected response message', function(done) { + const { message } = response.body; + + expect(message).to.equal(`Thank you ${param} for your submission!`); + done(); + }); + + it('should return the expected content type header', function(done) { + expect(response.headers['content-type']).to.equal('application/json'); + done(); + }); + + it('should return the secret header in the response', function(done) { + expect(response.headers['x-secret']).to.equal('1234'); + done(); + }); + }); + + describe('Develop command with POST API specific behaviors for FormData', function() { + const param = 'Greenwood'; + let response = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.post({ + url: `${hostname}:${port}/api/submit-form-data`, + form: { + name: param + } + }, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + + resolve(response); + }); + }); + }); + it('should return a 200 status', function(done) { expect(response.statusCode).to.equal(200); done(); }); + + it('should return the expected response message', function(done) { + expect(response.body).to.equal(`Thank you ${param} for your submission!`); + done(); + }); + + it('should return the expected content type header', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); }); }); diff --git a/packages/cli/test/cases/develop.default/src/api/fragment.js b/packages/cli/test/cases/develop.default/src/api/fragment.js new file mode 100644 index 000000000..ab5a5722c --- /dev/null +++ b/packages/cli/test/cases/develop.default/src/api/fragment.js @@ -0,0 +1,18 @@ +import { renderFromHTML } from 'wc-compiler'; + +export async function handler(request) { + const params = new URLSearchParams(request.url.slice(request.url.indexOf('?'))); + const name = params.has('name') ? params.get('name') : 'World'; + const { html } = await renderFromHTML(` + + `, [ + new URL('../components/card.js', import.meta.url) + ]); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html' + }), + statusText: 'SUCCESS!!!' + }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.default/src/api/missing.js b/packages/cli/test/cases/develop.default/src/api/missing.js new file mode 100644 index 000000000..fe2c58c32 --- /dev/null +++ b/packages/cli/test/cases/develop.default/src/api/missing.js @@ -0,0 +1,3 @@ +export async function handler() { + return new Response('Not Found', { status: 404 }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.default/src/api/nothing.js b/packages/cli/test/cases/develop.default/src/api/nothing.js index 4f49b7fb3..d7a1ac2a0 100644 --- a/packages/cli/test/cases/develop.default/src/api/nothing.js +++ b/packages/cli/test/cases/develop.default/src/api/nothing.js @@ -1,5 +1,5 @@ export async function handler() { - // TODO should support a non 200 status code - // https://github.com/ProjectEvergreen/greenwood/issues/1048 - return new Response(undefined); + return new Response(null, { + status: 204 + }); } \ No newline at end of file diff --git a/packages/cli/test/cases/develop.default/src/api/submit-form-data.js b/packages/cli/test/cases/develop.default/src/api/submit-form-data.js new file mode 100644 index 000000000..5a4716f26 --- /dev/null +++ b/packages/cli/test/cases/develop.default/src/api/submit-form-data.js @@ -0,0 +1,11 @@ +export async function handler(request) { + const formData = await request.formData(); + const name = formData.get('name'); + const body = `Thank you ${name} for your submission!`; + + return new Response(body, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.default/src/api/submit-json.js b/packages/cli/test/cases/develop.default/src/api/submit-json.js new file mode 100644 index 000000000..b43a3e367 --- /dev/null +++ b/packages/cli/test/cases/develop.default/src/api/submit-json.js @@ -0,0 +1,12 @@ +export async function handler(request) { + const formData = await request.json(); + const { name } = formData; + const body = { message: `Thank you ${name} for your submission!` }; + + return new Response(JSON.stringify(body), { + headers: new Headers({ + 'Content-Type': 'application/json', + 'x-secret': 1234 + }) + }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/develop.default/src/components/card.js b/packages/cli/test/cases/develop.default/src/components/card.js new file mode 100644 index 000000000..1e916cfff --- /dev/null +++ b/packages/cli/test/cases/develop.default/src/components/card.js @@ -0,0 +1,11 @@ +export default class Card extends HTMLElement { + connectedCallback() { + const name = this.getAttribute('name'); + + this.innerHTML = ` +

Hello ${name}!!!

+ `; + } +} + +customElements.define('x-card', Card); \ No newline at end of file diff --git a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js index dc923d816..54b57579b 100644 --- a/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js +++ b/packages/cli/test/cases/develop.plugins.context/develop.plugins.context.spec.js @@ -141,7 +141,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/css'); + expect(response.headers['content-type']).to.equal('text/css'); done(); }); @@ -179,7 +179,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); diff --git a/packages/cli/test/cases/develop.spa/develop.spa.spec.js b/packages/cli/test/cases/develop.spa/develop.spa.spec.js index fead6217d..6fffefd76 100644 --- a/packages/cli/test/cases/develop.spa/develop.spa.spec.js +++ b/packages/cli/test/cases/develop.spa/develop.spa.spec.js @@ -96,7 +96,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -135,7 +135,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -174,7 +174,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -211,7 +211,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/css'); + expect(response.headers['content-type']).to.equal('text/css'); done(); }); @@ -251,7 +251,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/css'); + expect(response.headers['content-type']).to.equal('text/css'); done(); }); diff --git a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js index 96be6851d..830079fdf 100644 --- a/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js +++ b/packages/cli/test/cases/develop.ssr/develop.ssr.spec.js @@ -166,7 +166,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js b/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js index 501b8129a..e990e335d 100644 --- a/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js +++ b/packages/cli/test/cases/serve.config.static-router/serve.config.static-router.spec.js @@ -95,7 +95,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js index c7e88120b..039d37c33 100644 --- a/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js +++ b/packages/cli/test/cases/serve.default.api/serve.default.api.spec.js @@ -14,7 +14,14 @@ * User Workspace * src/ * api/ + * fragment.js * greeting.js + * missing.js + * nothing.js + * submit-form-data.js + * submit-json.js + * components/ + * card.js */ import chai from 'chai'; import path from 'path'; @@ -37,7 +44,7 @@ describe('Serve Greenwood With: ', function() { this.context = { hostname }; - runner = new Runner(true); + runner = new Runner(); }); describe(LABEL, function() { @@ -62,7 +69,6 @@ describe('Serve Greenwood With: ', function() { let response = {}; before(async function() { - // TODO not sure why native `fetch` doesn't seem to work here, just hangs the test runner return new Promise((resolve, reject) => { request.get(`${hostname}/api/greeting?name=${name}`, (err, res, body) => { if (err) { @@ -82,8 +88,14 @@ describe('Serve Greenwood With: ', function() { done(); }); + it('should return a default status message', function(done) { + // OK appears to be a Koa default when statusText is an empty string + expect(response.statusMessage).to.equal('OK'); + done(); + }); + it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json'); done(); }); @@ -98,7 +110,6 @@ describe('Serve Greenwood With: ', function() { let response = {}; before(async function() { - // TODO not sure why native `fetch` doesn't seem to work here, just hangs the test runner return new Promise((resolve, reject) => { request.get(`${hostname}/api/fragment?name=${name}`, (err, res, body) => { if (err) { @@ -118,6 +129,11 @@ describe('Serve Greenwood With: ', function() { done(); }); + it('should return a custom status message', function(done) { + expect(response.statusMessage).to.equal('SUCCESS!!!'); + done(); + }); + it('should return the correct content type', function(done) { expect(response.headers['content-type']).to.equal('text/html'); done(); @@ -145,8 +161,8 @@ describe('Serve Greenwood With: ', function() { }); }); - it('should return a 200 status', function(done) { - expect(response.statusCode).to.equal(200); + it('should return a custom status code', function(done) { + expect(response.statusCode).to.equal(204); done(); }); }); @@ -174,6 +190,123 @@ describe('Serve Greenwood With: ', function() { done(); }); }); + + describe('Serve command with API specific behaviors with a custom response', function() { + let response = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.get(`${hostname}/api/missing`, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + + resolve(); + }); + }); + }); + + it('should return a 404 status', function(done) { + expect(response.statusCode).to.equal(404); + done(); + }); + + it('should return a body of not found', function(done) { + expect(response.body).to.equal('Not Found'); + done(); + }); + }); + + describe('Serve command with POST API specific behaviors for JSON', function() { + const param = 'Greenwood'; + let response = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.post({ + url: `${hostname}/api/submit-json`, + body: { + name: param + }, + json: true + }, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + + resolve(response); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the expected response message', function(done) { + const { message } = response.body; + + expect(message).to.equal(`Thank you ${param} for your submission!`); + done(); + }); + + it('should return the expected content type header', function(done) { + expect(response.headers['content-type']).to.equal('application/json'); + done(); + }); + + it('should return the secret header in the response', function(done) { + expect(response.headers['x-secret']).to.equal('1234'); + done(); + }); + }); + + describe('Serve command with POST API specific behaviors for FormData', function() { + const param = 'Greenwood'; + let response = {}; + + before(async function() { + return new Promise((resolve, reject) => { + request.post({ + url: `${hostname}/api/submit-form-data`, + form: { + name: param + } + }, (err, res, body) => { + if (err) { + reject(); + } + + response = res; + response.body = body; + + resolve(response); + }); + }); + }); + + it('should return a 200 status', function(done) { + expect(response.statusCode).to.equal(200); + done(); + }); + + it('should return the expected response message', function(done) { + expect(response.body).to.equal(`Thank you ${param} for your submission!`); + done(); + }); + + it('should return the expected content type header', function(done) { + expect(response.headers['content-type']).to.equal('text/html'); + done(); + }); + }); }); after(function() { diff --git a/packages/cli/test/cases/serve.default.api/src/api/fragment.js b/packages/cli/test/cases/serve.default.api/src/api/fragment.js index 5dc52e456..ab5a5722c 100644 --- a/packages/cli/test/cases/serve.default.api/src/api/fragment.js +++ b/packages/cli/test/cases/serve.default.api/src/api/fragment.js @@ -1,7 +1,6 @@ import { renderFromHTML } from 'wc-compiler'; export async function handler(request) { - const headers = new Headers(); const params = new URLSearchParams(request.url.slice(request.url.indexOf('?'))); const name = params.has('name') ? params.get('name') : 'World'; const { html } = await renderFromHTML(` @@ -10,9 +9,10 @@ export async function handler(request) { new URL('../components/card.js', import.meta.url) ]); - headers.append('Content-Type', 'text/html'); - return new Response(html, { - headers + headers: new Headers({ + 'Content-Type': 'text/html' + }), + statusText: 'SUCCESS!!!' }); } \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.api/src/api/missing.js b/packages/cli/test/cases/serve.default.api/src/api/missing.js new file mode 100644 index 000000000..fe2c58c32 --- /dev/null +++ b/packages/cli/test/cases/serve.default.api/src/api/missing.js @@ -0,0 +1,3 @@ +export async function handler() { + return new Response('Not Found', { status: 404 }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.api/src/api/nothing.js b/packages/cli/test/cases/serve.default.api/src/api/nothing.js index 4596641aa..d7a1ac2a0 100644 --- a/packages/cli/test/cases/serve.default.api/src/api/nothing.js +++ b/packages/cli/test/cases/serve.default.api/src/api/nothing.js @@ -1,3 +1,5 @@ export async function handler() { - return new Response(undefined); + return new Response(null, { + status: 204 + }); } \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.api/src/api/submit-form-data.js b/packages/cli/test/cases/serve.default.api/src/api/submit-form-data.js new file mode 100644 index 000000000..5a4716f26 --- /dev/null +++ b/packages/cli/test/cases/serve.default.api/src/api/submit-form-data.js @@ -0,0 +1,11 @@ +export async function handler(request) { + const formData = await request.formData(); + const name = formData.get('name'); + const body = `Thank you ${name} for your submission!`; + + return new Response(body, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.api/src/api/submit-json.js b/packages/cli/test/cases/serve.default.api/src/api/submit-json.js new file mode 100644 index 000000000..b43a3e367 --- /dev/null +++ b/packages/cli/test/cases/serve.default.api/src/api/submit-json.js @@ -0,0 +1,12 @@ +export async function handler(request) { + const formData = await request.json(); + const { name } = formData; + const body = { message: `Thank you ${name} for your submission!` }; + + return new Response(JSON.stringify(body), { + headers: new Headers({ + 'Content-Type': 'application/json', + 'x-secret': 1234 + }) + }); +} \ No newline at end of file diff --git a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js index aa18871d6..2b412e42b 100644 --- a/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js +++ b/packages/cli/test/cases/serve.default.ssr-prerender-api-hybrid/serve.default.ssr-prerender-api-hybrid.spec.js @@ -96,7 +96,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -162,7 +162,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json'); done(); }); diff --git a/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js b/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js index 453a08340..063f98db3 100644 --- a/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js +++ b/packages/cli/test/cases/serve.default.ssr-prerender/serve.default.ssr-prerender.spec.js @@ -93,7 +93,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js b/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js index 9be726711..e0ee5ba52 100644 --- a/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js +++ b/packages/cli/test/cases/serve.default.ssr-static-export/serve.default.ssr-static-export.spec.js @@ -170,7 +170,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js index 2eb743a67..e323513a6 100644 --- a/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js +++ b/packages/cli/test/cases/serve.default.ssr/serve.default.ssr.spec.js @@ -149,7 +149,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/serve.default/serve.default.spec.js b/packages/cli/test/cases/serve.default/serve.default.spec.js index 521af14c0..265576e8b 100644 --- a/packages/cli/test/cases/serve.default/serve.default.spec.js +++ b/packages/cli/test/cases/serve.default/serve.default.spec.js @@ -98,7 +98,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json; charset=utf-8'); done(); }); @@ -133,7 +133,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json; charset=utf-8'); done(); }); @@ -310,7 +310,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain(ext); + expect(response.headers['content-type']).to.equal(`video/${ext}`); done(); }); @@ -349,7 +349,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('audio/mpeg'); + expect(response.headers['content-type']).to.equal('audio/mpeg'); done(); }); @@ -388,7 +388,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json'); done(); }); @@ -422,7 +422,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json'); done(); }); diff --git a/packages/cli/test/cases/serve.spa/serve.spa.spec.js b/packages/cli/test/cases/serve.spa/serve.spa.spec.js index cfcf7558e..834016ae3 100644 --- a/packages/cli/test/cases/serve.spa/serve.spa.spec.js +++ b/packages/cli/test/cases/serve.spa/serve.spa.spec.js @@ -92,7 +92,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -130,7 +130,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -169,7 +169,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js b/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js index b6a583737..7cdefb39f 100644 --- a/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js +++ b/packages/cli/test/cases/theme-pack/theme-pack.develop.spec.js @@ -141,7 +141,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/css'); + expect(response.headers['content-type']).to.equal('text/css'); done(); }); @@ -179,7 +179,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); diff --git a/packages/init/test/cases/develop.default/develop.default.spec.js b/packages/init/test/cases/develop.default/develop.default.spec.js index c93689421..b9686d06c 100644 --- a/packages/init/test/cases/develop.default/develop.default.spec.js +++ b/packages/init/test/cases/develop.default/develop.default.spec.js @@ -113,7 +113,7 @@ xdescribe('Scaffold Greenwood and Run Develop command: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/plugin-adapter-netlify/src/index.js b/packages/plugin-adapter-netlify/src/index.js index 1cbacc112..c075eacc0 100644 --- a/packages/plugin-adapter-netlify/src/index.js +++ b/packages/plugin-adapter-netlify/src/index.js @@ -5,16 +5,40 @@ import { zip } from 'zip-a-folder'; // https://docs.netlify.com/functions/create/?fn-language=js function generateOutputFormat(id) { + const handlerAlias = '$handler'; + return ` - import { handler as ${id} } from './__${id}.js'; + import { handler as ${handlerAlias} } from './__${id}.js'; export async function handler (event, context = {}) { - const { rawUrl, headers, httpMethod } = event; + const { rawUrl, body, headers = {}, httpMethod } = event; + const contentType = headers['content-type'] || ''; + let format = body; + + if (['GET', 'HEAD'].includes(httpMethod.toUpperCase())) { + format = null + } else if (contentType.includes('application/x-www-form-urlencoded')) { + const formData = new FormData(); + + for (const key of Object.keys(body)) { + formData.append(key, body[key]); + } + + // when using FormData, let Request set the correct headers + // or else it will come out as multipart/form-data + // https://stackoverflow.com/a/43521052/417806 + format = formData; + delete headers['content-type']; + } else if(contentType.includes('application/json')) { + format = JSON.stringify(body); + } + const request = new Request(rawUrl, { + body: format, method: httpMethod, headers: new Headers(headers) }); - const response = await ${id}(request, context); + const response = await ${handlerAlias}(request, context); return { statusCode: response.status, diff --git a/packages/plugin-adapter-netlify/test/cases/build.default/build.default.spec.js b/packages/plugin-adapter-netlify/test/cases/build.default/build.default.spec.js index ef2f05c5c..7b9457929 100644 --- a/packages/plugin-adapter-netlify/test/cases/build.default/build.default.spec.js +++ b/packages/plugin-adapter-netlify/test/cases/build.default/build.default.spec.js @@ -20,6 +20,10 @@ * User Workspace * package.json * src/ + * api/ + * fragment.js + * greeting.js + * submit.js * components/ * card.js * pages/ @@ -72,11 +76,11 @@ describe('Build Greenwood With: ', function() { }); it('should output the expected number of serverless function zip files', function() { - expect(zipFiles.length).to.be.equal(4); + expect(zipFiles.length).to.be.equal(6); }); it('should output the expected number of serverless function API zip files', function() { - expect(zipFiles.filter(file => path.basename(file).startsWith('api-')).length).to.be.equal(2); + expect(zipFiles.filter(file => path.basename(file).startsWith('api-')).length).to.be.equal(4); }); it('should output the expected number of serverless function SSR page zip files', function() { @@ -108,7 +112,8 @@ describe('Build Greenwood With: ', function() { }); const { handler } = await import(new URL(`./${name}/${name}.js`, netlifyFunctionsOutputUrl)); const response = await handler({ - rawUrl: `http://localhost:8080/api/${name}?name=${param}` + rawUrl: `http://localhost:8080/api/greeting?name=${param}`, + httpMethod: 'GET' }, {}); const { statusCode, body, headers } = response; @@ -138,7 +143,8 @@ describe('Build Greenwood With: ', function() { }); const { handler } = await import(new URL(`./${name}/${name}.js`, netlifyFunctionsOutputUrl)); const response = await handler({ - rawUrl: `http://localhost:8080/api/${name}?name=${param}` + rawUrl: `http://localhost:8080/api/greeting?name=${param}`, + httpMethod: 'GET' }, {}); const { statusCode, body, headers } = response; const dom = new JSDOM(body); @@ -150,6 +156,77 @@ describe('Build Greenwood With: ', function() { }); }); + describe('Submit JSON API Route adapter', function() { + let apiFunctions; + + before(async function() { + apiFunctions = await glob.promise(path.join(normalizePathnameForWindows(netlifyFunctionsOutputUrl), 'api-submit-json.zip')); + }); + + it('should output one API route as a serverless function zip file', function() { + expect(apiFunctions.length).to.be.equal(1); + }); + + it('should return the expected response when the serverless adapter entry point handler is invoked', async function() { + const param = 'Greenwood'; + const name = path.basename(apiFunctions[0]).replace('.zip', ''); + + await extract(apiFunctions[0], { + dir: path.join(normalizePathnameForWindows(netlifyFunctionsOutputUrl), name) + }); + const { handler } = await import(new URL(`./${name}/${name}.js`, netlifyFunctionsOutputUrl)); + const response = await handler({ + rawUrl: 'http://localhost:8080/api/submit-json', + body: { name: param }, + httpMethod: 'POST', + headers: { + 'content-type': 'application/json' + } + }, {}); + const { statusCode, body, headers } = response; + + expect(statusCode).to.be.equal(200); + expect(JSON.parse(body).message).to.be.equal(`Thank you ${param} for your submission!`); + expect(headers.get('Content-Type')).to.be.equal('application/json'); + expect(headers.get('x-secret')).to.be.equal('1234'); + }); + }); + + describe('Submit FormData API Route adapter', function() { + let apiFunctions; + + before(async function() { + apiFunctions = await glob.promise(path.join(normalizePathnameForWindows(netlifyFunctionsOutputUrl), 'api-submit-form-data.zip')); + }); + + it('should output one API route as a serverless function zip file', function() { + expect(apiFunctions.length).to.be.equal(1); + }); + + it('should return the expected response when the serverless adapter entry point handler is invoked', async function() { + const param = 'Greenwood'; + const name = path.basename(apiFunctions[0]).replace('.zip', ''); + + await extract(apiFunctions[0], { + dir: path.join(normalizePathnameForWindows(netlifyFunctionsOutputUrl), name) + }); + const { handler } = await import(new URL(`./${name}/${name}.js`, netlifyFunctionsOutputUrl)); + const response = await handler({ + rawUrl: 'http://localhost:8080/api/submit-form-data', + body: { name: param }, + httpMethod: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + } + }, {}); + const { statusCode, body, headers } = response; + + expect(statusCode).to.be.equal(200); + expect(body).to.be.equal(`Thank you ${param} for your submission!`); + expect(headers.get('Content-Type')).to.be.equal('text/html'); + }); + }); + describe('Artists SSR Page adapter', function() { const count = 2; let pageFunctions; @@ -171,7 +248,8 @@ describe('Build Greenwood With: ', function() { }); const { handler } = await import(new URL(`./${name}/${name}.js`, netlifyFunctionsOutputUrl)); const response = await handler({ - rawUrl: `http://localhost:8080/${name}/` + rawUrl: 'http://localhost:8080/artists/', + httpMethod: 'GET' }, {}); const { statusCode, body, headers } = response; const dom = new JSDOM(body); @@ -207,7 +285,8 @@ describe('Build Greenwood With: ', function() { }); const { handler } = await import(new URL(`./${name}/${name}.js`, netlifyFunctionsOutputUrl)); const response = await handler({ - rawUrl: `http://localhost:8080/${name}/` + rawUrl: 'http://localhost:8080/users/', + httpMethod: 'GET' }, {}); const { statusCode, body, headers } = response; const dom = new JSDOM(body); diff --git a/packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-form-data.js b/packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-form-data.js new file mode 100644 index 000000000..5a4716f26 --- /dev/null +++ b/packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-form-data.js @@ -0,0 +1,11 @@ +export async function handler(request) { + const formData = await request.formData(); + const name = formData.get('name'); + const body = `Thank you ${name} for your submission!`; + + return new Response(body, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-json.js b/packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-json.js new file mode 100644 index 000000000..391443e10 --- /dev/null +++ b/packages/plugin-adapter-netlify/test/cases/build.default/src/api/submit-json.js @@ -0,0 +1,14 @@ +export async function handler(request) { + const formData = await request.json(); + const { name } = formData; + const body = { + message: `Thank you ${name} for your submission!` + }; + + return new Response(JSON.stringify(body), { + headers: new Headers({ + 'Content-Type': 'application/json', + 'x-secret': 1234 + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-adapter-vercel/src/index.js b/packages/plugin-adapter-vercel/src/index.js index 1553ff347..8b58d55b4 100644 --- a/packages/plugin-adapter-vercel/src/index.js +++ b/packages/plugin-adapter-vercel/src/index.js @@ -2,21 +2,45 @@ import fs from 'fs/promises'; import path from 'path'; import { checkResourceExists } from '@greenwood/cli/src/lib/resource-utils.js'; +// https://vercel.com/docs/functions/serverless-functions/runtimes/node-js#node.js-helpers function generateOutputFormat(id, type) { + const handlerAlias = '$handler'; const path = type === 'page' ? `__${id}` : id; return ` - import { handler as ${id} } from './${path}.js'; + import { handler as ${handlerAlias} } from './${path}.js'; export default async function handler (request, response) { - const { url, headers, method } = request; + const { body, url, headers = {}, method } = request; + const contentType = headers['content-type'] || ''; + let format = body; + + if (['GET', 'HEAD'].includes(method.toUpperCase())) { + format = null + } else if (contentType.includes('application/x-www-form-urlencoded')) { + const formData = new FormData(); + + for (const key of Object.keys(body)) { + formData.append(key, body[key]); + } + + // when using FormData, let Request set the correct headers + // or else it will come out as multipart/form-data + // https://stackoverflow.com/a/43521052/417806 + format = formData; + delete headers['content-type']; + } else if(contentType.includes('application/json')) { + format = JSON.stringify(body); + } + const req = new Request(new URL(url, \`http://\${headers.host}\`), { + body: format, headers: new Headers(headers), method }); - const res = await ${id}(req); + const res = await ${handlerAlias}(req); res.headers.forEach((value, key) => { response.setHeader(key, value); diff --git a/packages/plugin-adapter-vercel/test/cases/build.default/build.default.spec.js b/packages/plugin-adapter-vercel/test/cases/build.default/build.default.spec.js index 09aedd427..77f3d0678 100644 --- a/packages/plugin-adapter-vercel/test/cases/build.default/build.default.spec.js +++ b/packages/plugin-adapter-vercel/test/cases/build.default/build.default.spec.js @@ -23,6 +23,7 @@ * api/ * fragment.js * greeting.js + * submit.js * components/ * card.js * pages/ @@ -76,7 +77,7 @@ describe('Build Greenwood With: ', function() { }); it('should output the expected number of serverless function output folders', function() { - expect(functionFolders.length).to.be.equal(4); + expect(functionFolders.length).to.be.equal(6); }); it('should output the expected configuration file for the build output', function() { @@ -125,7 +126,8 @@ describe('Build Greenwood With: ', function() { url: `http://localhost:8080/api/greeting?name=${param}`, headers: { host: 'http://localhost:8080' - } + }, + method: 'GET' }, { status: function(code) { response.status = code; @@ -156,7 +158,8 @@ describe('Build Greenwood With: ', function() { url: 'http://localhost:8080/api/fragment', headers: { host: 'http://localhost:8080' - } + }, + method: 'GET' }, { status: function(code) { response.status = code; @@ -178,6 +181,79 @@ describe('Build Greenwood With: ', function() { }); }); + describe('Submit JSON API Route adapter', function() { + const name = 'Greenwood'; + + it('should return the expected response when the serverless adapter entry point handler is invoked', async function() { + const handler = (await import(new URL('./api/submit-json.func/index.js', vercelFunctionsOutputUrl))).default; + const response = { + headers: new Headers() + }; + + await handler({ + url: 'http://localhost:8080/api/submit-json', + headers: { + 'host': 'http://localhost:8080', + 'content-type': 'application/json' + }, + body: { name }, + method: 'POST' + }, { + status: function(code) { + response.status = code; + }, + send: function(body) { + response.body = body; + }, + setHeader: function(key, value) { + response.headers.set(key, value); + } + }); + const { status, body, headers } = response; + + expect(status).to.be.equal(200); + expect(JSON.parse(body).message).to.be.equal(`Thank you ${name} for your submission!`); + expect(headers.get('Content-Type')).to.be.equal('application/json'); + expect(headers.get('x-secret')).to.be.equal('1234'); + }); + }); + + describe('Submit FormData JSON API Route adapter', function() { + const name = 'Greenwood'; + + it('should return the expected response when the serverless adapter entry point handler is invoked', async function() { + const handler = (await import(new URL('./api/submit-form-data.func/index.js', vercelFunctionsOutputUrl))).default; + const response = { + headers: new Headers() + }; + + await handler({ + url: 'http://localhost:8080/api/submit-form-data', + headers: { + 'host': 'http://localhost:8080', + 'content-type': 'application/x-www-form-urlencoded' + }, + body: { name }, + method: 'POST' + }, { + status: function(code) { + response.status = code; + }, + send: function(body) { + response.body = body; + }, + setHeader: function(key, value) { + response.headers.set(key, value); + } + }); + const { status, body, headers } = response; + + expect(status).to.be.equal(200); + expect(body).to.be.equal(`Thank you ${name} for your submission!`); + expect(headers.get('Content-Type')).to.be.equal('text/html'); + }); + }); + describe('Artists SSR Page adapter', function() { it('should return the expected response when the serverless adapter entry point handler is invoked', async function() { const handler = (await import(new URL('./artists.func/index.js', vercelFunctionsOutputUrl))).default; @@ -190,7 +266,8 @@ describe('Build Greenwood With: ', function() { url: 'http://localhost:8080/artists', headers: { host: 'http://localhost:8080' - } + }, + method: 'GET' }, { status: function(code) { response.status = code; @@ -228,7 +305,8 @@ describe('Build Greenwood With: ', function() { url: 'http://localhost:8080/users', headers: { host: 'http://localhost:8080' - } + }, + method: 'GET' }, { status: function(code) { response.status = code; diff --git a/packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-form-data.js b/packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-form-data.js new file mode 100644 index 000000000..5a4716f26 --- /dev/null +++ b/packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-form-data.js @@ -0,0 +1,11 @@ +export async function handler(request) { + const formData = await request.formData(); + const name = formData.get('name'); + const body = `Thank you ${name} for your submission!`; + + return new Response(body, { + headers: new Headers({ + 'Content-Type': 'text/html' + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-json.js b/packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-json.js new file mode 100644 index 000000000..391443e10 --- /dev/null +++ b/packages/plugin-adapter-vercel/test/cases/build.default/src/api/submit-json.js @@ -0,0 +1,14 @@ +export async function handler(request) { + const formData = await request.json(); + const { name } = formData; + const body = { + message: `Thank you ${name} for your submission!` + }; + + return new Response(JSON.stringify(body), { + headers: new Headers({ + 'Content-Type': 'application/json', + 'x-secret': 1234 + }) + }); +} \ No newline at end of file diff --git a/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js b/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js index 84b016e8b..19b0cb6e9 100644 --- a/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-graphql/test/cases/develop.default/develop.default.spec.js @@ -89,7 +89,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); @@ -134,7 +134,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); @@ -170,7 +170,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); diff --git a/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js b/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js index 3f1a0a883..ead6131e9 100644 --- a/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js +++ b/packages/plugin-graphql/test/cases/qraphql-server/graphql-server.spec.js @@ -81,7 +81,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); }); @@ -125,7 +125,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('application/json'); + expect(response.headers['content-type']).to.equal('application/json; charset=utf-8'); done(); }); diff --git a/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js b/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js index 035711775..601c872a3 100644 --- a/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-import-css/test/cases/develop.default/develop.default.spec.js @@ -89,7 +89,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); @@ -132,7 +132,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); diff --git a/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js b/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js index 9a975f336..7a679fb5f 100644 --- a/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js +++ b/packages/plugin-renderer-lit/test/cases/serve.default/serve.default.spec.js @@ -183,7 +183,7 @@ describe('Serve Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/html'); + expect(response.headers['content-type']).to.equal('text/html'); done(); }); diff --git a/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js b/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js index 15f741133..b99d5d05d 100644 --- a/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js +++ b/packages/plugin-typescript/test/cases/develop.default/develop.default.spec.js @@ -82,7 +82,7 @@ describe('Develop Greenwood With: ', function() { }); it('should return the correct content type', function(done) { - expect(response.headers['content-type']).to.contain('text/javascript'); + expect(response.headers['content-type']).to.equal('text/javascript'); done(); }); diff --git a/yarn.lock b/yarn.lock index 1b69b8e11..332a5649f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3743,6 +3743,14 @@ "@types/connect" "*" "@types/node" "*" +"@types/co-body@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.0.tgz#b52625390eb0d113c9b697ea92c3ffae7740cdb9" + integrity sha512-3e0q2jyDAnx/DSZi0z2H0yoZ2wt5yRDZ+P7ymcMObvq0ufWRT4tsajyO+Q1VwVWiv9PRR4W3YEjEzBjeZlhF+w== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -3823,6 +3831,13 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/formidable@^2.0.5": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-2.0.6.tgz#811ed3cd8a8a7675e02420b3f861c317e055376a" + integrity sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w== + dependencies: + "@types/node" "*" + "@types/fs-capacitor@*": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e" @@ -3922,6 +3937,20 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/koa@^2.13.5": + version "2.13.8" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.8.tgz#4302d2f2712348aadb6c0b03eb614f30afde486b" + integrity sha512-Ugmxmgk/yPRW3ptBTh9VjOLwsKWJuGbymo1uGX0qdaqqL18uJiiG1ZoV0rxCOYSaDGhvEp5Ece02Amx0iwaxQQ== + dependencies: + "@types/accepts" "*" + "@types/content-disposition" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/http-errors" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -5991,6 +6020,16 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +co-body@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.1.0.tgz#d87a8efc3564f9bfe3aced8ef5cd04c7a8766547" + integrity sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ== + dependencies: + inflation "^2.0.0" + qs "^6.5.2" + raw-body "^2.3.3" + type-is "^1.6.16" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -6974,6 +7013,14 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diacritics-map@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/diacritics-map/-/diacritics-map-0.1.0.tgz#6dfc0ff9d01000a2edf2865371cac316e94977af" @@ -8282,6 +8329,16 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" +formidable@^2.0.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -9175,6 +9232,11 @@ hexer@^1.5.0: process "^0.10.0" xtend "^4.0.0" +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -9507,6 +9569,11 @@ infer-owner@^1.0.3, infer-owner@^1.0.4: resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== +inflation@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" + integrity sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -10481,6 +10548,18 @@ known-css-properties@^0.20.0: resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.20.0.tgz#0570831661b47dd835293218381166090ff60e96" integrity sha512-URvsjaA9ypfreqJ2/ylDr5MUERhJZ+DhguoWRr2xgS5C7aGCalXo+ewL+GixgKBfhT2vuL02nbIgNGqVWgTOYw== +koa-body@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/koa-body/-/koa-body-6.0.1.tgz#46c490033cceebb2874c53cfbb04c45562cf3c84" + integrity sha512-M8ZvMD8r+kPHy28aWP9VxL7kY8oPWA+C7ZgCljrCMeaU7uX6wsIQgDHskyrAr9sw+jqnIXyv4Mlxri5R4InIJg== + dependencies: + "@types/co-body" "^6.1.0" + "@types/formidable" "^2.0.5" + "@types/koa" "^2.13.5" + co-body "^6.1.0" + formidable "^2.0.1" + zod "^3.19.1" + koa-compose@^3.0.0: version "3.2.1" resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" @@ -13709,7 +13788,7 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.9.6: +qs@^6.11.0, qs@^6.5.2, qs@^6.9.6: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -13792,7 +13871,7 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@2.5.2: +raw-body@2.5.2, raw-body@^2.3.3: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== @@ -17274,6 +17353,11 @@ zip-stream@^4.1.0: compress-commons "^4.1.0" readable-stream "^3.6.0" +zod@^3.19.1: + version "3.22.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.1.tgz#815f850baf933fef96c1061322dbe579b1a80c27" + integrity sha512-+qUhAMl414+Elh+fRNtpU+byrwjDFOS1N7NioLY+tSlcADTx4TkCUua/hxJvxwDXcV4397/nZ420jy4n4+3WUg== + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"