From d9c4de4a277d79bb8fa74dd9e203543e47b08425 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 23 Feb 2024 11:10:23 +0100 Subject: [PATCH 01/15] feat(oidc-http-server-pages): generate JSON templates COMPASS-7646 --- packages/oidc-http-server-pages/package.json | 2 +- .../src/create-static-pages.ts | 272 ++++++++++++------ 2 files changed, 178 insertions(+), 96 deletions(-) diff --git a/packages/oidc-http-server-pages/package.json b/packages/oidc-http-server-pages/package.json index 64a581ca..3e504c68 100644 --- a/packages/oidc-http-server-pages/package.json +++ b/packages/oidc-http-server-pages/package.json @@ -31,7 +31,7 @@ "scripts": { "bootstrap": "npm run compile", "prepublishOnly": "npm run compile", - "compile": "tsc -p tsconfig.json && node dist/create-static-pages.js > dist/static-pages.js && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", + "compile": "tsc -p tsconfig.json && node dist/create-static-pages.js > dist/templates.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", "typecheck": "tsc --noEmit", "eslint": "eslint", "prettier": "prettier", diff --git a/packages/oidc-http-server-pages/src/create-static-pages.ts b/packages/oidc-http-server-pages/src/create-static-pages.ts index 9492528c..7278a6aa 100644 --- a/packages/oidc-http-server-pages/src/create-static-pages.ts +++ b/packages/oidc-http-server-pages/src/create-static-pages.ts @@ -34,113 +34,195 @@ function placeholder(prop: string): string { return `{{prop:${prop}}}`; } -const S = JSON.stringify; // Useful for embedding strings into real JS code. - -// Generate static pages based on a component. -// Props need to be optional string props; for every subset of props, -// a static page will be rendered that takes these props, and a function -// to take that static page and replace placeholder with the actual prop values -// will be emitted. -// Rather than including the generated markup directly, it is saved to a global -// table and emitted later, so that that global table can be compressed. -function generateStaticPage( - Component: React.FunctionComponent>> & { - name: string; - }, - props: string[], - markupTable: Record -): string { - let result = `module.exports[${S(Component.name)}] = function(props) { - const actualProps = JSON.stringify( - Object.keys(props) - .filter(prop => ${S( - props - )}.includes(prop) && props[prop] != null).sort());`; - for (const availableProps of allSubsets(props)) { - const propsObject: Partial> = {}; - const replacers: string[] = []; - for (const prop of availableProps) { - propsObject[prop] = placeholder(prop); - replacers.push( - `.replaceAll(${S(placeholder(prop))}, escapeHTML(props[${S(prop)}]))` - ); - } +// const S = JSON.stringify; // Useful for embedding strings into real JS code. - const markup = renderStylesToString( - renderToStaticMarkup(React.createElement(Component, propsObject)) - ); - const markupHash = createHash('sha256').update(markup).digest('hex'); - markupTable[markupHash] = markup; +// // Generate static pages based on a component. +// // Props need to be optional string props; for every subset of props, +// // a static page will be rendered that takes these props, and a function +// // to take that static page and replace placeholder with the actual prop values +// // will be emitted. +// // Rather than including the generated markup directly, it is saved to a global +// // table and emitted later, so that that global table can be compressed. +// function generateStaticPage( +// Component: React.FunctionComponent>> & { +// name: string; +// }, +// props: string[], +// markupTable: Record +// ): string { +// let result = `module.exports[${S(Component.name)}] = function(props) { +// const actualProps = JSON.stringify( +// Object.keys(props) +// .filter(prop => ${S( +// props +// )}.includes(prop) && props[prop] != null).sort());`; +// for (const availableProps of allSubsets(props)) { +// const propsObject: Partial> = {}; +// const replacers: string[] = []; +// for (const prop of availableProps) { +// propsObject[prop] = placeholder(prop); +// replacers.push( +// `.replaceAll(${S(placeholder(prop))}, escapeHTML(props[${S(prop)}]))` +// ); +// } - result += `if (actualProps === ${S(S(availableProps.sort()))}) { - return (getMarkup(${S(markupHash)})${replacers.join('')}); - }`; - } +// const markup = renderStylesToString( +// renderToStaticMarkup(React.createElement(Component, propsObject)) +// ); +// const markupHash = createHash('sha256').update(markup).digest('hex'); +// markupTable[markupHash] = markup; + +// result += `if (actualProps === ${S(S(availableProps.sort()))}) { +// return (getMarkup(${S(markupHash)})${replacers.join('')}); +// }`; +// } + +// result += '}\n'; +// return result; +// } + +// // Create CJS module with pages as exports. +// export function generateStaticPagesModule( +// components: [ +// React.FunctionComponent>> & { +// name: string; +// }, +// string[] +// ][] +// ): string { +// let result = ` +// 'use strict'; +// function escapeHTML(str) { +// return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); +// } +// `; +// const markupTable: Record = {}; +// for (const component of components) { +// result += generateStaticPage(...component, markupTable); +// } + +// // At the time of writing, brotli compression results in a 96% size decrease +// // in the resulting file. We decompress the markup table lazily, so that it +// // is not loaded into memory when OIDC is not in use. +// const compressedMarkupTable = brotliCompressSync(S(markupTable), { +// params: { +// [zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT, +// [zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY, +// }, +// }); +// result += ` +// const markupTableSrc = ${S(compressedMarkupTable.toString('base64'))}; +// let markupTable; +// function getMarkup(hash) { +// if (markupTable === undefined) { +// markupTable = JSON.parse( +// require('zlib').brotliDecompressSync( +// Buffer.from(markupTableSrc, 'base64'))); +// } +// return markupTable[hash]; +// } +// `; +// return result; +// } - result += '}\n'; - return result; +// if (require.main === module) { +// // eslint-disable-next-line no-console +// console.log( +// generateStaticPagesModule([ +// [ +// OIDCErrorPage, +// [ +// 'error', +// 'errorDescription', +// 'errorURI', +// 'productDocsLink', +// 'productName', +// ], +// ], +// [OIDCAcceptedPage, ['productDocsLink', 'productName']], +// [OIDCNotFoundPage, ['productDocsLink', 'productName']], +// ]) +// ); +// } + +interface ITemplate { + parameters: Record; + html: string; } +type ComponentTemplates = Record; -// Create CJS module with pages as exports. -export function generateStaticPagesModule( - components: [ - React.FunctionComponent>> & { - name: string; - }, - string[] - ][] -): string { - let result = ` - 'use strict'; - function escapeHTML(str) { - return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); - } - `; - const markupTable: Record = {}; - for (const component of components) { - result += generateStaticPage(...component, markupTable); +type Component = React.FunctionComponent< + Partial> +> & { + name: string; +}; + +function getComponentTemplates({ + Component, + parameters, +}: { + Component: Component; + parameters: string[]; +}): ITemplate[] { + const templates: ITemplate[] = []; + for (const paramsSubset of allSubsets(parameters)) { + const propsObject = paramsSubset.reduce((obj, prop) => { + obj[prop] = placeholder(prop); + return obj; + }, {} as Record); + const markup = renderStylesToString( + renderToStaticMarkup(React.createElement(Component, propsObject)) + ); + templates.push({ + parameters: propsObject, + html: markup, + }); } + return templates; +} - // At the time of writing, brotli compression results in a 96% size decrease - // in the resulting file. We decompress the markup table lazily, so that it - // is not loaded into memory when OIDC is not in use. - const compressedMarkupTable = brotliCompressSync(S(markupTable), { - params: { - [zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT, - [zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY, - }, - }); - result += ` - const markupTableSrc = ${S(compressedMarkupTable.toString('base64'))}; - let markupTable; - function getMarkup(hash) { - if (markupTable === undefined) { - markupTable = JSON.parse( - require('zlib').brotliDecompressSync( - Buffer.from(markupTableSrc, 'base64'))); - } - return markupTable[hash]; +function generateTemplates( + components: { + name: string; + Component: Component; + parameters: string[]; + }[] +): ComponentTemplates { + const templates: ComponentTemplates = {}; + for (const { name, Component, parameters } of components) { + const componentTemplates = getComponentTemplates({ Component, parameters }); + templates[name] = componentTemplates; } - `; - return result; + return templates; } if (require.main === module) { // eslint-disable-next-line no-console console.log( - generateStaticPagesModule([ - [ - OIDCErrorPage, - [ - 'error', - 'errorDescription', - 'errorURI', - 'productDocsLink', - 'productName', - ], - ], - [OIDCAcceptedPage, ['productDocsLink', 'productName']], - [OIDCNotFoundPage, ['productDocsLink', 'productName']], - ]) + JSON.stringify( + generateTemplates([ + { + name: 'OIDCErrorPage', + Component: OIDCErrorPage, + parameters: [ + 'error', + 'errorDescription', + 'errorURI', + 'productDocsLink', + 'productName', + ], + }, + { + name: 'OIDCAcceptedPage', + Component: OIDCAcceptedPage, + parameters: ['productDocsLink', 'productName'], + }, + { + name: 'OIDCNotFoundPage', + Component: OIDCNotFoundPage, + parameters: ['productDocsLink', 'productName'], + }, + ]) + ) ); } From c9881c3a988800e7168967cc2016360feb61598c Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 23 Feb 2024 14:18:53 +0100 Subject: [PATCH 02/15] add template reading util --- .../src/create-static-pages.spec.tsx | 130 +++++++++--------- ...te-static-pages.ts => create-templates.ts} | 47 +++---- .../src/get-static-page.ts | 34 +++++ .../src/templates.d.json.ts | 5 + packages/oidc-http-server-pages/src/types.ts | 11 ++ packages/oidc-http-server-pages/tsconfig.json | 3 +- 6 files changed, 138 insertions(+), 92 deletions(-) rename packages/oidc-http-server-pages/src/{create-static-pages.ts => create-templates.ts} (89%) create mode 100644 packages/oidc-http-server-pages/src/get-static-page.ts create mode 100644 packages/oidc-http-server-pages/src/templates.d.json.ts create mode 100644 packages/oidc-http-server-pages/src/types.ts diff --git a/packages/oidc-http-server-pages/src/create-static-pages.spec.tsx b/packages/oidc-http-server-pages/src/create-static-pages.spec.tsx index fad6d245..1befc2ad 100644 --- a/packages/oidc-http-server-pages/src/create-static-pages.spec.tsx +++ b/packages/oidc-http-server-pages/src/create-static-pages.spec.tsx @@ -1,71 +1,71 @@ -import React from 'react'; -import type { Component } from 'react'; -import { generateStaticPagesModule } from './create-static-pages'; -import { H1 } from '@mongodb-js/compass-components'; -import { runInNewContext } from 'vm'; -import zlib from 'zlib'; -import sinon from 'ts-sinon'; -import chai, { expect } from 'chai'; -import sinonChai from 'sinon-chai'; -chai.use(sinonChai); +// import React from 'react'; +// import type { Component } from 'react'; +// import { generateStaticPagesModule } from './create-static-pages'; +// import { H1 } from '@mongodb-js/compass-components'; +// import { runInNewContext } from 'vm'; +// import zlib from 'zlib'; +// import sinon from 'ts-sinon'; +// import chai, { expect } from 'chai'; +// import sinonChai from 'sinon-chai'; +// chai.use(sinonChai); -describe('generateStaticPagesModule', function () { - let exports: Record< - string, - (props: React.ComponentProps) => string - >; +// describe('generateStaticPagesModule', function () { +// let exports: Record< +// string, +// (props: React.ComponentProps) => string +// >; - beforeEach(function () { - function Component({ prop1, prop2 }: { prop1?: string; prop2?: string }) { - return ( -
-

{prop1 || 'default'}

- {prop2 &&

{prop2}

} -
- ); - } +// beforeEach(function () { +// function Component({ prop1, prop2 }: { prop1?: string; prop2?: string }) { +// return ( +//
+//

{prop1 || 'default'}

+// {prop2 &&

{prop2}

} +//
+// ); +// } - const result = generateStaticPagesModule([[Component, ['prop1', 'prop2']]]); - exports = {}; - const require = sinon.stub(); - require.throws(); - require.withArgs('zlib').returns(zlib); - runInNewContext(result, { - module: { exports }, - require, - Buffer, - }); - }); +// const result = generateStaticPagesModule([[Component, ['prop1', 'prop2']]]); +// exports = {}; +// const require = sinon.stub(); +// require.throws(); +// require.withArgs('zlib').returns(zlib); +// runInNewContext(result, { +// module: { exports }, +// require, +// Buffer, +// }); +// }); - it('includes selected props', function () { - expect(exports.Component({ prop1: 'prop1value' })).to.match( - /]+>prop1value<\/h1>/ - ); - expect(exports.Component({ prop1: 'prop1value' })).not.to.include('prop2'); - expect(exports.Component({ prop1: 'prop1value' })).not.to.include( - 'data-attr' - ); - expect(exports.Component({ prop2: 'prop2value' })).to.match( - /]+>default<\/h1>/ - ); - expect(exports.Component({ prop2: 'prop2value' })).to.match( - /]+>prop2value<\/p>/ - ); - expect(exports.Component({ prop2: 'prop2value' })).to.include( - 'data-attr="prop2value"' - ); - }); +// it('includes selected props', function () { +// expect(exports.Component({ prop1: 'prop1value' })).to.match( +// /]+>prop1value<\/h1>/ +// ); +// expect(exports.Component({ prop1: 'prop1value' })).not.to.include('prop2'); +// expect(exports.Component({ prop1: 'prop1value' })).not.to.include( +// 'data-attr' +// ); +// expect(exports.Component({ prop2: 'prop2value' })).to.match( +// /]+>default<\/h1>/ +// ); +// expect(exports.Component({ prop2: 'prop2value' })).to.match( +// /]+>prop2value<\/p>/ +// ); +// expect(exports.Component({ prop2: 'prop2value' })).to.include( +// 'data-attr="prop2value"' +// ); +// }); - it('properly escapes props', function () { - expect(exports.Component({ prop2: 'prop2<&"\'>value' })).to.include( - 'data-attr="prop2<&"'>value"' - ); - expect(exports.Component({ prop2: 'prop2<&"\'>value' })).to.include( - '>prop2<&"'>value

' - ); - }); +// it('properly escapes props', function () { +// expect(exports.Component({ prop2: 'prop2<&"\'>value' })).to.include( +// 'data-attr="prop2<&"'>value"' +// ); +// expect(exports.Component({ prop2: 'prop2<&"\'>value' })).to.include( +// '>prop2<&"'>value

' +// ); +// }); - it('includes relevant style information', function () { - expect(exports.Component({ prop1: 'prop1value' })).to.include('(array: T[]): Iterable { @@ -145,19 +146,13 @@ function placeholder(prop: string): string { // ); // } -interface ITemplate { - parameters: Record; - html: string; -} -type ComponentTemplates = Record; - type Component = React.FunctionComponent< Partial> > & { name: string; }; -function getComponentTemplates({ +function getPageTemplates({ Component, parameters, }: { @@ -182,27 +177,29 @@ function getComponentTemplates({ } function generateTemplates( - components: { - name: string; - Component: Component; - parameters: string[]; - }[] -): ComponentTemplates { - const templates: ComponentTemplates = {}; - for (const { name, Component, parameters } of components) { - const componentTemplates = getComponentTemplates({ Component, parameters }); - templates[name] = componentTemplates; + pages: Record< + Page, + { + Component: Component; + parameters: string[]; + } + > +): PageTemplates { + const templates: Partial = {}; + for (const pageName of Object.keys(pages) as Page[]) { + const { Component, parameters } = pages[pageName]; + const PageTemplates = getPageTemplates({ Component, parameters }); + templates[pageName] = PageTemplates; } - return templates; + return templates as PageTemplates; } if (require.main === module) { // eslint-disable-next-line no-console console.log( JSON.stringify( - generateTemplates([ - { - name: 'OIDCErrorPage', + generateTemplates({ + [Page.OIDCErrorPage]: { Component: OIDCErrorPage, parameters: [ 'error', @@ -212,17 +209,15 @@ if (require.main === module) { 'productName', ], }, - { - name: 'OIDCAcceptedPage', + [Page.OIDCAcceptedPage]: { Component: OIDCAcceptedPage, parameters: ['productDocsLink', 'productName'], }, - { - name: 'OIDCNotFoundPage', + [Page.OIDCNotFoundPage]: { Component: OIDCNotFoundPage, parameters: ['productDocsLink', 'productName'], }, - ]) + }) ) ); } diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts new file mode 100644 index 00000000..b86fb188 --- /dev/null +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -0,0 +1,34 @@ +import type { ITemplate, Page } from './types'; +import templates from './templates.json'; + +function findTemplate(templates: ITemplate[], parameterKeys: string[]) { + const parametersJoined = parameterKeys.sort().join('-'); + for (const template of templates) { + const templateParametersJoined = Object.keys(template.parameters) + .sort() + .join('-'); + if (parametersJoined === templateParametersJoined) return template; + } +} + +function replacePlaceholders( + template: ITemplate, + parameters: Record +) { + let { html } = template; + for (const [key, placeholder] of Object.entries(template.parameters)) { + html = html.replace(placeholder, parameters[key]); + } + return html; +} + +export function getStaticPage(page: Page, parameters: Record) { + const pageTemplates = templates[page]; + const nonEmptyParameters = Object.keys(parameters).filter( + (key) => typeof parameters[key] !== 'undefined' && parameters !== null + ); + const template = findTemplate(pageTemplates, nonEmptyParameters); + if (!template) return; // TODO: handle missing template + const html = replacePlaceholders(template, parameters); + return html; +} diff --git a/packages/oidc-http-server-pages/src/templates.d.json.ts b/packages/oidc-http-server-pages/src/templates.d.json.ts new file mode 100644 index 00000000..20157fb1 --- /dev/null +++ b/packages/oidc-http-server-pages/src/templates.d.json.ts @@ -0,0 +1,5 @@ +import type { PageTemplates } from './types'; + +declare const templates: PageTemplates; + +export default templates; diff --git a/packages/oidc-http-server-pages/src/types.ts b/packages/oidc-http-server-pages/src/types.ts new file mode 100644 index 00000000..6ace652e --- /dev/null +++ b/packages/oidc-http-server-pages/src/types.ts @@ -0,0 +1,11 @@ +export interface ITemplate { + parameters: Record; + html: string; +} +export type PageTemplates = Record; + +export enum Page { + OIDCErrorPage = 'OIDCErrorPage', + OIDCAcceptedPage = 'OIDCAcceptedPage', + OIDCNotFoundPage = 'OIDCNotFoundPage', +} diff --git a/packages/oidc-http-server-pages/tsconfig.json b/packages/oidc-http-server-pages/tsconfig.json index 18d1edbb..6b2a45ac 100644 --- a/packages/oidc-http-server-pages/tsconfig.json +++ b/packages/oidc-http-server-pages/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "dist", "allowJs": true, - "jsx": "react" + "jsx": "react", + "allowArbitraryExtensions": true }, "include": ["src/**/*"], "exclude": ["./src/**/*.spec.*"] From 2ce78e5a1bb9b6d40ae77861167356731c2f81ab Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 23 Feb 2024 14:34:58 +0100 Subject: [PATCH 03/15] update devtools-connect --- packages/devtools-connect/src/oidc/handler.ts | 30 ++++++++++++++----- .../src/get-static-page.ts | 18 +++++++++-- packages/oidc-http-server-pages/src/index.ts | 8 ++--- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/devtools-connect/src/oidc/handler.ts b/packages/devtools-connect/src/oidc/handler.ts index 57138948..2d9be3cc 100644 --- a/packages/devtools-connect/src/oidc/handler.ts +++ b/packages/devtools-connect/src/oidc/handler.ts @@ -1,10 +1,6 @@ import type { RedirectServerRequestInfo } from '@mongodb-js/oidc-plugin'; import type { DevtoolsConnectOptions } from '../connect'; -import { - OIDCAcceptedPage, - OIDCNotFoundPage, - OIDCErrorPage, -} from '@mongodb-js/oidc-http-server-pages'; +import { getStaticPage, StaticPage } from '@mongodb-js/oidc-http-server-pages'; export function oidcServerRequestHandler( options: Pick, @@ -31,13 +27,31 @@ export function oidcServerRequestHandler( switch (result) { case 'accepted': - res.end(OIDCAcceptedPage({ productDocsLink, productName })); + res.end( + getStaticPage(StaticPage.OIDCAcceptedPage, { + productDocsLink, + productName, + }) + ); break; case 'rejected': - res.end(OIDCErrorPage({ productDocsLink, productName, ...info })); + res.end( + getStaticPage(StaticPage.OIDCErrorPage, { + productDocsLink, + productName, + error: info.error, + errorDescription: info.errorDescription, + errorURI: info.errorURI, + }) + ); break; default: - res.end(OIDCNotFoundPage({ productDocsLink, productName })); + res.end( + getStaticPage(StaticPage.OIDCNotFoundPage, { + productDocsLink, + productName, + }) + ); break; } } diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index b86fb188..e277f521 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -13,16 +13,28 @@ function findTemplate(templates: ITemplate[], parameterKeys: string[]) { function replacePlaceholders( template: ITemplate, - parameters: Record + parameters: Record ) { let { html } = template; for (const [key, placeholder] of Object.entries(template.parameters)) { - html = html.replace(placeholder, parameters[key]); + html = html.replace(placeholder, escapeHTML(parameters[key] as string)); } return html; } -export function getStaticPage(page: Page, parameters: Record) { +function escapeHTML(str: string) { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export function getStaticPage( + page: Page, + parameters: Record +) { const pageTemplates = templates[page]; const nonEmptyParameters = Object.keys(parameters).filter( (key) => typeof parameters[key] !== 'undefined' && parameters !== null diff --git a/packages/oidc-http-server-pages/src/index.ts b/packages/oidc-http-server-pages/src/index.ts index a8f0610f..1601d3c1 100644 --- a/packages/oidc-http-server-pages/src/index.ts +++ b/packages/oidc-http-server-pages/src/index.ts @@ -1,6 +1,2 @@ -export { - OIDCPageBaseProps, - OIDCAcceptedPage, - OIDCNotFoundPage, - OIDCErrorPage, -} from './static-pages'; +export { getStaticPage } from './get-static-page'; +export { Page as StaticPage } from './types'; From cdbb3969737318fcea674a8265ef3f89b1742f91 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 23 Feb 2024 17:56:46 +0100 Subject: [PATCH 04/15] cleanup --- packages/oidc-http-server-pages/package.json | 2 +- .../src/create-templates.ts | 122 +----------------- .../src/get-static-page.ts | 2 +- 3 files changed, 8 insertions(+), 118 deletions(-) diff --git a/packages/oidc-http-server-pages/package.json b/packages/oidc-http-server-pages/package.json index 3e504c68..c7a62535 100644 --- a/packages/oidc-http-server-pages/package.json +++ b/packages/oidc-http-server-pages/package.json @@ -31,7 +31,7 @@ "scripts": { "bootstrap": "npm run compile", "prepublishOnly": "npm run compile", - "compile": "tsc -p tsconfig.json && node dist/create-static-pages.js > dist/templates.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", + "compile": "tsc -p tsconfig.json && node dist/create-templates.js > dist/templates.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", "typecheck": "tsc --noEmit", "eslint": "eslint", "prettier": "prettier", diff --git a/packages/oidc-http-server-pages/src/create-templates.ts b/packages/oidc-http-server-pages/src/create-templates.ts index ddb901fc..941de98f 100644 --- a/packages/oidc-http-server-pages/src/create-templates.ts +++ b/packages/oidc-http-server-pages/src/create-templates.ts @@ -1,10 +1,12 @@ -// Helper script to perform static server-side rendering of the +// Helper script to prepare templates of the // OIDC plugin pages. This script is run during package compilation. // // By generating static pages, we can avoid having all dependencies // of this package pulling in react, react-dom, leafygreen, compass-components, etc. // -// scripts/test-oidc-pages.js can be used to preview the generated pages. +// Furthermore, the templates are consumable by other languages +// https://jira.mongodb.org/browse/COMPASS-7646 +// import { renderToStaticMarkup } from 'react-dom/server'; import { renderStylesToString } from '@leafygreen-ui/emotion'; @@ -14,9 +16,8 @@ import { OIDCNotFoundPage, } from './pages-source'; import React from 'react'; -import { createHash } from 'crypto'; -import { brotliCompressSync, constants as zlibConstants } from 'zlib'; -import { PageTemplates, ITemplate, Page } from './types'; +import type { PageTemplates, ITemplate } from './types'; +import { Page } from './types'; /** Iterate all sub-arrays of an array */ function* allSubsets(array: T[]): Iterable { @@ -35,117 +36,6 @@ function placeholder(prop: string): string { return `{{prop:${prop}}}`; } -// const S = JSON.stringify; // Useful for embedding strings into real JS code. - -// // Generate static pages based on a component. -// // Props need to be optional string props; for every subset of props, -// // a static page will be rendered that takes these props, and a function -// // to take that static page and replace placeholder with the actual prop values -// // will be emitted. -// // Rather than including the generated markup directly, it is saved to a global -// // table and emitted later, so that that global table can be compressed. -// function generateStaticPage( -// Component: React.FunctionComponent>> & { -// name: string; -// }, -// props: string[], -// markupTable: Record -// ): string { -// let result = `module.exports[${S(Component.name)}] = function(props) { -// const actualProps = JSON.stringify( -// Object.keys(props) -// .filter(prop => ${S( -// props -// )}.includes(prop) && props[prop] != null).sort());`; -// for (const availableProps of allSubsets(props)) { -// const propsObject: Partial> = {}; -// const replacers: string[] = []; -// for (const prop of availableProps) { -// propsObject[prop] = placeholder(prop); -// replacers.push( -// `.replaceAll(${S(placeholder(prop))}, escapeHTML(props[${S(prop)}]))` -// ); -// } - -// const markup = renderStylesToString( -// renderToStaticMarkup(React.createElement(Component, propsObject)) -// ); -// const markupHash = createHash('sha256').update(markup).digest('hex'); -// markupTable[markupHash] = markup; - -// result += `if (actualProps === ${S(S(availableProps.sort()))}) { -// return (getMarkup(${S(markupHash)})${replacers.join('')}); -// }`; -// } - -// result += '}\n'; -// return result; -// } - -// // Create CJS module with pages as exports. -// export function generateStaticPagesModule( -// components: [ -// React.FunctionComponent>> & { -// name: string; -// }, -// string[] -// ][] -// ): string { -// let result = ` -// 'use strict'; -// function escapeHTML(str) { -// return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); -// } -// `; -// const markupTable: Record = {}; -// for (const component of components) { -// result += generateStaticPage(...component, markupTable); -// } - -// // At the time of writing, brotli compression results in a 96% size decrease -// // in the resulting file. We decompress the markup table lazily, so that it -// // is not loaded into memory when OIDC is not in use. -// const compressedMarkupTable = brotliCompressSync(S(markupTable), { -// params: { -// [zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT, -// [zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY, -// }, -// }); -// result += ` -// const markupTableSrc = ${S(compressedMarkupTable.toString('base64'))}; -// let markupTable; -// function getMarkup(hash) { -// if (markupTable === undefined) { -// markupTable = JSON.parse( -// require('zlib').brotliDecompressSync( -// Buffer.from(markupTableSrc, 'base64'))); -// } -// return markupTable[hash]; -// } -// `; -// return result; -// } - -// if (require.main === module) { -// // eslint-disable-next-line no-console -// console.log( -// generateStaticPagesModule([ -// [ -// OIDCErrorPage, -// [ -// 'error', -// 'errorDescription', -// 'errorURI', -// 'productDocsLink', -// 'productName', -// ], -// ], -// [OIDCAcceptedPage, ['productDocsLink', 'productName']], -// [OIDCNotFoundPage, ['productDocsLink', 'productName']], -// ]) -// ); -// } - type Component = React.FunctionComponent< Partial> > & { diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index e277f521..a360219e 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -17,7 +17,7 @@ function replacePlaceholders( ) { let { html } = template; for (const [key, placeholder] of Object.entries(template.parameters)) { - html = html.replace(placeholder, escapeHTML(parameters[key] as string)); + html = html.replaceAll(placeholder, escapeHTML(parameters[key] as string)); } return html; } From de0a4f47d35e1c830894665b103c153f4686a5c0 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 26 Feb 2024 10:42:21 +0100 Subject: [PATCH 05/15] add create-templates tests --- packages/devtools-connect/src/oidc/handler.ts | 11 ++-- .../src/create-templates.spec.tsx | 57 +++++++++++++++++++ .../src/create-templates.ts | 30 +++++----- .../src/get-static-page.ts | 4 +- packages/oidc-http-server-pages/src/index.ts | 2 +- ...emplates.d.json.ts => templates.json.d.ts} | 0 packages/oidc-http-server-pages/src/types.ts | 7 ++- packages/oidc-http-server-pages/tsconfig.json | 3 +- 8 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 packages/oidc-http-server-pages/src/create-templates.spec.tsx rename packages/oidc-http-server-pages/src/{templates.d.json.ts => templates.json.d.ts} (100%) diff --git a/packages/devtools-connect/src/oidc/handler.ts b/packages/devtools-connect/src/oidc/handler.ts index 2d9be3cc..543128a5 100644 --- a/packages/devtools-connect/src/oidc/handler.ts +++ b/packages/devtools-connect/src/oidc/handler.ts @@ -1,6 +1,9 @@ import type { RedirectServerRequestInfo } from '@mongodb-js/oidc-plugin'; import type { DevtoolsConnectOptions } from '../connect'; -import { getStaticPage, StaticPage } from '@mongodb-js/oidc-http-server-pages'; +import { + getStaticPage, + HttpServerPage, +} from '@mongodb-js/oidc-http-server-pages'; export function oidcServerRequestHandler( options: Pick, @@ -28,7 +31,7 @@ export function oidcServerRequestHandler( switch (result) { case 'accepted': res.end( - getStaticPage(StaticPage.OIDCAcceptedPage, { + getStaticPage(HttpServerPage.OIDCAcceptedPage, { productDocsLink, productName, }) @@ -36,7 +39,7 @@ export function oidcServerRequestHandler( break; case 'rejected': res.end( - getStaticPage(StaticPage.OIDCErrorPage, { + getStaticPage(HttpServerPage.OIDCErrorPage, { productDocsLink, productName, error: info.error, @@ -47,7 +50,7 @@ export function oidcServerRequestHandler( break; default: res.end( - getStaticPage(StaticPage.OIDCNotFoundPage, { + getStaticPage(HttpServerPage.OIDCNotFoundPage, { productDocsLink, productName, }) diff --git a/packages/oidc-http-server-pages/src/create-templates.spec.tsx b/packages/oidc-http-server-pages/src/create-templates.spec.tsx new file mode 100644 index 00000000..d36d7688 --- /dev/null +++ b/packages/oidc-http-server-pages/src/create-templates.spec.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { expect } from 'chai'; +import { H1 } from '@mongodb-js/compass-components'; +import { generateTemplates } from './create-templates'; + +describe('generateTemplates', function () { + enum TestPage { + Page1 = 'Page1', + } + function Component1({ prop1, prop2 }: { prop1?: string; prop2?: string }) { + return ( +
+

{prop1 || 'default'}

+ {prop2 &&

{prop2}

} +
+ ); + } + + let result; + before(function () { + result = generateTemplates({ + [TestPage.Page1]: { + Component: Component1, + parameters: ['prop1', 'prop2'], + }, + }); + }); + + it('creates 4 templates for Page1', function () { + expect(result).to.have.own.property(TestPage.Page1); + expect(result[TestPage.Page1]).to.have.length(4); + }); + + it('includes a template with placeholders for prop1 + prop2', function () { + const templates = result[TestPage.Page1]; + const fullTemplate = templates.find( + ({ parameters }) => + Object.keys(parameters).includes('prop1') && + Object.keys(parameters).includes('prop2') + ); + expect(fullTemplate).to.exist; + expect(fullTemplate).to.have.own.property('html'); + expect(fullTemplate.html).to.contain('{{prop:prop1}}'); + expect(fullTemplate.html).to.contain('{{prop:prop2}}'); + }); + + it('includes a template with placeholders for no props', function () { + const templates = result[TestPage.Page1]; + const fullTemplate = templates.find( + ({ parameters }) => Object.keys(parameters).length === 0 + ); + expect(fullTemplate).to.exist; + expect(fullTemplate).to.have.own.property('html'); + expect(fullTemplate.html).not.to.contain('{{prop:prop1}}'); + expect(fullTemplate.html).not.to.contain('{{prop:prop2}}'); + }); +}); diff --git a/packages/oidc-http-server-pages/src/create-templates.ts b/packages/oidc-http-server-pages/src/create-templates.ts index 941de98f..c0819e36 100644 --- a/packages/oidc-http-server-pages/src/create-templates.ts +++ b/packages/oidc-http-server-pages/src/create-templates.ts @@ -17,7 +17,7 @@ import { } from './pages-source'; import React from 'react'; import type { PageTemplates, ITemplate } from './types'; -import { Page } from './types'; +import { HttpServerPage } from './types'; /** Iterate all sub-arrays of an array */ function* allSubsets(array: T[]): Iterable { @@ -36,17 +36,15 @@ function placeholder(prop: string): string { return `{{prop:${prop}}}`; } -type Component = React.FunctionComponent< - Partial> -> & { +type Component = React.FunctionComponent>> & { name: string; }; -function getPageTemplates({ +function getPageTemplates({ Component, parameters, }: { - Component: Component; + Component: Component; parameters: string[]; }): ITemplate[] { const templates: ITemplate[] = []; @@ -66,22 +64,22 @@ function getPageTemplates({ return templates; } -function generateTemplates( +export function generateTemplates( pages: Record< - Page, + TPage, { - Component: Component; + Component: Component; parameters: string[]; } > -): PageTemplates { - const templates: Partial = {}; - for (const pageName of Object.keys(pages) as Page[]) { +): PageTemplates { + const templates: Partial> = {}; + for (const pageName of Object.keys(pages) as TPage[]) { const { Component, parameters } = pages[pageName]; const PageTemplates = getPageTemplates({ Component, parameters }); templates[pageName] = PageTemplates; } - return templates as PageTemplates; + return templates as PageTemplates; } if (require.main === module) { @@ -89,7 +87,7 @@ if (require.main === module) { console.log( JSON.stringify( generateTemplates({ - [Page.OIDCErrorPage]: { + [HttpServerPage.OIDCErrorPage]: { Component: OIDCErrorPage, parameters: [ 'error', @@ -99,11 +97,11 @@ if (require.main === module) { 'productName', ], }, - [Page.OIDCAcceptedPage]: { + [HttpServerPage.OIDCAcceptedPage]: { Component: OIDCAcceptedPage, parameters: ['productDocsLink', 'productName'], }, - [Page.OIDCNotFoundPage]: { + [HttpServerPage.OIDCNotFoundPage]: { Component: OIDCNotFoundPage, parameters: ['productDocsLink', 'productName'], }, diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index a360219e..6981ccd6 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -1,4 +1,4 @@ -import type { ITemplate, Page } from './types'; +import type { ITemplate, HttpServerPage } from './types'; import templates from './templates.json'; function findTemplate(templates: ITemplate[], parameterKeys: string[]) { @@ -32,7 +32,7 @@ function escapeHTML(str: string) { } export function getStaticPage( - page: Page, + page: HttpServerPage, parameters: Record ) { const pageTemplates = templates[page]; diff --git a/packages/oidc-http-server-pages/src/index.ts b/packages/oidc-http-server-pages/src/index.ts index 1601d3c1..c69fb20c 100644 --- a/packages/oidc-http-server-pages/src/index.ts +++ b/packages/oidc-http-server-pages/src/index.ts @@ -1,2 +1,2 @@ export { getStaticPage } from './get-static-page'; -export { Page as StaticPage } from './types'; +export { HttpServerPage } from './types'; diff --git a/packages/oidc-http-server-pages/src/templates.d.json.ts b/packages/oidc-http-server-pages/src/templates.json.d.ts similarity index 100% rename from packages/oidc-http-server-pages/src/templates.d.json.ts rename to packages/oidc-http-server-pages/src/templates.json.d.ts diff --git a/packages/oidc-http-server-pages/src/types.ts b/packages/oidc-http-server-pages/src/types.ts index 6ace652e..aa333666 100644 --- a/packages/oidc-http-server-pages/src/types.ts +++ b/packages/oidc-http-server-pages/src/types.ts @@ -2,9 +2,12 @@ export interface ITemplate { parameters: Record; html: string; } -export type PageTemplates = Record; +export type PageTemplates = Record< + TPage, + ITemplate[] +>; -export enum Page { +export enum HttpServerPage { OIDCErrorPage = 'OIDCErrorPage', OIDCAcceptedPage = 'OIDCAcceptedPage', OIDCNotFoundPage = 'OIDCNotFoundPage', diff --git a/packages/oidc-http-server-pages/tsconfig.json b/packages/oidc-http-server-pages/tsconfig.json index 6b2a45ac..18d1edbb 100644 --- a/packages/oidc-http-server-pages/tsconfig.json +++ b/packages/oidc-http-server-pages/tsconfig.json @@ -3,8 +3,7 @@ "compilerOptions": { "outDir": "dist", "allowJs": true, - "jsx": "react", - "allowArbitraryExtensions": true + "jsx": "react" }, "include": ["src/**/*"], "exclude": ["./src/**/*.spec.*"] From 9e73c460486f5f6c6832fe354004483e83607f38 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 26 Feb 2024 11:47:07 +0100 Subject: [PATCH 06/15] add get-static-page tests --- .../src/create-static-pages.spec.tsx | 71 ------------------- .../src/create-templates.spec.tsx | 12 ++-- .../src/get-static-page.spec.ts | 54 ++++++++++++++ .../src/get-static-page.ts | 18 +++-- 4 files changed, 73 insertions(+), 82 deletions(-) delete mode 100644 packages/oidc-http-server-pages/src/create-static-pages.spec.tsx create mode 100644 packages/oidc-http-server-pages/src/get-static-page.spec.ts diff --git a/packages/oidc-http-server-pages/src/create-static-pages.spec.tsx b/packages/oidc-http-server-pages/src/create-static-pages.spec.tsx deleted file mode 100644 index 1befc2ad..00000000 --- a/packages/oidc-http-server-pages/src/create-static-pages.spec.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// import React from 'react'; -// import type { Component } from 'react'; -// import { generateStaticPagesModule } from './create-static-pages'; -// import { H1 } from '@mongodb-js/compass-components'; -// import { runInNewContext } from 'vm'; -// import zlib from 'zlib'; -// import sinon from 'ts-sinon'; -// import chai, { expect } from 'chai'; -// import sinonChai from 'sinon-chai'; -// chai.use(sinonChai); - -// describe('generateStaticPagesModule', function () { -// let exports: Record< -// string, -// (props: React.ComponentProps) => string -// >; - -// beforeEach(function () { -// function Component({ prop1, prop2 }: { prop1?: string; prop2?: string }) { -// return ( -//
-//

{prop1 || 'default'}

-// {prop2 &&

{prop2}

} -//
-// ); -// } - -// const result = generateStaticPagesModule([[Component, ['prop1', 'prop2']]]); -// exports = {}; -// const require = sinon.stub(); -// require.throws(); -// require.withArgs('zlib').returns(zlib); -// runInNewContext(result, { -// module: { exports }, -// require, -// Buffer, -// }); -// }); - -// it('includes selected props', function () { -// expect(exports.Component({ prop1: 'prop1value' })).to.match( -// /]+>prop1value<\/h1>/ -// ); -// expect(exports.Component({ prop1: 'prop1value' })).not.to.include('prop2'); -// expect(exports.Component({ prop1: 'prop1value' })).not.to.include( -// 'data-attr' -// ); -// expect(exports.Component({ prop2: 'prop2value' })).to.match( -// /]+>default<\/h1>/ -// ); -// expect(exports.Component({ prop2: 'prop2value' })).to.match( -// /]+>prop2value<\/p>/ -// ); -// expect(exports.Component({ prop2: 'prop2value' })).to.include( -// 'data-attr="prop2value"' -// ); -// }); - -// it('properly escapes props', function () { -// expect(exports.Component({ prop2: 'prop2<&"\'>value' })).to.include( -// 'data-attr="prop2<&"'>value"' -// ); -// expect(exports.Component({ prop2: 'prop2<&"\'>value' })).to.include( -// '>prop2<&"'>value

' -// ); -// }); - -// it('includes relevant style information', function () { -// expect(exports.Component({ prop1: 'prop1value' })).to.include('; before(function () { result = generateTemplates({ [TestPage.Page1]: { @@ -40,8 +41,8 @@ describe('generateTemplates', function () { ); expect(fullTemplate).to.exist; expect(fullTemplate).to.have.own.property('html'); - expect(fullTemplate.html).to.contain('{{prop:prop1}}'); - expect(fullTemplate.html).to.contain('{{prop:prop2}}'); + expect(fullTemplate?.html).to.match(/]+>{{prop:prop1}}<\/h1>/); + expect(fullTemplate?.html).to.match(/]+>{{prop:prop2}}<\/p>/); }); it('includes a template with placeholders for no props', function () { @@ -51,7 +52,8 @@ describe('generateTemplates', function () { ); expect(fullTemplate).to.exist; expect(fullTemplate).to.have.own.property('html'); - expect(fullTemplate.html).not.to.contain('{{prop:prop1}}'); - expect(fullTemplate.html).not.to.contain('{{prop:prop2}}'); + expect(fullTemplate?.html).to.match(/]+>default<\/h1>/); + expect(fullTemplate?.html).not.to.contain('{{prop:prop1}}'); + expect(fullTemplate?.html).not.to.contain('{{prop:prop2}}'); }); }); diff --git a/packages/oidc-http-server-pages/src/get-static-page.spec.ts b/packages/oidc-http-server-pages/src/get-static-page.spec.ts new file mode 100644 index 00000000..203a3ae0 --- /dev/null +++ b/packages/oidc-http-server-pages/src/get-static-page.spec.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import type { PageTemplates } from './types'; +import { getStaticPage } from './get-static-page'; + +describe('getStaticPage', function () { + enum TestPage { + Page1 = 'Page1', + } + + const templates: PageTemplates = { + [TestPage.Page1]: [ + { + parameters: { prop1: '{{prop:prop1}}', prop2: '{{prop:prop2}}' }, + html: '

{{prop:prop1}}

{{prop:prop2}}', + }, + { + parameters: { prop1: '{{prop:prop1}}' }, + html: '

{{prop:prop1}}

', + }, + { + parameters: { prop2: '{{prop:prop2}}' }, + html: '

default

{{prop:prop2}}', + }, + { + parameters: {}, + html: '

default

', + }, + ], + }; + + it('scenario: prop1 + prop2', function () { + const prop1 = 'abc'; + const prop2 = 'def'; + const html = getStaticPage( + TestPage.Page1, + { prop1, prop2 }, + templates + ); + expect(html).to.equal(`

${prop1}

${prop2}`); + }); + + it('scenario: no props', function () { + const html = getStaticPage(TestPage.Page1, {}, templates); + expect(html).to.equal('

default

'); + }); + + it('scenario: prop1 with special characters', function () { + const prop1 = `'one' & "two" & `; + const html = getStaticPage(TestPage.Page1, { prop1 }, templates); + expect(html).to.equal( + '

'one' & "two" & <three>

' + ); + }); +}); diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index 6981ccd6..1378380a 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -1,5 +1,4 @@ -import type { ITemplate, HttpServerPage } from './types'; -import templates from './templates.json'; +import type { ITemplate, HttpServerPage, PageTemplates } from './types'; function findTemplate(templates: ITemplate[], parameterKeys: string[]) { const parametersJoined = parameterKeys.sort().join('-'); @@ -31,11 +30,18 @@ function escapeHTML(str: string) { .replace(/'/g, '''); } -export function getStaticPage( - page: HttpServerPage, - parameters: Record +export function getStaticPage( + page: TPage, + parameters: Record, + templates?: PageTemplates ) { - const pageTemplates = templates[page]; + let httpServerPageTemplates; + if (!templates) { + httpServerPageTemplates = require('./templates.json'); + } + const pageTemplates = templates + ? templates[page] + : httpServerPageTemplates[page as HttpServerPage]; const nonEmptyParameters = Object.keys(parameters).filter( (key) => typeof parameters[key] !== 'undefined' && parameters !== null ); From 0b45166f59cfe030af1b7884fdd019dc085607fa Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 26 Feb 2024 13:53:06 +0100 Subject: [PATCH 07/15] improve error handling --- package-lock.json | 6 ------ packages/oidc-http-server-pages/package.json | 3 --- .../src/get-static-page.spec.ts | 16 ++++++++++++++ .../src/get-static-page.ts | 21 +++++++++++++------ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index f336fa28..03ee0f24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25827,7 +25827,6 @@ "@mongodb-js/tsconfig-devtools": "^1.0.1", "@types/chai": "^4.2.21", "@types/mocha": "^9.1.1", - "@types/node": "^17.0.35", "@types/react": "^17.0.53", "@types/react-dom": "^17.0.19", "@types/sinon-chai": "^3.2.5", @@ -25840,8 +25839,6 @@ "prettier": "^2.3.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "sinon-chai": "^3.7.0", - "ts-sinon": "^2.0.1", "typescript": "^5.0.4" } }, @@ -31734,7 +31731,6 @@ "@mongodb-js/tsconfig-devtools": "^1.0.1", "@types/chai": "^4.2.21", "@types/mocha": "^9.1.1", - "@types/node": "^17.0.35", "@types/react": "^17.0.53", "@types/react-dom": "^17.0.19", "@types/sinon-chai": "^3.2.5", @@ -31747,8 +31743,6 @@ "prettier": "^2.3.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "sinon-chai": "^3.7.0", - "ts-sinon": "^2.0.1", "typescript": "^5.0.4" }, "dependencies": { diff --git a/packages/oidc-http-server-pages/package.json b/packages/oidc-http-server-pages/package.json index c7a62535..57c8b057 100644 --- a/packages/oidc-http-server-pages/package.json +++ b/packages/oidc-http-server-pages/package.json @@ -55,7 +55,6 @@ "@leafygreen-ui/emotion": "^4.0.7", "@types/chai": "^4.2.21", "@types/mocha": "^9.1.1", - "@types/node": "^17.0.35", "@types/react": "^17.0.53", "@types/react-dom": "^17.0.19", "@types/sinon-chai": "^3.2.5", @@ -68,8 +67,6 @@ "prettier": "^2.3.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "sinon-chai": "^3.7.0", - "ts-sinon": "^2.0.1", "typescript": "^5.0.4" } } diff --git a/packages/oidc-http-server-pages/src/get-static-page.spec.ts b/packages/oidc-http-server-pages/src/get-static-page.spec.ts index 203a3ae0..810fc2de 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.spec.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.spec.ts @@ -51,4 +51,20 @@ describe('getStaticPage', function () { '

'one' & "two" & <three>

' ); }); + + it('no page template is found', function () { + expect(() => + getStaticPage('UnknownPage' as TestPage, {}, templates) + ).to.throw(Error, /No template found/); + }); + + it('throws with incorrect properties', function () { + expect(() => + getStaticPage( + 'UnknownPage' as TestPage, + { never: 'no' }, + templates + ) + ).to.throw(Error, /No template found/); + }); }); diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index 1378380a..6ce5b42e 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -35,18 +35,27 @@ export function getStaticPage( parameters: Record, templates?: PageTemplates ) { - let httpServerPageTemplates; if (!templates) { - httpServerPageTemplates = require('./templates.json'); + templates = require('./templates.json'); } - const pageTemplates = templates - ? templates[page] - : httpServerPageTemplates[page as HttpServerPage]; + + const pageTemplates = templates && templates[page]; + if (!pageTemplates) { + throw new Error(`No template found for ${page}`); + } + const nonEmptyParameters = Object.keys(parameters).filter( (key) => typeof parameters[key] !== 'undefined' && parameters !== null ); const template = findTemplate(pageTemplates, nonEmptyParameters); - if (!template) return; // TODO: handle missing template + if (!template) { + throw new Error( + `No template found for ${page}; parameters: ${Object.keys( + parameters + ).join(',')}. The parameters might be incorrect.` + ); + } + const html = replacePlaceholders(template, parameters); return html; } From a42d91beaa368178f6832cf89594036bd644ec8e Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 26 Feb 2024 14:25:58 +0100 Subject: [PATCH 08/15] increase major oidc-http-server-pages version --- packages/oidc-http-server-pages/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/oidc-http-server-pages/package.json b/packages/oidc-http-server-pages/package.json index 57c8b057..044f9a67 100644 --- a/packages/oidc-http-server-pages/package.json +++ b/packages/oidc-http-server-pages/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/devtools-shared", - "version": "0.1.1", + "version": "1.0.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/devtools-shared.git" From c467c15f18c3a77c303d5d9ab8b93f2069801251 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 26 Feb 2024 16:38:02 +0100 Subject: [PATCH 09/15] update reference --- packages/devtools-connect/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools-connect/package.json b/packages/devtools-connect/package.json index ba550044..74e8c1c0 100644 --- a/packages/devtools-connect/package.json +++ b/packages/devtools-connect/package.json @@ -47,7 +47,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@mongodb-js/oidc-http-server-pages": "0.1.1", + "@mongodb-js/oidc-http-server-pages": "1.0.0", "lodash.merge": "^4.6.2", "mongodb-connection-string-url": "^3.0.0", "socks": "^2.7.3", From 906985ede3631b666ed2555635833ed2d6d0a0af Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 26 Feb 2024 16:39:21 +0100 Subject: [PATCH 10/15] Apply suggestions from code review Co-authored-by: Anna Henningsen --- packages/oidc-http-server-pages/src/create-templates.ts | 7 +++---- packages/oidc-http-server-pages/src/get-static-page.ts | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/oidc-http-server-pages/src/create-templates.ts b/packages/oidc-http-server-pages/src/create-templates.ts index c0819e36..2407f800 100644 --- a/packages/oidc-http-server-pages/src/create-templates.ts +++ b/packages/oidc-http-server-pages/src/create-templates.ts @@ -49,10 +49,9 @@ function getPageTemplates({ }): ITemplate[] { const templates: ITemplate[] = []; for (const paramsSubset of allSubsets(parameters)) { - const propsObject = paramsSubset.reduce((obj, prop) => { - obj[prop] = placeholder(prop); - return obj; - }, {} as Record); + const propsObject = Object.fromEntries( + paramsSubset.map((prop) => [prop, placeholder(prop)]) + ); const markup = renderStylesToString( renderToStaticMarkup(React.createElement(Component, propsObject)) ); diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index 6ce5b42e..5529ff11 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -1,6 +1,6 @@ import type { ITemplate, HttpServerPage, PageTemplates } from './types'; -function findTemplate(templates: ITemplate[], parameterKeys: string[]) { +function findTemplate(templates: ITemplate[], parameterKeys: string[]): ITemplate | undefined { const parametersJoined = parameterKeys.sort().join('-'); for (const template of templates) { const templateParametersJoined = Object.keys(template.parameters) @@ -13,7 +13,7 @@ function findTemplate(templates: ITemplate[], parameterKeys: string[]) { function replacePlaceholders( template: ITemplate, parameters: Record -) { +): string { let { html } = template; for (const [key, placeholder] of Object.entries(template.parameters)) { html = html.replaceAll(placeholder, escapeHTML(parameters[key] as string)); @@ -21,7 +21,7 @@ function replacePlaceholders( return html; } -function escapeHTML(str: string) { +function escapeHTML(str: string): string { return str .replace(/&/g, '&') .replace(/( page: TPage, parameters: Record, templates?: PageTemplates -) { +): string { if (!templates) { templates = require('./templates.json'); } From a562cedf305ebea4ddf79d5dcd87a92ba6f8d42f Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 27 Feb 2024 11:08:48 +0100 Subject: [PATCH 11/15] reformat --- packages/oidc-http-server-pages/src/get-static-page.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index 5529ff11..4beff0ef 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -1,6 +1,9 @@ import type { ITemplate, HttpServerPage, PageTemplates } from './types'; -function findTemplate(templates: ITemplate[], parameterKeys: string[]): ITemplate | undefined { +function findTemplate( + templates: ITemplate[], + parameterKeys: string[] +): ITemplate | undefined { const parametersJoined = parameterKeys.sort().join('-'); for (const template of templates) { const templateParametersJoined = Object.keys(template.parameters) From 8d8dcd7bb731d8ca21abb4fa0ee44693ea66c90e Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 27 Feb 2024 14:05:41 +0100 Subject: [PATCH 12/15] add compression --- packages/oidc-http-server-pages/package.json | 2 +- .../src/create-templates.ts | 78 +++++++++++++------ .../src/get-static-page.ts | 2 +- ...{templates.json.d.ts => templates.js.d.ts} | 0 4 files changed, 55 insertions(+), 27 deletions(-) rename packages/oidc-http-server-pages/src/{templates.json.d.ts => templates.js.d.ts} (100%) diff --git a/packages/oidc-http-server-pages/package.json b/packages/oidc-http-server-pages/package.json index 044f9a67..ee87f90f 100644 --- a/packages/oidc-http-server-pages/package.json +++ b/packages/oidc-http-server-pages/package.json @@ -31,7 +31,7 @@ "scripts": { "bootstrap": "npm run compile", "prepublishOnly": "npm run compile", - "compile": "tsc -p tsconfig.json && node dist/create-templates.js > dist/templates.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", + "compile": "tsc -p tsconfig.json && node dist/create-templates.js && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", "typecheck": "tsc --noEmit", "eslint": "eslint", "prettier": "prettier", diff --git a/packages/oidc-http-server-pages/src/create-templates.ts b/packages/oidc-http-server-pages/src/create-templates.ts index 2407f800..0d2172ef 100644 --- a/packages/oidc-http-server-pages/src/create-templates.ts +++ b/packages/oidc-http-server-pages/src/create-templates.ts @@ -16,6 +16,9 @@ import { OIDCNotFoundPage, } from './pages-source'; import React from 'react'; +import { gzipSync } from 'zlib'; +import { writeFileSync } from 'fs'; +import path from 'path'; import type { PageTemplates, ITemplate } from './types'; import { HttpServerPage } from './types'; @@ -81,30 +84,55 @@ export function generateTemplates( return templates as PageTemplates; } -if (require.main === module) { - // eslint-disable-next-line no-console - console.log( - JSON.stringify( - generateTemplates({ - [HttpServerPage.OIDCErrorPage]: { - Component: OIDCErrorPage, - parameters: [ - 'error', - 'errorDescription', - 'errorURI', - 'productDocsLink', - 'productName', - ], - }, - [HttpServerPage.OIDCAcceptedPage]: { - Component: OIDCAcceptedPage, - parameters: ['productDocsLink', 'productName'], - }, - [HttpServerPage.OIDCNotFoundPage]: { - Component: OIDCNotFoundPage, - parameters: ['productDocsLink', 'productName'], - }, - }) - ) +export function generateCompressedTemplates< + TPage extends string = HttpServerPage +>( + pages: Record< + TPage, + { + Component: Component; + parameters: string[]; + } + > +): void { + const templates = generateTemplates(pages); + const buffer = gzipSync(JSON.stringify(templates)); + writeFileSync(path.join(__dirname, 'templates.gz'), buffer, 'binary'); + + writeFileSync( + path.join(__dirname, 'templates.js'), + ` + const { gunzipSync } = require('zlib'); + const buffer = gunzipSync( + Buffer.from( + '${buffer.toString('base64')}', + 'base64' + ) + ); + module.exports = JSON.parse(buffer.toString()); + ` ); } + +if (require.main === module) { + generateCompressedTemplates({ + [HttpServerPage.OIDCErrorPage]: { + Component: OIDCErrorPage, + parameters: [ + 'error', + 'errorDescription', + 'errorURI', + 'productDocsLink', + 'productName', + ], + }, + [HttpServerPage.OIDCAcceptedPage]: { + Component: OIDCAcceptedPage, + parameters: ['productDocsLink', 'productName'], + }, + [HttpServerPage.OIDCNotFoundPage]: { + Component: OIDCNotFoundPage, + parameters: ['productDocsLink', 'productName'], + }, + }); +} diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index 4beff0ef..bb05a88a 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -39,7 +39,7 @@ export function getStaticPage( templates?: PageTemplates ): string { if (!templates) { - templates = require('./templates.json'); + templates = require('./templates.js'); } const pageTemplates = templates && templates[page]; diff --git a/packages/oidc-http-server-pages/src/templates.json.d.ts b/packages/oidc-http-server-pages/src/templates.js.d.ts similarity index 100% rename from packages/oidc-http-server-pages/src/templates.json.d.ts rename to packages/oidc-http-server-pages/src/templates.js.d.ts From 24b7e2171978111c38e96712c53a6cf4207a1cbc Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Tue, 27 Feb 2024 15:24:05 +0100 Subject: [PATCH 13/15] separate compression --- .../src/create-templates.ts | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/oidc-http-server-pages/src/create-templates.ts b/packages/oidc-http-server-pages/src/create-templates.ts index 0d2172ef..8c2ab266 100644 --- a/packages/oidc-http-server-pages/src/create-templates.ts +++ b/packages/oidc-http-server-pages/src/create-templates.ts @@ -16,7 +16,7 @@ import { OIDCNotFoundPage, } from './pages-source'; import React from 'react'; -import { gzipSync } from 'zlib'; +import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'zlib'; import { writeFileSync } from 'fs'; import path from 'path'; import type { PageTemplates, ITemplate } from './types'; @@ -84,26 +84,23 @@ export function generateTemplates( return templates as PageTemplates; } -export function generateCompressedTemplates< - TPage extends string = HttpServerPage ->( - pages: Record< - TPage, - { - Component: Component; - parameters: string[]; - } - > -): void { - const templates = generateTemplates(pages); - const buffer = gzipSync(JSON.stringify(templates)); +function generateGzip(data: string): void { + const buffer = gzipSync(data); writeFileSync(path.join(__dirname, 'templates.gz'), buffer, 'binary'); +} +function generateJS(data: string): void { + const buffer = brotliCompressSync(data, { + params: { + [zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT, + [zlibConstants.BROTLI_PARAM_QUALITY]: zlibConstants.BROTLI_MAX_QUALITY, + }, + }); writeFileSync( path.join(__dirname, 'templates.js'), ` - const { gunzipSync } = require('zlib'); - const buffer = gunzipSync( + const { brotliDecompressSync } = require('zlib'); + const buffer = brotliDecompressSync( Buffer.from( '${buffer.toString('base64')}', 'base64' @@ -114,6 +111,22 @@ export function generateCompressedTemplates< ); } +export function generateCompressedTemplates< + TPage extends string = HttpServerPage +>( + pages: Record< + TPage, + { + Component: Component; + parameters: string[]; + } + > +): void { + const templates = JSON.stringify(generateTemplates(pages)); + generateGzip(templates); + generateJS(templates); +} + if (require.main === module) { generateCompressedTemplates({ [HttpServerPage.OIDCErrorPage]: { From 5679f30d4c81b1ce014779190f6001840532aae1 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 28 Feb 2024 11:26:02 +0100 Subject: [PATCH 14/15] getTemplates --- packages/oidc-http-server-pages/package.json | 2 +- .../src/create-templates.ts | 19 +++++++++++-------- .../src/get-static-page.spec.ts | 2 +- .../src/get-static-page.ts | 3 ++- .../src/get-templates.js.d.ts | 5 +++++ .../src/templates.js.d.ts | 5 ----- 6 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 packages/oidc-http-server-pages/src/get-templates.js.d.ts delete mode 100644 packages/oidc-http-server-pages/src/templates.js.d.ts diff --git a/packages/oidc-http-server-pages/package.json b/packages/oidc-http-server-pages/package.json index ee87f90f..ff213deb 100644 --- a/packages/oidc-http-server-pages/package.json +++ b/packages/oidc-http-server-pages/package.json @@ -39,7 +39,7 @@ "depcheck": "depcheck", "check": "npm run typecheck && npm run lint && npm run depcheck", "check-ci": "npm run check", - "test": "mocha", + "test": "npm run compile && mocha", "test-cov": "nyc -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", "test-watch": "npm run test -- --watch", "test-ci": "npm run test-cov", diff --git a/packages/oidc-http-server-pages/src/create-templates.ts b/packages/oidc-http-server-pages/src/create-templates.ts index 8c2ab266..9a4f0b42 100644 --- a/packages/oidc-http-server-pages/src/create-templates.ts +++ b/packages/oidc-http-server-pages/src/create-templates.ts @@ -97,16 +97,19 @@ function generateJS(data: string): void { }, }); writeFileSync( - path.join(__dirname, 'templates.js'), + path.join(__dirname, 'get-templates.js'), ` const { brotliDecompressSync } = require('zlib'); - const buffer = brotliDecompressSync( - Buffer.from( - '${buffer.toString('base64')}', - 'base64' - ) - ); - module.exports = JSON.parse(buffer.toString()); + function getTemplates() { + const buffer = brotliDecompressSync( + Buffer.from( + '${buffer.toString('base64')}', + 'base64' + ) + ); + return JSON.parse(buffer.toString()); + } + module.exports = getTemplates; ` ); } diff --git a/packages/oidc-http-server-pages/src/get-static-page.spec.ts b/packages/oidc-http-server-pages/src/get-static-page.spec.ts index 810fc2de..1f47e777 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.spec.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import type { PageTemplates } from './types'; -import { getStaticPage } from './get-static-page'; +import { getStaticPage } from '../dist/get-static-page'; describe('getStaticPage', function () { enum TestPage { diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index bb05a88a..27097db7 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -1,4 +1,5 @@ import type { ITemplate, HttpServerPage, PageTemplates } from './types'; +import getTemplates from './get-templates.js'; function findTemplate( templates: ITemplate[], @@ -39,7 +40,7 @@ export function getStaticPage( templates?: PageTemplates ): string { if (!templates) { - templates = require('./templates.js'); + templates = getTemplates() as PageTemplates; } const pageTemplates = templates && templates[page]; diff --git a/packages/oidc-http-server-pages/src/get-templates.js.d.ts b/packages/oidc-http-server-pages/src/get-templates.js.d.ts new file mode 100644 index 00000000..545f403c --- /dev/null +++ b/packages/oidc-http-server-pages/src/get-templates.js.d.ts @@ -0,0 +1,5 @@ +import type { PageTemplates } from './types'; + +declare const TGetTemplates: () => PageTemplates; + +export default TGetTemplates; diff --git a/packages/oidc-http-server-pages/src/templates.js.d.ts b/packages/oidc-http-server-pages/src/templates.js.d.ts deleted file mode 100644 index 20157fb1..00000000 --- a/packages/oidc-http-server-pages/src/templates.js.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PageTemplates } from './types'; - -declare const templates: PageTemplates; - -export default templates; From dcfa54d287eefd83e171206e15e458416ad8cf64 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 28 Feb 2024 15:43:45 +0100 Subject: [PATCH 15/15] improved types --- .../src/create-templates.spec.tsx | 25 ++++++----- .../src/create-templates.ts | 41 +++++++++++++------ .../src/get-static-page.spec.ts | 39 +++++++++++++----- .../src/get-static-page.ts | 22 ++++++---- .../src/pages-source.tsx | 28 ++++++++----- packages/oidc-http-server-pages/src/types.ts | 30 +++++++++++--- 6 files changed, 126 insertions(+), 59 deletions(-) diff --git a/packages/oidc-http-server-pages/src/create-templates.spec.tsx b/packages/oidc-http-server-pages/src/create-templates.spec.tsx index 8297f828..db0c47da 100644 --- a/packages/oidc-http-server-pages/src/create-templates.spec.tsx +++ b/packages/oidc-http-server-pages/src/create-templates.spec.tsx @@ -5,9 +5,14 @@ import { generateTemplates } from './create-templates'; import type { PageTemplates } from './types'; describe('generateTemplates', function () { - enum TestPage { - Page1 = 'Page1', - } + type TestPage = 'Page1'; + type TestPagesProps = { + Page1: { + prop1?: string; + prop2?: string; + never?: string; + }; + }; function Component1({ prop1, prop2 }: { prop1?: string; prop2?: string }) { return (
@@ -17,10 +22,10 @@ describe('generateTemplates', function () { ); } - let result: PageTemplates; + let result: PageTemplates; before(function () { - result = generateTemplates({ - [TestPage.Page1]: { + result = generateTemplates({ + Page1: { Component: Component1, parameters: ['prop1', 'prop2'], }, @@ -28,12 +33,12 @@ describe('generateTemplates', function () { }); it('creates 4 templates for Page1', function () { - expect(result).to.have.own.property(TestPage.Page1); - expect(result[TestPage.Page1]).to.have.length(4); + expect(result).to.have.own.property('Page1'); + expect(result['Page1']).to.have.length(4); }); it('includes a template with placeholders for prop1 + prop2', function () { - const templates = result[TestPage.Page1]; + const templates = result['Page1']; const fullTemplate = templates.find( ({ parameters }) => Object.keys(parameters).includes('prop1') && @@ -46,7 +51,7 @@ describe('generateTemplates', function () { }); it('includes a template with placeholders for no props', function () { - const templates = result[TestPage.Page1]; + const templates = result['Page1']; const fullTemplate = templates.find( ({ parameters }) => Object.keys(parameters).length === 0 ); diff --git a/packages/oidc-http-server-pages/src/create-templates.ts b/packages/oidc-http-server-pages/src/create-templates.ts index 9a4f0b42..d44225a3 100644 --- a/packages/oidc-http-server-pages/src/create-templates.ts +++ b/packages/oidc-http-server-pages/src/create-templates.ts @@ -19,7 +19,7 @@ import React from 'react'; import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'zlib'; import { writeFileSync } from 'fs'; import path from 'path'; -import type { PageTemplates, ITemplate } from './types'; +import type { PageTemplates, ITemplate, HttpServerPageProps } from './types'; import { HttpServerPage } from './types'; /** Iterate all sub-arrays of an array */ @@ -43,14 +43,14 @@ type Component = React.FunctionComponent>> & { name: string; }; -function getPageTemplates({ +function getPageTemplates>({ Component, parameters, }: { Component: Component; - parameters: string[]; -}): ITemplate[] { - const templates: ITemplate[] = []; + parameters: (string & keyof TParameters)[]; +}): ITemplate[] { + const templates: ITemplate[] = []; for (const paramsSubset of allSubsets(parameters)) { const propsObject = Object.fromEntries( paramsSubset.map((prop) => [prop, placeholder(prop)]) @@ -59,14 +59,20 @@ function getPageTemplates({ renderToStaticMarkup(React.createElement(Component, propsObject)) ); templates.push({ - parameters: propsObject, + parameters: propsObject as TParameters, html: markup, }); } return templates; } -export function generateTemplates( +export function generateTemplates< + TPageParameters extends Record< + string, + Record + > = HttpServerPageProps, + TPage extends string & keyof TPageParameters = string & keyof TPageParameters +>( pages: Record< TPage, { @@ -74,14 +80,17 @@ export function generateTemplates( parameters: string[]; } > -): PageTemplates { - const templates: Partial> = {}; +): PageTemplates { + const templates: Partial> = {}; for (const pageName of Object.keys(pages) as TPage[]) { const { Component, parameters } = pages[pageName]; - const PageTemplates = getPageTemplates({ Component, parameters }); + const PageTemplates = getPageTemplates({ + Component, + parameters, + }); templates[pageName] = PageTemplates; } - return templates as PageTemplates; + return templates as PageTemplates; } function generateGzip(data: string): void { @@ -115,7 +124,11 @@ function generateJS(data: string): void { } export function generateCompressedTemplates< - TPage extends string = HttpServerPage + TPageParameters extends Record< + string, + Record + > = HttpServerPageProps, + TPage extends string & keyof TPageParameters = string & keyof TPageParameters >( pages: Record< TPage, @@ -125,7 +138,9 @@ export function generateCompressedTemplates< } > ): void { - const templates = JSON.stringify(generateTemplates(pages)); + const templates = JSON.stringify( + generateTemplates(pages) + ); generateGzip(templates); generateJS(templates); } diff --git a/packages/oidc-http-server-pages/src/get-static-page.spec.ts b/packages/oidc-http-server-pages/src/get-static-page.spec.ts index 1f47e777..34240d47 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.spec.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.spec.ts @@ -3,12 +3,17 @@ import type { PageTemplates } from './types'; import { getStaticPage } from '../dist/get-static-page'; describe('getStaticPage', function () { - enum TestPage { - Page1 = 'Page1', - } + type TestPage = 'Page1'; + type TestPagesProps = { + Page1: { + prop1?: string; + prop2?: string; + never?: string; + }; + }; - const templates: PageTemplates = { - [TestPage.Page1]: [ + const templates: PageTemplates = { + Page1: [ { parameters: { prop1: '{{prop:prop1}}', prop2: '{{prop:prop2}}' }, html: '

{{prop:prop1}}

{{prop:prop2}}', @@ -31,8 +36,8 @@ describe('getStaticPage', function () { it('scenario: prop1 + prop2', function () { const prop1 = 'abc'; const prop2 = 'def'; - const html = getStaticPage( - TestPage.Page1, + const html = getStaticPage( + 'Page1', { prop1, prop2 }, templates ); @@ -40,13 +45,21 @@ describe('getStaticPage', function () { }); it('scenario: no props', function () { - const html = getStaticPage(TestPage.Page1, {}, templates); + const html = getStaticPage( + 'Page1', + {}, + templates + ); expect(html).to.equal('

default

'); }); it('scenario: prop1 with special characters', function () { const prop1 = `'one' & "two" & `; - const html = getStaticPage(TestPage.Page1, { prop1 }, templates); + const html = getStaticPage( + 'Page1', + { prop1 }, + templates + ); expect(html).to.equal( '

'one' & "two" & <three>

' ); @@ -54,13 +67,17 @@ describe('getStaticPage', function () { it('no page template is found', function () { expect(() => - getStaticPage('UnknownPage' as TestPage, {}, templates) + getStaticPage( + 'UnknownPage' as TestPage, + {}, + templates + ) ).to.throw(Error, /No template found/); }); it('throws with incorrect properties', function () { expect(() => - getStaticPage( + getStaticPage( 'UnknownPage' as TestPage, { never: 'no' }, templates diff --git a/packages/oidc-http-server-pages/src/get-static-page.ts b/packages/oidc-http-server-pages/src/get-static-page.ts index 27097db7..7cc6a3b4 100644 --- a/packages/oidc-http-server-pages/src/get-static-page.ts +++ b/packages/oidc-http-server-pages/src/get-static-page.ts @@ -1,4 +1,4 @@ -import type { ITemplate, HttpServerPage, PageTemplates } from './types'; +import type { ITemplate, PageTemplates, HttpServerPageProps } from './types'; import getTemplates from './get-templates.js'; function findTemplate( @@ -15,12 +15,12 @@ function findTemplate( } function replacePlaceholders( - template: ITemplate, - parameters: Record + template: ITemplate>, + parameters: Record ): string { let { html } = template; for (const [key, placeholder] of Object.entries(template.parameters)) { - html = html.replaceAll(placeholder, escapeHTML(parameters[key] as string)); + html = html.replaceAll(placeholder, escapeHTML(parameters[key])); } return html; } @@ -34,13 +34,19 @@ function escapeHTML(str: string): string { .replace(/'/g, '''); } -export function getStaticPage( +export function getStaticPage< + TPageParameters extends Record< + string, + Record + > = HttpServerPageProps, + TPage extends string & keyof TPageParameters = string & keyof TPageParameters +>( page: TPage, - parameters: Record, - templates?: PageTemplates + parameters: TPageParameters[TPage], + templates?: PageTemplates ): string { if (!templates) { - templates = getTemplates() as PageTemplates; + templates = getTemplates() as PageTemplates; } const pageTemplates = templates && templates[page]; diff --git a/packages/oidc-http-server-pages/src/pages-source.tsx b/packages/oidc-http-server-pages/src/pages-source.tsx index 48803dbe..0c4a9d0c 100644 --- a/packages/oidc-http-server-pages/src/pages-source.tsx +++ b/packages/oidc-http-server-pages/src/pages-source.tsx @@ -56,10 +56,10 @@ const failureIconStyles = css({ marginBottom: spacing[3] + spacing[1], }); -export interface OIDCPageBaseProps { +export type OIDCPageBaseProps = { productDocsLink?: string; productName?: string; -} +}; function DocsLink({ productDocsLink, @@ -101,7 +101,9 @@ function PageContainer({ ); } -export function OIDCAcceptedPage(baseProps: OIDCPageBaseProps): JSX.Element { +export type OIDCAcceptedPageProps = OIDCPageBaseProps; + +export function OIDCAcceptedPage(props: OIDCAcceptedPageProps): JSX.Element { return (

Login Successful

You can close this window now. - +
); } -export function OIDCNotFoundPage(baseProps: OIDCPageBaseProps): JSX.Element { +export type OIDCNotFoundPageProps = OIDCPageBaseProps; + +export function OIDCNotFoundPage(props: OIDCNotFoundPageProps): JSX.Element { return (

Page Not Found

This page is not available. - +
); } +export type OIDCErrorPageProps = { + error?: string; + errorDescription?: string; + errorURI?: string; +} & OIDCPageBaseProps; + export function OIDCErrorPage({ error, errorDescription, errorURI, ...baseProps -}: { - error?: string; - errorDescription?: string; - errorURI?: string; -} & OIDCPageBaseProps): JSX.Element { +}: OIDCErrorPageProps): JSX.Element { return ( diff --git a/packages/oidc-http-server-pages/src/types.ts b/packages/oidc-http-server-pages/src/types.ts index aa333666..591bfd46 100644 --- a/packages/oidc-http-server-pages/src/types.ts +++ b/packages/oidc-http-server-pages/src/types.ts @@ -1,14 +1,32 @@ -export interface ITemplate { - parameters: Record; +import type { + OIDCAcceptedPageProps, + OIDCErrorPageProps, + OIDCNotFoundPageProps, +} from './pages-source'; +export interface ITemplate< + TParameters extends Record = Record +> { + parameters: TParameters; html: string; } -export type PageTemplates = Record< - TPage, - ITemplate[] ->; export enum HttpServerPage { OIDCErrorPage = 'OIDCErrorPage', OIDCAcceptedPage = 'OIDCAcceptedPage', OIDCNotFoundPage = 'OIDCNotFoundPage', } + +export type HttpServerPageProps = { + [HttpServerPage.OIDCErrorPage]: OIDCErrorPageProps; + [HttpServerPage.OIDCAcceptedPage]: OIDCAcceptedPageProps; + [HttpServerPage.OIDCNotFoundPage]: OIDCNotFoundPageProps; +}; + +export type PageTemplates< + TPageProps extends Record< + string, + Record + > = HttpServerPageProps +> = { + [Key in keyof TPageProps]: ITemplate[]; +};