@jpmorganchase/mosaic-site-middleware 0.1.0-beta.9
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.
- package/.turbo/turbo-build.log +2 -0
- package/README.md +14 -0
- package/dist/MiddlewareError.d.ts +14 -0
- package/dist/MiddlewareError.js +18 -0
- package/dist/MiddlewareError.js.map +1 -0
- package/dist/compileMdx.d.ts +1 -0
- package/dist/compileMdx.js +14 -0
- package/dist/compileMdx.js.map +1 -0
- package/dist/createMiddlewareRunner.d.ts +28 -0
- package/dist/createMiddlewareRunner.js +73 -0
- package/dist/createMiddlewareRunner.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/middlewarePresets.d.ts +34 -0
- package/dist/middlewarePresets.js +12 -0
- package/dist/middlewarePresets.js.map +1 -0
- package/dist/plugins/codeBlocks.d.ts +22 -0
- package/dist/plugins/codeBlocks.js +46 -0
- package/dist/plugins/codeBlocks.js.map +1 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +91 -0
- package/dist/session.js.map +1 -0
- package/dist/withContent.d.ts +17 -0
- package/dist/withContent.js +54 -0
- package/dist/withContent.js.map +1 -0
- package/dist/withMosaicMode.d.ts +13 -0
- package/dist/withMosaicMode.js +15 -0
- package/dist/withMosaicMode.js.map +1 -0
- package/dist/withSession.d.ts +21 -0
- package/dist/withSession.js +67 -0
- package/dist/withSession.js.map +1 -0
- package/dist/withSharedConfig.d.ts +9 -0
- package/dist/withSharedConfig.js +83 -0
- package/dist/withSharedConfig.js.map +1 -0
- package/dist/withStaticContent.d.ts +7 -0
- package/dist/withStaticContent.js +42 -0
- package/dist/withStaticContent.js.map +1 -0
- package/package.json +35 -0
- package/src/MiddlewareError.ts +26 -0
- package/src/__tests__/createMDXScope.test.ts +79 -0
- package/src/compileMdx.ts +19 -0
- package/src/createMiddlewareRunner.ts +108 -0
- package/src/index.ts +9 -0
- package/src/middlewarePresets.ts +52 -0
- package/src/plugins/codeBlocks.ts +52 -0
- package/src/session.ts +131 -0
- package/src/withContent.ts +67 -0
- package/src/withMosaicMode.ts +26 -0
- package/src/withSession.ts +103 -0
- package/src/withSharedConfig.ts +92 -0
- package/src/withStaticContent.ts +45 -0
- package/tsconfig.json +10 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import cookie from 'node-cookie';
|
|
2
|
+
import decode from 'jwt-decode';
|
|
3
|
+
import { Session, JWT } from '@jpmorganchase/mosaic-types';
|
|
4
|
+
|
|
5
|
+
if (typeof window !== 'undefined') {
|
|
6
|
+
throw new Error('This file should not be loaded on the client.');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const cookieSecret = process.env.COOKIE_SECRET;
|
|
10
|
+
const isCookieSecretSet = !!cookieSecret;
|
|
11
|
+
const TOKEN_COOKIE_NAME = 'access_token';
|
|
12
|
+
const DEFAULT_COOKIE_CONFIG = {
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
secure: process.env.NODE_ENV !== 'development',
|
|
15
|
+
sameSite: 'lax',
|
|
16
|
+
path: '/'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getToken(req): string | null {
|
|
20
|
+
if (!req) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (req.headers.authorization?.startsWith('Bearer ')) {
|
|
25
|
+
return req.headers.authorization.split(' ')[1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return cookie.get(req, TOKEN_COOKIE_NAME, cookieSecret, isCookieSecretSet);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getIsCypressSession(req): boolean {
|
|
32
|
+
if (!req) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Boolean(req.headers?.cypresse2e === 'true');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearSession(res) {
|
|
40
|
+
if (!res) {
|
|
41
|
+
throw new Error('Cannot clear session - response object invalid');
|
|
42
|
+
}
|
|
43
|
+
return cookie.clear(res, TOKEN_COOKIE_NAME, DEFAULT_COOKIE_CONFIG);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isSessionAgeMoreThan(session: Session, minRemainingTime = 0) {
|
|
47
|
+
const { expires = 0 } = session;
|
|
48
|
+
return expires >= Date.now() + minRemainingTime;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next complexity
|
|
52
|
+
export function getSessionFromToken(accessToken): Session | null {
|
|
53
|
+
try {
|
|
54
|
+
if (!accessToken) {
|
|
55
|
+
throw new Error('No token provided');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const decodedToken = decode(accessToken) as JWT;
|
|
59
|
+
|
|
60
|
+
if (!decodedToken) {
|
|
61
|
+
throw new Error('Invalid token');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
exp = -1,
|
|
66
|
+
UserID = '',
|
|
67
|
+
JPMCIdentifier = '',
|
|
68
|
+
UID = '',
|
|
69
|
+
EmployeeCorporateID = '',
|
|
70
|
+
CostCenter = '',
|
|
71
|
+
Country = '',
|
|
72
|
+
CountryCode = '',
|
|
73
|
+
Department = '',
|
|
74
|
+
DisplayName = '',
|
|
75
|
+
Email = '',
|
|
76
|
+
EmployeeType = '',
|
|
77
|
+
FirstName = '',
|
|
78
|
+
LastName = '',
|
|
79
|
+
LOB = '',
|
|
80
|
+
LocationCode = ''
|
|
81
|
+
} = decodedToken;
|
|
82
|
+
if (decodedToken) {
|
|
83
|
+
const expires = exp * 1000;
|
|
84
|
+
|
|
85
|
+
if (expires < Date.now()) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sid = UserID || JPMCIdentifier || UID || EmployeeCorporateID;
|
|
90
|
+
return {
|
|
91
|
+
expires,
|
|
92
|
+
isLoggedIn: true,
|
|
93
|
+
accessToken,
|
|
94
|
+
user: {
|
|
95
|
+
avatarUrl: process.env.AVATAR_URL!,
|
|
96
|
+
sid,
|
|
97
|
+
country: Country,
|
|
98
|
+
countryCode: CountryCode,
|
|
99
|
+
costCenter: CostCenter,
|
|
100
|
+
department: Department,
|
|
101
|
+
displayName: DisplayName,
|
|
102
|
+
email: Email,
|
|
103
|
+
employeeType: EmployeeType,
|
|
104
|
+
firstName: FirstName,
|
|
105
|
+
lastName: LastName,
|
|
106
|
+
lob: LOB,
|
|
107
|
+
locationCode: LocationCode
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
throw new Error('Could not decode token');
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error(e);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createSession(res, accessToken, expires) {
|
|
120
|
+
return cookie.create(
|
|
121
|
+
res,
|
|
122
|
+
TOKEN_COOKIE_NAME,
|
|
123
|
+
accessToken,
|
|
124
|
+
{
|
|
125
|
+
...DEFAULT_COOKIE_CONFIG,
|
|
126
|
+
maxAge: (expires - new Date().getTime()) / 1000
|
|
127
|
+
},
|
|
128
|
+
cookieSecret,
|
|
129
|
+
isCookieSecretSet
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { compileMDX } from './compileMdx.js';
|
|
2
|
+
import { MosaicMiddleware } from './createMiddlewareRunner.js';
|
|
3
|
+
import MiddlewareError from './MiddlewareError.js';
|
|
4
|
+
|
|
5
|
+
if (typeof window !== 'undefined') {
|
|
6
|
+
throw new Error('This file should not be loaded on the client.');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* [[`ContentProps`]] specifies the page source/content
|
|
11
|
+
*/
|
|
12
|
+
export interface ContentProps {
|
|
13
|
+
/** Page source, which will be page type specific */
|
|
14
|
+
source?: Record<string, unknown>;
|
|
15
|
+
/** Page type */
|
|
16
|
+
type?: string;
|
|
17
|
+
/** Raw page content, includes markdown and frontmatter */
|
|
18
|
+
raw?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds the [[`ContentProps`]] object to the page props
|
|
23
|
+
* @param context
|
|
24
|
+
*/
|
|
25
|
+
export const withContent: MosaicMiddleware<ContentProps> = async context => {
|
|
26
|
+
if (context.res.getHeader('X-Mosaic-Mode') !== 'active') {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { resolvedUrl } = context;
|
|
31
|
+
try {
|
|
32
|
+
const mosaicUrl = context.res.getHeader('X-Mosaic-Content-Url');
|
|
33
|
+
const req = await fetch(`${mosaicUrl}${resolvedUrl}`);
|
|
34
|
+
|
|
35
|
+
if (req.ok) {
|
|
36
|
+
const contentType = req.headers.get('content-type');
|
|
37
|
+
if (contentType && contentType.includes('/mdx')) {
|
|
38
|
+
const text = await req.text();
|
|
39
|
+
const mdxSource = await compileMDX(text);
|
|
40
|
+
return { props: { type: 'mdx', source: mdxSource, raw: text } };
|
|
41
|
+
}
|
|
42
|
+
if (contentType && contentType.includes('/json')) {
|
|
43
|
+
const json = await req.json();
|
|
44
|
+
return { props: { type: 'json', ...json } };
|
|
45
|
+
}
|
|
46
|
+
// If redirect url was returned
|
|
47
|
+
} else if (req.status === 302) {
|
|
48
|
+
return {
|
|
49
|
+
redirect: {
|
|
50
|
+
destination: (await req.json()).redirect,
|
|
51
|
+
permanent: true
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
console.error(error.message);
|
|
58
|
+
throw new MiddlewareError(500, resolvedUrl, [error.message], { show500: true });
|
|
59
|
+
} else {
|
|
60
|
+
console.error('unexpected error');
|
|
61
|
+
throw new MiddlewareError(500, resolvedUrl, ['unexpected error'], { show500: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new MiddlewareError(404, resolvedUrl, [`Could not find any content for ${resolvedUrl}`], {
|
|
65
|
+
show404: true
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MosaicMode } from '@jpmorganchase/mosaic-types';
|
|
2
|
+
|
|
3
|
+
import type { MosaicMiddleware } from './createMiddlewareRunner.js';
|
|
4
|
+
|
|
5
|
+
if (typeof window !== 'undefined') {
|
|
6
|
+
throw new Error('This file should not be loaded on the client.');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* [[`MosaicModeProps`]] specifies the mode mosaic uses
|
|
11
|
+
*/
|
|
12
|
+
export interface MosaicModeProps {
|
|
13
|
+
mode?: MosaicMode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Adds the [[`MosaicModeProps`]] object to the page props
|
|
18
|
+
* @param context
|
|
19
|
+
*/
|
|
20
|
+
export const withMosaicMode: MosaicMiddleware<MosaicModeProps> = async context => {
|
|
21
|
+
const mode: MosaicMode = (process.env.MOSAIC_MODE || 'active') as MosaicMode;
|
|
22
|
+
const mosaicContentUrl = process.env[`MOSAIC_${mode.toUpperCase()}_MODE_URL`] || '';
|
|
23
|
+
context.res.setHeader('X-Mosaic-Mode', mode);
|
|
24
|
+
context.res.setHeader('X-Mosaic-Content-Url', mosaicContentUrl);
|
|
25
|
+
return { props: { mode } };
|
|
26
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { GetServerSidePropsContext } from 'next';
|
|
2
|
+
import type { Session } from '@jpmorganchase/mosaic-types';
|
|
3
|
+
|
|
4
|
+
import { getSessionFromToken, getToken, getIsCypressSession } from './session.js';
|
|
5
|
+
import { MosaicMiddleware } from './createMiddlewareRunner.js';
|
|
6
|
+
import MiddlewareError from './MiddlewareError.js';
|
|
7
|
+
|
|
8
|
+
if (typeof window !== 'undefined') {
|
|
9
|
+
throw new Error('This file should not be loaded on the client.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* [[`SessionProps`]] specifies session object containing user profile of logged in user
|
|
14
|
+
*/
|
|
15
|
+
export interface SessionProps {
|
|
16
|
+
session?: Session;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* [[`SessionOptions`]] specifies a configuration object for the withSession middleware
|
|
21
|
+
*/
|
|
22
|
+
export interface SessionOptions {
|
|
23
|
+
/** is login required for the environment */
|
|
24
|
+
loginRequired: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getRedirectOptions = (context: GetServerSidePropsContext) => {
|
|
28
|
+
const url = new URL(
|
|
29
|
+
`${process.env.HOSTNAME?.replace(/\/$/, '')}/${context.resolvedUrl.replace(/^\//, '')}`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (url.search.includes('code=')) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Authorisation loop detected. `code` should never appear in the client query string.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
url.searchParams.delete('namespace');
|
|
38
|
+
url.searchParams.delete('slug');
|
|
39
|
+
return {
|
|
40
|
+
redirect: {
|
|
41
|
+
destination: `/api/auth/login?referrer=${encodeURIComponent(`${url.pathname}${url.search}`)}`,
|
|
42
|
+
permanent: false
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
async function resolveSession(accessToken) {
|
|
48
|
+
return (
|
|
49
|
+
accessToken &&
|
|
50
|
+
process.env.DISABLE_IDA_SERVICE_AUTH_TOKEN_VERIFICATION === 'true' &&
|
|
51
|
+
(getSessionFromToken(accessToken) as Session)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Adds the [[`Session`]] object to the page props
|
|
57
|
+
* @param context
|
|
58
|
+
* @param options
|
|
59
|
+
*/
|
|
60
|
+
export const withSession: MosaicMiddleware<SessionProps, SessionOptions> = async (
|
|
61
|
+
context,
|
|
62
|
+
options
|
|
63
|
+
) => {
|
|
64
|
+
if (process.env.ENABLE_LOGIN !== 'true') {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
if (!process.env.COOKIE_SECRET) {
|
|
68
|
+
const errorMessage = '`process.env.COOKIE_SECRET` must be set in environment variables.';
|
|
69
|
+
throw new MiddlewareError(500, context.resolvedUrl, [errorMessage], {
|
|
70
|
+
show500: true
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const accessToken = getToken(context.req);
|
|
74
|
+
const isCypressSession = getIsCypressSession(context.req);
|
|
75
|
+
const session = await resolveSession(accessToken);
|
|
76
|
+
// Do the middleware options have `loginRequired` true?
|
|
77
|
+
const isRedirectEnabled = options?.loginRequired;
|
|
78
|
+
const isLoggedIn = !!(
|
|
79
|
+
accessToken &&
|
|
80
|
+
session &&
|
|
81
|
+
// If session remaining time is <15m we'll request a re-login
|
|
82
|
+
isSessionRemainingMoreThan(session, 900000 /* 15 mins */)
|
|
83
|
+
);
|
|
84
|
+
if (isRedirectEnabled && !isLoggedIn) {
|
|
85
|
+
return getRedirectOptions(context);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
props: {
|
|
90
|
+
session: {
|
|
91
|
+
...session,
|
|
92
|
+
isLoggedIn,
|
|
93
|
+
accessToken,
|
|
94
|
+
isCypressSession
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function isSessionRemainingMoreThan(session: Session, minRemainingTime = 1800000 /* 30 mins */) {
|
|
101
|
+
const { expires = 0 } = session;
|
|
102
|
+
return expires >= Date.now() + minRemainingTime;
|
|
103
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { GetServerSidePropsContext } from 'next';
|
|
4
|
+
import type { SharedConfig, SharedConfigSlice } from '@jpmorganchase/mosaic-store';
|
|
5
|
+
import { MosaicMiddleware } from './createMiddlewareRunner.js';
|
|
6
|
+
import MiddlewareError from './MiddlewareError.js';
|
|
7
|
+
|
|
8
|
+
if (typeof window !== 'undefined') {
|
|
9
|
+
throw new Error('This file should not be loaded on the client.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { SharedConfig };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetch from mosaic the shared-config.json.
|
|
16
|
+
*
|
|
17
|
+
* @param url the shared config file URL
|
|
18
|
+
* @returns shared config JSON
|
|
19
|
+
*/
|
|
20
|
+
async function fetchSharedConfig(url: string) {
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json'
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read from the file system the shared config
|
|
31
|
+
*
|
|
32
|
+
* @param url the shared config file URL
|
|
33
|
+
* @returns shared config JSON
|
|
34
|
+
*/
|
|
35
|
+
async function readSharedConfig(url: string) {
|
|
36
|
+
const mosaicSnapshotDir = process.env.MOSAIC_SNAPSHOT_DIR || '';
|
|
37
|
+
const filePath = path.posix.join(process.cwd(), mosaicSnapshotDir, url);
|
|
38
|
+
try {
|
|
39
|
+
const realPath = await fs.promises.realpath(filePath);
|
|
40
|
+
const data = await fs.promises.readFile(realPath, 'utf-8');
|
|
41
|
+
return new Response(data.toString(), { status: 200 });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// it doesn't matter if no shared config file was found
|
|
44
|
+
return new Response('', { status: 204 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Adds the [[`SharedConfig`]] props to the page props
|
|
50
|
+
* @param _context
|
|
51
|
+
* @param _params
|
|
52
|
+
*/
|
|
53
|
+
export const withSharedConfig: MosaicMiddleware<SharedConfigSlice> = async (
|
|
54
|
+
context: GetServerSidePropsContext
|
|
55
|
+
) => {
|
|
56
|
+
const { resolvedUrl, res } = context;
|
|
57
|
+
const isStatic = res.getHeader('X-Mosaic-Mode') === 'static';
|
|
58
|
+
const matches = resolvedUrl.match(/(.*)[!/]/);
|
|
59
|
+
const urlPath = matches?.length ? matches[1] : '';
|
|
60
|
+
const mosaicUrl = isStatic ? '' : res.getHeader('X-Mosaic-Content-Url');
|
|
61
|
+
const sharedConfigUrl = `${mosaicUrl}${urlPath}/shared-config.json`;
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = isStatic
|
|
65
|
+
? await readSharedConfig(sharedConfigUrl)
|
|
66
|
+
: await fetchSharedConfig(sharedConfigUrl);
|
|
67
|
+
|
|
68
|
+
if (response.ok && response.status !== 204) {
|
|
69
|
+
const { config } = (await response.json()) as { config: SharedConfig };
|
|
70
|
+
|
|
71
|
+
return { props: { sharedConfig: config } };
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof Error) {
|
|
75
|
+
console.error(error.message);
|
|
76
|
+
throw new MiddlewareError(500, sharedConfigUrl, [error.message], { show500: true });
|
|
77
|
+
} else {
|
|
78
|
+
console.error('unexpected error');
|
|
79
|
+
throw new MiddlewareError(500, sharedConfigUrl, ['unexpected error'], { show500: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const show500 = response.status !== 404 && response.status !== 204;
|
|
83
|
+
const show404 = response.status === 404;
|
|
84
|
+
let errorMessage = `Could not find any shared config defined for ${sharedConfigUrl}`;
|
|
85
|
+
if (show500) {
|
|
86
|
+
errorMessage = `An un-expected error occurred reading ${sharedConfigUrl}`;
|
|
87
|
+
}
|
|
88
|
+
throw new MiddlewareError(response.status, sharedConfigUrl, [errorMessage], {
|
|
89
|
+
show404,
|
|
90
|
+
show500
|
|
91
|
+
});
|
|
92
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
import { compileMDX } from './compileMdx.js';
|
|
5
|
+
import { MosaicMiddleware } from './createMiddlewareRunner.js';
|
|
6
|
+
import MiddlewareError from './MiddlewareError.js';
|
|
7
|
+
import { ContentProps } from './withContent.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Adds the [[`ContentProps`]] object to the page props
|
|
11
|
+
* @param context
|
|
12
|
+
*/
|
|
13
|
+
export const withStaticContent: MosaicMiddleware<ContentProps> = async context => {
|
|
14
|
+
const { resolvedUrl, res } = context;
|
|
15
|
+
const isStatic = res.getHeader('X-Mosaic-Mode') === 'static';
|
|
16
|
+
|
|
17
|
+
if (!isStatic) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const mosaicSnapshotDir = process.env.MOSAIC_SNAPSHOT_DIR || '';
|
|
21
|
+
// Find the absolute path for the file/dir requested
|
|
22
|
+
const filePath = path.posix.join(process.cwd(), mosaicSnapshotDir, resolvedUrl);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const realPath = await fs.promises.realpath(filePath);
|
|
26
|
+
const extname = path.extname(realPath);
|
|
27
|
+
if (extname === '.mdx') {
|
|
28
|
+
const data = await fs.promises.readFile(filePath, 'utf-8');
|
|
29
|
+
const text = data.toString();
|
|
30
|
+
const mdxSource = await compileMDX(text);
|
|
31
|
+
return { props: { type: 'mdx', source: mdxSource, raw: text } };
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof Error) {
|
|
35
|
+
console.error(error.message);
|
|
36
|
+
throw new MiddlewareError(500, resolvedUrl, [error.message], { show500: true });
|
|
37
|
+
} else {
|
|
38
|
+
console.error('unexpected error');
|
|
39
|
+
throw new MiddlewareError(500, resolvedUrl, ['unexpected error'], { show500: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new MiddlewareError(404, resolvedUrl, [`Could not find any content for ${resolvedUrl}`], {
|
|
43
|
+
show404: true
|
|
44
|
+
});
|
|
45
|
+
};
|
package/tsconfig.json
ADDED