Skip to content

Commit

Permalink
feat: --serveProxy: [non-dev] uses webpack proxy config
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed May 29, 2022
1 parent a770c77 commit ab97d56
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 52 deletions.
2 changes: 1 addition & 1 deletion examples/concurrent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"start": "anansi serve --dev ./src/index.tsx",
"start:server": "anansi serve -a --pubPath=/assets/ ./dist-server/App.js",
"start:server": "WEBPACK_PUBLIC_PATH='/assets/' anansi serve ./dist-server/App.js",
"build": "WEBPACK_PUBLIC_PATH='/assets/' webpack --mode=production",
"build:server": "WEBPACK_PUBLIC_PATH='/assets/' webpack --mode=production --target=node --env entrypath=index.server.tsx",
"build:clean": "rm -rf dist && rm -rf dist-server",
Expand Down
15 changes: 8 additions & 7 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,24 @@ anansi add testing
## Running SSR

```bash
Usage: run serve [options] <entrypath>
Usage: anansi serve [options] <entrypath>

runs server for SSR projects

Arguments:
entrypath Path to entrypoint
entrypath Path to entrypoint

Options:
-p, --pubPath <path> Where to serve assets from
-d, --dev Run devserver rather than using previously compiled output
-a, --serveAssets [only prod] also serves client assets
-h, --help display help for command
--pubPath <path> Where to serve assets from
-d, --dev Run devserver rather than using previously compiled output
-a, --serveAssets [non-dev] also serves client assets
-p, --serveProxy [non-dev] uses webpack proxy config
-h, --help display help for command
```

```json
{
"start": "anansi serve --dev ./src/index.tsx",
"start:server": "anansi serve -a --pubPath=/assets/ ./dist-server/App.js",
"start:server": "anansi serve ./dist-server/App.js",
}
```
5 changes: 3 additions & 2 deletions packages/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,13 @@ program
.command('serve')
.description('runs server for SSR projects')
.argument('<entrypath>', 'Path to entrypoint')
.option('-p, --pubPath <path>', 'Where to serve assets from')
.option('--pubPath <path>', 'Where to serve assets from')
.option(
'-d, --dev',
'Run devserver rather than using previously compiled output',
)
.option('-a, --serveAssets', '[only prod] also serves client assets')
.option('-a, --serveAssets', '[non-dev] also serves client assets')
.option('-p, --serveProxy', '[non-dev] uses webpack proxy config')
.action(async (entrypath, options) => {
try {
const { serve, devServe } = await import('@anansi/core/scripts');
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"cross-fetch": "^3.1.5",
"fs-monkey": "^1.0.3",
"history": "^5.3.0",
"http-proxy-middleware": "^2.0.6",
"import-fresh": "^3.3.0",
"memfs": "^3.4.1",
"ora": "^5.0.0",
Expand Down
129 changes: 129 additions & 0 deletions packages/core/src/scripts/getProxyMiddlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
RequestHandler,
ProxyConfigArray,
Response,
Request,
NextFunction,
ByPass,
ProxyConfigArrayItem,
ProxyConfigMap,
} from 'webpack-dev-server';

// Essentially taken from https://github.com/webpack/webpack-dev-server/blob/b5e5b67398f97c7a2934e12ebe34fb03cc06c473/lib/Server.js#L2123
export default function getProxyMiddlewares(
proxyConfig: ProxyConfigArrayItem | ProxyConfigMap | ProxyConfigArray,
) {
const middlewares: any[] = [];
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { createProxyMiddleware } = require('http-proxy-middleware');

const proxy =
!Array.isArray(proxyConfig) &&
typeof proxyConfig === 'object' &&
Object.keys(proxyConfig).length &&
Object.keys(proxyConfig)[0].startsWith('/')
? Object.entries(proxyConfig).map(([path, v]) => ({ path, ...v }))
: proxyConfig;

const getProxyMiddleware = (
proxyConfig: ProxyConfigArrayItem,
): RequestHandler | undefined => {
// It is possible to use the `bypass` method without a `target` or `router`.
// However, the proxy middleware has no use in this case, and will fail to instantiate.
if (proxyConfig.target) {
const context = proxyConfig.context || proxyConfig.path;

return createProxyMiddleware(/** @type {string} */ context, proxyConfig);
}

if (proxyConfig.router) {
return createProxyMiddleware(proxyConfig);
}
};

/**
* Assume a proxy configuration specified as:
* proxy: [
* {
* context: "value",
* ...options,
* },
* // or:
* function() {
* return {
* context: "context",
* ...options,
* };
* }
* ]
*/
proxy.forEach(proxyConfigOrCallback => {
let proxyMiddleware: RequestHandler | undefined;

let proxyConfig =
typeof proxyConfigOrCallback === 'function'
? proxyConfigOrCallback()
: proxyConfigOrCallback;

proxyMiddleware = getProxyMiddleware(proxyConfig);

/* TODO: figure out how to make this work
if (proxyConfig.ws) {
this.webSocketProxies.push(proxyMiddleware);
}
*/

const handler = async (req: Request, res: Response, next: NextFunction) => {
if (typeof proxyConfigOrCallback === 'function') {
const newProxyConfig = proxyConfigOrCallback(req, res, next);

if (newProxyConfig !== proxyConfig) {
proxyConfig = newProxyConfig;
proxyMiddleware = getProxyMiddleware(proxyConfig);
}
}

// - Check if we have a bypass function defined
// - In case the bypass function is defined we'll retrieve the
// bypassUrl from it otherwise bypassUrl would be null
// TODO remove in the next major in favor `context` and `router` options
const bypassUrl: ByPass | null =
typeof proxyConfig.bypass === 'function'
? await proxyConfig.bypass(req, res, proxyConfig)
: null;

if (typeof bypassUrl === 'boolean') {
// skip the proxy
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
req.url = null;
next();
} else if (typeof bypassUrl === 'string') {
// byPass to that url
req.url = bypassUrl;
next();
} else if (proxyMiddleware) {
return proxyMiddleware(req, res, next);
} else {
next();
}
};

middlewares.push({
name: 'http-proxy-middleware',
middleware: handler,
});
// Also forward error requests to the proxy so it can handle them.
middlewares.push({
name: 'http-proxy-middleware-error-handler',
middleware: (
error: Error,
req: Request,
res: Response,
next: NextFunction,
) => handler(req, res, next),
});
});

return middlewares;
}
89 changes: 56 additions & 33 deletions packages/core/src/scripts/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import compress from 'compression';

import 'cross-fetch/polyfill';
import { Render } from './types';
import getProxyMiddlewares from './getProxyMiddlewares';

// run directly from node
if (require.main === module) {
Expand All @@ -25,13 +26,24 @@ if (require.main === module) {

export default function serve(
entrypoint: string,
options: { serveAssets?: boolean } = {},
options: { serveAssets?: boolean; serveProxy?: boolean } = {},
) {
const PORT = process.env.PORT || 8080;

const loader = ora('Initializing').start();

const manifestPath = getManifestPathFromWebpackconfig();
const webpackConfig: (
env: any,
argv: any,
// eslint-disable-next-line @typescript-eslint/no-var-requires
) => webpack.Configuration = require(require.resolve(
// TODO: use normal resolution algorithm to find webpack file
path.join(process.cwd(), 'webpack.config'),
));

const manifestPath = getManifestPathFromWebpackconfig(
webpackConfig({}, { mode: 'production' }),
);

const readFile = promisify(diskFs.readFile);
let server: Server | undefined;
Expand Down Expand Up @@ -69,31 +81,9 @@ export default function serve(
//@ts-ignore
wrappingApp.use(compress());

// SERVER SIDE RENDERING
// eslint-disable-next-line @typescript-eslint/no-var-requires
const render: Render = require(path.join(
process.cwd(),
entrypoint,
)).default;
const handlers = [
handleErrors(async function (req: any, res: any) {
if (req.url.endsWith('favicon.ico')) {
res.statusCode = 404;
res.setHeader('Content-type', 'text/html');
res.send('not found');
return;
}
res.socket.on('error', (error: unknown) => {
console.error('Fatal', error);
});

await render(clientManifest, req, res);
}),
];

// ASSETS
if (options.serveAssets) {
handlers.unshift(
wrappingApp.use(
async (
req: Request | IncomingMessage,
res: any,
Expand All @@ -117,7 +107,7 @@ export default function serve(
res.contentType(filename);
res.send(fileContent);
} catch (e) {
return next(e);
return next();
}
} else {
next();
Expand All @@ -126,7 +116,43 @@ export default function serve(
);
}

wrappingApp.get('/*', ...handlers);
// PROXIES
if (options.serveProxy) {
const devConfig: webpack.Configuration = webpackConfig(
{},
{ mode: 'development' },
);
if (devConfig.devServer?.proxy) {
const middlewares = getProxyMiddlewares(devConfig.devServer?.proxy);
if (middlewares) {
wrappingApp.use(...middlewares.map(({ middleware }) => middleware));
}
}
}

// SERVER SIDE RENDERING
// eslint-disable-next-line @typescript-eslint/no-var-requires
const render: Render = require(path.join(
process.cwd(),
entrypoint,
)).default;

wrappingApp.get(
'/*',
handleErrors(async function (req: any, res: any) {
if (req.url.endsWith('favicon.ico')) {
res.statusCode = 404;
res.setHeader('Content-type', 'text/html');
res.send('not found');
return;
}
res.socket.on('error', (error: unknown) => {
console.error('Fatal', error);
});

await render(clientManifest, req, res);
}),
);

server = wrappingApp
.listen(PORT, () => {
Expand Down Expand Up @@ -167,12 +193,9 @@ export default function serve(
});
}

function getManifestPathFromWebpackconfig() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const webpackConfig: webpack.Configuration = require(require.resolve(
// TODO: use normal resolution algorithm to find webpack file
path.join(process.cwd(), 'webpack.config'),
))({}, { mode: 'production' });
function getManifestPathFromWebpackconfig(
webpackConfig: webpack.Configuration,
) {
const manifestFilename: string =
(
webpackConfig?.plugins?.find(plugin => {
Expand Down
9 changes: 0 additions & 9 deletions packages/core/src/scripts/startDevserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import { BoundRender } from './types';
// run directly from node
if (require.main === module) {
const entrypoint = process.argv[2];
//process.env.WEBPACK_PUBLIC_HOST = `http://localhost:${PORT}`; this breaks compatibility with stackblitz
process.env.WEBPACK_PUBLIC_PATH = '/assets/';

if (!entrypoint) {
console.log(`Usage: start-anansi <entrypoint-file>`);
Expand Down Expand Up @@ -169,13 +167,6 @@ export default function startDevServer(
// write to memory filesystem so we can import
{
...webpackConfigs[0].devServer,
/*client: {
...webpackConfigs[0].devServer?.client,
webSocketURL: {
...webpackConfigs[0].devServer?.client.webSocketURL,
port: 8080,
},
},*/
devMiddleware: {
...webpackConfigs[0]?.devServer?.devMiddleware,
outputFileSystem: {
Expand Down
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ __metadata:
cross-fetch: ^3.1.5
fs-monkey: ^1.0.3
history: ^5.3.0
http-proxy-middleware: ^2.0.6
import-fresh: ^3.3.0
jest: 28.1.0
memfs: ^3.4.1
Expand Down Expand Up @@ -16504,6 +16505,24 @@ __metadata:
languageName: node
linkType: hard

"http-proxy-middleware@npm:^2.0.6":
version: 2.0.6
resolution: "http-proxy-middleware@npm:2.0.6"
dependencies:
"@types/http-proxy": ^1.17.8
http-proxy: ^1.18.1
is-glob: ^4.0.1
is-plain-obj: ^3.0.0
micromatch: ^4.0.2
peerDependencies:
"@types/express": ^4.17.13
peerDependenciesMeta:
"@types/express":
optional: true
checksum: 2ee85bc878afa6cbf34491e972ece0f5be0a3e5c98a60850cf40d2a9a5356e1fc57aab6cff33c1fc37691b0121c3a42602d2b1956c52577e87a5b77b62ae1c3a
languageName: node
linkType: hard

"http-proxy@npm:^1.18.1":
version: 1.18.1
resolution: "http-proxy@npm:1.18.1"
Expand Down

0 comments on commit ab97d56

Please sign in to comment.