Compare commits

...

11 Commits

Author SHA1 Message Date
Steven
bf5cfa9a41 Publish Stable
- @vercel/build-utils@5.5.1
 - vercel@28.4.3
 - @vercel/client@12.2.8
 - @vercel/go@2.2.9
 - @vercel/hydrogen@0.0.22
 - @vercel/next@3.1.30
 - @vercel/node@2.5.19
 - @vercel/python@3.1.18
 - @vercel/redwood@1.0.27
 - @vercel/remix@1.0.28
 - @vercel/ruby@1.3.35
 - @vercel/static-build@1.0.27
2022-09-27 09:39:04 -04:00
JJ Kasper
12121b7a71 [next] Fix build time 404 route with trailingSlash: true (#8627)
This ensures we properly handle trailing slashes in the `notFound: true` build time routes. 

Fixes: [slack thread](https://vercel.slack.com/archives/C03S8ED1DKM/p1663691719703509)
2022-09-27 09:21:49 -04:00
Steven
baa56aed2c [tests] Fix timeout for actions/setup-node (#8639)
Try fixing the timeout again. For example: https://github.com/vercel/vercel/actions/runs/3130757219/jobs/5081381465

- Follow up to #8613 
- Related to https://github.com/actions/cache/issues/810 
- Related to https://github.com/Azure/azure-sdk-for-js/issues/22321
2022-09-26 18:33:43 -04:00
Steven
6f767367e4 [build-utils] Adjust nodejs12.x discontinueDate to Monday (#8638)
The previous date was on a Saturday so lets move it to the following Monday to ensure support tickets are quickly answered.
2022-09-26 22:22:32 +00:00
Sean Massa
0e4124f94c [cli] fix vc bisect off by one (#8399)
Co-authored-by: Nathan Rajlich <n@n8.io>
2022-09-26 14:47:15 -05:00
Sean Massa
30503d0a3f [go] remove unused, breaking watchlist from Go builder (#8633) 2022-09-26 14:31:36 -05:00
Steven
6c9164f67d [cli] Refactor doBuild to return void (#8626)
This PR refactor `doBuild()` to return `void`.

This will prevent accidental bugs like #8623 where an exit code number was returned instead of throwing on error.
2022-09-24 01:25:59 +00:00
Steven
906b7a8f2c [cli] Fix invalid vercel.json config error serialization (#8623)
Follow up to #8622 since we should be throwing errors so the error is correctly serialized to `builds.json`.
2022-09-23 22:08:53 +00:00
Steven
43499b13d8 [cli] Add vercel.json validation to vercel build (#8622)
We were doing this validation in `vercel dev` but not `vercel build`.

This PR adds `vercel.json` validation to `vercel build` too.

Note I am calling this a patch because invalid `vercel.json` was already failing when passed to the API so this allows a nice error message earlier in the process.
2022-09-23 20:33:28 +00:00
Sean Massa
7d6e56670f [cli][dev] Add strict mode to vc dev edge function handlers (#8616)
Add strict mode to `vc dev` edge function handlers. This is behind a flag in production, but that flag has been at 100% for a while. So, it seems safe to include it here unconditionally.

Also remove legal comments.

These changes bring `vc dev` edge function support closer to production.
2022-09-23 14:23:03 +00:00
Sean Massa
dba337f148 [cli][dev] extract edge/serverless handler logic into separate files (#8615) 2022-09-22 16:34:32 -05:00
48 changed files with 754 additions and 633 deletions

View File

@@ -37,8 +37,7 @@ jobs:
- name: Setup Node
if: ${{ steps.check-release.outputs.IS_RELEASE == 'true' }}
uses: actions/setup-node@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: 5 # https://github.com/actions/cache/issues/810
timeout-minutes: 5 # See https://github.com/actions/cache/issues/810
with:
node-version: 14
cache: 'yarn'

View File

@@ -31,8 +31,7 @@ jobs:
with:
go-version: '1.13.15'
- uses: actions/setup-node@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: 5 # https://github.com/actions/cache/issues/810
timeout-minutes: 5 # See https://github.com/actions/cache/issues/810
with:
node-version: ${{ matrix.node }}
cache: 'yarn'

View File

@@ -31,8 +31,7 @@ jobs:
with:
fetch-depth: 2
- uses: actions/setup-node@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: 5 # https://github.com/actions/cache/issues/810
timeout-minutes: 5 # See https://github.com/actions/cache/issues/810
with:
node-version: ${{ matrix.node }}
cache: 'yarn'

View File

@@ -29,8 +29,7 @@ jobs:
with:
go-version: '1.13.15'
- uses: actions/setup-node@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: 5 # https://github.com/actions/cache/issues/810
timeout-minutes: 5 # See https://github.com/actions/cache/issues/810
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
@@ -67,8 +66,7 @@ jobs:
with:
go-version: '1.13.15'
- uses: actions/setup-node@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: 5 # https://github.com/actions/cache/issues/810
timeout-minutes: 5 # See https://github.com/actions/cache/issues/810
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "5.5.0",
"version": "5.5.1",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",

View File

@@ -61,14 +61,14 @@ export function getPrettyError(obj: {
}
return new NowBuildError({
code: 'DEV_VALIDATE_CONFIG',
code: 'INVALID_VERCEL_CONFIG',
message: message,
link: prop ? `${docsUrl}#project/${prop.toLowerCase()}` : docsUrl,
action: 'View Documentation',
});
} catch (e) {
return new NowBuildError({
code: 'DEV_VALIDATE_CONFIG',
code: 'INVALID_VERCEL_CONFIG',
message: `Failed to validate configuration.`,
link: docsUrl,
action: 'View Documentation',

View File

@@ -10,7 +10,7 @@ const allOptions = [
major: 12,
range: '12.x',
runtime: 'nodejs12.x',
discontinueDate: new Date('2022-10-01'),
discontinueDate: new Date('2022-10-03'),
},
{
major: 10,

View File

@@ -434,8 +434,8 @@ it('should warn for deprecated versions, soon to be discontinued', async () => {
expect(warningMessages).toStrictEqual([
'Error: Node.js version 10.x has reached End-of-Life. Deployments created on or after 2021-04-20 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
'Error: Node.js version 10.x has reached End-of-Life. Deployments created on or after 2021-04-20 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.',
'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-10-01 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-10-01 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.',
'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-10-03 will fail to build. Please set "engines": { "node": "16.x" } in your `package.json` file to use Node.js 16.',
'Error: Node.js version 12.x has reached End-of-Life. Deployments created on or after 2022-10-03 will fail to build. Please set Node.js Version to 16.x in your Project Settings to use Node.js 16.',
]);
global.Date.now = realDateNow;

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "28.4.2",
"version": "28.4.3",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -41,16 +41,16 @@
"node": ">= 14"
},
"dependencies": {
"@vercel/build-utils": "5.5.0",
"@vercel/go": "2.2.8",
"@vercel/hydrogen": "0.0.21",
"@vercel/next": "3.1.29",
"@vercel/node": "2.5.18",
"@vercel/python": "3.1.17",
"@vercel/redwood": "1.0.26",
"@vercel/remix": "1.0.27",
"@vercel/ruby": "1.3.34",
"@vercel/static-build": "1.0.26",
"@vercel/build-utils": "5.5.1",
"@vercel/go": "2.2.9",
"@vercel/hydrogen": "0.0.22",
"@vercel/next": "3.1.30",
"@vercel/node": "2.5.19",
"@vercel/python": "3.1.18",
"@vercel/redwood": "1.0.27",
"@vercel/remix": "1.0.28",
"@vercel/ruby": "1.3.35",
"@vercel/static-build": "1.0.27",
"update-notifier": "5.1.0"
},
"devDependencies": {
@@ -95,7 +95,7 @@
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.2.7",
"@vercel/client": "12.2.8",
"@vercel/frameworks": "1.1.6",
"@vercel/fs-detectors": "3.4.0",
"@vercel/fun": "1.0.4",

View File

@@ -2,7 +2,6 @@ import open from 'open';
import boxen from 'boxen';
import execa from 'execa';
import plural from 'pluralize';
import inquirer from 'inquirer';
import { resolve } from 'path';
import chalk, { Chalk } from 'chalk';
import { URLSearchParams, parse } from 'url';
@@ -150,7 +149,9 @@ export default async function main(client: Client): Promise<number> {
if (badDeployment) {
if (badDeployment instanceof Error) {
badDeployment.message += ` "${bad}"`;
badDeployment.message += ` when requesting bad deployment "${normalizeURL(
bad
)}"`;
output.prettyError(badDeployment);
return 1;
}
@@ -165,7 +166,9 @@ export default async function main(client: Client): Promise<number> {
if (goodDeployment) {
if (goodDeployment instanceof Error) {
goodDeployment.message += ` "${good}"`;
goodDeployment.message += ` when requesting good deployment "${normalizeURL(
good
)}"`;
output.prettyError(goodDeployment);
return 1;
}
@@ -226,7 +229,8 @@ export default async function main(client: Client): Promise<number> {
// If we have the "good" deployment in this chunk, then we're done
for (let i = 0; i < newDeployments.length; i++) {
if (newDeployments[i].url === good) {
newDeployments = newDeployments.slice(0, i + 1);
// grab all deployments up until the good one
newDeployments = newDeployments.slice(0, i);
next = undefined;
break;
}
@@ -316,7 +320,7 @@ export default async function main(client: Client): Promise<number> {
if (openEnabled) {
await open(testUrl);
}
const answer = await inquirer.prompt({
const answer = await client.prompt({
type: 'expand',
name: 'action',
message: 'Select an action:',

View File

@@ -25,6 +25,7 @@ import {
MergeRoutesProps,
Route,
} from '@vercel/routing-utils';
import { fileNameSymbol } from '@vercel/client';
import type { VercelConfig } from '@vercel/client';
import pull from './pull';
@@ -54,6 +55,7 @@ import { importBuilders } from '../util/build/import-builders';
import { initCorepack, cleanupCorepack } from '../util/build/corepack';
import { sortBuilders } from '../util/build/sort-builders';
import { toEnumerableError } from '../util/error';
import { validateConfig } from '../util/validate-config';
type BuildResult = BuildResultV2 | BuildResultV3;
@@ -232,7 +234,8 @@ export default async function main(client: Client): Promise<number> {
process.env.VERCEL = '1';
process.env.NOW_BUILDER = '1';
return await doBuild(client, project, buildsJson, cwd, outputDir);
await doBuild(client, project, buildsJson, cwd, outputDir);
return 0;
} catch (err: any) {
output.prettyError(err);
@@ -265,23 +268,36 @@ async function doBuild(
buildsJson: BuildsManifest,
cwd: string,
outputDir: string
): Promise<number> {
): Promise<void> {
const { output } = client;
const workPath = join(cwd, project.settings.rootDirectory || '.');
// Load `package.json` and `vercel.json` files
const [pkg, vercelConfig] = await Promise.all([
const [pkg, vercelConfig, nowConfig] = await Promise.all([
readJSONFile<PackageJson>(join(workPath, 'package.json')),
readJSONFile<VercelConfig>(join(workPath, 'vercel.json')).then(
config => config || readJSONFile<VercelConfig>(join(workPath, 'now.json'))
),
readJSONFile<VercelConfig>(join(workPath, 'vercel.json')),
readJSONFile<VercelConfig>(join(workPath, 'now.json')),
]);
if (pkg instanceof CantParseJSONFile) throw pkg;
if (vercelConfig instanceof CantParseJSONFile) throw vercelConfig;
if (nowConfig instanceof CantParseJSONFile) throw nowConfig;
if (vercelConfig) {
vercelConfig[fileNameSymbol] = 'vercel.json';
} else if (nowConfig) {
nowConfig[fileNameSymbol] = 'now.json';
}
const localConfig = vercelConfig || nowConfig || {};
const validateError = validateConfig(localConfig);
if (validateError) {
throw validateError;
}
const projectSettings = {
...project.settings,
...pickOverrides(vercelConfig || {}),
...pickOverrides(localConfig),
};
// Get a list of source files
@@ -289,12 +305,12 @@ async function doBuild(
normalizePath(relative(workPath, f))
);
const routesResult = getTransformedRoutes(vercelConfig || {});
const routesResult = getTransformedRoutes(localConfig);
if (routesResult.error) {
throw routesResult.error;
}
if (vercelConfig?.builds && vercelConfig.functions) {
if (localConfig.builds && localConfig.functions) {
throw new NowBuildError({
code: 'bad_request',
message:
@@ -303,7 +319,7 @@ async function doBuild(
});
}
let builds = vercelConfig?.builds || [];
let builds = localConfig.builds || [];
let zeroConfigRoutes: Route[] = [];
let isZeroConfig = false;
@@ -318,7 +334,7 @@ async function doBuild(
// Detect the Vercel Builders that will need to be invoked
const detectedBuilders = await detectBuilders(files, pkg, {
...vercelConfig,
...localConfig,
projectSettings,
ignoreBuildScript: true,
featHandleMiss: true,
@@ -395,13 +411,10 @@ async function doBuild(
})
);
buildsJson.builds = Array.from(buildsJsonBuilds.values());
const buildsJsonPath = join(outputDir, 'builds.json');
const writeBuildsJsonPromise = fs.writeJSON(buildsJsonPath, buildsJson, {
await fs.writeJSON(join(outputDir, 'builds.json'), buildsJson, {
spaces: 2,
});
ops.push(writeBuildsJsonPromise);
// The `meta` config property is re-used for each Builder
// invocation so that Builders can share state between
// subsequent entrypoint builds.
@@ -466,7 +479,7 @@ async function doBuild(
build,
builder,
builderPkg,
vercelConfig
localConfig
).then(
override => {
if (override) overrides.push(override);
@@ -475,26 +488,11 @@ async function doBuild(
)
);
} catch (err: any) {
output.prettyError(err);
const writeConfigJsonPromise = fs.writeJSON(
join(outputDir, 'config.json'),
{ version: 3 },
{ spaces: 2 }
);
await Promise.all([writeBuildsJsonPromise, writeConfigJsonPromise]);
const buildJsonBuild = buildsJsonBuilds.get(build);
if (buildJsonBuild) {
buildJsonBuild.error = toEnumerableError(err);
await fs.writeJSON(buildsJsonPath, buildsJson, {
spaces: 2,
});
}
return 1;
throw err;
}
}
@@ -555,150 +553,7 @@ async function doBuild(
builds: builderRoutes,
});
const images = vercelConfig?.images
if (images) {
if (typeof images !== 'object') {
throw new Error(
`vercel.json "images" should be an object received ${typeof images}.`
);
}
if (!Array.isArray(images.domains)) {
throw new Error(
`vercel.json "images.domains" should be an Array received ${typeof images.domains}.`
);
}
if (images.domains.length > 50) {
throw new Error(
`vercel.json "images.domains" exceeds length of 50 received length (${images.domains.length}).`
);
}
const invalidImageDomains = images.domains.filter(
(d: unknown) => typeof d !== 'string'
);
if (invalidImageDomains.length > 0) {
throw new Error(
`vercel.json "images.domains" should be an Array of strings received invalid values (${invalidImageDomains.join(
', '
)}).`
);
}
if (images.remotePatterns) {
if (!Array.isArray(images.remotePatterns)) {
throw new Error(
`vercel.json "images.remotePatterns" should be an Array received ${typeof images.remotePatterns}.`
);
}
if (images.remotePatterns.length > 50) {
throw new Error(
`vercel.json "images.remotePatterns" exceeds length of 50, received length (${images.remotePatterns.length}).`
);
}
const validProps = new Set(['protocol', 'hostname', 'pathname', 'port']);
const requiredProps = ['hostname'];
const invalidPatterns = images.remotePatterns.filter(
(d: unknown) =>
!d ||
typeof d !== 'object' ||
Object.entries(d).some(
([k, v]) => !validProps.has(k) || typeof v !== 'string'
) ||
requiredProps.some(k => !(k in d))
);
if (invalidPatterns.length > 0) {
throw new Error(
`vercel.json "images.remotePatterns" received invalid values:\n${invalidPatterns
.map(item => JSON.stringify(item))
.join(
'\n'
)}\n\nremotePatterns value must follow format { protocol: 'https', hostname: 'example.com', port: '', pathname: '/imgs/**' }.`
);
}
}
if (!Array.isArray(images.sizes)) {
throw new Error(
`vercel.json "images.sizes" should be an Array received ${typeof images.sizes}.`
);
}
if (images.sizes.length < 1 || images.sizes.length > 50) {
throw new Error(
`vercel.json "images.sizes" should be an Array of length between 1 to 50 received length (${images.sizes.length}).`
);
}
const invalidImageSizes = images.sizes.filter((d: unknown) => {
return typeof d !== 'number' || d < 1 || d > 10000;
});
if (invalidImageSizes.length > 0) {
throw new Error(
`vercel.json "images.sizes" should be an Array of numbers that are between 1 and 10000, received invalid values (${invalidImageSizes.join(
', '
)}).`
);
}
if (images.minimumCacheTTL) {
if (
!Number.isInteger(images.minimumCacheTTL) ||
images.minimumCacheTTL < 0
) {
throw new Error(
`vercel.json "images.minimumCacheTTL" should be an integer 0 or more received (${images.minimumCacheTTL}).`
);
}
}
if (images.formats) {
if (!Array.isArray(images.formats)) {
throw new Error(
`vercel.json "images.formats" should be an Array received ${typeof images.formats}.`
);
}
if (images.formats.length < 1 || images.formats.length > 2) {
throw new Error(
`vercel.json "images.formats" must be length 1 or 2, received length (${images.formats.length}).`
);
}
const invalid = images.formats.filter(f => {
return f !== 'image/avif' && f !== 'image/webp';
});
if (invalid.length > 0) {
throw new Error(
`vercel.json "images.formats" should be an Array of mime type strings, received invalid values (${invalid.join(
', '
)}).`
);
}
}
if (
typeof images.dangerouslyAllowSVG !== 'undefined' &&
typeof images.dangerouslyAllowSVG !== 'boolean'
) {
throw new Error(
`vercel.json "images.dangerouslyAllowSVG" should be a boolean received (${images.dangerouslyAllowSVG}).`
);
}
if (
typeof images.contentSecurityPolicy !== 'undefined' &&
typeof images.contentSecurityPolicy !== 'string'
) {
throw new Error(
`vercel.json "images.contentSecurityPolicy" should be a string received ${images.contentSecurityPolicy}`
);
}
}
const mergedImages = mergeImages(images, buildResults.values());
const mergedImages = mergeImages(localConfig.images, buildResults.values());
const mergedWildcard = mergeWildcard(buildResults.values());
const mergedOverrides: Record<string, PathOverride> =
overrides.length > 0 ? Object.assign({}, ...overrides) : undefined;
@@ -724,8 +579,6 @@ async function doBuild(
emoji('success')
)}\n`
);
return 0;
}
function expandBuild(files: string[], build: Builder): Builder[] {

View File

@@ -23,6 +23,7 @@ import type {
} from '../types';
import { sharedPromise } from './promise';
import { APIError } from './errors-ts';
import { normalizeError } from './is-error';
const isSAMLError = (v: any): v is SAMLError => {
return v && v.saml;
@@ -146,10 +147,15 @@ export default class Client extends EventEmitter implements Stdio {
const error = await responseError(res);
if (isSAMLError(error)) {
// A SAML error means the token is expired, or is not
// designated for the requested team, so the user needs
// to re-authenticate
await this.reauthenticate(error);
try {
// A SAML error means the token is expired, or is not
// designated for the requested team, so the user needs
// to re-authenticate
await this.reauthenticate(error);
} catch (reauthError) {
// there's no sense in retrying
return bail(normalizeError(reauthError));
}
} else if (res.status >= 400 && res.status < 500) {
// Any other 4xx should bail without retrying
return bail(error);
@@ -186,7 +192,7 @@ export default class Client extends EventEmitter implements Stdio {
`Failed to re-authenticate for ${bold(error.scope)} scope`
);
}
process.exit(1);
throw error;
}
this.authConfig.token = result.token;

View File

@@ -57,7 +57,7 @@ import { MissingDotenvVarsError } from '../errors-ts';
import cliPkg from '../pkg';
import { getVercelDirectory } from '../projects/link';
import { staticFiles as getFiles } from '../get-files';
import { validateConfig } from './validate';
import { validateConfig } from '../validate-config';
import { devRouter, getRoutesTypes } from './router';
import getMimeType from './mime-type';
import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';

View File

@@ -7,7 +7,7 @@ import {
rewritesSchema,
trailingSlashSchema,
} from '@vercel/routing-utils';
import { VercelConfig } from './types';
import { VercelConfig } from './dev/types';
import {
functionsSchema,
buildsSchema,
@@ -16,6 +16,83 @@ import {
} from '@vercel/build-utils';
import { fileNameSymbol } from '@vercel/client';
const imagesSchema = {
type: 'object',
additionalProperties: false,
required: ['sizes'],
properties: {
contentSecurityPolicy: {
type: 'string',
minLength: 1,
maxLength: 256,
},
dangerouslyAllowSVG: {
type: 'boolean',
},
domains: {
type: 'array',
minItems: 0,
maxItems: 50,
items: {
type: 'string',
minLength: 1,
maxLength: 256,
},
},
formats: {
type: 'array',
minItems: 1,
maxItems: 4,
items: {
enum: ['image/avif', 'image/webp', 'image/jpeg', 'image/png'],
},
},
minimumCacheTTL: {
type: 'integer',
minimum: 1,
maximum: 315360000,
},
remotePatterns: {
type: 'array',
minItems: 0,
maxItems: 50,
items: {
type: 'object',
additionalProperties: false,
required: ['hostname'],
properties: {
protocol: {
enum: ['http', 'https'],
},
hostname: {
type: 'string',
minLength: 1,
maxLength: 256,
},
port: {
type: 'string',
minLength: 1,
maxLength: 5,
},
pathname: {
type: 'string',
minLength: 1,
maxLength: 256,
},
},
},
},
sizes: {
type: 'array',
minItems: 1,
maxItems: 50,
items: {
type: 'number',
},
},
},
};
const vercelConfigSchema = {
type: 'object',
// These are not all possibilities because `vc dev`
@@ -30,6 +107,7 @@ const vercelConfigSchema = {
rewrites: rewritesSchema,
trailingSlash: trailingSlashSchema,
functions: functionsSchema,
images: imagesSchema,
},
};

View File

@@ -3,6 +3,6 @@
"sizes": [256, 384, 600, 1000],
"domains": [],
"minimumCacheTTL": 60,
"formats": ["image/webp", "image/avif"]
"formats": ["image/avif", "image/webp"]
}
}

View File

@@ -0,0 +1,7 @@
{
"orgId": ".",
"projectId": ".",
"settings": {
"framework": null
}
}

View File

@@ -0,0 +1 @@
<h1>Vercel</h1>

View File

@@ -0,0 +1,16 @@
{
"rewrites": [
{
"source": "/one",
"destination": "/api/one"
},
{
"source": "/two",
"destination": "/api/two"
},
{
"src": "/three",
"dest": "/api/three"
}
]
}

View File

@@ -3,20 +3,26 @@ import chance from 'chance';
import { Deployment } from '@vercel/client';
import { client } from './client';
import { Build, User } from '../../src/types';
import type { Request, Response } from 'express';
let deployments = new Map<string, Deployment>();
let deploymentBuilds = new Map<Deployment, Build[]>();
let alreadySetupDeplomentEndpoints = false;
type State = Deployment['readyState'];
export function useDeployment({
creator,
state = 'READY',
createdAt,
}: {
creator: Pick<User, 'id' | 'email' | 'name' | 'username'>;
state?: State;
createdAt?: number;
}) {
const createdAt = Date.now();
setupDeploymentEndpoints();
createdAt = createdAt || Date.now();
const url = new URL(chance().url());
const name = chance().name();
const id = `dpl_${chance().guid()}`;
@@ -99,6 +105,15 @@ export function useDeploymentMissingProjectSettings() {
beforeEach(() => {
deployments = new Map();
deploymentBuilds = new Map();
alreadySetupDeplomentEndpoints = false;
});
function setupDeploymentEndpoints() {
if (alreadySetupDeplomentEndpoints) {
return;
}
alreadySetupDeplomentEndpoints = true;
client.scenario.get('/:version/deployments/:id', (req, res) => {
const { id } = req.params;
@@ -136,8 +151,21 @@ beforeEach(() => {
res.json({ builds });
});
client.scenario.get('/:version/now/deployments', (req, res) => {
const deploymentsList = Array.from(deployments.values());
res.json({ deployments: deploymentsList });
});
});
function handleGetDeployments(req: Request, res: Response) {
const currentDeployments = Array.from(deployments.values()).sort(
(a: Deployment, b: Deployment) => {
// sort in reverse chronological order
return b.createdAt - a.createdAt;
}
);
res.json({
pagination: {
count: currentDeployments.length,
},
deployments: currentDeployments,
});
}
client.scenario.get('/:version/now/deployments', handleGetDeployments);
client.scenario.get('/:version/deployments', handleGetDeployments);
}

View File

@@ -0,0 +1,55 @@
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';
import bisect from '../../../src/commands/bisect';
import { useDeployment } from '../../mocks/deployment';
describe('bisect', () => {
it('should find the bad deployment', async () => {
const user = useUser();
const now = Date.now();
const deployment1 = useDeployment({ creator: user, createdAt: now });
const deployment2 = useDeployment({
creator: user,
createdAt: now + 10000,
});
const deployment3 = useDeployment({
creator: user,
createdAt: now + 20000,
});
// also create an extra deployment before the known good deployment
// to make sure the bisect pool doesn't include it
useDeployment({
creator: user,
createdAt: now - 30000,
});
const bisectPromise = bisect(client);
await expect(client.stderr).toOutput('Specify a URL where the bug occurs:');
client.stdin.write(`https://${deployment3.url}\n`);
await expect(client.stderr).toOutput(
'Specify a URL where the bug does not occur:'
);
client.stdin.write(`https://${deployment1.url}\n`);
await expect(client.stderr).toOutput(
'Specify the URL subpath where the bug occurs:'
);
client.stdin.write('/docs\n');
await expect(client.stderr).toOutput('Bisecting');
await expect(client.stderr).toOutput(
`Deployment URL: https://${deployment2.url}`
);
client.stdin.write('b\n');
await expect(client.stderr).toOutput(
`The first bad deployment is: https://${deployment2.url}`
);
await expect(bisectPromise).resolves.toEqual(0);
});
});

View File

@@ -750,11 +750,22 @@ describe('build', () => {
const errorBuilds = builds.builds.filter((b: any) => 'error' in b);
expect(errorBuilds).toHaveLength(1);
expect(errorBuilds[0].error.name).toEqual('Error');
expect(errorBuilds[0].error.message).toMatch(`TS1005`);
expect(errorBuilds[0].error.message).toMatch(`',' expected.`);
expect(errorBuilds[0].error.hideStackTrace).toEqual(true);
expect(errorBuilds[0].error.code).toEqual('NODE_TYPESCRIPT_ERROR');
expect(errorBuilds[0].error).toEqual({
name: 'Error',
message: expect.stringContaining('TS1005'),
stack: expect.stringContaining('api/typescript.ts'),
hideStackTrace: true,
code: 'NODE_TYPESCRIPT_ERROR',
});
// top level "error" also contains the same error
expect(builds.error).toEqual({
name: 'Error',
message: expect.stringContaining('TS1005'),
stack: expect.stringContaining('api/typescript.ts'),
hideStackTrace: true,
code: 'NODE_TYPESCRIPT_ERROR',
});
// `config.json` contains `version`
const configJson = await fs.readJSON(join(output, 'config.json'));
@@ -920,7 +931,7 @@ describe('build', () => {
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
it('should apply "images" configuration from `vercel.json`', async () => {
const cwd = fixture('images');
const output = join(cwd, '.vercel/output');
@@ -936,7 +947,7 @@ describe('build', () => {
sizes: [256, 384, 600, 1000],
domains: [],
minimumCacheTTL: 60,
formats: ['image/webp', 'image/avif'],
formats: ['image/avif', 'image/webp'],
},
});
} finally {
@@ -945,6 +956,38 @@ describe('build', () => {
}
});
it('should fail with invalid "rewrites" configuration from `vercel.json`', async () => {
const cwd = fixture('invalid-rewrites');
const output = join(cwd, '.vercel/output');
try {
process.chdir(cwd);
const exitCode = await build(client);
expect(exitCode).toEqual(1);
await expect(client.stderr).toOutput(
'Error: Invalid vercel.json - `rewrites[2]` should NOT have additional property `src`. Did you mean `source`?' +
'\n' +
'View Documentation: https://vercel.com/docs/configuration#project/rewrites'
);
const builds = await fs.readJSON(join(output, 'builds.json'));
expect(builds.builds).toBeUndefined();
expect(builds.error).toEqual({
name: 'Error',
message:
'Invalid vercel.json - `rewrites[2]` should NOT have additional property `src`. Did you mean `source`?',
stack: expect.stringContaining('at validateConfig'),
hideStackTrace: true,
code: 'INVALID_VERCEL_CONFIG',
link: 'https://vercel.com/docs/configuration#project/rewrites',
action: 'View Documentation',
});
const configJson = await fs.readJSON(join(output, 'config.json'));
expect(configJson.version).toBe(3);
} finally {
process.chdir(originalCwd);
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
describe('should find packages with different main/module/browser keys', function () {
let output: string;

View File

@@ -1,4 +1,4 @@
import { validateConfig } from '../../../../src/util/dev/validate';
import { validateConfig } from '../../../../src/util/validate-config';
describe('validateConfig', () => {
it('should not error with empty config', async () => {

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "12.2.7",
"version": "12.2.8",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -43,7 +43,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"@vercel/routing-utils": "2.0.2",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",

View File

@@ -46,7 +46,6 @@ interface Analyzed {
found?: boolean;
packageName: string;
functionName: string;
watch: string[];
}
interface PortInfo {
@@ -498,18 +497,8 @@ export async function build({
environment: {},
});
const watch = parsedAnalyzed.watch;
let watchSub: string[] = [];
// if `entrypoint` located in subdirectory
// we will need to concat it with return watch array
if (entrypointArr.length > 1) {
entrypointArr.pop();
watchSub = parsedAnalyzed.watch.map(file => join(...entrypointArr, file));
}
return {
output: lambda,
watch: watch.concat(watchSub),
};
} catch (error) {
debug('Go Builder Error: ' + error);

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/go",
"version": "2.2.8",
"version": "2.2.9",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
@@ -35,7 +35,7 @@
"@types/jest": "28.1.6",
"@types/node-fetch": "^2.3.0",
"@types/tar": "^4.0.0",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"@vercel/ncc": "0.24.0",
"async-retry": "1.3.1",
"execa": "^1.0.0",

View File

@@ -0,0 +1,10 @@
package handler
import (
"fmt"
"net/http"
)
func Handler2(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "from one.go")
}

View File

@@ -0,0 +1,10 @@
package handler
import (
"fmt"
"net/http"
)
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "from two.go")
}

View File

@@ -0,0 +1,3 @@
module handler
go 1.16

View File

@@ -0,0 +1,12 @@
{
"probes": [
{
"path": "/api/one",
"mustContain": "from one.go"
},
{
"path": "/api/two",
"mustContain": "from two.go"
}
]
}

View File

@@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/parser"
@@ -104,84 +103,6 @@ func main() {
if err != nil {
log.Fatal(err)
}
se := string(rf)
var files []string
var relatedFiles []string
// Add entrypoint to watchlist
relFileName, err := filepath.Rel(filepath.Dir(fileName), fileName)
if err != nil {
log.Fatal(err)
}
relatedFiles = append(relatedFiles, relFileName)
// looking for all go files that have export func
// using in entrypoint
err = filepath.Walk(filepath.Dir(fileName), visit(&files))
if err != nil {
log.Fatal(err)
}
// looking related packages
var modPath string
flag.StringVar(&modPath, "modpath", "", "module path")
flag.Parse()
if len(modPath) > 1 {
err = filepath.Walk(modPath, visit(&files))
if err != nil {
log.Fatal(err)
}
}
for _, file := range files {
absFileName, _ := filepath.Abs(fileName)
absFile, _ := filepath.Abs(file)
// if it isn't entrypoint
if absFileName != absFile {
// find all export structs and functions
pf := parse(file)
var exportedDecl []string
ast.Inspect(pf, func(n ast.Node) bool {
switch t := n.(type) {
case *ast.FuncDecl:
if t.Name.IsExported() {
exportedDecl = append(exportedDecl, t.Name.Name)
}
// find variable declarations
case *ast.TypeSpec:
// which are public
if t.Name.IsExported() {
switch t.Type.(type) {
// and are interfaces
case *ast.StructType:
exportedDecl = append(exportedDecl, t.Name.Name)
}
}
}
return true
})
for _, ed := range exportedDecl {
if strings.Contains(se, ed) {
// find relative path of related file
var basePath string
if modPath == "" {
basePath = filepath.Dir(fileName)
} else {
basePath = modPath
}
rel, err := filepath.Rel(basePath, file)
if err != nil {
log.Fatal(err)
}
relatedFiles = append(relatedFiles, rel)
}
}
}
}
parsed := parse(fileName)
offset := parsed.Pos()
@@ -207,7 +128,6 @@ func main() {
analyzed := analyze{
PackageName: parsed.Name.Name,
FuncName: fn.Name.Name,
Watch: unique(relatedFiles),
}
analyzedJSON, _ := json.Marshal(analyzed)
fmt.Print(string(analyzedJSON))
@@ -229,7 +149,6 @@ func main() {
analyzed := analyze{
PackageName: parsed.Name.Name,
FuncName: fn.Name.Name,
Watch: unique(relatedFiles),
}
analyzedJSON, _ := json.Marshal(analyzed)
fmt.Print(string(analyzedJSON))

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/hydrogen",
"version": "0.0.21",
"version": "0.0.22",
"license": "MIT",
"main": "./dist/index.js",
"homepage": "https://vercel.com/docs",
@@ -21,7 +21,7 @@
"devDependencies": {
"@types/jest": "27.5.1",
"@types/node": "*",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"typescript": "4.6.4"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "3.1.29",
"version": "3.1.30",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -44,7 +44,7 @@
"@types/semver": "6.0.0",
"@types/text-table": "0.2.1",
"@types/webpack-sources": "3.2.0",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"@vercel/nft": "0.22.1",
"@vercel/routing-utils": "2.0.2",
"async-sema": "3.0.1",

View File

@@ -1007,7 +1007,7 @@ export async function serverBuild({
currentRouteSrc.length - 1
)}${
currentRouteSrc[currentRouteSrc.length - 2] === '(' ? '' : '|'
}${route})`;
}${route}/?)`;
if (isLastRoute) {
pushRoute(currentRouteSrc);

View File

@@ -0,0 +1,21 @@
export default function Page(props) {
return (
<>
<p>/ssg/[slug]</p>
<p>{JSON.stringify(props)}</p>
</>
);
}
export function getStaticProps() {
return {
notFound: true,
};
}
export function getStaticPaths() {
return {
paths: ['/ssg/first', '/ssg/second'],
fallback: 'blocking',
};
}

View File

@@ -1,7 +1,20 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
"probes": [
{
"path": "/ssg/first/",
"status": 404,
"mustContain": "This page could not be found"
},
{
"path": "/ssg/second/",
"status": 404,
"mustContain": "This page could not be found"
},
{
"path": "/ssg/third/",
"status": 404,
"mustContain": "This page could not be found"
},
{ "path": "/foo/", "status": 200, "mustContain": "foo page" },
{
"fetchOptions": { "redirect": "manual" },

View File

@@ -3,6 +3,13 @@ const fs = require('fs-extra');
const execa = require('execa');
const { join } = require('path');
async function copyToDist(sourcePath, outDir) {
return fs.copyFile(
join(__dirname, sourcePath),
join(outDir, 'edge-functions/edge-handler-template.js')
);
}
async function main() {
const srcDir = join(__dirname, 'src');
const outDir = join(__dirname, 'dist');
@@ -50,6 +57,8 @@ async function main() {
join(outDir, 'index.d.ts'),
join(__dirname, 'test/fixtures/15-helpers/ts/types.d.ts')
);
await copyToDist('src/edge-functions/edge-handler-template.js', outDir);
}
main().catch(err => {

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/node",
"version": "2.5.18",
"version": "2.5.19",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/node-js",
@@ -31,7 +31,7 @@
"dependencies": {
"@edge-runtime/vm": "1.1.0-beta.32",
"@types/node": "*",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"@vercel/node-bridge": "3.0.0",
"@vercel/static-config": "2.0.3",
"edge-runtime": "1.1.0-beta.32",

View File

@@ -70,38 +70,13 @@ if (!process.env.VERCEL_DEV_IS_ESM) {
}
import { createServer, Server, IncomingMessage, ServerResponse } from 'http';
import { Readable } from 'stream';
import type { Bridge } from '@vercel/node-bridge/bridge';
import { getVercelLauncher } from '@vercel/node-bridge/launcher.js';
import { VercelProxyResponse } from '@vercel/node-bridge/types';
import { Config, streamToBuffer, debug } from '@vercel/build-utils';
import exitHook from 'exit-hook';
import { EdgeRuntime, runServer } from 'edge-runtime';
import type { EdgeContext } from '@edge-runtime/vm';
import { Config } from '@vercel/build-utils';
import { getConfig } from '@vercel/static-config';
import { Project } from 'ts-morph';
import esbuild from 'esbuild';
import fetch from 'node-fetch';
import { createEdgeWasmPlugin, WasmAssets } from './edge-wasm-plugin';
const NODE_VERSION_MAJOR = process.version.match(/^v(\d+)\.\d+/)?.[1];
const NODE_VERSION_IDENTIFIER = `node${NODE_VERSION_MAJOR}`;
if (!NODE_VERSION_MAJOR) {
throw new Error(
`Unable to determine current node version: process.version=${process.version}`
);
}
function logError(error: Error) {
console.error(error.message);
if (error.stack) {
// only show the stack trace if debug is enabled
// because it points to internals, not user code
const errorPrefixLength = 'Error: '.length;
const errorMessageLength = errorPrefixLength + error.message.length;
debug(error.stack.substring(errorMessageLength + 1));
}
}
import { logError } from './utils';
import { createEdgeEventHandler } from './edge-functions/edge-handler';
import { createServerlessEventHandler } from './serverless-functions/serverless-handler';
function listen(server: Server, port: number, host: string): Promise<void> {
return new Promise(resolve => {
@@ -111,245 +86,6 @@ function listen(server: Server, port: number, host: string): Promise<void> {
});
}
async function createServerlessEventHandler(
entrypoint: string,
options: { shouldAddHelpers: boolean }
): Promise<(request: IncomingMessage) => Promise<VercelProxyResponse>> {
const launcher = getVercelLauncher({
entrypointPath: entrypoint,
helpersPath: './helpers.js',
shouldAddHelpers: options.shouldAddHelpers,
useRequire,
// not used
bridgePath: '',
sourcemapSupportPath: '',
});
const bridge: Bridge = launcher();
return async function (request: IncomingMessage) {
const body = await rawBody(request);
const event = {
Action: 'Invoke',
body: JSON.stringify({
method: request.method,
path: request.url,
headers: request.headers,
encoding: 'base64',
body: body.toString('base64'),
}),
};
return bridge.launcher(event, {
callbackWaitsForEmptyEventLoop: false,
});
};
}
async function serializeRequest(message: IncomingMessage) {
const bodyBuffer = await streamToBuffer(message);
const body = bodyBuffer.toString('base64');
return JSON.stringify({
url: message.url,
method: message.method,
headers: message.headers,
body,
});
}
async function compileUserCode(
entrypointPath: string,
entrypointLabel: string,
isMiddleware: boolean
): Promise<undefined | { userCode: string; wasmAssets: WasmAssets }> {
const { wasmAssets, plugin: edgeWasmPlugin } = createEdgeWasmPlugin();
try {
const result = await esbuild.build({
// bundling behavior: use globals (like "browser") instead
// of "require" statements for core libraries (like "node")
platform: 'browser',
// target syntax: only use syntax available on the current
// version of node
target: NODE_VERSION_IDENTIFIER,
sourcemap: 'inline',
bundle: true,
plugins: [edgeWasmPlugin],
entryPoints: [entrypointPath],
write: false, // operate in memory
format: 'cjs',
});
const compiledFile = result.outputFiles?.[0];
if (!compiledFile) {
throw new Error(
`Compilation of ${entrypointLabel} produced no output files.`
);
}
const userCode = `
${compiledFile.text};
const isMiddleware = ${isMiddleware};
addEventListener('fetch', async (event) => {
try {
let serializedRequest = await event.request.text();
let requestDetails = JSON.parse(serializedRequest);
let body;
if (requestDetails.method !== 'GET' && requestDetails.method !== 'HEAD') {
body = Uint8Array.from(atob(requestDetails.body), c => c.charCodeAt(0));
}
let requestUrl = requestDetails.headers['x-forwarded-proto'] + '://' + requestDetails.headers['x-forwarded-host'] + requestDetails.url;
let request = new Request(requestUrl, {
headers: requestDetails.headers,
method: requestDetails.method,
body: body
});
event.request = request;
let edgeHandler = module.exports.default;
if (!edgeHandler) {
throw new Error('No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware');
}
let response = await edgeHandler(event.request, event);
if (!response) {
if (isMiddleware) {
// allow empty responses to pass through
response = new Response(null, {
headers: {
'x-middleware-next': '1',
},
});
} else {
throw new Error('Edge Function "${entrypointLabel}" did not return a response.');
}
}
return event.respondWith(response);
} catch (error) {
// we can't easily show a meaningful stack trace
// so, stick to just the error message for now
const msg = error.cause
? (error.message + ': ' + (error.cause.message || error.cause))
: error.message;
event.respondWith(new Response(msg, {
status: 500,
headers: {
'x-vercel-failed': 'edge-wrapper'
}
}));
}
})`;
return { userCode, wasmAssets };
} catch (error) {
// We can't easily show a meaningful stack trace from ncc -> edge-runtime.
// So, stick with just the message for now.
console.error(`Failed to compile user code for edge runtime.`);
logError(error);
return undefined;
}
}
async function createEdgeRuntime(params?: {
userCode: string;
wasmAssets: WasmAssets;
}) {
try {
if (!params) {
return undefined;
}
const wasmBindings = await params.wasmAssets.getContext();
const edgeRuntime = new EdgeRuntime({
initialCode: params.userCode,
extend: (context: EdgeContext) => {
Object.assign(context, {
// This is required for esbuild wrapping logic to resolve
module: {},
// This is required for environment variable access.
// In production, env var access is provided by static analysis
// so that only the used values are available.
process: {
env: process.env,
},
// These are the global bindings for WebAssembly module
...wasmBindings,
});
return context;
},
});
const server = await runServer({ runtime: edgeRuntime });
exitHook(server.close);
return server;
} catch (error) {
// We can't easily show a meaningful stack trace from ncc -> edge-runtime.
// So, stick with just the message for now.
console.error('Failed to instantiate edge runtime.');
logError(error);
return undefined;
}
}
async function createEdgeEventHandler(
entrypointPath: string,
entrypointLabel: string,
isMiddleware: boolean
): Promise<(request: IncomingMessage) => Promise<VercelProxyResponse>> {
const userCode = await compileUserCode(
entrypointPath,
entrypointLabel,
isMiddleware
);
const server = await createEdgeRuntime(userCode);
return async function (request: IncomingMessage) {
if (!server) {
// this error state is already logged, but we have to wait until here to exit the process
// this matches the serverless function bridge launcher's behavior when
// an error is thrown in the function
process.exit(1);
}
const response = await fetch(server.url, {
redirect: 'manual',
method: 'post',
body: await serializeRequest(request),
});
const body = await response.text();
const isUserError =
response.headers.get('x-vercel-failed') === 'edge-wrapper';
if (isUserError && response.status >= 500) {
// this error was "unhandled" from the user code's perspective
console.log(`Unhandled rejection: ${body}`);
// this matches the serverless function bridge launcher's behavior when
// an error is thrown in the function
process.exit(1);
}
return {
statusCode: response.status,
headers: response.headers.raw(),
body,
encoding: 'utf8',
};
};
}
const validRuntimes = ['experimental-edge'];
function parseRuntime(
entrypoint: string,
@@ -388,7 +124,10 @@ async function createEventHandler(
);
}
return createServerlessEventHandler(entrypointPath, options);
return createServerlessEventHandler(entrypointPath, {
shouldAddHelpers: options.shouldAddHelpers,
useRequire,
});
}
let handleEvent: (request: IncomingMessage) => Promise<VercelProxyResponse>;
@@ -425,21 +164,6 @@ async function main() {
}
}
export function rawBody(readable: Readable): Promise<Buffer> {
return new Promise((resolve, reject) => {
let bytes = 0;
const chunks: Buffer[] = [];
readable.on('error', reject);
readable.on('data', chunk => {
chunks.push(chunk);
bytes += chunk.length;
});
readable.on('end', () => {
resolve(Buffer.concat(chunks, bytes));
});
});
}
export async function onDevRequest(
req: IncomingMessage,
res: ServerResponse

View File

@@ -0,0 +1,73 @@
// provided by the edge runtime:
/* global addEventListener Request Response atob */
// provided by our edge handler logic:
/* global IS_MIDDLEWARE ENTRYPOINT_LABEL */
function buildUrl(requestDetails) {
let proto = requestDetails.headers['x-forwarded-proto'];
let host = requestDetails.headers['x-forwarded-host'];
let path = requestDetails.url;
return `${proto}://${host}${path}`;
}
addEventListener('fetch', async event => {
try {
let serializedRequest = await event.request.text();
let requestDetails = JSON.parse(serializedRequest);
let body;
if (requestDetails.method !== 'GET' && requestDetails.method !== 'HEAD') {
body = Uint8Array.from(atob(requestDetails.body), c => c.charCodeAt(0));
}
let request = new Request(buildUrl(requestDetails), {
headers: requestDetails.headers,
method: requestDetails.method,
body: body,
});
event.request = request;
let edgeHandler = module.exports.default;
if (!edgeHandler) {
throw new Error(
'No default export was found. Add a default export to handle requests. Learn more: https://vercel.link/creating-edge-middleware'
);
}
let response = await edgeHandler(event.request, event);
if (!response) {
if (IS_MIDDLEWARE) {
// allow empty responses to pass through
response = new Response(null, {
headers: {
'x-middleware-next': '1',
},
});
} else {
throw new Error(
`Edge Function "${ENTRYPOINT_LABEL}" did not return a response.`
);
}
}
return event.respondWith(response);
} catch (error) {
// we can't easily show a meaningful stack trace
// so, stick to just the error message for now
const msg = error.cause
? error.message + ': ' + (error.cause.message || error.cause)
: error.message;
event.respondWith(
new Response(msg, {
status: 500,
headers: {
'x-vercel-failed': 'edge-wrapper',
},
})
);
}
});

View File

@@ -0,0 +1,182 @@
import { IncomingMessage } from 'http';
import { VercelProxyResponse } from '@vercel/node-bridge/types';
import { streamToBuffer } from '@vercel/build-utils';
import exitHook from 'exit-hook';
import { EdgeRuntime, runServer } from 'edge-runtime';
import type { EdgeContext } from '@edge-runtime/vm';
import esbuild from 'esbuild';
import fetch from 'node-fetch';
import { createEdgeWasmPlugin, WasmAssets } from './edge-wasm-plugin';
import { logError } from '../utils';
import { readFileSync } from 'fs';
const NODE_VERSION_MAJOR = process.version.match(/^v(\d+)\.\d+/)?.[1];
const NODE_VERSION_IDENTIFIER = `node${NODE_VERSION_MAJOR}`;
if (!NODE_VERSION_MAJOR) {
throw new Error(
`Unable to determine current node version: process.version=${process.version}`
);
}
const edgeHandlerTemplate = readFileSync(
`${__dirname}/edge-handler-template.js`
);
async function serializeRequest(message: IncomingMessage) {
const bodyBuffer = await streamToBuffer(message);
const body = bodyBuffer.toString('base64');
return JSON.stringify({
url: message.url,
method: message.method,
headers: message.headers,
body,
});
}
async function compileUserCode(
entrypointPath: string,
entrypointLabel: string,
isMiddleware: boolean
): Promise<undefined | { userCode: string; wasmAssets: WasmAssets }> {
const { wasmAssets, plugin: edgeWasmPlugin } = createEdgeWasmPlugin();
try {
const result = await esbuild.build({
// bundling behavior: use globals (like "browser") instead
// of "require" statements for core libraries (like "node")
platform: 'browser',
// target syntax: only use syntax available on the current
// version of node
target: NODE_VERSION_IDENTIFIER,
sourcemap: 'inline',
legalComments: 'none',
bundle: true,
plugins: [edgeWasmPlugin],
entryPoints: [entrypointPath],
write: false, // operate in memory
format: 'cjs',
});
const compiledFile = result.outputFiles?.[0];
if (!compiledFile) {
throw new Error(
`Compilation of ${entrypointLabel} produced no output files.`
);
}
const userCode = `
// strict mode
"use strict";var regeneratorRuntime;
// user code
${compiledFile.text};
// request metadata
const IS_MIDDLEWARE = ${isMiddleware};
const ENTRYPOINT_LABEL = '${entrypointLabel}';
// edge handler
${edgeHandlerTemplate}
`;
return { userCode, wasmAssets };
} catch (error) {
// We can't easily show a meaningful stack trace from ncc -> edge-runtime.
// So, stick with just the message for now.
console.error(`Failed to compile user code for edge runtime.`);
logError(error);
return undefined;
}
}
async function createEdgeRuntime(params?: {
userCode: string;
wasmAssets: WasmAssets;
}) {
try {
if (!params) {
return undefined;
}
const wasmBindings = await params.wasmAssets.getContext();
const edgeRuntime = new EdgeRuntime({
initialCode: params.userCode,
extend: (context: EdgeContext) => {
Object.assign(context, {
// This is required for esbuild wrapping logic to resolve
module: {},
// This is required for environment variable access.
// In production, env var access is provided by static analysis
// so that only the used values are available.
process: {
env: process.env,
},
// These are the global bindings for WebAssembly module
...wasmBindings,
});
return context;
},
});
const server = await runServer({ runtime: edgeRuntime });
exitHook(server.close);
return server;
} catch (error) {
// We can't easily show a meaningful stack trace from ncc -> edge-runtime.
// So, stick with just the message for now.
console.error('Failed to instantiate edge runtime.');
logError(error);
return undefined;
}
}
export async function createEdgeEventHandler(
entrypointPath: string,
entrypointLabel: string,
isMiddleware: boolean
): Promise<(request: IncomingMessage) => Promise<VercelProxyResponse>> {
const userCode = await compileUserCode(
entrypointPath,
entrypointLabel,
isMiddleware
);
const server = await createEdgeRuntime(userCode);
return async function (request: IncomingMessage) {
if (!server) {
// this error state is already logged, but we have to wait until here to exit the process
// this matches the serverless function bridge launcher's behavior when
// an error is thrown in the function
process.exit(1);
}
const response = await fetch(server.url, {
redirect: 'manual',
method: 'post',
body: await serializeRequest(request),
});
const body = await response.text();
const isUserError =
response.headers.get('x-vercel-failed') === 'edge-wrapper';
if (isUserError && response.status >= 500) {
// this error was "unhandled" from the user code's perspective
console.log(`Unhandled rejection: ${body}`);
// this matches the serverless function bridge launcher's behavior when
// an error is thrown in the function
process.exit(1);
}
return {
statusCode: response.status,
headers: response.headers.raw(),
body,
encoding: 'utf8',
};
};
}

View File

@@ -0,0 +1,58 @@
import { IncomingMessage } from 'http';
import { Readable } from 'stream';
import type { Bridge } from '@vercel/node-bridge/bridge';
import { getVercelLauncher } from '@vercel/node-bridge/launcher.js';
import { VercelProxyResponse } from '@vercel/node-bridge/types';
function rawBody(readable: Readable): Promise<Buffer> {
return new Promise((resolve, reject) => {
let bytes = 0;
const chunks: Buffer[] = [];
readable.on('error', reject);
readable.on('data', chunk => {
chunks.push(chunk);
bytes += chunk.length;
});
readable.on('end', () => {
resolve(Buffer.concat(chunks, bytes));
});
});
}
export async function createServerlessEventHandler(
entrypoint: string,
options: {
shouldAddHelpers: boolean;
useRequire: boolean;
}
): Promise<(request: IncomingMessage) => Promise<VercelProxyResponse>> {
const launcher = getVercelLauncher({
entrypointPath: entrypoint,
helpersPath: './helpers.js',
shouldAddHelpers: options.shouldAddHelpers,
useRequire: options.useRequire,
// not used
bridgePath: '',
sourcemapSupportPath: '',
});
const bridge: Bridge = launcher();
return async function (request: IncomingMessage) {
const body = await rawBody(request);
const event = {
Action: 'Invoke',
body: JSON.stringify({
method: request.method,
path: request.url,
headers: request.headers,
encoding: 'base64',
body: body.toString('base64'),
}),
};
return bridge.launcher(event, {
callbackWaitsForEmptyEventLoop: false,
});
};
}

View File

@@ -1,5 +1,6 @@
import { extname } from 'path';
import { pathToRegexp } from 'path-to-regexp';
import { debug } from '@vercel/build-utils';
export function getRegExpFromMatchers(matcherOrMatchers: unknown): string {
if (!matcherOrMatchers) {
@@ -55,3 +56,14 @@ export function entrypointToOutputPath(
}
return entrypoint;
}
export function logError(error: Error) {
console.error(error.message);
if (error.stack) {
// only show the stack trace if debug is enabled
// because it points to internals, not user code
const errorPrefixLength = 'Error: '.length;
const errorMessageLength = errorPrefixLength + error.message.length;
debug(error.stack.substring(errorMessageLength + 1));
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/python",
"version": "3.1.17",
"version": "3.1.18",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -22,7 +22,7 @@
"devDependencies": {
"@types/execa": "^0.9.0",
"@types/jest": "27.4.1",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"@vercel/ncc": "0.24.0",
"execa": "^1.0.0",
"typescript": "4.3.4"

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/redwood",
"version": "1.0.26",
"version": "1.0.27",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://vercel.com/docs",
@@ -27,6 +27,6 @@
"@types/aws-lambda": "8.10.19",
"@types/node": "*",
"@types/semver": "6.0.0",
"@vercel/build-utils": "5.5.0"
"@vercel/build-utils": "5.5.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/remix",
"version": "1.0.27",
"version": "1.0.28",
"license": "MIT",
"main": "./dist/index.js",
"homepage": "https://vercel.com/docs",
@@ -25,7 +25,7 @@
"devDependencies": {
"@types/jest": "27.5.1",
"@types/node": "*",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"typescript": "4.6.4"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@vercel/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.3.34",
"version": "1.3.35",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",
@@ -22,7 +22,7 @@
"devDependencies": {
"@types/fs-extra": "8.0.0",
"@types/semver": "6.0.0",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"@vercel/ncc": "0.24.0",
"execa": "2.0.4",
"fs-extra": "^7.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "1.0.26",
"version": "1.0.27",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step",
@@ -36,7 +36,7 @@
"@types/ms": "0.7.31",
"@types/node-fetch": "2.5.4",
"@types/promise-timeout": "1.3.0",
"@vercel/build-utils": "5.5.0",
"@vercel/build-utils": "5.5.1",
"@vercel/frameworks": "1.1.6",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "2.0.2",