From 1d56266e7cbc11e4ad2bf40f777096e1f1199e18 Mon Sep 17 00:00:00 2001 From: Stefan Meyer Date: Mon, 11 Aug 2025 09:30:51 +0200 Subject: [PATCH 1/4] feat: add OPTION preflight CORS responses on local --- src/start-command.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/start-command.ts b/src/start-command.ts index d91c2a0..faa8d7a 100644 --- a/src/start-command.ts +++ b/src/start-command.ts @@ -1,4 +1,4 @@ -import type { LambdaRoute } from './parse-stack-config.js'; +import type { LambdaRoute, Route } from './parse-stack-config.js'; import type { APIGatewayProxyResult } from 'aws-lambda'; import type { CommandModule } from 'yargs'; @@ -51,6 +51,8 @@ export const startCommand: CommandModule<{}, { readonly port: number }> = { const routes = sortRoutes(stackConfig.routes); + routeOptionsRequestsForCors(routes, app); + for (const route of routes) { if (route.type === `function`) { const { cacheTtlInSeconds = 300 } = route; @@ -117,3 +119,27 @@ export const startCommand: CommandModule<{}, { readonly port: number }> = { }); }, }; +function routeOptionsRequestsForCors(routes: readonly Route[], app) { + const corsEnabledMethodsByRoute = routes.reduce((corsEnabledMethodsByRoute, route) => { + if (route.corsEnabled && route.httpMethod) { + print.info(`CORS is enabled for route: ${route.publicPath} with method: ${route.httpMethod}`); + + if (!corsEnabledMethodsByRoute.has(route.publicPath)) { + corsEnabledMethodsByRoute.set(route.publicPath, [route.httpMethod]); + } else { + corsEnabledMethodsByRoute.set(route.publicPath, [route.httpMethod]); + } + } + return corsEnabledMethodsByRoute; + }, new Map()); + corsEnabledMethodsByRoute.forEach((methods, path) => { + print.info(`Setting up CORS for path: ${path} with methods: ${methods.join(`, `)}`); + + app.options(path, (_req, res) => { + res.header(`Access-Control-Allow-Origin`, `*`); + res.header(`Access-Control-Allow-Methods`, methods.join(`, `)); + res.header(`Access-Control-Allow-Headers`, `Content-Type, Authorization`); + res.sendStatus(204); + }); + }); +} From 14d33d88cc074b7997fe3b730b34afd3efbc78af Mon Sep 17 00:00:00 2001 From: Stefan Meyer Date: Mon, 11 Aug 2025 09:34:49 +0200 Subject: [PATCH 2/4] feat: add OPTION preflight CORS responses on local --- src/start-command.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/start-command.ts b/src/start-command.ts index faa8d7a..9ddd98c 100644 --- a/src/start-command.ts +++ b/src/start-command.ts @@ -12,7 +12,7 @@ import { readStackConfig } from './read-stack-config.js'; import { print } from './utils/print.js'; import { watch } from 'chokidar'; import compression from 'compression'; -import express from 'express'; +import express, { type Express } from 'express'; import getPort from 'get-port'; import * as lambdaLocal from 'lambda-local'; import { mkdirp } from 'mkdirp'; @@ -119,7 +119,7 @@ export const startCommand: CommandModule<{}, { readonly port: number }> = { }); }, }; -function routeOptionsRequestsForCors(routes: readonly Route[], app) { +function routeOptionsRequestsForCors(routes: readonly Route[], app: Express) { const corsEnabledMethodsByRoute = routes.reduce((corsEnabledMethodsByRoute, route) => { if (route.corsEnabled && route.httpMethod) { print.info(`CORS is enabled for route: ${route.publicPath} with method: ${route.httpMethod}`); From b5a807a17ce27ea34a3fc4cc26b5a8c4e1e790d5 Mon Sep 17 00:00:00 2001 From: Stefan Meyer Date: Mon, 11 Aug 2025 10:23:04 +0200 Subject: [PATCH 3/4] feat: add OPTION preflight CORS responses on local --- src/start-command.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/start-command.ts b/src/start-command.ts index 9ddd98c..e3734c8 100644 --- a/src/start-command.ts +++ b/src/start-command.ts @@ -124,8 +124,9 @@ function routeOptionsRequestsForCors(routes: readonly Route[], app: Express) { if (route.corsEnabled && route.httpMethod) { print.info(`CORS is enabled for route: ${route.publicPath} with method: ${route.httpMethod}`); - if (!corsEnabledMethodsByRoute.has(route.publicPath)) { - corsEnabledMethodsByRoute.set(route.publicPath, [route.httpMethod]); + const existingMethods = corsEnabledMethodsByRoute.get(route.publicPath); + if (existingMethods) { + existingMethods.push(route.httpMethod); } else { corsEnabledMethodsByRoute.set(route.publicPath, [route.httpMethod]); } From 87d69455c283b631aff55768d3d46dace8f0c8ff Mon Sep 17 00:00:00 2001 From: Stefan Meyer Date: Mon, 11 Aug 2025 11:06:45 +0200 Subject: [PATCH 4/4] feat: NGWD6-45982 showroom links in nav flyout --- src/route-options-requests-for-cors.test.ts | 94 +++++++++++++++++++++ src/route-options-requests-for-cors.ts | 29 +++++++ src/start-command.ts | 30 +------ src/utils/__mocks__/print.ts | 15 ++++ 4 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 src/route-options-requests-for-cors.test.ts create mode 100644 src/route-options-requests-for-cors.ts create mode 100644 src/utils/__mocks__/print.ts diff --git a/src/route-options-requests-for-cors.test.ts b/src/route-options-requests-for-cors.test.ts new file mode 100644 index 0000000..3feee23 --- /dev/null +++ b/src/route-options-requests-for-cors.test.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import type { Express } from 'express'; +import type { Route } from './parse-stack-config.js'; +import { routeOptionsRequestsForCors } from './route-options-requests-for-cors.js'; + +// Mock print utility +jest.mock('./utils/print'); + +describe('routeOptionsRequestsForCors', () => { + let app: jest.Mocked; + let optionsHandlers: Record; + + beforeEach(() => { + optionsHandlers = {}; + app = { + options: jest.fn((path: string, handler: Function) => { + optionsHandlers[path] = handler; + }), + } as any; + jest.clearAllMocks(); + }); + + it('should set up CORS OPTIONS handlers for routes with corsEnabled', () => { + const routes: Route[] = [ + { publicPath: '/foo', httpMethod: 'GET', corsEnabled: true } as any, + { publicPath: '/foo', httpMethod: 'POST', corsEnabled: true } as any, + { publicPath: '/bar', httpMethod: 'PUT', corsEnabled: true } as any, + { publicPath: '/baz', httpMethod: 'DELETE', corsEnabled: false } as any, + ]; + + routeOptionsRequestsForCors(routes, app); + + expect(app.options).toHaveBeenCalledTimes(2); + expect(app.options).toHaveBeenCalledWith('/foo', expect.any(Function)); + expect(app.options).toHaveBeenCalledWith('/bar', expect.any(Function)); + expect(optionsHandlers['/foo']).toBeDefined(); + expect(optionsHandlers['/bar']).toBeDefined(); + expect(optionsHandlers['/baz']).toBeUndefined(); + }); + + it('should set correct headers and status in the handler', () => { + const routes: Route[] = [ + { publicPath: '/foo', httpMethod: 'GET', corsEnabled: true } as any, + { publicPath: '/foo', httpMethod: 'POST', corsEnabled: true } as any, + ]; + + routeOptionsRequestsForCors(routes, app); + + const res = { + header: jest.fn(), + sendStatus: jest.fn(), + }; + + if (!optionsHandlers['/foo']) { + throw new Error('Expected options handler for /foo to be defined'); + } + optionsHandlers['/foo']({}, res); + + expect(res.header).toHaveBeenCalledWith('Access-Control-Allow-Origin', '*'); + expect(res.header).toHaveBeenCalledWith('Access-Control-Allow-Methods', 'GET, POST'); + expect(res.header).toHaveBeenCalledWith( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization', + ); + expect(res.sendStatus).toHaveBeenCalledWith(204); + }); + + it('should not set up handler for routes without corsEnabled', () => { + const routes: Route[] = [ + { publicPath: '/foo', httpMethod: 'GET', corsEnabled: false } as any, + { publicPath: '/bar', httpMethod: 'POST', corsEnabled: false } as any, + ]; + + routeOptionsRequestsForCors(routes, app); + + expect(app.options).not.toHaveBeenCalled(); + expect(optionsHandlers['/foo']).toBeUndefined(); + expect(optionsHandlers['/bar']).toBeUndefined(); + }); + + it('should handle routes with missing httpMethod gracefully', () => { + const routes: Route[] = [ + { publicPath: '/foo', corsEnabled: true } as any, + { publicPath: '/bar', httpMethod: 'PUT', corsEnabled: true } as any, + ]; + + routeOptionsRequestsForCors(routes, app); + + expect(app.options).toHaveBeenCalledTimes(1); + expect(app.options).toHaveBeenCalledWith('/bar', expect.any(Function)); + expect(optionsHandlers['/foo']).toBeUndefined(); + expect(optionsHandlers['/bar']).toBeDefined(); + }); +}); diff --git a/src/route-options-requests-for-cors.ts b/src/route-options-requests-for-cors.ts new file mode 100644 index 0000000..c7709c9 --- /dev/null +++ b/src/route-options-requests-for-cors.ts @@ -0,0 +1,29 @@ +import type { Express } from 'express'; +import type { Route } from './parse-stack-config.js'; +import { print } from './utils/print.js'; + +export function routeOptionsRequestsForCors(routes: readonly Route[], app: Express) { + const corsEnabledMethodsByRoute = routes.reduce((corsEnabledMethodsByRoute, route) => { + if (route.corsEnabled && route.httpMethod) { + print.info(`CORS is enabled for route: ${route.publicPath} with method: ${route.httpMethod}`); + + const existingMethods = corsEnabledMethodsByRoute.get(route.publicPath); + if (existingMethods) { + existingMethods.push(route.httpMethod); + } else { + corsEnabledMethodsByRoute.set(route.publicPath, [route.httpMethod]); + } + } + return corsEnabledMethodsByRoute; + }, new Map()); + corsEnabledMethodsByRoute.forEach((methods, path) => { + print.info(`Setting up CORS for path: ${path} with methods: ${methods.join(`, `)}`); + + app.options(path, (_req, res) => { + res.header(`Access-Control-Allow-Origin`, `*`); + res.header(`Access-Control-Allow-Methods`, methods.join(`, `)); + res.header(`Access-Control-Allow-Headers`, `Content-Type, Authorization`); + res.sendStatus(204); + }); + }); +} diff --git a/src/start-command.ts b/src/start-command.ts index e3734c8..b26ec02 100644 --- a/src/start-command.ts +++ b/src/start-command.ts @@ -1,4 +1,4 @@ -import type { LambdaRoute, Route } from './parse-stack-config.js'; +import type { LambdaRoute } from './parse-stack-config.js'; import type { APIGatewayProxyResult } from 'aws-lambda'; import type { CommandModule } from 'yargs'; @@ -12,11 +12,12 @@ import { readStackConfig } from './read-stack-config.js'; import { print } from './utils/print.js'; import { watch } from 'chokidar'; import compression from 'compression'; -import express, { type Express } from 'express'; +import express from 'express'; import getPort from 'get-port'; import * as lambdaLocal from 'lambda-local'; import { mkdirp } from 'mkdirp'; import { dirname } from 'path'; +import { routeOptionsRequestsForCors } from './route-options-requests-for-cors.js'; const commandName = `start`; @@ -119,28 +120,3 @@ export const startCommand: CommandModule<{}, { readonly port: number }> = { }); }, }; -function routeOptionsRequestsForCors(routes: readonly Route[], app: Express) { - const corsEnabledMethodsByRoute = routes.reduce((corsEnabledMethodsByRoute, route) => { - if (route.corsEnabled && route.httpMethod) { - print.info(`CORS is enabled for route: ${route.publicPath} with method: ${route.httpMethod}`); - - const existingMethods = corsEnabledMethodsByRoute.get(route.publicPath); - if (existingMethods) { - existingMethods.push(route.httpMethod); - } else { - corsEnabledMethodsByRoute.set(route.publicPath, [route.httpMethod]); - } - } - return corsEnabledMethodsByRoute; - }, new Map()); - corsEnabledMethodsByRoute.forEach((methods, path) => { - print.info(`Setting up CORS for path: ${path} with methods: ${methods.join(`, `)}`); - - app.options(path, (_req, res) => { - res.header(`Access-Control-Allow-Origin`, `*`); - res.header(`Access-Control-Allow-Methods`, methods.join(`, `)); - res.header(`Access-Control-Allow-Headers`, `Content-Type, Authorization`); - res.sendStatus(204); - }); - }); -} diff --git a/src/utils/__mocks__/print.ts b/src/utils/__mocks__/print.ts new file mode 100644 index 0000000..af7d2a7 --- /dev/null +++ b/src/utils/__mocks__/print.ts @@ -0,0 +1,15 @@ +export function print(): void {} + +print.warning = (): void => {}; + +print.error = (): void => {}; + +print.confirmation = async (): Promise => { + return false; +}; + +print.listItem = (): void => {}; + +print.success = (): void => {}; + +print.info = (): void => {};