mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 12:57:46 +00:00
[cli] Support root-level Middleware file in vc dev (#7973)
Adds initial support for a root-level `middleware.js` / `middleware.ts` file in the `vercel dev` CLI command. This leverages the existing Edge Function invoking logic in `@vercel/node`'s `startDevServer()` function and applies the necessary response / rewrites / mutations to the HTTP request based on the result of the middleware invocation.
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import ms from 'ms';
|
||||
import url, { URL } from 'url';
|
||||
import http from 'http';
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import fetch from 'node-fetch';
|
||||
import plural from 'pluralize';
|
||||
import rawBody from 'raw-body';
|
||||
import listen from 'async-listen';
|
||||
import minimatch from 'minimatch';
|
||||
import ms from 'ms';
|
||||
import httpProxy from 'http-proxy';
|
||||
import { randomBytes } from 'crypto';
|
||||
import serveHandler from 'serve-handler';
|
||||
@@ -16,11 +17,11 @@ import path, { isAbsolute, basename, dirname, extname, join } from 'path';
|
||||
import once from '@tootallnate/once';
|
||||
import directoryTemplate from 'serve-handler/src/directory';
|
||||
import getPort from 'get-port';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import which from 'which';
|
||||
import npa from 'npm-package-arg';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
import { getVercelIgnore, fileNameSymbol } from '@vercel/client';
|
||||
import {
|
||||
@@ -90,6 +91,7 @@ import {
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import exposeSystemEnvs from './expose-system-envs';
|
||||
import { treeKill } from '../tree-kill';
|
||||
import { nodeHeadersToFetchHeaders } from './headers';
|
||||
|
||||
const frontendRuntimeSet = new Set(
|
||||
frameworkList.map(f => f.useRuntime?.use || '@vercel/static-build')
|
||||
@@ -593,7 +595,7 @@ export default class DevServer {
|
||||
await this.exit();
|
||||
}
|
||||
|
||||
if (warnings && warnings.length > 0) {
|
||||
if (warnings?.length > 0) {
|
||||
warnings.forEach(warning =>
|
||||
this.output.warn(warning.message, null, warning.link, warning.action)
|
||||
);
|
||||
@@ -1337,32 +1339,6 @@ export default class DevServer {
|
||||
return false;
|
||||
};
|
||||
|
||||
/*
|
||||
runDevMiddleware = async (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
) => {
|
||||
const { devMiddlewarePlugins } = await loadCliPlugins(
|
||||
this.cwd,
|
||||
this.output
|
||||
);
|
||||
try {
|
||||
for (let plugin of devMiddlewarePlugins) {
|
||||
const result = await plugin.plugin.runDevMiddleware(req, res, this.cwd);
|
||||
if (result.finished) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { finished: false };
|
||||
} catch (e) {
|
||||
return {
|
||||
finished: true,
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serve project directory as a v2 deployment.
|
||||
*/
|
||||
@@ -1430,12 +1406,45 @@ export default class DevServer {
|
||||
let prevUrl = req.url;
|
||||
let prevHeaders: HttpHeadersConfig = {};
|
||||
|
||||
/*
|
||||
const middlewareResult = await this.runDevMiddleware(req, res);
|
||||
// Run the middleware file, if present, and apply any
|
||||
// mutations to the incoming request based on the
|
||||
// result of the middleware invocation.
|
||||
const middleware = [...this.buildMatches.values()].find(
|
||||
m => m.config?.middleware === true
|
||||
);
|
||||
if (middleware) {
|
||||
let startMiddlewareResult: StartDevServerResult | undefined;
|
||||
// TODO: can we add some caching to prevent (re-)starting
|
||||
// the middleware server for every HTTP request?
|
||||
const { envConfigs, files, devCacheDir, cwd: workPath } = this;
|
||||
try {
|
||||
startMiddlewareResult =
|
||||
await middleware.builderWithPkg.builder.startDevServer?.({
|
||||
files,
|
||||
entrypoint: middleware.entrypoint,
|
||||
workPath,
|
||||
repoRootPath: this.cwd,
|
||||
config: middleware.config || {},
|
||||
meta: {
|
||||
isDev: true,
|
||||
devCacheDir,
|
||||
env: { ...envConfigs.runEnv },
|
||||
buildEnv: { ...envConfigs.buildEnv },
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// `startDevServer()` threw an error. Most likely this means the dev
|
||||
// server process exited before sending the port information message
|
||||
// (missing dependency at runtime, for example).
|
||||
if (err.code === 'ENOENT') {
|
||||
err.message = `Command not found: ${chalk.cyan(
|
||||
err.path,
|
||||
...err.spawnargs
|
||||
)}\nPlease ensure that ${cmd(err.path)} is properly installed`;
|
||||
err.link = 'https://vercel.link/command-not-found';
|
||||
}
|
||||
|
||||
if (middlewareResult) {
|
||||
if (middlewareResult.error) {
|
||||
this.sendError(
|
||||
await this.sendError(
|
||||
req,
|
||||
res,
|
||||
requestId,
|
||||
@@ -1444,23 +1453,105 @@ export default class DevServer {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (middlewareResult.finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (middlewareResult.pathname) {
|
||||
const origUrl = url.parse(req.url || '/', true);
|
||||
origUrl.pathname = middlewareResult.pathname;
|
||||
prevUrl = url.format(origUrl);
|
||||
}
|
||||
if (middlewareResult.query && prevUrl) {
|
||||
const origUrl = url.parse(req.url || '/', true);
|
||||
delete origUrl.search;
|
||||
Object.assign(origUrl.query, middlewareResult.query);
|
||||
prevUrl = url.format(origUrl);
|
||||
if (startMiddlewareResult) {
|
||||
const { port, pid } = startMiddlewareResult;
|
||||
this.devServerPids.add(pid);
|
||||
|
||||
const middlewareReqHeaders = nodeHeadersToFetchHeaders(req.headers);
|
||||
|
||||
// Add the Vercel platform proxy request headers
|
||||
const proxyHeaders = this.getProxyHeaders(req, requestId, true);
|
||||
for (const [name, value] of nodeHeadersToFetchHeaders(proxyHeaders)) {
|
||||
middlewareReqHeaders.set(name, value);
|
||||
}
|
||||
|
||||
try {
|
||||
const middlewareRes = await fetch(
|
||||
`http://127.0.0.1:${port}${parsed.path}`,
|
||||
{
|
||||
headers: middlewareReqHeaders,
|
||||
method: req.method,
|
||||
redirect: 'manual',
|
||||
}
|
||||
);
|
||||
|
||||
if (middlewareRes.status === 500) {
|
||||
await this.sendError(
|
||||
req,
|
||||
res,
|
||||
requestId,
|
||||
'EDGE_FUNCTION_INVOCATION_FAILED',
|
||||
500
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply status code from middleware invocation,
|
||||
// for i.e. redirects or a custom 404 page
|
||||
res.statusCode = middlewareRes.status;
|
||||
|
||||
let rewritePath = '';
|
||||
let contentType = '';
|
||||
let shouldContinue = false;
|
||||
const skipMiddlewareHeaders = new Set([
|
||||
'date',
|
||||
'connection',
|
||||
'content-length',
|
||||
'transfer-encoding',
|
||||
]);
|
||||
for (const [name, value] of middlewareRes.headers) {
|
||||
if (name === 'x-middleware-next') {
|
||||
shouldContinue = value === '1';
|
||||
} else if (name === 'x-middleware-rewrite') {
|
||||
rewritePath = value;
|
||||
shouldContinue = true;
|
||||
} else if (name === 'content-type') {
|
||||
contentType = value;
|
||||
} else if (!skipMiddlewareHeaders.has(name)) {
|
||||
// Any other kind of response header should be included
|
||||
// on both the incoming HTTP request (for when proxying
|
||||
// to another function) and the outgoing HTTP response.
|
||||
res.setHeader(name, value);
|
||||
req.headers[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldContinue) {
|
||||
const middlewareBody = await middlewareRes.buffer();
|
||||
this.setResponseHeaders(res, requestId);
|
||||
if (middlewareBody.length > 0) {
|
||||
res.setHeader('content-length', middlewareBody.length);
|
||||
if (contentType) {
|
||||
res.setHeader('content-type', contentType);
|
||||
}
|
||||
res.end(middlewareBody);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (rewritePath) {
|
||||
// TODO: add validation?
|
||||
debug(`Detected rewrite path from middleware: "${rewritePath}"`);
|
||||
prevUrl = rewritePath;
|
||||
|
||||
// Retain orginal pathname, but override query parameters from the rewrite
|
||||
const beforeRewriteUrl = req.url || '/';
|
||||
const rewriteUrlParsed = url.parse(beforeRewriteUrl, true);
|
||||
delete rewriteUrlParsed.search;
|
||||
rewriteUrlParsed.query = url.parse(rewritePath, true).query;
|
||||
req.url = url.format(rewriteUrlParsed);
|
||||
debug(
|
||||
`Rewrote incoming HTTP URL from "${beforeRewriteUrl}" to "${req.url}"`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this.killBuilderDevServer(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
for (const phase of phases) {
|
||||
statusCode = undefined;
|
||||
@@ -2269,11 +2360,12 @@ async function findBuildMatch(
|
||||
if (!isIndex(match.src)) {
|
||||
return match;
|
||||
} else {
|
||||
// if isIndex === true and ends in .html, we're done. Otherwise, keep searching
|
||||
bestIndexMatch = match;
|
||||
// If isIndex === true and ends in `.html`, we're done.
|
||||
// Otherwise, keep searching.
|
||||
if (extname(match.src) === '.html') {
|
||||
return bestIndexMatch;
|
||||
return match;
|
||||
}
|
||||
bestIndexMatch = match;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2295,6 +2387,13 @@ async function shouldServe(
|
||||
config,
|
||||
builderWithPkg: { builder },
|
||||
} = match;
|
||||
|
||||
// "middleware" file is not served as a regular asset,
|
||||
// instead it gets invoked as part of the routing logic.
|
||||
if (config?.middleware === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cleanSrc = src.endsWith('.html') ? src.slice(0, -5) : src;
|
||||
const trimmedPath = requestPath.endsWith('/')
|
||||
? requestPath.slice(0, -1)
|
||||
|
||||
Reference in New Issue
Block a user