mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
10 Commits
@vercel/py
...
trek/zero-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30ada9855e | ||
|
|
bfe60ec497 | ||
|
|
3247bee5f4 | ||
|
|
b0743a166a | ||
|
|
a27b41e841 | ||
|
|
7a0fed970c | ||
|
|
2f461a8b0b | ||
|
|
ec894bdf7f | ||
|
|
009cea6d30 | ||
|
|
1bab21026e |
2
.changeset/fast-sheep-hunt.md
Normal file
2
.changeset/fast-sheep-hunt.md
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
---
|
||||
5
.changeset/heavy-bags-marry.md
Normal file
5
.changeset/heavy-bags-marry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@vercel/next": patch
|
||||
---
|
||||
|
||||
provide `experimentalBypassFor` to Prerender from manifest
|
||||
5
.changeset/old-teachers-dress.md
Normal file
5
.changeset/old-teachers-dress.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@vercel/next": patch
|
||||
---
|
||||
|
||||
next.js: move app route handlers in their own lambda grouping, add flag to use bundled runtime
|
||||
5
.changeset/rich-gifts-sell.md
Normal file
5
.changeset/rich-gifts-sell.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@vercel/remix-builder': patch
|
||||
---
|
||||
|
||||
Fix usage with `bun install`
|
||||
5
.changeset/sharp-bears-scream.md
Normal file
5
.changeset/sharp-bears-scream.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@vercel/frameworks": patch
|
||||
---
|
||||
|
||||
Add `bun install` placeholder
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<!-- Replace the API key with your own, see:
|
||||
https://developers.google.com/maps/documentation/javascript/get-api-key -->
|
||||
<!-- <script async="" defer="" src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB8pf6ZdFQj5qw7rc_HSGrhUwQKfIe9ICw"></script> -->
|
||||
<!-- <script async="" defer="" src="https://maps.googleapis.com/maps/api/js?key=<YOUR_GOOGLE_MAPS_API_KEY>"></script> -->
|
||||
|
||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||
</body>
|
||||
|
||||
@@ -978,6 +978,7 @@ export default class DevServer {
|
||||
// log address without trailing slash to maintain backwards compatibility
|
||||
addressFormatted = addressFormatted.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
this.output.ready(`Available at ${link(addressFormatted)}`);
|
||||
}
|
||||
|
||||
@@ -1450,7 +1451,7 @@ export default class DevServer {
|
||||
);
|
||||
|
||||
const middlewareBody = await middlewareRes.buffer();
|
||||
|
||||
console.error('Here?');
|
||||
if (middlewareRes.status === 500 && middlewareBody.byteLength === 0) {
|
||||
await this.sendError(
|
||||
req,
|
||||
|
||||
@@ -42,7 +42,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `blitz build`',
|
||||
@@ -82,7 +83,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `next build`',
|
||||
@@ -125,7 +127,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `gatsby build`',
|
||||
@@ -214,7 +217,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
value: 'remix build',
|
||||
@@ -252,7 +256,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install` or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
value: 'astro build',
|
||||
@@ -299,7 +304,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `hexo generate`',
|
||||
@@ -334,7 +340,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `npx @11ty/eleventy`',
|
||||
@@ -371,7 +378,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `docusaurus build`',
|
||||
@@ -457,7 +465,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `docusaurus-build`',
|
||||
@@ -508,7 +517,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `preact build`',
|
||||
@@ -555,7 +565,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `solid-start build`',
|
||||
@@ -591,7 +602,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `dojo build`',
|
||||
@@ -651,7 +663,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `ember build`',
|
||||
@@ -696,7 +709,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `vue-cli-service build`',
|
||||
@@ -749,7 +763,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `ng build && scully`',
|
||||
@@ -784,7 +799,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `ng build`',
|
||||
@@ -827,7 +843,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `ng build`',
|
||||
@@ -885,7 +902,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `polymer build`',
|
||||
@@ -944,7 +962,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `rollup -c`',
|
||||
@@ -994,7 +1013,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `svelte-kit build`',
|
||||
@@ -1032,7 +1052,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: 'vite build',
|
||||
@@ -1066,7 +1087,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `react-scripts build`',
|
||||
@@ -1129,7 +1151,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `react-scripts build`',
|
||||
@@ -1188,7 +1211,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `gridsome build`',
|
||||
@@ -1223,7 +1247,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `umi build`',
|
||||
@@ -1267,7 +1292,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `sapper export`',
|
||||
@@ -1302,7 +1328,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `saber build`',
|
||||
@@ -1351,7 +1378,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `stencil build`',
|
||||
@@ -1420,7 +1448,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `nuxt generate`',
|
||||
@@ -1476,7 +1505,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
value: 'yarn rw deploy vercel',
|
||||
@@ -1606,7 +1636,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `brunch build --production`',
|
||||
@@ -1717,7 +1748,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
value: 'shopify hydrogen build',
|
||||
@@ -1753,7 +1785,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `vite build`',
|
||||
@@ -1787,7 +1820,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `vitepress build docs`',
|
||||
@@ -1819,7 +1853,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `vuepress build src`',
|
||||
@@ -1852,7 +1887,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `parcel build`',
|
||||
@@ -1909,7 +1945,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run build` or `sanity build`',
|
||||
@@ -1953,7 +1990,8 @@ export const frameworks = [
|
||||
},
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
value: 'storybook build',
|
||||
@@ -1975,7 +2013,8 @@ export const frameworks = [
|
||||
description: 'No framework or an unoptimized framework.',
|
||||
settings: {
|
||||
installCommand: {
|
||||
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||
placeholder:
|
||||
'`yarn install`, `pnpm install`, `npm install`, or `bun install`',
|
||||
},
|
||||
buildCommand: {
|
||||
placeholder: '`npm run vercel-build` or `npm run build`',
|
||||
|
||||
@@ -65,6 +65,12 @@ const NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION = 'v12.1.7-canary.33';
|
||||
const EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION = 'v12.2.0';
|
||||
const CORRECTED_MANIFESTS_VERSION = 'v12.2.0';
|
||||
|
||||
// related PR: https://github.com/vercel/next.js/pull/52997
|
||||
const BUNDLED_SERVER_NEXT_VERSION = '13.4.20-canary.26';
|
||||
|
||||
const BUNDLED_SERVER_NEXT_PATH =
|
||||
'next/dist/compiled/next-server/server.runtime.prod.js';
|
||||
|
||||
export async function serverBuild({
|
||||
dynamicPages,
|
||||
pagesDir,
|
||||
@@ -305,16 +311,32 @@ export async function serverBuild({
|
||||
let nextServerBuildTrace;
|
||||
let instrumentationHookBuildTrace;
|
||||
|
||||
const useBundledServer =
|
||||
semver.gte(nextVersion, BUNDLED_SERVER_NEXT_VERSION) &&
|
||||
process.env.VERCEL_NEXT_BUNDLED_SERVER;
|
||||
|
||||
if (useBundledServer) {
|
||||
debug('Using bundled Next.js server');
|
||||
}
|
||||
|
||||
const nextServerFile = resolveFrom(
|
||||
projectDir,
|
||||
`${getNextServerPath(nextVersion)}/next-server.js`
|
||||
useBundledServer
|
||||
? BUNDLED_SERVER_NEXT_PATH
|
||||
: `${getNextServerPath(nextVersion)}/next-server.js`
|
||||
);
|
||||
|
||||
try {
|
||||
// leverage next-server trace from build if available
|
||||
nextServerBuildTrace = JSON.parse(
|
||||
await fs.readFile(
|
||||
path.join(entryPath, outputDirectory, 'next-server.js.nft.json'),
|
||||
path.join(
|
||||
entryPath,
|
||||
outputDirectory,
|
||||
useBundledServer
|
||||
? 'next-minimal-server.js.nft.json'
|
||||
: 'next-server.js.nft.json'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
@@ -407,6 +429,7 @@ export async function serverBuild({
|
||||
const apiPages: string[] = [];
|
||||
const nonApiPages: string[] = [];
|
||||
const appRouterPages: string[] = [];
|
||||
const appRouteHandlers: string[] = [];
|
||||
|
||||
lambdaPageKeys.forEach(page => {
|
||||
if (
|
||||
@@ -428,7 +451,11 @@ export async function serverBuild({
|
||||
}
|
||||
|
||||
if (lambdaAppPaths[page]) {
|
||||
appRouterPages.push(page);
|
||||
if (lambdaAppPaths[page].fsPath.endsWith('route.js')) {
|
||||
appRouteHandlers.push(page);
|
||||
} else {
|
||||
appRouterPages.push(page);
|
||||
}
|
||||
} else if (pageMatchesApi(page)) {
|
||||
apiPages.push(page);
|
||||
} else {
|
||||
@@ -596,7 +623,9 @@ export async function serverBuild({
|
||||
)
|
||||
.replace(
|
||||
'__NEXT_SERVER_PATH__',
|
||||
`${getNextServerPath(nextVersion)}/next-server.js`
|
||||
useBundledServer
|
||||
? BUNDLED_SERVER_NEXT_PATH
|
||||
: `${getNextServerPath(nextVersion)}/next-server.js`
|
||||
);
|
||||
|
||||
const appLauncher = launcher.replace(
|
||||
@@ -632,6 +661,7 @@ export async function serverBuild({
|
||||
const mergedPageKeys = [
|
||||
...nonApiPages,
|
||||
...appRouterPages,
|
||||
...appRouteHandlers,
|
||||
...apiPages,
|
||||
...internalPages,
|
||||
];
|
||||
@@ -793,6 +823,10 @@ export async function serverBuild({
|
||||
pageExtensions,
|
||||
});
|
||||
|
||||
for (const group of pageLambdaGroups) {
|
||||
group.isPages = true;
|
||||
}
|
||||
|
||||
const appRouterLambdaGroups = await getPageLambdaGroups({
|
||||
entryPath: projectDir,
|
||||
config,
|
||||
@@ -809,6 +843,22 @@ export async function serverBuild({
|
||||
pageExtensions,
|
||||
});
|
||||
|
||||
const appRouteHandlersLambdaGroups = await getPageLambdaGroups({
|
||||
entryPath: projectDir,
|
||||
config,
|
||||
functionsConfigManifest,
|
||||
pages: appRouteHandlers,
|
||||
prerenderRoutes,
|
||||
pageTraces,
|
||||
compressedPages,
|
||||
tracedPseudoLayer: tracedPseudoLayer.pseudoLayer,
|
||||
initialPseudoLayer,
|
||||
lambdaCompressedByteLimit,
|
||||
initialPseudoLayerUncompressed: uncompressedInitialSize,
|
||||
internalPages,
|
||||
pageExtensions,
|
||||
});
|
||||
|
||||
for (const group of appRouterLambdaGroups) {
|
||||
if (!group.isPrerenders) {
|
||||
group.isStreaming = true;
|
||||
@@ -816,6 +866,14 @@ export async function serverBuild({
|
||||
group.isAppRouter = true;
|
||||
}
|
||||
|
||||
for (const group of appRouteHandlersLambdaGroups) {
|
||||
if (!group.isPrerenders) {
|
||||
group.isStreaming = true;
|
||||
}
|
||||
group.isAppRouter = true;
|
||||
group.isAppRouteHandler = true;
|
||||
}
|
||||
|
||||
const apiLambdaGroups = await getPageLambdaGroups({
|
||||
entryPath: projectDir,
|
||||
config,
|
||||
@@ -857,6 +915,14 @@ export async function serverBuild({
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})),
|
||||
appRouteHandlersLambdaGroups: appRouteHandlersLambdaGroups.map(
|
||||
group => ({
|
||||
pages: group.pages,
|
||||
isPrerender: group.isPrerenders,
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})
|
||||
),
|
||||
nextServerLayerSize: initialPseudoLayer.pseudoLayerBytes,
|
||||
},
|
||||
null,
|
||||
@@ -867,6 +933,7 @@ export async function serverBuild({
|
||||
...pageLambdaGroups,
|
||||
...appRouterLambdaGroups,
|
||||
...apiLambdaGroups,
|
||||
...appRouteHandlersLambdaGroups,
|
||||
];
|
||||
|
||||
await detectLambdaLimitExceeding(
|
||||
|
||||
@@ -29,6 +29,7 @@ const nextServer = new NextServer({
|
||||
minimalMode: true,
|
||||
customServer: false,
|
||||
});
|
||||
|
||||
const requestHandler = nextServer.getRequestHandler();
|
||||
|
||||
module.exports = async (req: IncomingMessage, res: ServerResponse) => {
|
||||
|
||||
@@ -856,6 +856,7 @@ export type NextPrerenderedRoutes = {
|
||||
srcRoute: string | null;
|
||||
initialStatus?: number;
|
||||
initialHeaders?: Record<string, string>;
|
||||
experimentalBypassFor?: HasField;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1084,6 +1085,7 @@ export async function getPrerenderManifest(
|
||||
dataRoute: string | null;
|
||||
initialStatus?: number;
|
||||
initialHeaders?: Record<string, string>;
|
||||
experimentalBypassFor?: HasField;
|
||||
};
|
||||
};
|
||||
dynamicRoutes: {
|
||||
@@ -1177,10 +1179,12 @@ export async function getPrerenderManifest(
|
||||
|
||||
let initialStatus: undefined | number;
|
||||
let initialHeaders: undefined | Record<string, string>;
|
||||
let experimentalBypassFor: undefined | HasField;
|
||||
|
||||
if (manifest.version === 4) {
|
||||
initialStatus = manifest.routes[route].initialStatus;
|
||||
initialHeaders = manifest.routes[route].initialHeaders;
|
||||
experimentalBypassFor = manifest.routes[route].experimentalBypassFor;
|
||||
}
|
||||
|
||||
ret.staticRoutes[route] = {
|
||||
@@ -1192,6 +1196,7 @@ export async function getPrerenderManifest(
|
||||
srcRoute,
|
||||
initialStatus,
|
||||
initialHeaders,
|
||||
experimentalBypassFor,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1391,8 +1396,10 @@ export type LambdaGroup = {
|
||||
memory?: number;
|
||||
maxDuration?: number;
|
||||
isAppRouter?: boolean;
|
||||
isAppRouteHandler?: boolean;
|
||||
isStreaming?: boolean;
|
||||
isPrerenders?: boolean;
|
||||
isPages?: boolean;
|
||||
isApiLambda: boolean;
|
||||
pseudoLayer: PseudoLayer;
|
||||
pseudoLayerBytes: number;
|
||||
@@ -1907,6 +1914,7 @@ export const onPrerenderRoute =
|
||||
let dataRoute: string | null;
|
||||
let initialStatus: number | undefined;
|
||||
let initialHeaders: Record<string, string> | undefined;
|
||||
let experimentalBypassFor: HasField | undefined;
|
||||
|
||||
if (isFallback || isBlocking) {
|
||||
const pr = isFallback
|
||||
@@ -1935,6 +1943,7 @@ export const onPrerenderRoute =
|
||||
dataRoute,
|
||||
initialHeaders,
|
||||
initialStatus,
|
||||
experimentalBypassFor,
|
||||
} = pr);
|
||||
}
|
||||
|
||||
@@ -2175,6 +2184,7 @@ export const onPrerenderRoute =
|
||||
fallback: htmlFsRef,
|
||||
group: prerenderGroup,
|
||||
bypassToken: prerenderManifest.bypassToken,
|
||||
experimentalBypassFor,
|
||||
initialStatus,
|
||||
initialHeaders,
|
||||
sourcePath,
|
||||
|
||||
1
packages/next/test/fixtures/37-bundled-server/.gitignore
vendored
Normal file
1
packages/next/test/fixtures/37-bundled-server/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.vercel
|
||||
5
packages/next/test/fixtures/37-bundled-server/app/app-route/route.js
vendored
Normal file
5
packages/next/test/fixtures/37-bundled-server/app/app-route/route.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export async function GET() {
|
||||
return new Response('hello world from app route')
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
5
packages/next/test/fixtures/37-bundled-server/app/app/page.js
vendored
Normal file
5
packages/next/test/fixtures/37-bundled-server/app/app/page.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function Page() {
|
||||
return <p>hello world from app router</p>
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
10
packages/next/test/fixtures/37-bundled-server/app/layout.js
vendored
Normal file
10
packages/next/test/fixtures/37-bundled-server/app/layout.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function Layout({children}) {
|
||||
return <html>
|
||||
<head>
|
||||
<title>My page</title>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
8
packages/next/test/fixtures/37-bundled-server/index.test.js
vendored
Normal file
8
packages/next/test/fixtures/37-bundled-server/index.test.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const { deployAndTest } = require('../../utils');
|
||||
|
||||
describe(`${__dirname.split(path.sep).pop()}`, () => {
|
||||
it('should deploy and pass probe checks', async () => {
|
||||
await deployAndTest(__dirname);
|
||||
});
|
||||
});
|
||||
15
packages/next/test/fixtures/37-bundled-server/package.json
vendored
Normal file
15
packages/next/test/fixtures/37-bundled-server/package.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "bundled-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "latest",
|
||||
"react-dom": "latest"
|
||||
}
|
||||
}
|
||||
5
packages/next/test/fixtures/37-bundled-server/pages/api/hello.js
vendored
Normal file
5
packages/next/test/fixtures/37-bundled-server/pages/api/hello.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export default async (req, res) => {
|
||||
res.status(200).json({
|
||||
payload: `hello world from api`
|
||||
});
|
||||
};
|
||||
7
packages/next/test/fixtures/37-bundled-server/pages/index.js
vendored
Normal file
7
packages/next/test/fixtures/37-bundled-server/pages/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Home(props) {
|
||||
return `hello world from pages`;
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return {};
|
||||
}
|
||||
24
packages/next/test/fixtures/37-bundled-server/probes.json
vendored
Normal file
24
packages/next/test/fixtures/37-bundled-server/probes.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"probes": [
|
||||
{
|
||||
"path": "/app",
|
||||
"status": 200,
|
||||
"mustContain": "hello world from app router"
|
||||
},
|
||||
{
|
||||
"path": "/app-route",
|
||||
"status": 200,
|
||||
"mustContain": "hello world from app route"
|
||||
},
|
||||
{
|
||||
"path": "/api/hello",
|
||||
"status": 200,
|
||||
"mustContain": "hello world from api"
|
||||
},
|
||||
{
|
||||
"path": "/",
|
||||
"status": 200,
|
||||
"mustContain": "hello world from pages"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/next/test/fixtures/37-bundled-server/vercel.json
vendored
Normal file
7
packages/next/test/fixtures/37-bundled-server/vercel.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"build": {
|
||||
"env": {
|
||||
"VERCEL_NEXT_BUNDLED_SERVER": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ if (parseInt(process.versions.node.split('.')[0], 10) >= 16) {
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(lambdas.size).toBe(4);
|
||||
expect(lambdas.size).toBe(5);
|
||||
|
||||
// RSC, root-level page.js
|
||||
expect(buildResult.output['index']).toBeDefined();
|
||||
|
||||
36
packages/python/dev-server.py
Normal file
36
packages/python/dev-server.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from http.server import HTTPServer
|
||||
import os
|
||||
import sys
|
||||
__HANDLER_CLASS_TEMPLATE
|
||||
|
||||
if __name__ == "__main__":
|
||||
hostName = "localhost"
|
||||
errorMessage = 'Neither `app` nor `handler` defined in serverless function {}. See: https://vercel.com/docs/functions/serverless-functions/runtimes/python'.format(__file__)
|
||||
|
||||
if 'handler' in dir():
|
||||
appOrHandler = handler
|
||||
|
||||
if 'app' in dir():
|
||||
appOrHandler = app
|
||||
|
||||
if not 'appOrHandler' in dir():
|
||||
raise Exception(errorMessage)
|
||||
|
||||
# Port 0 is unix-speak for 'first available port'
|
||||
httpd = HTTPServer((hostName, 0), appOrHandler)
|
||||
serverPort = httpd.socket.getsockname()[1]
|
||||
|
||||
print("Server started http://%s:%s" % (hostName, serverPort))
|
||||
|
||||
fd = os.open("pipe", os.O_RDWR|os.O_CREAT)
|
||||
with os.fdopen(fd, 'w') as fdfile:
|
||||
fdfile.write(str(serverPort))
|
||||
fdfile.close()
|
||||
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
httpd.server_close()
|
||||
print("Server stopped.")
|
||||
@@ -20,6 +20,7 @@
|
||||
"test-e2e": "pnpm test test/integration-*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tootallnate/once": "1.1.2",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "14.18.33",
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
import { join, dirname, basename } from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import execa from 'execa';
|
||||
import fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
const readFile = promisify(fs.readFile);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
import { tmpdir } from 'os';
|
||||
import retry from 'async-retry';
|
||||
import { Readable } from 'stream';
|
||||
import once from '@tootallnate/once';
|
||||
|
||||
import {
|
||||
GlobOptions,
|
||||
BuildOptions,
|
||||
getWriteableDirectory,
|
||||
download,
|
||||
StartDevServerOptions,
|
||||
StartDevServerResult,
|
||||
glob,
|
||||
createLambda,
|
||||
download,
|
||||
getWriteableDirectory,
|
||||
shouldServe,
|
||||
debug,
|
||||
NowBuildError,
|
||||
cloneEnv,
|
||||
} from '@vercel/build-utils';
|
||||
|
||||
import { readFile, writeFile, mkdirp, remove } from 'fs-extra';
|
||||
|
||||
import { GlobOptions, createLambda, NowBuildError } from '@vercel/build-utils';
|
||||
|
||||
import { installRequirement, installRequirementsFile } from './install';
|
||||
import { getLatestPythonVersion, getSupportedPythonVersion } from './version';
|
||||
|
||||
const TMP = tmpdir();
|
||||
|
||||
function isReadable(v: any): v is Readable {
|
||||
return v && v.readable === true;
|
||||
}
|
||||
|
||||
async function pipenvConvert(cmd: string, srcDir: string) {
|
||||
debug('Running pipfile2req...');
|
||||
try {
|
||||
@@ -204,8 +218,10 @@ export const build = async ({
|
||||
: 'node_modules/**',
|
||||
};
|
||||
|
||||
const files = await glob('**', globOptions);
|
||||
console.log(files);
|
||||
const lambda = await createLambda({
|
||||
files: await glob('**', globOptions),
|
||||
files,
|
||||
handler: `${handlerPyFilename}.vc_handler`,
|
||||
runtime: pythonVersion.runtime,
|
||||
environment: {},
|
||||
@@ -214,6 +230,146 @@ export const build = async ({
|
||||
return { output: lambda };
|
||||
};
|
||||
|
||||
interface PortInfo {
|
||||
port: number;
|
||||
}
|
||||
|
||||
function isPortInfo(v: any): v is PortInfo {
|
||||
return v && typeof v.port === 'number';
|
||||
}
|
||||
|
||||
export interface CancelablePromise<T> extends Promise<T> {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
function waitForPortFile(portFile: string) {
|
||||
const opts = { portFile, canceled: false };
|
||||
const promise = waitForPortFile_(opts) as CancelablePromise<PortInfo | void>;
|
||||
promise.cancel = () => {
|
||||
opts.canceled = true;
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function waitForPortFile_(opts: {
|
||||
portFile: string;
|
||||
canceled: boolean;
|
||||
}): Promise<PortInfo | void> {
|
||||
while (!opts.canceled) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
try {
|
||||
const port = Number(await readFile(opts.portFile, 'ascii'));
|
||||
retry(() => remove(opts.portFile)).catch((err: Error) => {
|
||||
console.error(`Could not delete port file: ${opts.portFile}: ${err}`);
|
||||
});
|
||||
return { port };
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDevServer(
|
||||
serverlessFunctionString: string,
|
||||
dest: string
|
||||
): Promise<void> {
|
||||
const serverTemplate = await readFile(
|
||||
join(__dirname, '../dev-server.py'),
|
||||
'utf8'
|
||||
);
|
||||
const patched = serverTemplate.replace(
|
||||
'__HANDLER_CLASS_TEMPLATE',
|
||||
serverlessFunctionString
|
||||
);
|
||||
|
||||
console.log('Writing vercel-dev-server-main.py to ', dest);
|
||||
await writeFile(join(dest, 'vercel-dev-server-main.py'), patched);
|
||||
}
|
||||
|
||||
export async function startDevServer(
|
||||
opts: StartDevServerOptions
|
||||
): Promise<StartDevServerResult> {
|
||||
opts.config;
|
||||
|
||||
const { entrypoint, workPath, meta = {} } = opts;
|
||||
const { devCacheDir = join(workPath, '.vercel', 'cache') } = meta;
|
||||
const entrypointDir = dirname(entrypoint);
|
||||
entrypointDir;
|
||||
|
||||
const tmp = join(
|
||||
devCacheDir,
|
||||
'python',
|
||||
Math.random().toString(32).substring(2)
|
||||
);
|
||||
const tmpPackage = join(tmp, entrypointDir);
|
||||
await mkdirp(tmpPackage);
|
||||
|
||||
const serverlessFunctionBody = await readFile(join(workPath, entrypoint));
|
||||
await copyDevServer(serverlessFunctionBody, tmpPackage);
|
||||
|
||||
const portFile = join(
|
||||
TMP,
|
||||
`vercel-dev-port-${Math.random().toString(32).substring(2)}`
|
||||
);
|
||||
|
||||
const env = cloneEnv(process.env, meta.env, {
|
||||
VERCEL_DEV_PORT_FILE: portFile,
|
||||
});
|
||||
|
||||
const executable = 'python3';
|
||||
|
||||
// run the dev server
|
||||
debug(`SPAWNING ${executable} CWD=${tmp}`);
|
||||
const child = spawn(executable, ['api/vercel-dev-server-main.py'], {
|
||||
cwd: tmp,
|
||||
env,
|
||||
stdio: ['ignore', 'inherit', 'inherit', 'pipe'],
|
||||
});
|
||||
|
||||
child.on('close', async () => {
|
||||
try {
|
||||
await retry(() => remove(tmp));
|
||||
} catch (err: any) {
|
||||
console.error(`Could not delete tmp directory: ${tmp}: ${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
const portPipe = child.stdio[3];
|
||||
if (!isReadable(portPipe)) {
|
||||
throw new Error('File descriptor 3 is not readable');
|
||||
}
|
||||
|
||||
// // `dev-server.python` writes the ephemeral port number to FD 3 to be consumed here
|
||||
const onPort = new Promise<PortInfo>(resolve => {
|
||||
portPipe.setEncoding('utf8');
|
||||
portPipe.once('data', d => {
|
||||
resolve({ port: Number(d) });
|
||||
});
|
||||
});
|
||||
const onPortFile = waitForPortFile(portFile);
|
||||
const onExit = once.spread<[number, string | null]>(child, 'exit');
|
||||
const result = await Promise.race([onPort, onPortFile, onExit]);
|
||||
onExit.cancel();
|
||||
onPortFile.cancel();
|
||||
|
||||
console.log(`Hosting on http://127.0.0.1:${result.port}`);
|
||||
if (isPortInfo(result)) {
|
||||
return {
|
||||
port: result.port,
|
||||
pid: child.pid,
|
||||
};
|
||||
} else if (Array.isArray(result)) {
|
||||
// Got "exit" event from child process
|
||||
const [exitCode, signal] = result;
|
||||
const reason = signal ? `"${signal}" signal` : `exit code ${exitCode}`;
|
||||
throw new Error(`\`python3 ${entrypoint}\` failed with ${reason}`);
|
||||
} else {
|
||||
throw new Error(`Unexpected result type: ${typeof result}`);
|
||||
}
|
||||
}
|
||||
|
||||
export { shouldServe };
|
||||
|
||||
// internal only - expect breaking changes if other packages depend on these exports
|
||||
|
||||
@@ -278,29 +278,41 @@ export function addDependencies(
|
||||
}
|
||||
const args: string[] = [];
|
||||
|
||||
if (cliType === 'npm' || cliType === 'pnpm') {
|
||||
args.push('install');
|
||||
if (opts.saveDev) {
|
||||
args.push('--save-dev');
|
||||
}
|
||||
} else {
|
||||
// 'yarn'
|
||||
args.push('add');
|
||||
if (opts.saveDev) {
|
||||
args.push('--dev');
|
||||
}
|
||||
const yarnVersion = execSync('yarn -v', { encoding: 'utf8' }).trim();
|
||||
const isYarnV1 = semver.satisfies(yarnVersion, '1');
|
||||
if (isYarnV1) {
|
||||
// Ignoring workspace check is only needed on Yarn v1
|
||||
args.push('--ignore-workspace-root-check');
|
||||
}
|
||||
}
|
||||
switch (cliType) {
|
||||
case 'npm':
|
||||
case 'pnpm':
|
||||
args.push('install');
|
||||
if (opts.saveDev) {
|
||||
args.push('--save-dev');
|
||||
}
|
||||
|
||||
// Don't fail if pnpm is being run at the workspace root
|
||||
if (cliType === 'pnpm' && opts.cwd) {
|
||||
if (existsSync(join(opts.cwd, 'pnpm-workspace.yaml'))) {
|
||||
args.push('--workspace-root');
|
||||
// Don't fail if pnpm is being run at the workspace root
|
||||
if (cliType === 'pnpm' && opts.cwd) {
|
||||
if (existsSync(join(opts.cwd, 'pnpm-workspace.yaml'))) {
|
||||
args.push('--workspace-root');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bun':
|
||||
case 'yarn':
|
||||
args.push('add');
|
||||
if (opts.saveDev) {
|
||||
args.push('--dev');
|
||||
}
|
||||
if (cliType === 'yarn') {
|
||||
const yarnVersion = execSync('yarn -v', { encoding: 'utf8' }).trim();
|
||||
const isYarnV1 = semver.satisfies(yarnVersion, '1');
|
||||
if (isYarnV1) {
|
||||
// Ignoring workspace check is only needed on Yarn v1
|
||||
args.push('--ignore-workspace-root-check');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default: {
|
||||
const n: never = cliType;
|
||||
throw new Error(`Unexpected package manager: ${n}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -1337,6 +1337,9 @@ importers:
|
||||
|
||||
packages/python:
|
||||
devDependencies:
|
||||
'@tootallnate/once':
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2
|
||||
'@types/execa':
|
||||
specifier: ^0.9.0
|
||||
version: 0.9.0
|
||||
|
||||
Reference in New Issue
Block a user