Compare commits

..

5 Commits

Author SHA1 Message Date
Vercel Release Bot
d91f3afcbf Version Packages (#11689)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## vercel@34.2.5

### Patch Changes

- Adds a route for the `.rsc` pathname as well when app has ppr enabled
but not all routes.
([#11681](https://github.com/vercel/vercel/pull/11681))

- Updated dependencies
\[[`7457767a7`](7457767a77),
[`4337ea065`](4337ea0654)]:
    -   @vercel/next@4.2.15

## @vercel/fs-detectors@5.2.4

### Patch Changes

- Add support for detecting Turborepo 2
([#11680](https://github.com/vercel/vercel/pull/11680))

## @vercel/next@4.2.15

### Patch Changes

- ensure unmatched action rewrites are routed to correct handler
([#11686](https://github.com/vercel/vercel/pull/11686))

- Adds a route for the `.rsc` pathname as well when app has ppr enabled
but not all routes.
([#11681](https://github.com/vercel/vercel/pull/11681))

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-04 11:29:56 -07:00
Lee Robinson
58d9789e60 examples: Update Astro template. (#11687) 2024-06-04 18:28:11 +00:00
Zack Tanner
7457767a77 [next]: ensure unmatched action rewrites are routed to correct handler (#11686)
User defined rewrites are "normalized" so that our internal rewrites are still properly handled. Before normalizing these rewrites, the Next.js builder will attempt to match server action requests to a`.action` variant. Then the user-defined rewrites flow through the afterFiles normalization ([this part](https://github.com/vercel/vercel/blob/fix/unmatched-action-rewrites/packages/next/src/server-build.ts#L254-L279)) so that when we add `.action` in the builder, we don't drop the suffix. 

But this normalization can lead to a malformed `dest`. e.g., if I had rewrite like this:

```js
{
  source: '/greedy-rewrite/static/:path*',
  destination: '/static/:path*',
}
```

The builder would go through this flow on an action request to `/greedy-rewrite/static`:

1. It'll attempt to match it to a `.action` output, so `/greedy-rewrite/static` -> `/greedy-rewrite/static.action`
2. The afterFiles normalization will take place, so the original `dest` of `/static/$1` will become `/static/$1$rscsuff`
3. $1 will be an empty string, because it doesn't match the existing capture group. So now `/greedy-rewrite/static.action` -> `/greedy-rewrite/static/.action`
4. `static/.action` is not a valid output, so it'll 404 and the action will break. 

Existing handling exists for `.rsc` outputs for a similar reason, but only on the index route. I added a similar fix for this in #11688.
2024-06-04 18:05:11 +00:00
Chris Olszewski
5dedc7b2ce feat(turbo): add support for turbo 2 configuration (#11680)
With the release of Turborepo 2 we're renaming `pipeline` to `tasks`. 

This PR updates the default settings logic to look in `tasks` for a
`build` task definition in addition to looking at `pipeline`. It also
updates the message to no longer mention Turbo configuration in
`package.json` as this is fully ignored in Turborepo 2.

Added a quick unit test to verify `build` task definitions are found in
the `tasks` section of `turbo.json`.

Please let me know if there are other tests/places I should update.
2024-06-04 10:36:35 -07:00
Wyatt Johnson
4337ea0654 [ppr] Add missng .rsc route for non-ppr enabled pages (#11681)
When deploying partial prerendering (PPR), there may some pages that are
not enabled for PPR but still appear in the `prerender-manifest.json`.
Due to the branching of the client router, these routes also have to
have a `.rsc` as well as a `.prefetch.rsc` variants in order to prevent
404's. This change adds support for adding the extra route to the
prerender for pages that have PPR disabled.
2024-06-03 17:52:30 -07:00
31 changed files with 321 additions and 78 deletions

View File

@@ -4,6 +4,12 @@
### Patch Changes
- examples: Update Astro template. ([#11687](https://github.com/vercel/vercel/pull/11687))
## null
### Patch Changes
- chore: update Nuxt example ([#10869](https://github.com/vercel/vercel/pull/10869))
## null

View File

@@ -1,3 +1,6 @@
# astro
.astro
# build output
dist/
.output/
@@ -11,7 +14,6 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production

View File

@@ -3,10 +3,9 @@
This directory is a brief example of an [Astro](https://astro.build/) site that can be deployed to Vercel with zero configuration. This demo showcases:
- `/` - A static page (pre-rendered)
- `/ssr` - A page that uses server-side rendering (through [Vercel Edge Functions](https://vercel.com/docs/functions/edge-functions))
- `/ssr` - A page that uses server-side rendering (through [Vercel Functions](https://vercel.com/docs/functions))
- `/ssr-with-swr-caching` - Similar to the previous page, but also caches the response on the [Vercel Edge Network](https://vercel.com/docs/edge-network/overview) using `cache-control` headers
- `/image` - Astro [Asset](https://docs.astro.build/en/guides/assets/) using Vercel [Image Optimization](https://vercel.com/docs/image-optimization)
- `/edge.json` - An Astro API Endpoint that returns JSON data using [Vercel Edge Functions](https://vercel.com/docs/functions/edge-functions)
- `/image` - Astro [Asset](https://docs.astro.build/en/guides/images/) using Vercel [Image Optimization](https://vercel.com/docs/image-optimization)
Learn more about [Astro on Vercel](https://vercel.com/docs/frameworks/astro).

View File

@@ -1,17 +0,0 @@
import { defineConfig } from 'astro/config';
// Use Vercel Edge Functions (Recommended)
import vercel from '@astrojs/vercel/edge';
// Can also use Serverless Functions
// import vercel from '@astrojs/vercel/serverless';
// Or a completely static build
// import vercel from '@astrojs/vercel/static';
export default defineConfig({
output: 'server',
experimental: {
assets: true
},
adapter: vercel({
imageService: true,
}),
});

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
import vercelServerless from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercelServerless({
imageService: true,
}),
});

View File

@@ -8,9 +8,9 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/vercel": "3.8.2",
"astro": "^2.10.14",
"react": "18.2.0",
"web-vitals": "^3.3.1"
"@astrojs/vercel": "7.6.0",
"astro": "^4.9.2",
"react": "18.3.1",
"web-vitals": "^4.0.1"
}
}

View File

@@ -1,9 +0,0 @@
export async function get() {
return new Response(JSON.stringify({ time: new Date() }), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 's-maxage=10, stale-while-revalidate',
},
});
}

View File

@@ -1,5 +1,14 @@
# vercel
## 34.2.5
### Patch Changes
- Adds a route for the `.rsc` pathname as well when app has ppr enabled but not all routes. ([#11681](https://github.com/vercel/vercel/pull/11681))
- Updated dependencies [[`7457767a7`](https://github.com/vercel/vercel/commit/7457767a77b03662c103a658273a46cf78359068), [`4337ea065`](https://github.com/vercel/vercel/commit/4337ea0654c4ee2c91c4464540f879d43da6696f)]:
- @vercel/next@4.2.15
## 34.2.4
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "34.2.4",
"version": "34.2.5",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -36,7 +36,7 @@
"@vercel/fun": "1.1.0",
"@vercel/go": "3.1.1",
"@vercel/hydrogen": "1.0.2",
"@vercel/next": "4.2.14",
"@vercel/next": "4.2.15",
"@vercel/node": "3.1.6",
"@vercel/python": "4.3.0",
"@vercel/redwood": "2.0.9",
@@ -96,7 +96,7 @@
"@vercel/client": "13.2.8",
"@vercel/error-utils": "2.0.2",
"@vercel/frameworks": "3.0.2",
"@vercel/fs-detectors": "5.2.3",
"@vercel/fs-detectors": "5.2.4",
"@vercel/routing-utils": "3.1.0",
"@vitest/expect": "1.4.0",
"ajv": "6.12.2",

View File

@@ -1,5 +1,11 @@
# @vercel/fs-detectors
## 5.2.4
### Patch Changes
- Add support for detecting Turborepo 2 ([#11680](https://github.com/vercel/vercel/pull/11680))
## 5.2.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/fs-detectors",
"version": "5.2.3",
"version": "5.2.4",
"description": "Vercel filesystem detectors",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@@ -7,10 +7,11 @@ import JSON5 from 'json5';
import semver from 'semver';
export class MissingBuildPipeline extends Error {
constructor() {
super(
'Missing required `build` pipeline in turbo.json or package.json Turbo configuration.'
);
constructor(usesTasks: boolean) {
const message = usesTasks
? 'Missing required `build` task in turbo.json.'
: 'Missing required `build` pipeline in turbo.json or package.json Turbo configuration.';
super(message);
}
}
@@ -65,12 +66,15 @@ export async function getMonorepoDefaultSettings(
]);
let hasBuildPipeline = false;
let hasTurboTasks = false;
let turboSemVer = null;
if (turboJSONBuf !== null) {
const turboJSON = JSON5.parse(turboJSONBuf.toString('utf-8'));
if (turboJSON?.pipeline?.build) {
hasTurboTasks = 'tasks' in (turboJSON || {});
if (turboJSON?.pipeline?.build || turboJSON?.tasks?.build) {
hasBuildPipeline = true;
}
}
@@ -89,7 +93,7 @@ export async function getMonorepoDefaultSettings(
}
if (!hasBuildPipeline) {
throw new MissingBuildPipeline();
throw new MissingBuildPipeline(hasTurboTasks);
}
if (projectPath === '/') {

View File

@@ -0,0 +1,9 @@
{
"private": true,
"workspaces": [
"packages/*"
],
"devDependencies": {
"turbo": "latest"
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "app-14",
"version": "0.0.1",
"main": "index.js"
}

View File

@@ -0,0 +1,2 @@
// TEST COMMENT TO VERIFY JSON5 SUPPORT
{ "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] } } }

View File

@@ -17,11 +17,17 @@ describe('getMonorepoDefaultSettings', () => {
);
});
test('MissingBuildPipeline is an error', () => {
const missingBuildPipeline = new MissingBuildPipeline();
const missingBuildPipeline = new MissingBuildPipeline(false);
expect(missingBuildPipeline).toBeInstanceOf(Error);
expect(missingBuildPipeline.message).toBe(
'Missing required `build` pipeline in turbo.json or package.json Turbo configuration.'
);
const missingBuildTask = new MissingBuildPipeline(true);
expect(missingBuildTask).toBeInstanceOf(Error);
expect(missingBuildTask.message).toBe(
'Missing required `build` task in turbo.json.'
);
});
test.each([
@@ -31,6 +37,7 @@ describe('getMonorepoDefaultSettings', () => {
['turbo-npm', 'turbo', true, 'app-15', false, false],
['turbo-npm-root-proj', 'turbo', true, 'app-root-proj', true, false],
['turbo-latest', 'turbo', false, 'app-14', false, false],
['turbo-2', 'turbo', false, 'app-14', false, false],
['nx', 'nx', false, 'app-12', false, false],
['nx-package-config', 'nx', false, 'app-11', false, false],
['nx-project-and-package-config-1', 'nx', false, 'app-10', false, false],

View File

@@ -1,5 +1,13 @@
# @vercel/next
## 4.2.15
### Patch Changes
- ensure unmatched action rewrites are routed to correct handler ([#11686](https://github.com/vercel/vercel/pull/11686))
- Adds a route for the `.rsc` pathname as well when app has ppr enabled but not all routes. ([#11681](https://github.com/vercel/vercel/pull/11681))
## 4.2.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "4.2.14",
"version": "4.2.15",
"license": "Apache-2.0",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",

View File

@@ -2089,6 +2089,25 @@ export async function serverBuild({
// with that routing section
...afterFilesRewrites,
// Ensure that after we normalize `afterFilesRewrites`, unmatched actions are routed to the correct handler
// e.g. /foo/.action -> /foo.action. This should only ever match in cases where we're routing to an action handler
// and the rewrite normalization led to something like /foo/$1$rscsuff, and $1 had no match.
// This is meant to have parity with the .rsc handling below.
...(hasActionOutputSupport
? [
{
src: `${path.posix.join('/', entryDirectory, '/\\.action$')}`,
dest: `${path.posix.join('/', entryDirectory, '/index.action')}`,
check: true,
},
{
src: `${path.posix.join('/', entryDirectory, '(.+)/\\.action$')}`,
dest: `${path.posix.join('/', entryDirectory, '$1.action')}`,
check: true,
},
]
: []),
// ensure non-normalized /.rsc from rewrites is handled
...(appPathRoutesManifest
? [

View File

@@ -2110,14 +2110,14 @@ export const onPrerenderRoute =
// If enabled, try to get the postponed route information from the file
// system and use it to assemble the prerender.
let prerender: string | undefined;
let postponedPrerender: string | undefined;
if (experimentalPPR && appDir) {
const htmlPath = path.join(appDir, `${routeFileNoExt}.html`);
const metaPath = path.join(appDir, `${routeFileNoExt}.meta`);
if (fs.existsSync(htmlPath) && fs.existsSync(metaPath)) {
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
if ('postponed' in meta && typeof meta.postponed === 'string') {
prerender = meta.postponed;
postponedPrerender = meta.postponed;
// Assign the headers Content-Type header to the prerendered type.
initialHeaders ??= {};
@@ -2127,7 +2127,7 @@ export const onPrerenderRoute =
// Read the HTML file and append it to the prerendered content.
const html = await fs.readFileSync(htmlPath, 'utf8');
prerender += html;
postponedPrerender += html;
}
}
@@ -2144,14 +2144,14 @@ export const onPrerenderRoute =
}
}
if (prerender) {
if (postponedPrerender) {
const contentType = initialHeaders?.['content-type'];
if (!contentType) {
throw new Error("Invariant: contentType can't be undefined");
}
// Assemble the prerendered file.
htmlFsRef = new FileBlob({ contentType, data: prerender });
htmlFsRef = new FileBlob({ contentType, data: postponedPrerender });
} else if (
appDir &&
!dataRoute &&
@@ -2215,7 +2215,14 @@ export const onPrerenderRoute =
? addLocaleOrDefault('/404.html', routesManifest, locale)
: '/404.html'
: isAppPathRoute
? prefetchDataRoute || dataRoute
? // When experimental PPR is enabled, we expect that the data
// that should be served as a part of the prerender should
// be from the prefetch data route. If this isn't enabled
// for ppr, the only way to get the data is from the data
// route.
experimentalPPR
? prefetchDataRoute
: dataRoute
: routeFileNoExt + '.json'
}`
),
@@ -2272,10 +2279,6 @@ export const onPrerenderRoute =
throw new Error('Invariant: expected to find prefetch data route PPR');
}
// When the prefetch data path is available, use it for the prerender,
// otherwise use the data path.
const outputPrerenderPathData = outputPathPrefetchData || outputPathData;
if (isSharedLambdas) {
const outputSrcPathPage = normalizeIndexOutput(
path.join(
@@ -2328,8 +2331,14 @@ export const onPrerenderRoute =
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
if (outputPrerenderPathData) {
prerenders[outputPrerenderPathData] = jsonFsRef;
if (outputPathPrefetchData) {
prerenders[outputPathPrefetchData] = jsonFsRef;
}
// If experimental ppr is not enabled for this route, then add the data
// route as a target for the prerender as well.
if (outputPathData && !experimentalPPR) {
prerenders[outputPathData] = jsonFsRef;
}
}
}
@@ -2465,21 +2474,20 @@ export const onPrerenderRoute =
: {}),
});
if (outputPrerenderPathData) {
let normalizedPathData = outputPrerenderPathData;
const normalizePathData = (pathData: string) => {
if (
(srcRoute === '/' || srcRoute == '/index') &&
outputPrerenderPathData.endsWith(RSC_PREFETCH_SUFFIX)
pathData.endsWith(RSC_PREFETCH_SUFFIX)
) {
delete lambdas[normalizedPathData];
normalizedPathData = normalizedPathData.replace(
/([^/]+\.prefetch\.rsc)$/,
'__$1'
);
delete lambdas[pathData];
return pathData.replace(/([^/]+\.prefetch\.rsc)$/, '__$1');
}
prerenders[normalizedPathData] = new Prerender({
return pathData;
};
if (outputPathData || outputPathPrefetchData) {
const prerender = new Prerender({
expiration: initialRevalidate,
lambda,
allowQuery,
@@ -2500,21 +2508,30 @@ export const onPrerenderRoute =
...initialHeaders,
'content-type': rscContentTypeHeader,
vary: rscVaryHeader,
// If it contains a pre-render, then it was postponed.
...(prerender && rscDidPostponeHeader
...(postponedPrerender && rscDidPostponeHeader
? { [rscDidPostponeHeader]: '1' }
: {}),
},
}
: {}),
});
if (outputPathPrefetchData) {
prerenders[normalizePathData(outputPathPrefetchData)] = prerender;
}
// If experimental ppr is not enabled for this route, then add the data
// route as a target for the prerender as well.
if (outputPathData && !experimentalPPR) {
prerenders[normalizePathData(outputPathData)] = prerender;
}
}
// we need to ensure all prerenders have a matching .rsc output
// otherwise routing could fall through unexpectedly for the
// fallback: false case as it doesn't have a dynamic route
// to catch the `.rsc` request for app -> pages routing
if (outputPrerenderPathData?.endsWith('.json') && appDir) {
if (outputPathData?.endsWith('.json') && appDir) {
const dummyOutput = new FileBlob({
data: '{}',
contentType: 'application/json',

View File

@@ -0,0 +1,25 @@
"use client";
import { useState } from "react";
import { increment } from "../../actions";
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
{count}
<button
onClick={async () => {
const actionResult = await increment(count);
// @ts-ignore
setCount(actionResult);
console.log(actionResult);
}}
>
Trigger
</button>
Static
</div>
);
}

View File

@@ -0,0 +1,25 @@
"use client";
import { useState } from "react";
import { increment } from "./actions";
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
{count}
<button
onClick={async () => {
const actionResult = await increment(count);
// @ts-ignore
setCount(actionResult);
console.log(actionResult);
}}
>
Trigger
</button>
Static
</div>
);
}

View File

@@ -0,0 +1,25 @@
"use client";
import { useState } from "react";
import { increment } from "../actions";
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
{count}
<button
onClick={async () => {
const actionResult = await increment(count);
// @ts-ignore
setCount(actionResult);
console.log(actionResult);
}}
>
Trigger
</button>
Static
</div>
);
}

View File

@@ -293,6 +293,43 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(res.headers.get('x-edge-runtime')).toBe('1');
}
});
it('should work when a rewrite greedy matches an action rewrite', async () => {
const targetPath = `${basePath}/static`;
const canonicalPath = `/greedy-rewrite/${basePath}/static`;
const actionId = findActionId(targetPath, runtime);
const res = await fetch(
`${ctx.deploymentUrl}${canonicalPath}`,
generateFormDataPayload(actionId)
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe(targetPath + '.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
if (runtime === 'node') {
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
} else {
expect(res.headers.get('x-edge-runtime')).toBe('1');
}
});
});
describe('rewrite to index', () => {
it('should work when user has a rewrite to the index route', async () => {
const canonicalPath = '/rewritten-to-index';
const actionId = findActionId('', 'node');
const res = await fetch(
`${ctx.deploymentUrl}${canonicalPath}`,
generateFormDataPayload(actionId)
);
expect(res.status).toEqual(200);
expect(res.headers.get('x-matched-path')).toBe('/index.action');
expect(res.headers.get('content-type')).toBe('text/x-component');
expect(res.headers.get('x-vercel-cache')).toBe('MISS');
});
});
describe('pages', () => {

View File

@@ -9,6 +9,18 @@ module.exports = {
source: '/rewrite/edge/rsc/static',
destination: '/edge/rsc/static',
},
{
source: '/greedy-rewrite/static/:path*',
destination: '/static/:path*',
},
{
source: '/greedy-rewrite/edge/static/:path*',
destination: '/edge/static/:path*',
},
{
source: '/rewritten-to-index',
destination: '/?fromRewrite=1',
},
];
},
};

View File

@@ -4,8 +4,7 @@ const { deployAndTest } = require('../../utils');
const ctx = {};
// TODO: investigate invariant
describe.skip(`${__dirname.split(path.sep).pop()}`, () => {
describe(`${__dirname.split(path.sep).pop()}`, () => {
it('should deploy and pass probe checks', async () => {
const info = await deployAndTest(__dirname);
Object.assign(ctx, info);

View File

@@ -61,6 +61,23 @@
"status": 200,
"mustContain": "sentinel:static"
},
{
"path": "/static",
"status": 200,
"mustContain": "sentinel:static",
"headers": {
"RSC": "1"
}
},
{
"path": "/static",
"status": 200,
"mustContain": "sentinel:static",
"headers": {
"RSC": "1",
"Next-Router-Prefetch": "1"
}
},
{
"path": "/disabled",
"headers": {
@@ -68,7 +85,7 @@
"Next-Router-Prefetch": "1"
},
"status": 200,
"mustContain": "sentinel:dynamic"
"mustNotContain": "sentinel:dynamic"
},
{
"path": "/disabled",

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <p>static page</p>;
}

View File

@@ -349,6 +349,30 @@
"path": "/api/pages-headers",
"status": 200,
"mustContain": "{\"port\":\"443\"}"
},
{
"path": "/static",
"status": 200,
"mustContain": "static page"
},
{
"path": "/static",
"status": 200,
"headers": {
"RSC": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
},
{
"path": "/static",
"status": 200,
"headers": {
"RSC": "1",
"Next-Router-Prefetch": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
}
]
}

View File

@@ -38,7 +38,7 @@
"@vercel/build-utils": "8.2.1",
"@vercel/error-utils": "2.0.2",
"@vercel/frameworks": "3.0.2",
"@vercel/fs-detectors": "5.2.3",
"@vercel/fs-detectors": "5.2.4",
"@vercel/routing-utils": "3.1.0",
"execa": "3.2.0",
"fs-extra": "10.0.0",

6
pnpm-lock.yaml generated
View File

@@ -325,7 +325,7 @@ importers:
specifier: 1.0.2
version: link:../hydrogen
'@vercel/next':
specifier: 4.2.14
specifier: 4.2.15
version: link:../next
'@vercel/node':
specifier: 3.1.6
@@ -500,7 +500,7 @@ importers:
specifier: 3.0.2
version: link:../frameworks
'@vercel/fs-detectors':
specifier: 5.2.3
specifier: 5.2.4
version: link:../fs-detectors
'@vercel/routing-utils':
specifier: 3.1.0
@@ -1566,7 +1566,7 @@ importers:
specifier: 3.0.2
version: link:../frameworks
'@vercel/fs-detectors':
specifier: 5.2.3
specifier: 5.2.4
version: link:../fs-detectors
'@vercel/routing-utils':
specifier: 3.1.0