@jpmorganchase/mosaic-site-middleware 0.1.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +14 -0
  4. package/dist/MiddlewareError.d.ts +14 -0
  5. package/dist/MiddlewareError.js +18 -0
  6. package/dist/MiddlewareError.js.map +1 -0
  7. package/dist/compileMdx.d.ts +1 -0
  8. package/dist/compileMdx.js +14 -0
  9. package/dist/compileMdx.js.map +1 -0
  10. package/dist/createMiddlewareRunner.d.ts +28 -0
  11. package/dist/createMiddlewareRunner.js +73 -0
  12. package/dist/createMiddlewareRunner.js.map +1 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +10 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/middlewarePresets.d.ts +34 -0
  17. package/dist/middlewarePresets.js +12 -0
  18. package/dist/middlewarePresets.js.map +1 -0
  19. package/dist/plugins/codeBlocks.d.ts +22 -0
  20. package/dist/plugins/codeBlocks.js +46 -0
  21. package/dist/plugins/codeBlocks.js.map +1 -0
  22. package/dist/session.d.ts +7 -0
  23. package/dist/session.js +91 -0
  24. package/dist/session.js.map +1 -0
  25. package/dist/withContent.d.ts +17 -0
  26. package/dist/withContent.js +54 -0
  27. package/dist/withContent.js.map +1 -0
  28. package/dist/withMosaicMode.d.ts +13 -0
  29. package/dist/withMosaicMode.js +15 -0
  30. package/dist/withMosaicMode.js.map +1 -0
  31. package/dist/withSession.d.ts +21 -0
  32. package/dist/withSession.js +67 -0
  33. package/dist/withSession.js.map +1 -0
  34. package/dist/withSharedConfig.d.ts +9 -0
  35. package/dist/withSharedConfig.js +83 -0
  36. package/dist/withSharedConfig.js.map +1 -0
  37. package/dist/withStaticContent.d.ts +7 -0
  38. package/dist/withStaticContent.js +42 -0
  39. package/dist/withStaticContent.js.map +1 -0
  40. package/package.json +35 -0
  41. package/src/MiddlewareError.ts +26 -0
  42. package/src/__tests__/createMDXScope.test.ts +79 -0
  43. package/src/compileMdx.ts +19 -0
  44. package/src/createMiddlewareRunner.ts +108 -0
  45. package/src/index.ts +9 -0
  46. package/src/middlewarePresets.ts +52 -0
  47. package/src/plugins/codeBlocks.ts +52 -0
  48. package/src/session.ts +131 -0
  49. package/src/withContent.ts +67 -0
  50. package/src/withMosaicMode.ts +26 -0
  51. package/src/withSession.ts +103 -0
  52. package/src/withSharedConfig.ts +92 -0
  53. package/src/withStaticContent.ts +45 -0
  54. package/tsconfig.json +10 -0
@@ -0,0 +1,67 @@
1
+ import { getSessionFromToken, getToken, getIsCypressSession } from './session.js';
2
+ import MiddlewareError from './MiddlewareError.js';
3
+ if (typeof window !== 'undefined') {
4
+ throw new Error('This file should not be loaded on the client.');
5
+ }
6
+ const getRedirectOptions = (context) => {
7
+ const url = new URL(`${process.env.HOSTNAME?.replace(/\/$/, '')}/${context.resolvedUrl.replace(/^\//, '')}`);
8
+ if (url.search.includes('code=')) {
9
+ throw new Error('Authorisation loop detected. `code` should never appear in the client query string.');
10
+ }
11
+ url.searchParams.delete('namespace');
12
+ url.searchParams.delete('slug');
13
+ return {
14
+ redirect: {
15
+ destination: `/api/auth/login?referrer=${encodeURIComponent(`${url.pathname}${url.search}`)}`,
16
+ permanent: false
17
+ }
18
+ };
19
+ };
20
+ async function resolveSession(accessToken) {
21
+ return (accessToken &&
22
+ process.env.DISABLE_IDA_SERVICE_AUTH_TOKEN_VERIFICATION === 'true' &&
23
+ getSessionFromToken(accessToken));
24
+ }
25
+ /**
26
+ * Adds the [[`Session`]] object to the page props
27
+ * @param context
28
+ * @param options
29
+ */
30
+ export const withSession = async (context, options) => {
31
+ if (process.env.ENABLE_LOGIN !== 'true') {
32
+ return {};
33
+ }
34
+ if (!process.env.COOKIE_SECRET) {
35
+ const errorMessage = '`process.env.COOKIE_SECRET` must be set in environment variables.';
36
+ throw new MiddlewareError(500, context.resolvedUrl, [errorMessage], {
37
+ show500: true
38
+ });
39
+ }
40
+ const accessToken = getToken(context.req);
41
+ const isCypressSession = getIsCypressSession(context.req);
42
+ const session = await resolveSession(accessToken);
43
+ // Do the middleware options have `loginRequired` true?
44
+ const isRedirectEnabled = options?.loginRequired;
45
+ const isLoggedIn = !!(accessToken &&
46
+ session &&
47
+ // If session remaining time is <15m we'll request a re-login
48
+ isSessionRemainingMoreThan(session, 900000 /* 15 mins */));
49
+ if (isRedirectEnabled && !isLoggedIn) {
50
+ return getRedirectOptions(context);
51
+ }
52
+ return {
53
+ props: {
54
+ session: {
55
+ ...session,
56
+ isLoggedIn,
57
+ accessToken,
58
+ isCypressSession
59
+ }
60
+ }
61
+ };
62
+ };
63
+ function isSessionRemainingMoreThan(session, minRemainingTime = 1800000 /* 30 mins */) {
64
+ const { expires = 0 } = session;
65
+ return expires >= Date.now() + minRemainingTime;
66
+ }
67
+ //# sourceMappingURL=withSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withSession.js","sourceRoot":"","sources":["../src/withSession.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAElF,OAAO,eAAe,MAAM,sBAAsB,CAAC;AAEnD,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE;IACjC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;CAClE;AAiBD,MAAM,kBAAkB,GAAG,CAAC,OAAkC,EAAE,EAAE;IAChE,MAAM,GAAG,GAAG,IAAI,GAAG,CACjB,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CACxF,CAAC;IAEF,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE;QAChC,MAAM,IAAI,KAAK,CACb,qFAAqF,CACtF,CAAC;KACH;IACD,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACrC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO;QACL,QAAQ,EAAE;YACR,WAAW,EAAE,4BAA4B,kBAAkB,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;YAC7F,SAAS,EAAE,KAAK;SACjB;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,KAAK,UAAU,cAAc,CAAC,WAAW;IACvC,OAAO,CACL,WAAW;QACX,OAAO,CAAC,GAAG,CAAC,2CAA2C,KAAK,MAAM;QACjE,mBAAmB,CAAC,WAAW,CAAa,CAC9C,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAmD,KAAK,EAC9E,OAAO,EACP,OAAO,EACP,EAAE;IACF,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,MAAM,EAAE;QACvC,OAAO,EAAE,CAAC;KACX;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE;QAC9B,MAAM,YAAY,GAAG,mEAAmE,CAAC;QACzF,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,YAAY,CAAC,EAAE;YAClE,OAAO,EAAE,IAAI;SACd,CAAC,CAAC;KACJ;IACD,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;IAClD,uDAAuD;IACvD,MAAM,iBAAiB,GAAG,OAAO,EAAE,aAAa,CAAC;IACjD,MAAM,UAAU,GAAG,CAAC,CAAC,CACnB,WAAW;QACX,OAAO;QACP,6DAA6D;QAC7D,0BAA0B,CAAC,OAAO,EAAE,MAAM,CAAC,aAAa,CAAC,CAC1D,CAAC;IACF,IAAI,iBAAiB,IAAI,CAAC,UAAU,EAAE;QACpC,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;KACpC;IAED,OAAO;QACL,KAAK,EAAE;YACL,OAAO,EAAE;gBACP,GAAG,OAAO;gBACV,UAAU;gBACV,WAAW;gBACX,gBAAgB;aACjB;SACF;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,SAAS,0BAA0B,CAAC,OAAgB,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa;IAC5F,MAAM,EAAE,OAAO,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC;IAChC,OAAO,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,CAAC;AAClD,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { SharedConfig, SharedConfigSlice } from '@jpmorganchase/mosaic-store';
2
+ import { MosaicMiddleware } from './createMiddlewareRunner.js';
3
+ export { SharedConfig };
4
+ /**
5
+ * Adds the [[`SharedConfig`]] props to the page props
6
+ * @param _context
7
+ * @param _params
8
+ */
9
+ export declare const withSharedConfig: MosaicMiddleware<SharedConfigSlice>;
@@ -0,0 +1,83 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import MiddlewareError from './MiddlewareError.js';
4
+ if (typeof window !== 'undefined') {
5
+ throw new Error('This file should not be loaded on the client.');
6
+ }
7
+ /**
8
+ * Fetch from mosaic the shared-config.json.
9
+ *
10
+ * @param url the shared config file URL
11
+ * @returns shared config JSON
12
+ */
13
+ async function fetchSharedConfig(url) {
14
+ const response = await fetch(url, {
15
+ headers: {
16
+ 'Content-Type': 'application/json'
17
+ }
18
+ });
19
+ return response;
20
+ }
21
+ /**
22
+ * Read from the file system the shared config
23
+ *
24
+ * @param url the shared config file URL
25
+ * @returns shared config JSON
26
+ */
27
+ async function readSharedConfig(url) {
28
+ const mosaicSnapshotDir = process.env.MOSAIC_SNAPSHOT_DIR || '';
29
+ const filePath = path.posix.join(process.cwd(), mosaicSnapshotDir, url);
30
+ try {
31
+ const realPath = await fs.promises.realpath(filePath);
32
+ const data = await fs.promises.readFile(realPath, 'utf-8');
33
+ return new Response(data.toString(), { status: 200 });
34
+ }
35
+ catch (e) {
36
+ // it doesn't matter if no shared config file was found
37
+ return new Response('', { status: 204 });
38
+ }
39
+ }
40
+ /**
41
+ * Adds the [[`SharedConfig`]] props to the page props
42
+ * @param _context
43
+ * @param _params
44
+ */
45
+ export const withSharedConfig = async (context) => {
46
+ const { resolvedUrl, res } = context;
47
+ const isStatic = res.getHeader('X-Mosaic-Mode') === 'static';
48
+ const matches = resolvedUrl.match(/(.*)[!/]/);
49
+ const urlPath = matches?.length ? matches[1] : '';
50
+ const mosaicUrl = isStatic ? '' : res.getHeader('X-Mosaic-Content-Url');
51
+ const sharedConfigUrl = `${mosaicUrl}${urlPath}/shared-config.json`;
52
+ let response;
53
+ try {
54
+ response = isStatic
55
+ ? await readSharedConfig(sharedConfigUrl)
56
+ : await fetchSharedConfig(sharedConfigUrl);
57
+ if (response.ok && response.status !== 204) {
58
+ const { config } = (await response.json());
59
+ return { props: { sharedConfig: config } };
60
+ }
61
+ }
62
+ catch (error) {
63
+ if (error instanceof Error) {
64
+ console.error(error.message);
65
+ throw new MiddlewareError(500, sharedConfigUrl, [error.message], { show500: true });
66
+ }
67
+ else {
68
+ console.error('unexpected error');
69
+ throw new MiddlewareError(500, sharedConfigUrl, ['unexpected error'], { show500: true });
70
+ }
71
+ }
72
+ const show500 = response.status !== 404 && response.status !== 204;
73
+ const show404 = response.status === 404;
74
+ let errorMessage = `Could not find any shared config defined for ${sharedConfigUrl}`;
75
+ if (show500) {
76
+ errorMessage = `An un-expected error occurred reading ${sharedConfigUrl}`;
77
+ }
78
+ throw new MiddlewareError(response.status, sharedConfigUrl, [errorMessage], {
79
+ show404,
80
+ show500
81
+ });
82
+ };
83
+ //# sourceMappingURL=withSharedConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withSharedConfig.js","sourceRoot":"","sources":["../src/withSharedConfig.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,OAAO,eAAe,MAAM,sBAAsB,CAAC;AAEnD,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE;IACjC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;CAClE;AAID;;;;;GAKG;AACH,KAAK,UAAU,iBAAiB,CAAC,GAAW;IAC1C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;SACnC;KACF,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,gBAAgB,CAAC,GAAW;IACzC,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,EAAE,CAAC;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,iBAAiB,EAAE,GAAG,CAAC,CAAC;IACxE,IAAI;QACF,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC3D,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;KACvD;IAAC,OAAO,CAAC,EAAE;QACV,uDAAuD;QACvD,OAAO,IAAI,QAAQ,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;KAC1C;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAwC,KAAK,EACxE,OAAkC,EAClC,EAAE;IACF,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IACrC,MAAM,QAAQ,GAAG,GAAG,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,QAAQ,CAAC;IAC7D,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAClD,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;IACxE,MAAM,eAAe,GAAG,GAAG,SAAS,GAAG,OAAO,qBAAqB,CAAC;IACpE,IAAI,QAAQ,CAAC;IACb,IAAI;QACF,QAAQ,GAAG,QAAQ;YACjB,CAAC,CAAC,MAAM,gBAAgB,CAAC,eAAe,CAAC;YACzC,CAAC,CAAC,MAAM,iBAAiB,CAAC,eAAe,CAAC,CAAC;QAE7C,IAAI,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE;YAC1C,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAC;YAEvE,OAAO,EAAE,KAAK,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;SAC5C;KACF;IAAC,OAAO,KAAK,EAAE;QACd,IAAI,KAAK,YAAY,KAAK,EAAE;YAC1B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7B,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;SACrF;aAAM;YACL,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;YAClC,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,CAAC,kBAAkB,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;SAC1F;KACF;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC;IACnE,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,KAAK,GAAG,CAAC;IACxC,IAAI,YAAY,GAAG,gDAAgD,eAAe,EAAE,CAAC;IACrF,IAAI,OAAO,EAAE;QACX,YAAY,GAAG,yCAAyC,eAAe,EAAE,CAAC;KAC3E;IACD,MAAM,IAAI,eAAe,CAAC,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,YAAY,CAAC,EAAE;QAC1E,OAAO;QACP,OAAO;KACR,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { MosaicMiddleware } from './createMiddlewareRunner.js';
2
+ import { ContentProps } from './withContent.js';
3
+ /**
4
+ * Adds the [[`ContentProps`]] object to the page props
5
+ * @param context
6
+ */
7
+ export declare const withStaticContent: MosaicMiddleware<ContentProps>;
@@ -0,0 +1,42 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { compileMDX } from './compileMdx.js';
4
+ import MiddlewareError from './MiddlewareError.js';
5
+ /**
6
+ * Adds the [[`ContentProps`]] object to the page props
7
+ * @param context
8
+ */
9
+ export const withStaticContent = async (context) => {
10
+ const { resolvedUrl, res } = context;
11
+ const isStatic = res.getHeader('X-Mosaic-Mode') === 'static';
12
+ if (!isStatic) {
13
+ return {};
14
+ }
15
+ const mosaicSnapshotDir = process.env.MOSAIC_SNAPSHOT_DIR || '';
16
+ // Find the absolute path for the file/dir requested
17
+ const filePath = path.posix.join(process.cwd(), mosaicSnapshotDir, resolvedUrl);
18
+ try {
19
+ const realPath = await fs.promises.realpath(filePath);
20
+ const extname = path.extname(realPath);
21
+ if (extname === '.mdx') {
22
+ const data = await fs.promises.readFile(filePath, 'utf-8');
23
+ const text = data.toString();
24
+ const mdxSource = await compileMDX(text);
25
+ return { props: { type: 'mdx', source: mdxSource, raw: text } };
26
+ }
27
+ }
28
+ catch (error) {
29
+ if (error instanceof Error) {
30
+ console.error(error.message);
31
+ throw new MiddlewareError(500, resolvedUrl, [error.message], { show500: true });
32
+ }
33
+ else {
34
+ console.error('unexpected error');
35
+ throw new MiddlewareError(500, resolvedUrl, ['unexpected error'], { show500: true });
36
+ }
37
+ }
38
+ throw new MiddlewareError(404, resolvedUrl, [`Could not find any content for ${resolvedUrl}`], {
39
+ show404: true
40
+ });
41
+ };
42
+ //# sourceMappingURL=withStaticContent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withStaticContent.js","sourceRoot":"","sources":["../src/withStaticContent.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,eAAe,MAAM,sBAAsB,CAAC;AAGnD;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAmC,KAAK,EAAC,OAAO,EAAC,EAAE;IAC/E,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IACrC,MAAM,QAAQ,GAAG,GAAG,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,QAAQ,CAAC;IAE7D,IAAI,CAAC,QAAQ,EAAE;QACb,OAAO,EAAE,CAAC;KACX;IACD,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,EAAE,CAAC;IAChE,qDAAqD;IACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,iBAAiB,EAAE,WAAW,CAAC,CAAC;IAEhF,IAAI;QACF,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACtD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,OAAO,KAAK,MAAM,EAAE;YACtB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;YACzC,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC;SACjE;KACF;IAAC,OAAO,KAAK,EAAE;QACd,IAAI,KAAK,YAAY,KAAK,EAAE;YAC1B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7B,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;SACjF;aAAM;YACL,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;YAClC,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,kBAAkB,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;SACtF;KACF;IACD,MAAM,IAAI,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,kCAAkC,WAAW,EAAE,CAAC,EAAE;QAC7F,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;AACL,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@jpmorganchase/mosaic-site-middleware",
3
+ "version": "0.1.0-beta.10",
4
+ "license": "Apache-2.0",
5
+ "description": "Mosaic - Site middleware",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "clean": "rm -fr dist",
11
+ "lint": "eslint --ignore-pattern \"**/__tests__/**\""
12
+ },
13
+ "devDependencies": {
14
+ "typescript": "^4.8.3"
15
+ },
16
+ "dependencies": {
17
+ "@jpmorganchase/mosaic-types": "^0.1.0-beta.10",
18
+ "@jpmorganchase/mosaic-store": "^0.1.0-beta.10",
19
+ "@types/mdast": "^3.0.0",
20
+ "acorn": "^8.0.0",
21
+ "acorn-jsx": "^5.0.0",
22
+ "deepmerge": "^4.2.2",
23
+ "jwt-decode": "^3.1.2",
24
+ "lodash": "^4.17.21",
25
+ "next": "^12.1.0",
26
+ "next-mdx-remote": "^4.0.0",
27
+ "node-cookie": "^2.1.2",
28
+ "react-error-boundary": "^3.1.4",
29
+ "react-pro-sidebar": "1.0.0-alpha.7",
30
+ "rehype-slug": "^5.0.1",
31
+ "unified": "^10.0.0",
32
+ "unist-util-visit": "^2.0.0",
33
+ "warning": "^3.0.0"
34
+ }
35
+ }
@@ -0,0 +1,26 @@
1
+ export default class MiddlewareError extends Error {
2
+ /** Response code */
3
+ public status: number;
4
+
5
+ /** Related route location */
6
+ public location: string | undefined;
7
+
8
+ /** Array of errors */
9
+ public errors: string[];
10
+
11
+ /** Props to pass back */
12
+ public props: { show404?: boolean; show500?: boolean };
13
+
14
+ constructor(
15
+ status: MiddlewareError['status'],
16
+ location: MiddlewareError['location'],
17
+ errors: MiddlewareError['errors'] = [],
18
+ props: MiddlewareError['props'] = {}
19
+ ) {
20
+ super(`${status} error`);
21
+ this.status = status;
22
+ this.location = location;
23
+ this.errors = errors;
24
+ this.props = props;
25
+ }
26
+ }
@@ -0,0 +1,79 @@
1
+ import { createMDXScope } from '../utils/createMDXScope';
2
+
3
+ describe('GIVEN createMDXScope', () => {
4
+ test("should add meta to it's scope", () => {
5
+ const scope = createMDXScope({ test: 'TEST META' });
6
+ expect(scope.meta).toEqual({ test: 'TEST META' });
7
+ });
8
+ test('should provide a filter helper which filters by callback', () => {
9
+ const scope = createMDXScope();
10
+ const filter = scope.helpers.filter({ filter: item => item.product === 'PRODUCT_C' });
11
+ const filteredView = filter([
12
+ { product: 'PRODUCT_A' },
13
+ { product: 'PRODUCT_B' },
14
+ { product: 'PRODUCT_C' }
15
+ ]);
16
+ expect(filteredView).toEqual([{ product: 'PRODUCT_C' }]);
17
+ });
18
+ test('should provide a limit helper which limits the displayed results', () => {
19
+ const scope = createMDXScope();
20
+ const limit = scope.helpers.limit({ max: 2 });
21
+ const filteredView = limit([
22
+ { product: 'PRODUCT_A' },
23
+ { product: 'PRODUCT_B' },
24
+ { product: 'PRODUCT_C' }
25
+ ]);
26
+ expect(filteredView).toEqual([{ product: 'PRODUCT_A' }, { product: 'PRODUCT_B' }]);
27
+ });
28
+ test('should provide a sort helper which can sort the results by date key', () => {
29
+ const scope = createMDXScope();
30
+ const sort = scope.helpers.sortViewByDate({ dateKey: 'releaseDate' });
31
+ const filteredView = sort([
32
+ { product: 'PRODUCT_A', releaseDate: '2020/01/01' },
33
+ { product: 'PRODUCT_B', releaseDate: '2020/01/02' },
34
+ { product: 'PRODUCT_C', releaseDate: '2020/01/03' }
35
+ ]);
36
+ expect(filteredView[0].product).toEqual('PRODUCT_C');
37
+ expect(filteredView[1].product).toEqual('PRODUCT_B');
38
+ expect(filteredView[2].product).toEqual('PRODUCT_A');
39
+ });
40
+ test('should provide a sort helper which can sort the results by date function', () => {
41
+ const scope = createMDXScope();
42
+ const sort = scope.helpers.sortViewByDate({ dateKey: item => item.releaseDate });
43
+ const filteredView = sort([
44
+ { product: 'PRODUCT_A', releaseDate: '2020/01/01' },
45
+ { product: 'PRODUCT_B', releaseDate: '2020/01/02' },
46
+ { product: 'PRODUCT_C', releaseDate: '2020/01/03' }
47
+ ]);
48
+ expect(filteredView[0].product).toEqual('PRODUCT_C');
49
+ expect(filteredView[1].product).toEqual('PRODUCT_B');
50
+ expect(filteredView[2].product).toEqual('PRODUCT_A');
51
+ });
52
+ test('should provide a flow function which enables you to compose helpers', () => {
53
+ const scope = createMDXScope();
54
+ const sort = scope.helpers.flow([
55
+ scope.helpers.sortViewByDate({ dateKey: item => item.releaseDate }),
56
+ scope.helpers.filter({ filter: item => item.product === 'PRODUCT_A' }),
57
+ scope.helpers.limit({ max: 3 })
58
+ ]);
59
+ const filteredView = sort([
60
+ { product: 'PRODUCT_C', releaseDate: '2022/01/01' },
61
+ { product: 'PRODUCT_A', releaseDate: '2020/01/02' },
62
+ { product: 'PRODUCT_A', releaseDate: '2020/01/04' },
63
+ { product: 'PRODUCT_A', releaseDate: '2020/01/05' },
64
+ { product: 'PRODUCT_A', releaseDate: '2020/01/04' }
65
+ ]);
66
+ expect(filteredView.length).toEqual(3);
67
+ expect(filteredView[0]).toEqual({ product: 'PRODUCT_A', releaseDate: '2020/01/05' });
68
+ expect(filteredView[1]).toEqual({ product: 'PRODUCT_A', releaseDate: '2020/01/04' });
69
+ expect(filteredView[2]).toEqual({ product: 'PRODUCT_A', releaseDate: '2020/01/04' });
70
+ });
71
+ test('should provide a collection of hooks', () => {
72
+ const scope = createMDXScope();
73
+ expect(scope.hooks).toBeDefined();
74
+ });
75
+ test('should provide a collection of recipes', () => {
76
+ const scope = createMDXScope();
77
+ expect(scope.recipes).toBeDefined();
78
+ });
79
+ });
@@ -0,0 +1,19 @@
1
+ import { serialize } from 'next-mdx-remote/serialize';
2
+ import rehypeSlug from 'rehype-slug';
3
+ import { codeBlocks } from './plugins/codeBlocks.js';
4
+
5
+ export async function compileMDX(
6
+ content,
7
+ parseFrontmatter = true,
8
+ rehypePlugins = [],
9
+ remarkPlugins = []
10
+ ) {
11
+ const mdxSource = await serialize(content, {
12
+ mdxOptions: {
13
+ rehypePlugins: [rehypeSlug, ...remarkPlugins],
14
+ remarkPlugins: [codeBlocks, ...rehypePlugins]
15
+ },
16
+ parseFrontmatter
17
+ });
18
+ return mdxSource;
19
+ }
@@ -0,0 +1,108 @@
1
+ import type { GetServerSidePropsContext } from 'next';
2
+ import deepmerge from 'deepmerge';
3
+
4
+ import type { MosaicAppProps } from './middlewarePresets.js';
5
+ import MiddlewareError from './MiddlewareError.js';
6
+
7
+ const overwriteMerge = (_, sourceArray) => sourceArray;
8
+
9
+ /** Props returned after running Middleware */
10
+ export type MiddlewareResult<TProps> = Awaited<MosaicAppProps<TProps>>;
11
+
12
+ /** Options to be passed to Middleware */
13
+ export type MosaicMiddlewareOptions = Record<string, unknown>;
14
+
15
+ /** Middleware callbacks return props to be added to the page */
16
+ export type MosaicMiddleware<TProps = unknown, TOptions = MosaicMiddlewareOptions> = (
17
+ /** Context provides information on the current page */
18
+ context: GetServerSidePropsContext,
19
+ /** Middleware specific options */
20
+ options: TOptions,
21
+ /** Previous result created by plugins */
22
+ lastState: Record<string, unknown>
23
+ ) => Promise<MiddlewareResult<TProps>>;
24
+
25
+ /** Middleware with a configuration object specifying options */
26
+ export type MosaicMiddlewareWithConfig<TProps, TConfig> = [
27
+ MosaicMiddleware<TProps, TConfig>,
28
+ TConfig
29
+ ];
30
+
31
+ /** Function to run all provided middleware */
32
+ export type MosaicMiddlewareRunner<TProps> = (
33
+ context: GetServerSidePropsContext,
34
+ options: MosaicMiddlewareOptions
35
+ ) => Promise<MiddlewareResult<TProps>>;
36
+
37
+ /**
38
+ * Creates a middleware runner from the collection of provided middleware
39
+ * @param initialState initial state
40
+ * @param middleware a collection of middleware to be executed in order to get the page props
41
+ * @returns a middleware runner
42
+ */
43
+ export function createMiddlewareRunner<TProps>(
44
+ initialState: Record<string, unknown>,
45
+ middleware: Array<MosaicMiddleware<any> | MosaicMiddlewareWithConfig<any, any>>
46
+ ): MosaicMiddlewareRunner<TProps> {
47
+ return async function middlewareRunner(context, options) {
48
+ let result: MiddlewareResult<TProps> = initialState;
49
+ const errors: Error[] = [];
50
+ // eslint-disable-next-line no-restricted-syntax
51
+ for (const fnOrConfig of middleware) {
52
+ let fn: MosaicMiddleware<TProps, any>;
53
+ let finalOptions = options;
54
+ try {
55
+ if (typeof fnOrConfig === 'function') {
56
+ fn = fnOrConfig;
57
+ } else {
58
+ fn = fnOrConfig[0];
59
+ finalOptions = { ...options, ...fnOrConfig[1] };
60
+ }
61
+ // eslint-disable-next-line no-await-in-loop
62
+ const nextState = await fn(context, finalOptions, result);
63
+ result = deepmerge<MiddlewareResult<TProps>>(result, nextState, {
64
+ arrayMerge: overwriteMerge
65
+ });
66
+ } catch (error) {
67
+ if (error instanceof Error) {
68
+ errors.push(error);
69
+ } else {
70
+ const unexpectedError = new MiddlewareError(500, undefined, [String(error)], {
71
+ show500: true
72
+ });
73
+ errors.push(unexpectedError);
74
+ }
75
+ }
76
+ }
77
+ if (result.redirect) {
78
+ return { redirect: result.redirect };
79
+ }
80
+ const show404 = errors.some(error => {
81
+ if (error instanceof MiddlewareError) {
82
+ return error.props?.show404;
83
+ }
84
+ return false;
85
+ });
86
+ const show500 =
87
+ !show404 &&
88
+ errors.some(error => {
89
+ if (error instanceof MiddlewareError) {
90
+ return error.props?.show500;
91
+ }
92
+ return true;
93
+ });
94
+
95
+ if (show500) {
96
+ console.error('An un-expected error(s) was thrown which caused the 500 page to appear');
97
+ errors.forEach(error => {
98
+ console.error(error.message);
99
+ });
100
+ }
101
+
102
+ if (show404 || show500) {
103
+ context.res.setHeader(`X-Mosaic-${show404 ? '404' : '500'}`, 'true');
104
+ }
105
+ const props: TProps = { ...result.props, show404, show500 } as TProps;
106
+ return { props };
107
+ };
108
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from './compileMdx.js';
2
+ export * from './createMiddlewareRunner.js';
3
+ export * from './MiddlewareError.js';
4
+ export * from './middlewarePresets.js';
5
+ export * from './withContent.js';
6
+ export * from './withMosaicMode.js';
7
+ export * from './withSession.js';
8
+ export * from './withSharedConfig.js';
9
+ export * from './withStaticContent.js';
@@ -0,0 +1,52 @@
1
+ import type { Redirect } from 'next';
2
+ import { SharedConfigSlice } from '@jpmorganchase/mosaic-store';
3
+
4
+ import { withSession, type SessionProps, type SessionOptions } from './withSession.js';
5
+ import { withSharedConfig } from './withSharedConfig.js';
6
+ import { withContent, type ContentProps } from './withContent.js';
7
+ import { MosaicMiddleware, MosaicMiddlewareWithConfig } from './createMiddlewareRunner.js';
8
+ import { withMosaicMode, type MosaicModeProps } from './withMosaicMode.js';
9
+
10
+ export type BaseMosaicAppProps = {
11
+ /** Flag to show custom 404 page, whilst maintaining current state/layout */
12
+ show404?: boolean;
13
+ /** Flag to show custom 500 page, whilst maintaining current state/layout */
14
+ show500?: boolean;
15
+ };
16
+
17
+ /** Abstract Mosaic App Props */
18
+ export type MosaicAppProps<T> = {
19
+ /** Page props created by [[`Middleware`]] */
20
+ props?: T;
21
+ /** Error description */
22
+ errors?: {
23
+ /** Stacktrace * */
24
+ location?: string;
25
+ /** Error message * */
26
+ statusText?: string;
27
+ /** Error code */
28
+ status?: number;
29
+ } & BaseMosaicAppProps;
30
+ /** Next JS Redirect */
31
+ redirect?: Redirect;
32
+ };
33
+
34
+ /** Mosaic getServerSideProps result which supports error handling */
35
+ export type GetMosaicServerSidePropsResult<T> = MosaicAppProps<T>;
36
+
37
+ /** MiddlewarePresets props */
38
+ export type MiddlewarePresetsProps = MosaicModeProps &
39
+ ContentProps &
40
+ SessionProps &
41
+ SharedConfigSlice;
42
+
43
+ /** A collection of preset [[`Middleware`]] plugins that will compose together the page props */
44
+ export const middlewarePresets: Array<
45
+ | MosaicMiddleware<MiddlewarePresetsProps>
46
+ | MosaicMiddlewareWithConfig<MiddlewarePresetsProps, SessionOptions>
47
+ > = [
48
+ [withSession, { loginRequired: process.env.DISABLE_GLOBAL_AUTH !== 'true' }],
49
+ withMosaicMode,
50
+ withSharedConfig,
51
+ withContent
52
+ ];
@@ -0,0 +1,52 @@
1
+ import { Parser } from 'acorn';
2
+ import jsx from 'acorn-jsx';
3
+ import type { Code, Literal } from 'mdast';
4
+ import { Plugin, Transformer } from 'unified';
5
+ import visit from 'unist-util-visit';
6
+
7
+ const parser = Parser.extend(jsx());
8
+
9
+ /**
10
+ * Modified from: https://github.com/remcohaszing/remark-mdx-code-meta
11
+ *
12
+ * Custom meta parser for codefences that have extra params. e.g.
13
+ * ```jsx filename="hello.jsx"
14
+ * <div>Test!</div>
15
+ * ```
16
+ *
17
+ * Custom meta parser for codefences that have eval or eval="true".
18
+ * This allows vars to be resolved and their values used as the code block body.
19
+ * ```jsx eval
20
+ * meta.data.someStringOnlyAvailableAtRuntime
21
+ * ```
22
+ */
23
+ export const transformer: Transformer = ast => {
24
+ visit<Code>(ast, 'code', (node, index, parent) => {
25
+ if (!node.meta) {
26
+ return;
27
+ }
28
+ // Limit eval to just basic strings that start with "meta."
29
+ const isEval =
30
+ /(^| )eval(="true"| |$)/.test(node.meta) && /^meta\.[a-z0-9_[\].$"']+$/i.test(node.value);
31
+
32
+ let code;
33
+ if (!isEval) {
34
+ code = JSON.stringify(`${node.value}\n`);
35
+ } else {
36
+ code = node.value;
37
+ }
38
+
39
+ const codeProps = node.lang ? `className="language-${node.lang}"` : '';
40
+ const value = `<pre ${node.meta}><code ${codeProps}>{${code}}</code></pre>`;
41
+ const estree = parser.parse(value, { ecmaVersion: 'latest' });
42
+ // eslint-disable-next-line no-param-reassign
43
+ parent!.children[index] = { type: 'mdxFlowExpression', value, data: { estree } } as Literal;
44
+ });
45
+ };
46
+
47
+ /**
48
+ * A markdown plugin for transforming code metadata.
49
+ *
50
+ * @returns A unified transformer.
51
+ */
52
+ export const codeBlocks: Plugin<[]> = () => transformer;