-
Notifications
You must be signed in to change notification settings - Fork 0
/
webpack.ts
177 lines (157 loc) · 6.91 KB
/
webpack.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
* Copyright 2023 Kapeta Inc.
* SPDX-License-Identifier: MIT
*/
/// <reference path="../express.d.ts" />
import express, { Express, NextFunction, Request, Response } from 'express';
import FS from 'fs';
import * as Path from 'node:path';
import { asTemplates, TemplatesOverrides } from './templates';
import { isDevMode } from '../index';
const ensureArray = (value: string | string[]): string[] => {
if (Array.isArray(value)) {
return value;
}
return [value].filter(Boolean);
};
/**
* Applies webpack handlers to the express app.
* This is used to serve the frontend in dev mode and in prod mode.
* In dev mode, the webpack dev middleware is used to serve the frontend.
* In prod mode, the frontend is served from the dist folder.
*
* @param distFolder The absolute path to the dist folder where the build artifacts are located.
* @param webpackConfig The webpack config used in dev mode.
* @param app The express app to apply the handlers to.
* @param templateOverrides Optional overrides for the templates used when rendering the HTML page.
* @internal
*/
export const applyWebpackHandlers = (
distFolder: string,
webpackConfig: any,
app: Express,
templateOverrides?: TemplatesOverrides
) => {
const templates = asTemplates(templateOverrides || {});
const distPath: string = webpackConfig.output.publicPath || 'dist/';
if (distPath && distPath.startsWith('/')) {
throw new Error('The publicPath in the webpack config must be a relative path to work with fragments');
}
if (!distPath.endsWith('/')) {
throw new Error(
'The publicPath in the webpack config should end with a trailing slash to work properly w/ assets'
);
}
// Set up the two different ways of getting the webpack assets, either devmode rendering,
// or by reading the manifest in production mode
if (isDevMode()) {
/* eslint-disable @typescript-eslint/no-var-requires */
console.log('Serving development version');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const compiler = webpack(webpackConfig);
app.use(
webpackDevMiddleware(compiler, {
serverSideRender: true,
publicPath: '/' + distPath,
})
);
app.use(require('webpack-hot-middleware')(compiler));
// expose asset info on the request to be picked up by renderPage
app.use((_req, res, next) => {
const { devMiddleware } = res.locals.webpack;
// Extract just the fields we need, since the webpack stats object is huge
const { entrypoints } = devMiddleware.stats.toJson({
all: false,
entrypoints: true,
}) as { entrypoints: { [key: string]: { assets: { name: string }[] } } };
const assets = Object.keys(entrypoints).reduce((agg, pageName) => {
const entryAssets = entrypoints[pageName].assets;
agg[pageName] = {
js: entryAssets
.filter((chunk) => chunk.name.endsWith('.js'))
.map((chunk) => `${distPath}${chunk.name}`),
css: entryAssets
.filter((chunk) => chunk.name.endsWith('.css'))
.map((chunk) => `${distPath}${chunk.name}`),
};
return agg;
}, {} as { [key: string]: { js: string[]; css: string[] } });
res.locals.webpackAssets = assets;
next();
});
/* eslint-enable */
} else {
console.log('Serving production version');
if (!FS.existsSync(distFolder)) {
console.error(
'Distribution folder (%s) is missing - did you remember to build before running?',
distFolder
);
process.exit(1);
}
const assetsDataFile = Path.join(distFolder, 'assets.json');
if (!FS.existsSync(assetsDataFile)) {
console.error(
'Assets information (%s) is missing - did you remember to build before running?',
assetsDataFile
);
process.exit(1);
}
// expose asset info on the request to be picked up by renderPage
const assets = JSON.parse(FS.readFileSync(assetsDataFile, 'utf-8'));
app.use((_req, res, next) => {
res.locals.webpackAssets = assets;
next();
});
}
app.use((req, res, next) => {
// Method to pass templateProps to the render method, without rendering immediately
function setRenderValue(obj: { [key: string]: any }): void;
function setRenderValue(key: string, value: any): void;
function setRenderValue(key: string | { [key: string]: any }, value?: any): void {
res.locals.templateProps = res.locals.templateProps || {};
if (typeof key === 'object') {
Object.entries(key).forEach(([keyX, valueX]) => res.setRenderValue(keyX, valueX));
} else if (typeof key === 'string') {
res.locals.templateProps[key] = value;
}
}
res.setRenderValue = setRenderValue;
// TODO: idea; viewName could be a kapeta page
res.renderPage = (pageName: string, options: any) => {
const baseUrl = (req.query._kap_basepath ? req.query._kap_basepath : '/').toString();
const webpackAssets: { [key: string]: { js: string | string[]; css: string | string[] } } =
res.locals.webpackAssets;
if (!(pageName in res.locals.webpackAssets)) {
// TODO: nicer error to point to the kapeta pages
throw new Error('Invalid page render, page not found in asset map');
}
res.render(options?.viewName || pageName, {
...res.locals.templateProps,
...options,
baseUrl,
styles: ensureArray(webpackAssets[pageName].css)
.filter((path) => !path.endsWith('.hot-update.css'))
// Replace the auto/ prefix (webpack default) with the dist path
.map((path) => templates.renderStylesheet(req, res, path.replace(/^auto\//, `${distPath}`)))
.join('\n'),
scripts: ensureArray(webpackAssets[pageName].js)
.filter((path) => !path.endsWith('.hot-update.js'))
// Replace the auto/ prefix (webpack default) with the dist path
.map((path) => templates.renderScript(req, res, path.replace(/^auto\//, `${distPath}`)))
.join('\n'),
});
};
next();
});
app.use(
`/${distPath}`,
express.static(distFolder, {
index: false,
immutable: true,
maxAge: 60 * 60 * 24 * 365 * 1000,
fallthrough: false,
})
);
};