Compare commits

..

7 Commits

Author SHA1 Message Date
Sean Massa
8b3c52b9e8 Publish Stable
- vercel@28.2.4
 - @vercel/frameworks@1.1.5
 - @vercel/fs-detectors@3.1.0
 - @vercel/static-build@1.0.23
2022-09-09 16:28:36 -05:00
Sean Massa
fc74300ad0 [dev] fix middleware rewrites to relative paths that do not exist (#8535)
When using a `rewrite` in middleware to a relative path that does not exist, the logic gets confused and falls back to the original path.

This was caused by two changes that, in combination, caused this behavior.

1. `prevUrl` was created to keep track of route results that point to other routes, but falls back to `req.url` (`prevUrl`'s initial value): [#4033](40e4b69267 (diff-142c93a61d03a1718eb765cd25e5f18d07a95358bb27ce5d5d4da314ee2fa286R1283-R1284))
2. `prevUrl` was reassigned when handling middleware: [#7973](ee1211416f (diff-00ef6e7b63ed4cae298afc2a8c84f49247855a2506270f748e4d3e41da97ad99R1538))

Because `prevUrl` was reset back to `req.url` after the first `phase` (null) was run, the updated `prevUrl` value from middleware was lost. This only matters if the second `phase` ("filesystem") was going to be hit.

Further confusing matters, this was partially fixed by https://github.com/vercel/vercel/pull/8457 because it either returned a proxy pass (which doesn't have this problem) or assigned `req.url` to include the `rewritePath`, which meant that later when `prevUrl` would default to `req.url`, it still had come from `rewritePath`. So, this is fixed for the absolute URL case, but not the relative path case.

Given all that, I think the fix we need is to keep `prevUrl` at its current value when it's not being updated based on the `routeResult`.

---

So, I made that change here. I added a test that exercises this specific behavior.
2022-09-09 20:34:14 +00:00
Andrew Gadzik
28f8a38e00 [fs-detectors] Add the writeFile function to DetectorFilesystem (#8536)
Adds a `writeFile` function to `DetectorFilesystem` that will be used to update the various file cache maps.

**Why is this needed?**

When detecting npm7+ monorepos, we identified a performance improvement where the service can inspect the `package-lock.json` file for workspaces, and reuse the package information for each workspace in framework-detection.

The pseudo code in `vercel/api` will look something like this

For a given lockfile
```json
{
  ...,
  "packages": {
    "": {
      "name": "npm-workspaces",
      "version": "1.0.0",
      "license": "ISC",
      "workspaces": {
        "packages": [
          "apps/*"
        ]
      }
    },
    "apps/admin": {
      "version": "0.1.0",
      "dependencies": {
        "next": "12.2.5",
        "react": "18.2.0",
        "react-dom": "18.2.0"
      },
      "devDependencies": {
        "eslint": "8.23.0",
        "eslint-config-next": "12.2.5"
      }
    },
    ...,
}
```

```ts
// for each projectPath we detect in package-lock.json
// switch the cwd of the fs to the project directory
const projectFs = fs.chdir(projectPath);
// gets the package info from the lockfile
const projectPackageInfo = lockFile.packages[projectPath];
// insert this content into fs cache
await projectFs.writeFile('package.json', projectPackageInfo)
// call detectFramework, which should now have a cached "package.json" file 
const projectFramework = await detectFramework(projectFs);
```

### Related Issues

Related to https://linear.app/vercel/issue/HIT-57/monorepo-detection-api-prevent-rate-limits

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [x] This PR has a concise title and thorough description useful to a reviewer
- [x] Issue from task tracker has a link to this PR
2022-09-09 19:39:05 +00:00
Jacob Ford
c318ce9695 [frameworks] Detect Sanity v3 config (#8540)
Sanity v3 is currently in [developer preview](https://www.sanity.io/blog/sanity-studio-v3-developer-preview), and has deprecated `sanity.json` in the project root, replacing it with `sanity.config.js`. This should detect either.

All commands remain the same in both v2 and v3.

Co-authored-by: Steven <steven@ceriously.com>
2022-09-09 14:59:12 -04:00
Nathan Rajlich
5b36eaacff [cli] Poll the Dev Command server on both IPv4 and IPv6 in vc dev (#8542)
This change will allow the downstream Dev Command server be able to listen on either of `127.0.0.1` for IPv4 or `[::1]` for IPv6.

- Fixes #6639
- Fixes #8511
- Fixes #8121
- Closes #8512
- Closes #8384
2022-09-09 14:57:50 -04:00
Chris Barber
c9f7ca23a8 [tests] Skip tests that won't run on Windows due to 'mkdir -p' and symlinks (#8531)
Found a few tests that won't run on Windows. The project settings override test runs `mkdir -p` which errors. The other two build tests fail because they try to create symlinks.

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
2022-09-08 14:26:30 +00:00
Sean Massa
57e0db0f65 [dev] allow middleware rewrites to change origin in vc dev (#8457)
Middleware rewrites in `vc dev` were not respecting changes to the origin. This PR implements that to match prodution.

---

Paired with: @TooTallNate
2022-09-07 20:30:10 +00:00
17 changed files with 215 additions and 44 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "28.2.3",
"version": "28.2.4",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -50,7 +50,7 @@
"@vercel/redwood": "1.0.23",
"@vercel/remix": "1.0.24",
"@vercel/ruby": "1.3.31",
"@vercel/static-build": "1.0.22",
"@vercel/static-build": "1.0.23",
"update-notifier": "5.1.0"
},
"devDependencies": {
@@ -96,8 +96,8 @@
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.2.4",
"@vercel/frameworks": "1.1.4",
"@vercel/fs-detectors": "3.0.0",
"@vercel/frameworks": "1.1.5",
"@vercel/fs-detectors": "3.1.0",
"@vercel/fun": "1.0.4",
"@vercel/ncc": "0.24.0",
"@zeit/source-map-support": "0.6.2",

View File

@@ -101,6 +101,7 @@ import {
isError,
isSpawnError,
} from '../is-error';
import isURL from './is-url';
import { pickOverrides } from '../projects/project-settings';
const frontendRuntimeSet = new Set(
@@ -152,7 +153,7 @@ export default class DevServer {
private filter: (path: string) => boolean;
private podId: string;
private devProcess?: ChildProcess;
private devProcessPort?: number;
private devProcessOrigin?: string;
private devServerPids: Set<number>;
private originalProjectSettings?: ProjectSettings;
private projectSettings?: ProjectSettings;
@@ -180,11 +181,17 @@ export default class DevServer {
this.caseSensitive = false;
this.apiDir = null;
this.apiExtensions = new Set();
this.proxy = httpProxy.createProxyServer({
changeOrigin: true,
ws: true,
xfwd: true,
});
this.proxy.on('proxyRes', proxyRes => {
// override "server" header, like production
proxyRes.headers['server'] = 'Vercel';
});
this.server = http.createServer(this.devServerHandler);
this.server.timeout = 0; // Disable timeout
this.stopping = false;
@@ -1007,14 +1014,14 @@ export default class DevServer {
// Configure the server to forward WebSocket "upgrade" events to the proxy.
this.server.on('upgrade', async (req, socket, head) => {
await this.startPromise;
if (!this.devProcessPort) {
if (!this.devProcessOrigin) {
this.output.debug(
`Detected "upgrade" event, but closing socket because no frontend dev server is running`
);
socket.destroy();
return;
}
const target = `http://127.0.0.1:${this.devProcessPort}`;
const target = this.devProcessOrigin;
this.output.debug(`Detected "upgrade" event, proxying to ${target}`);
this.proxy.ws(req, socket, head, { target });
});
@@ -1560,15 +1567,35 @@ export default class DevServer {
}
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);
rewriteUrlParsed.search = url.parse(rewritePath).search;
req.url = url.format(rewriteUrlParsed);
if (isURL(rewritePath)) {
const rewriteUrlParsed = new URL(rewritePath);
// `this.address` already has localhost normalized from ip4 and ip6 values
const devServerParsed = new URL(this.address);
if (devServerParsed.origin === rewriteUrlParsed.origin) {
// remove origin, leaving the path
req.url =
rewritePath.slice(rewriteUrlParsed.origin.length) || '/';
prevUrl = req.url;
} else {
// Proxy to absolute URL with different origin
debug(`ProxyPass: ${rewritePath}`);
this.setResponseHeaders(res, requestId);
proxyPass(req, res, rewritePath, this, requestId);
return;
}
} else {
// Retain orginal pathname, but override query parameters from the rewrite
const rewriteUrlParsed = url.parse(beforeRewriteUrl);
rewriteUrlParsed.search = url.parse(rewritePath).search;
req.url = url.format(rewriteUrlParsed);
}
debug(
`Rewrote incoming HTTP URL from "${beforeRewriteUrl}" to "${req.url}"`
);
@@ -1617,12 +1644,16 @@ export default class DevServer {
missRoutes,
phase
);
prevUrl =
routeResult.continue && routeResult.dest
? getReqUrl(routeResult)
: req.url;
prevHeaders =
routeResult.continue && routeResult.headers ? routeResult.headers : {};
if (routeResult.continue) {
if (routeResult.dest) {
prevUrl = getReqUrl(routeResult);
}
if (routeResult.headers) {
prevHeaders = routeResult.headers;
}
}
if (routeResult.isDestUrl) {
// Mix the `routes` result dest query params into the req path
@@ -1795,8 +1826,8 @@ export default class DevServer {
if (!match) {
// If the dev command is started, then proxy to it
if (this.devProcessPort) {
const upstream = `http://127.0.0.1:${this.devProcessPort}`;
if (this.devProcessOrigin) {
const upstream = this.devProcessOrigin;
debug(`Proxying to frontend dev server: ${upstream}`);
// Add the Vercel platform proxy request headers
@@ -1975,7 +2006,7 @@ export default class DevServer {
// - when there is no asset
// - when the asset is not a Lambda (the dev server must take care of all static files)
if (
this.devProcessPort &&
this.devProcessOrigin &&
(!foundAsset || (foundAsset && foundAsset.asset.type !== 'Lambda'))
) {
debug('Proxying to frontend dev server');
@@ -1987,14 +2018,7 @@ export default class DevServer {
}
this.setResponseHeaders(res, requestId);
return proxyPass(
req,
res,
`http://127.0.0.1:${this.devProcessPort}`,
this,
requestId,
false
);
return proxyPass(req, res, this.devProcessOrigin, this, requestId, false);
}
if (!foundAsset) {
@@ -2323,12 +2347,11 @@ export default class DevServer {
p.on('exit', (code, signal) => {
this.output.debug(`Dev command exited with "${signal || code}"`);
this.devProcessPort = undefined;
this.devProcessOrigin = undefined;
});
await checkForPort(port, 1000 * 60 * 5);
this.devProcessPort = port;
const devProcessHost = await checkForPort(port, 1000 * 60 * 5);
this.devProcessOrigin = `http://${devProcessHost}:${port}`;
}
}
@@ -2616,15 +2639,29 @@ function needsBlockingBuild(buildMatch: BuildMatch): boolean {
return typeof builder.shouldServe !== 'function';
}
async function checkForPort(port: number, timeout: number): Promise<void> {
const opts = { host: '127.0.0.1' };
async function checkForPort(port: number, timeout: number): Promise<string> {
let host;
const start = Date.now();
while (!(await isPortReachable(port, opts))) {
while (!(host = await getReachableHostOnPort(port))) {
if (Date.now() - start > timeout) {
throw new Error(`Detecting port ${port} timed out after ${timeout}ms`);
break;
}
await sleep(100);
}
if (!host) {
throw new Error(`Detecting port ${port} timed out after ${timeout}ms`);
}
return host;
}
async function getReachableHostOnPort(port: number): Promise<string | false> {
const optsIpv4 = { host: '127.0.0.1' };
const optsIpv6 = { host: '::1' };
const results = await Promise.all([
isPortReachable(port, optsIpv6).then(r => r && `[${optsIpv6.host}]`),
isPortReachable(port, optsIpv4).then(r => r && optsIpv4.host),
]);
return results.find(Boolean) || false;
}
function filterFrontendBuilds(build: Builder) {

View File

@@ -0,0 +1,7 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request: Request, event: Event) {
return new Response('heyo');
}

View File

@@ -0,0 +1 @@
<h1>Hello from Index</h1>

View File

@@ -0,0 +1,21 @@
export const config = {
runtime: 'experimental-edge',
};
export default async function edge(request: Request, event: Event) {
if (request.url.indexOf('/index.html') > -1) {
return new Response(null, {
headers: {
'x-middleware-rewrite': '/does-not-exist.html',
},
});
}
if (request.url.indexOf('/api/edge') > -1) {
return new Response(null, {
headers: {
'x-middleware-rewrite': '/api/does-not-exist',
},
});
}
}

View File

@@ -1,6 +1,15 @@
export default req => {
const url = new URL(req.url);
const rewriteTo = url.searchParams.get('to');
if (rewriteTo) {
return new Response(null, {
headers: {
'x-middleware-rewrite': rewriteTo,
},
});
}
if (url.pathname === '/') {
// Pass-through "index.html" page
return new Response(null, {

View File

@@ -1,7 +1,6 @@
{
"private": true,
"scripts": {
"dev": "vite --port $PORT",
"build": "vite build",
"serve": "vite preview"
},

View File

@@ -0,0 +1,3 @@
{
"devCommand": "vite --port $PORT --host ::1"
}

View File

@@ -457,9 +457,42 @@ test(
await testPath(200, '/another', '<h1>Another</h1>');
await testPath(200, '/another.html', '<h1>Another</h1>');
await testPath(200, '/foo', '<h1>Another</h1>');
// different origin
await testPath(200, '?to=http://example.com', /Example Domain/);
})
);
test('[vercel dev] Middleware rewrites with same origin', async () => {
const directory = fixture('middleware-rewrite');
const { dev, port, readyResolver } = await testFixture(directory);
try {
dev.unref();
await readyResolver;
let response = await fetch(
`http://localhost:${port}?to=http://localhost:${port}`
);
validateResponseHeaders(response);
expect(response.status).toBe(200);
expect(await response.text()).toMatch(/<h1>Index<\/h1>/);
response = await fetch(
`http://localhost:${port}?to=http://127.0.0.1:${port}`
);
validateResponseHeaders(response);
expect(response.status).toBe(200);
expect(await response.text()).toMatch(/<h1>Index<\/h1>/);
response = await fetch(`http://localhost:${port}?to=http://[::1]:${port}`);
validateResponseHeaders(response);
expect(response.status).toBe(200);
expect(await response.text()).toMatch(/<h1>Index<\/h1>/);
} finally {
await dev.kill('SIGTERM');
}
});
test(
'[vercel dev] Middleware that rewrites with custom query params',
testFixtureStdio('middleware-rewrite-query', async (testPath: any) => {
@@ -477,6 +510,14 @@ test(
})
);
test(
'[vercel dev] Middleware that rewrites to 404s',
testFixtureStdio('middleware-rewrite-404', async (testPath: any) => {
await testPath(404, '/api/edge', /NOT_FOUND/);
await testPath(404, '/index.html', /NOT_FOUND/);
})
);
test(
'[vercel dev] Middleware that redirects',
testFixtureStdio('middleware-redirect', async (testPath: any) => {

View File

@@ -895,6 +895,12 @@ describe('build', () => {
});
it('should apply project settings overrides from "vercel.json"', async () => {
if (process.platform === 'win32') {
// this test runs a build command with `mkdir -p` which is unsupported on Windows
console.log('Skipping test on Windows');
return;
}
const cwd = fixture('project-settings-override');
const output = join(cwd, '.vercel/output');
try {

View File

@@ -28,6 +28,12 @@ describe('importBuilders()', () => {
});
it('should import 3rd party Builders', async () => {
if (process.platform === 'win32') {
// this test creates symlinks which require admin by default on Windows
console.log('Skipping test on Windows');
return;
}
const cwd = await getWriteableDirectory();
try {
const spec = 'vercel-deno@2.0.1';
@@ -46,6 +52,12 @@ describe('importBuilders()', () => {
});
it('should import legacy `@now/build-utils` Builders', async () => {
if (process.platform === 'win32') {
// this test creates symlinks which require admin by default on Windows
console.log('Skipping test on Windows');
return;
}
const cwd = await getWriteableDirectory();
try {
const spec = '@frontity/now@1.2.0';

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "1.1.4",
"version": "1.1.5",
"main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts",
"files": [

View File

@@ -1785,10 +1785,16 @@ export const frameworks = [
website: 'https://www.sanity.io',
envPrefix: 'SANITY_STUDIO_',
detectors: {
every: [
some: [
{
path: 'sanity.json',
},
{
path: 'sanity.config.js',
},
{
path: 'sanity.config.ts',
},
],
},
settings: {

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/fs-detectors",
"version": "3.0.0",
"version": "3.1.0",
"description": "Vercel filesystem detectors",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -19,7 +19,7 @@
"test-unit": "yarn test"
},
"dependencies": {
"@vercel/frameworks": "1.1.4",
"@vercel/frameworks": "1.1.5",
"@vercel/routing-utils": "2.0.2",
"glob": "8.0.3",
"js-yaml": "4.1.0",

View File

@@ -125,4 +125,15 @@ export abstract class DetectorFilesystem {
public chdir = (name: string): DetectorFilesystem => {
return this._chdir(name);
};
/**
* Writes a file to the filesystem cache.
* @param name the name of the file to write
* @param content The content of the file
*/
public writeFile = async (name: string, content: string): Promise<void> => {
this.readFileCache.set(name, Promise.resolve(Buffer.from(content)));
this.fileCache.set(name, Promise.resolve(true));
this.pathCache.set(name, Promise.resolve(true));
};
}

View File

@@ -173,6 +173,24 @@ describe('DetectorFilesystem', () => {
expect(hasPathSpy).not.toHaveBeenCalled();
});
it('should be able to write files', async () => {
const files = {};
const fs = new VirtualFilesystem(files);
const hasPathSpy = jest.spyOn(fs, '_hasPath');
const isFileSpy = jest.spyOn(fs, '_isFile');
const readFileSpy = jest.spyOn(fs, '_readFile');
await fs.writeFile('file.txt', 'Hello World');
expect(await fs.readFile('file.txt')).toEqual(Buffer.from('Hello World'));
expect(await fs.hasPath('file.txt')).toBe(true);
expect(await fs.isFile('file.txt')).toBe(true);
// We expect that the fs returned values from it's caches instead of calling the underlying functions
expect(hasPathSpy).not.toHaveBeenCalled();
expect(isFileSpy).not.toHaveBeenCalled();
expect(readFileSpy).not.toHaveBeenCalled();
});
it('should be able to change directories', async () => {
const nextPackageJson = JSON.stringify({
dependencies: {

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "1.0.22",
"version": "1.0.23",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step",
@@ -37,7 +37,7 @@
"@types/node-fetch": "2.5.4",
"@types/promise-timeout": "1.3.0",
"@vercel/build-utils": "5.4.2",
"@vercel/frameworks": "1.1.4",
"@vercel/frameworks": "1.1.5",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "2.0.2",
"fs-extra": "10.0.0",