mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-23 01:49:13 +00:00
Compare commits
25 Commits
@vercel/bu
...
feature/cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a419762690 | ||
|
|
7759f36894 | ||
|
|
4fa050ea25 | ||
|
|
1e229b8bbb | ||
|
|
caa3245fc8 | ||
|
|
23dd29e269 | ||
|
|
4eb4d2b355 | ||
|
|
3590ea06a4 | ||
|
|
314a97b318 | ||
|
|
d41d9e7374 | ||
|
|
80b211fb4a | ||
|
|
ffaf5c9143 | ||
|
|
ba1c2a7e54 | ||
|
|
30a9183836 | ||
|
|
df9accfd6c | ||
|
|
b388357c0b | ||
|
|
90291525c2 | ||
|
|
812dd43b6a | ||
|
|
9e97e0fd58 | ||
|
|
74528c2160 | ||
|
|
82fd2b8068 | ||
|
|
dd94dcab32 | ||
|
|
300e6c6ebb | ||
|
|
cfe6550ac8 | ||
|
|
dfe009ffe2 |
12
.github/workflows/required-pr-label.yml
vendored
12
.github/workflows/required-pr-label.yml
vendored
@@ -10,13 +10,17 @@ jobs:
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let missing = false;
|
||||
const labels = context.payload.pull_request.labels.map(l => l.name);
|
||||
if (labels.filter(l => l.startsWith('area:')).length === 0) {
|
||||
console.error('\u001b[31mMissing label: Please add at least one "area" label.');
|
||||
process.exit(1);
|
||||
console.error('::error::Missing label: Please add at least one "area" label.');
|
||||
missing = true;
|
||||
}
|
||||
if (labels.filter(l => l.startsWith('semver:')).length !== 1) {
|
||||
console.error('\u001b[31mMissing label: Please add exactly one "semver" label.');
|
||||
console.error('::error::Missing label: Please add exactly one "semver" label.');
|
||||
missing = true;
|
||||
}
|
||||
if (missing) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\u001b[32mSuccess: This pull request has correct labels, thanks!');
|
||||
console.log('::notice::Success: This pull request has correct labels, thanks!');
|
||||
|
||||
@@ -35,4 +35,4 @@ For details on how to use Vercel, check out our [documentation](https://vercel.c
|
||||
|
||||
- [Code of Conduct](./.github/CODE_OF_CONDUCT.md)
|
||||
- [Contributing Guidelines](./.github/CONTRIBUTING.md)
|
||||
- [MIT License](./LICENSE)
|
||||
- [Apache 2.0 License](./LICENSE)
|
||||
|
||||
5171
examples/nextjs/package-lock.json
generated
Normal file
5171
examples/nextjs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,12 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "12.3.1",
|
||||
"next": "13.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.23.1",
|
||||
"eslint-config-next": "12.3.1"
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-next": "13.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export default function Home() {
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.card}
|
||||
>
|
||||
<h2>Deploy →</h2>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ export class EdgeFunction {
|
||||
assets?: { name: string; path: string }[];
|
||||
|
||||
/** The regions where the edge function will be executed on */
|
||||
regions?: 'auto' | string[] | 'all' | 'default';
|
||||
regions?: string | string[];
|
||||
|
||||
constructor(params: Omit<EdgeFunction, 'type'>) {
|
||||
this.type = 'EdgeFunction';
|
||||
|
||||
@@ -8,6 +8,8 @@ interface PrerenderOptions {
|
||||
group?: number;
|
||||
bypassToken?: string | null /* optional to be non-breaking change */;
|
||||
allowQuery?: string[];
|
||||
initialHeaders?: Record<string, string>;
|
||||
initialStatus?: number;
|
||||
}
|
||||
|
||||
export class Prerender {
|
||||
@@ -18,6 +20,8 @@ export class Prerender {
|
||||
public group?: number;
|
||||
public bypassToken: string | null;
|
||||
public allowQuery?: string[];
|
||||
public initialHeaders?: Record<string, string>;
|
||||
public initialStatus?: number;
|
||||
|
||||
constructor({
|
||||
expiration,
|
||||
@@ -26,6 +30,8 @@ export class Prerender {
|
||||
group,
|
||||
bypassToken,
|
||||
allowQuery,
|
||||
initialHeaders,
|
||||
initialStatus,
|
||||
}: PrerenderOptions) {
|
||||
this.type = 'Prerender';
|
||||
this.expiration = expiration;
|
||||
@@ -64,6 +70,30 @@ export class Prerender {
|
||||
}
|
||||
this.fallback = fallback;
|
||||
|
||||
if (initialHeaders !== undefined) {
|
||||
if (
|
||||
!initialHeaders ||
|
||||
typeof initialHeaders !== 'object' ||
|
||||
Object.entries(initialHeaders).some(
|
||||
([key, value]) => typeof key !== 'string' || typeof value !== 'string'
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`The \`initialHeaders\` argument for \`Prerender\` must be an object with string key/values`
|
||||
);
|
||||
}
|
||||
this.initialHeaders = initialHeaders;
|
||||
}
|
||||
|
||||
if (initialStatus !== undefined) {
|
||||
if (initialStatus <= 0 || !Number.isInteger(initialStatus)) {
|
||||
throw new Error(
|
||||
`The \`initialStatus\` argument for \`Prerender\` must be a natural number.`
|
||||
);
|
||||
}
|
||||
this.initialStatus = initialStatus;
|
||||
}
|
||||
|
||||
if (allowQuery !== undefined) {
|
||||
if (!Array.isArray(allowQuery)) {
|
||||
throw new Error(
|
||||
|
||||
32
packages/build-utils/test/unit.test.ts
vendored
32
packages/build-utils/test/unit.test.ts
vendored
@@ -433,6 +433,38 @@ it('should warn for deprecated versions, soon to be discontinued', async () => {
|
||||
global.Date.now = realDateNow;
|
||||
});
|
||||
|
||||
it('should support initialHeaders and initialStatus correctly', async () => {
|
||||
const { Prerender } = require('@vercel/build-utils/dist/prerender.js');
|
||||
new Prerender({
|
||||
expiration: 1,
|
||||
fallback: null,
|
||||
group: 1,
|
||||
bypassToken: 'some-long-bypass-token-to-make-it-work',
|
||||
initialHeaders: {
|
||||
'content-type': 'application/json',
|
||||
'x-initial': 'true',
|
||||
},
|
||||
initialStatus: 308,
|
||||
});
|
||||
new Prerender({
|
||||
expiration: 1,
|
||||
fallback: null,
|
||||
group: 1,
|
||||
bypassToken: 'some-long-bypass-token-to-make-it-work',
|
||||
initialStatus: 308,
|
||||
});
|
||||
new Prerender({
|
||||
expiration: 1,
|
||||
fallback: null,
|
||||
group: 1,
|
||||
bypassToken: 'some-long-bypass-token-to-make-it-work',
|
||||
initialHeaders: {
|
||||
'content-type': 'application/json',
|
||||
'x-initial': 'true',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should support require by path for legacy builders', () => {
|
||||
const index = require('@vercel/build-utils');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "28.4.11",
|
||||
"version": "28.4.12",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -42,10 +42,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "5.5.5",
|
||||
"@vercel/error-utils": "1.0.0",
|
||||
"@vercel/go": "2.2.13",
|
||||
"@vercel/hydrogen": "0.0.26",
|
||||
"@vercel/next": "3.2.5",
|
||||
"@vercel/node": "2.5.25",
|
||||
"@vercel/next": "3.2.6",
|
||||
"@vercel/node": "2.5.26",
|
||||
"@vercel/python": "3.1.22",
|
||||
"@vercel/redwood": "1.0.31",
|
||||
"@vercel/remix": "1.0.32",
|
||||
|
||||
@@ -70,7 +70,7 @@ import getPrebuiltJson from '../../util/deploy/get-prebuilt-json';
|
||||
import { createGitMeta } from '../../util/create-git-meta';
|
||||
import { isValidArchive } from '../../util/deploy/validate-archive-format';
|
||||
import { parseEnv } from '../../util/parse-env';
|
||||
import { errorToString, isErrnoException, isError } from '../../util/is-error';
|
||||
import { errorToString, isErrnoException, isError } from '@vercel/error-utils';
|
||||
import { pickOverrides } from '../../util/projects/project-settings';
|
||||
|
||||
export default async (client: Client): Promise<number> => {
|
||||
|
||||
@@ -3,16 +3,14 @@ import fs from 'fs-extra';
|
||||
|
||||
import DevServer from '../../util/dev/server';
|
||||
import { parseListen } from '../../util/dev/parse-listen';
|
||||
import { ProjectEnvVariable } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import { getLinkedProject } from '../../util/projects/link';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
|
||||
import setupAndLink from '../../util/link/setup-and-link';
|
||||
import getSystemEnvValues from '../../util/env/get-system-env-values';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import param from '../../util/output/param';
|
||||
import { OUTPUT_DIR } from '../../util/build/write-build-result';
|
||||
import { pullEnvRecords } from '../../util/env/get-env-records';
|
||||
|
||||
type Options = {
|
||||
'--listen': string;
|
||||
@@ -57,8 +55,7 @@ export default async function dev(
|
||||
}
|
||||
|
||||
let projectSettings: ProjectSettings | undefined;
|
||||
let projectEnvs: ProjectEnvVariable[] = [];
|
||||
let systemEnvValues: string[] = [];
|
||||
let envValues: Record<string, string> = {};
|
||||
if (link.status === 'linked') {
|
||||
const { project, org } = link;
|
||||
client.config.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
@@ -69,19 +66,15 @@ export default async function dev(
|
||||
cwd = join(cwd, project.rootDirectory);
|
||||
}
|
||||
|
||||
[{ envs: projectEnvs }, { systemEnvValues }] = await Promise.all([
|
||||
getDecryptedEnvRecords(output, client, project.id, 'vercel-cli:dev'),
|
||||
project.autoExposeSystemEnvs
|
||||
? getSystemEnvValues(output, client, project.id)
|
||||
: { systemEnvValues: [] },
|
||||
]);
|
||||
envValues = (
|
||||
await pullEnvRecords(output, client, project.id, 'vercel-cli:dev')
|
||||
).env;
|
||||
}
|
||||
|
||||
const devServer = new DevServer(cwd, {
|
||||
output,
|
||||
projectSettings,
|
||||
projectEnvs,
|
||||
systemEnvValues,
|
||||
envValues,
|
||||
});
|
||||
|
||||
// If there is no Development Command, we must delete the
|
||||
|
||||
@@ -15,7 +15,7 @@ import readConfig from '../../util/config/read-config';
|
||||
import readJSONFile from '../../util/read-json-file';
|
||||
import { getPkgName, getCommandName } from '../../util/pkg-name';
|
||||
import { CantParseJSONFile } from '../../util/errors-ts';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
dev: ['dev'],
|
||||
|
||||
@@ -11,7 +11,7 @@ import promptBool from '../../util/input/prompt-bool';
|
||||
import purchaseDomain from '../../util/domains/purchase-domain';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { errorToString } from '../../util/is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
type Options = {};
|
||||
|
||||
|
||||
29
packages/cli/src/commands/env/pull.ts
vendored
29
packages/cli/src/commands/env/pull.ts
vendored
@@ -4,21 +4,21 @@ import { closeSync, openSync, readSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { Project, ProjectEnvTarget } from '../../types';
|
||||
import Client from '../../util/client';
|
||||
import exposeSystemEnvs from '../../util/dev/expose-system-envs';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import getSystemEnvValues from '../../util/env/get-system-env-values';
|
||||
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import { Output } from '../../util/output';
|
||||
import param from '../../util/output/param';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { EnvRecordsSource } from '../../util/env/get-env-records';
|
||||
import {
|
||||
EnvRecordsSource,
|
||||
pullEnvRecords,
|
||||
} from '../../util/env/get-env-records';
|
||||
import {
|
||||
buildDeltaString,
|
||||
createEnvObject,
|
||||
} from '../../util/env/diff-env-files';
|
||||
import { isErrnoException } from '../../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
|
||||
|
||||
@@ -97,20 +97,11 @@ export default async function pull(
|
||||
const pullStamp = stamp();
|
||||
output.spinner('Downloading');
|
||||
|
||||
const [{ envs: projectEnvs }, { systemEnvValues }] = await Promise.all([
|
||||
getDecryptedEnvRecords(output, client, project.id, source, environment),
|
||||
project.autoExposeSystemEnvs
|
||||
? getSystemEnvValues(output, client, project.id)
|
||||
: { systemEnvValues: [] },
|
||||
]);
|
||||
|
||||
const records = exposeSystemEnvs(
|
||||
projectEnvs,
|
||||
systemEnvValues,
|
||||
project.autoExposeSystemEnvs,
|
||||
undefined,
|
||||
environment
|
||||
);
|
||||
const records = (
|
||||
await pullEnvRecords(output, client, project.id, source, {
|
||||
target: environment || ProjectEnvTarget.Development,
|
||||
})
|
||||
).env;
|
||||
|
||||
let deltaString = '';
|
||||
let oldEnv;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../../util/client';
|
||||
import { ensureLink } from '../../util/ensure-link';
|
||||
import { ensureLink } from '../../util/link/ensure-link';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import handleError from '../../util/handle-error';
|
||||
@@ -80,7 +80,7 @@ export default async function main(client: Client) {
|
||||
argv._ = argv._.slice(1);
|
||||
subcommand = argv._[0];
|
||||
const args = argv._.slice(1);
|
||||
const confirm = Boolean(argv['--yes']);
|
||||
const autoConfirm = Boolean(argv['--yes']);
|
||||
const { output } = client;
|
||||
|
||||
let paths = [process.cwd()];
|
||||
@@ -90,7 +90,7 @@ export default async function main(client: Client) {
|
||||
}
|
||||
const { path } = pathValidation;
|
||||
|
||||
const linkedProject = await ensureLink('git', client, path, confirm);
|
||||
const linkedProject = await ensureLink('git', client, path, { autoConfirm });
|
||||
if (typeof linkedProject === 'number') {
|
||||
return linkedProject;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import init from './init';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import { isError } from '../../util/is-error';
|
||||
import { isError } from '@vercel/error-utils';
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
init: ['init'],
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getDeployment } from '../util/get-deployment';
|
||||
import { Deployment } from '@vercel/client';
|
||||
import { Build } from '../types';
|
||||
import title from 'title';
|
||||
import { isErrnoException } from '../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { URL } from 'url';
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import Client from '../../util/client';
|
||||
import getArgs from '../../util/get-args';
|
||||
import logo from '../../util/output/logo';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import setupAndLink from '../../util/link/setup-and-link';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import param from '../../util/output/param';
|
||||
import { ensureLink } from '../../util/link/ensure-link';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -70,31 +68,16 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
const cwd = argv._[1] || process.cwd();
|
||||
const link = await setupAndLink(client, cwd, {
|
||||
|
||||
const link = await ensureLink('link', client, cwd, {
|
||||
autoConfirm: !!argv['--yes'],
|
||||
forceDelete: true,
|
||||
autoConfirm: argv['--yes'],
|
||||
projectName: argv['--project'],
|
||||
successEmoji: 'success',
|
||||
setupMsg: 'Set up',
|
||||
});
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
'link'
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
} else if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
} else if (link.status === 'linked') {
|
||||
// Successfully linked
|
||||
return 0;
|
||||
} else {
|
||||
const err: never = link;
|
||||
throw new Error('Unknown link status: ' + err);
|
||||
if (typeof link === 'number') {
|
||||
return link;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ import Client from '../util/client';
|
||||
import { Deployment } from '@vercel/client';
|
||||
import validatePaths from '../util/validate-paths';
|
||||
import { getLinkedProject } from '../util/projects/link';
|
||||
import { ensureLink } from '../util/ensure-link';
|
||||
import { ensureLink } from '../util/link/ensure-link';
|
||||
import getScope from '../util/get-scope';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { isErrnoException } from '../util/is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -56,7 +56,7 @@ const help = () => {
|
||||
${chalk.gray('–')} List all deployments for the project ${chalk.dim(
|
||||
'`my-app`'
|
||||
)} in the team of the currently linked project
|
||||
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} ls my-app`)}
|
||||
|
||||
${chalk.gray('–')} Filter deployments by metadata
|
||||
@@ -112,7 +112,7 @@ export default async function main(client: Client) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
const yes = !!argv['--yes'];
|
||||
const autoConfirm = !!argv['--yes'];
|
||||
const prod = argv['--prod'] || false;
|
||||
|
||||
const meta = parseMeta(argv['--meta']);
|
||||
@@ -145,7 +145,9 @@ export default async function main(client: Client) {
|
||||
// If there's no linked project and user doesn't pass `app` arg,
|
||||
// prompt to link their current directory.
|
||||
if (status === 'not_linked' && !app) {
|
||||
const linkedProject = await ensureLink('list', client, path, yes);
|
||||
const linkedProject = await ensureLink('list', client, path, {
|
||||
autoConfirm,
|
||||
});
|
||||
if (typeof linkedProject === 'number') {
|
||||
return linkedProject;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import getArgs from '../util/get-args';
|
||||
import Client from '../util/client';
|
||||
import { getCommandName, getPkgName } from '../util/pkg-name';
|
||||
import { isAPIError } from '../util/errors-ts';
|
||||
import { errorToString } from '../util/is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
|
||||
@@ -4,24 +4,18 @@ import Client from '../util/client';
|
||||
import { ProjectEnvTarget } from '../types';
|
||||
import { emoji, prependEmoji } from '../util/emoji';
|
||||
import getArgs from '../util/get-args';
|
||||
import setupAndLink from '../util/link/setup-and-link';
|
||||
import logo from '../util/output/logo';
|
||||
import stamp from '../util/output/stamp';
|
||||
import { getPkgName } from '../util/pkg-name';
|
||||
import {
|
||||
getLinkedProject,
|
||||
VERCEL_DIR,
|
||||
VERCEL_DIR_PROJECT,
|
||||
} from '../util/projects/link';
|
||||
import { VERCEL_DIR, VERCEL_DIR_PROJECT } from '../util/projects/link';
|
||||
import { writeProjectSettings } from '../util/projects/project-settings';
|
||||
import envPull from './env/pull';
|
||||
import { getCommandName } from '../util/pkg-name';
|
||||
import param from '../util/output/param';
|
||||
import type { Project, Org } from '../types';
|
||||
import type { Project } from '../types';
|
||||
import {
|
||||
isValidEnvTarget,
|
||||
getEnvTargetPlaceholder,
|
||||
} from '../util/env/env-target';
|
||||
import { ensureLink } from '../util/link/ensure-link';
|
||||
|
||||
const help = () => {
|
||||
return console.log(`
|
||||
@@ -83,43 +77,6 @@ function parseArgs(client: Client) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
type LinkResult = {
|
||||
org: Org;
|
||||
project: Project;
|
||||
};
|
||||
async function ensureLink(
|
||||
client: Client,
|
||||
cwd: string,
|
||||
yes: boolean
|
||||
): Promise<LinkResult | number> {
|
||||
let link = await getLinkedProject(client, cwd);
|
||||
if (link.status === 'not_linked') {
|
||||
link = await setupAndLink(client, cwd, {
|
||||
autoConfirm: yes,
|
||||
successEmoji: 'link',
|
||||
setupMsg: 'Set up',
|
||||
});
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
'pull'
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
return { org: link.org, project: link.project };
|
||||
}
|
||||
|
||||
async function pullAllEnvFiles(
|
||||
environment: ProjectEnvTarget,
|
||||
client: Client,
|
||||
@@ -140,7 +97,9 @@ async function pullAllEnvFiles(
|
||||
);
|
||||
}
|
||||
|
||||
function parseEnvironment(environment = 'development'): ProjectEnvTarget {
|
||||
export function parseEnvironment(
|
||||
environment = 'development'
|
||||
): ProjectEnvTarget {
|
||||
if (!isValidEnvTarget(environment)) {
|
||||
throw new Error(
|
||||
`environment "${environment}" not supported; must be one of ${getEnvTargetPlaceholder()}`
|
||||
@@ -156,10 +115,10 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
const cwd = argv._[1] || process.cwd();
|
||||
const yes = Boolean(argv['--yes']);
|
||||
const autoConfirm = Boolean(argv['--yes']);
|
||||
const environment = parseEnvironment(argv['--environment'] || undefined);
|
||||
|
||||
const link = await ensureLink(client, cwd, yes);
|
||||
const link = await ensureLink('pull', client, cwd, { autoConfirm });
|
||||
if (typeof link === 'number') {
|
||||
return link;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getPkgName, getCommandName } from '../../util/pkg-name';
|
||||
import Client from '../../util/client';
|
||||
import createTeam from '../../util/teams/create-team';
|
||||
import patchTeam from '../../util/teams/patch-team';
|
||||
import { errorToString, isError } from '../../util/is-error';
|
||||
import { errorToString, isError } from '@vercel/error-utils';
|
||||
|
||||
const validateSlugKeypress = (data: string, value: string) =>
|
||||
// TODO: the `value` here should contain the current value + the keypress
|
||||
|
||||
@@ -12,7 +12,7 @@ import { email as regexEmail } from '../../util/input/regexes';
|
||||
import getTeams from '../../util/teams/get-teams';
|
||||
import inviteUserToTeam from '../../util/teams/invite-user-to-team';
|
||||
import { isAPIError } from '../../util/errors-ts';
|
||||
import { errorToString, isError } from '../../util/is-error';
|
||||
import { errorToString, isError } from '@vercel/error-utils';
|
||||
|
||||
const validateEmail = (data: string) =>
|
||||
regexEmail.test(data.trim()) || data.length === 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { isErrnoException, isError, errorToString } from './util/is-error';
|
||||
import { isErrnoException, isError, errorToString } from '@vercel/error-utils';
|
||||
|
||||
try {
|
||||
// Test to see if cwd has been deleted before
|
||||
@@ -610,6 +610,21 @@ const main = async () => {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isErrnoException(err) && err.code === 'ECONNRESET') {
|
||||
// Error message will look like the following:
|
||||
// request to https://api.vercel.com/v2/user failed, reason: socket hang up
|
||||
const matches = /request to https:\/\/(.*?)\//.exec(err.message || '');
|
||||
const hostname = matches?.[1];
|
||||
if (hostname) {
|
||||
output.error(
|
||||
`Connection to ${highlight(
|
||||
hostname
|
||||
)} interrupted. Please verify your internet connectivity and DNS configuration.`
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
isErrnoException(err) &&
|
||||
(err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED')
|
||||
|
||||
@@ -16,7 +16,7 @@ import { VERCEL_DIR } from '../projects/link';
|
||||
import { Output } from '../output';
|
||||
import readJSONFile from '../read-json-file';
|
||||
import { CantParseJSONFile } from '../errors-ts';
|
||||
import { errorToString, isErrnoException, isError } from '../is-error';
|
||||
import { errorToString, isErrnoException, isError } from '@vercel/error-utils';
|
||||
import cmd from '../output/cmd';
|
||||
import code from '../output/code';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import Client from '../client';
|
||||
import { Cert } from '../../types';
|
||||
import { isErrnoException } from '../is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
|
||||
export default async function createCertFromFile(
|
||||
|
||||
@@ -2,7 +2,7 @@ import retry from 'async-retry';
|
||||
import { Cert } from '../../types';
|
||||
import Client from '../client';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
import { isError } from '../is-error';
|
||||
import { isError } from '@vercel/error-utils';
|
||||
|
||||
// When it's a configuration error we should retry because of the DNS propagation
|
||||
// otherwise we bail to handle the error in the upper level
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
} from '../types';
|
||||
import { sharedPromise } from './promise';
|
||||
import { APIError } from './errors-ts';
|
||||
import { normalizeError } from './is-error';
|
||||
import { normalizeError } from '@vercel/error-utils';
|
||||
|
||||
const isSAMLError = (v: any): v is SAMLError => {
|
||||
return v && v.saml;
|
||||
|
||||
@@ -10,7 +10,7 @@ import error from '../output/error';
|
||||
import highlight from '../output/highlight';
|
||||
import { VercelConfig } from '../dev/types';
|
||||
import { AuthConfig, GlobalConfig } from '../../types';
|
||||
import { isErrnoException, isError } from '../is-error';
|
||||
import { isErrnoException, isError } from '@vercel/error-utils';
|
||||
|
||||
const VERCEL_DIR = getGlobalPathConfig();
|
||||
const CONFIG_FILE_PATH = join(VERCEL_DIR, 'config.json');
|
||||
|
||||
@@ -5,7 +5,7 @@ import git from 'git-last-commit';
|
||||
import { exec } from 'child_process';
|
||||
import { GitMetadata, Project } from '../types';
|
||||
import { Output } from './output';
|
||||
import { errorToString } from './is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
export async function createGitMeta(
|
||||
directory: string,
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
ProjectEnvType,
|
||||
ProjectEnvVariable,
|
||||
ProjectEnvTarget,
|
||||
} from '../../types';
|
||||
import { Env } from '@vercel/build-utils';
|
||||
|
||||
function getSystemEnvValue(
|
||||
systemEnvRef: string,
|
||||
{ vercelUrl }: { vercelUrl?: string }
|
||||
) {
|
||||
if (systemEnvRef === 'VERCEL_URL') {
|
||||
return vercelUrl || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function exposeSystemEnvs(
|
||||
projectEnvs: ProjectEnvVariable[],
|
||||
systemEnvValues: string[],
|
||||
autoExposeSystemEnvs: boolean | undefined,
|
||||
vercelUrl?: string,
|
||||
target?: ProjectEnvTarget
|
||||
) {
|
||||
const envs: Env = {};
|
||||
|
||||
if (autoExposeSystemEnvs) {
|
||||
envs['VERCEL'] = '1';
|
||||
envs['VERCEL_ENV'] = target || 'development';
|
||||
|
||||
for (const key of systemEnvValues) {
|
||||
envs[key] = getSystemEnvValue(key, { vercelUrl });
|
||||
}
|
||||
}
|
||||
|
||||
for (let env of projectEnvs) {
|
||||
if (env.type === ProjectEnvType.System) {
|
||||
envs[env.key] = getSystemEnvValue(env.value, { vercelUrl });
|
||||
} else {
|
||||
envs[env.key] = env.value;
|
||||
}
|
||||
}
|
||||
|
||||
return envs;
|
||||
}
|
||||
@@ -16,3 +16,75 @@ export function nodeHeadersToFetchHeaders(
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request headers that are not allowed to be overridden by a middleware.
|
||||
*/
|
||||
const NONOVERRIDABLE_HEADERS: Set<string> = new Set([
|
||||
'host',
|
||||
'connection',
|
||||
'content-length',
|
||||
'transfer-encoding',
|
||||
'keep-alive',
|
||||
'transfer-encoding',
|
||||
'te',
|
||||
'upgrade',
|
||||
'trailer',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Adds/Updates/Deletes headers in `reqHeaders` based on the response headers
|
||||
* from a middleware (`respHeaders`).
|
||||
*
|
||||
* `x-middleware-override-headers` is a comma-separated list of *all* header
|
||||
* names that should appear in new request headers. Names not in this list
|
||||
* will be deleted.
|
||||
*
|
||||
* `x-middleware-request-*` is the new value for each header. This can't be
|
||||
* omitted, even if the header is not being modified.
|
||||
*
|
||||
*/
|
||||
export function applyOverriddenHeaders(
|
||||
reqHeaders: { [k: string]: string | string[] | undefined },
|
||||
respHeaders: Headers
|
||||
) {
|
||||
const overriddenHeaders = respHeaders.get('x-middleware-override-headers');
|
||||
if (!overriddenHeaders) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overriddenKeys: Set<string> = new Set();
|
||||
for (const key of overriddenHeaders.split(',')) {
|
||||
overriddenKeys.add(key.trim());
|
||||
}
|
||||
|
||||
respHeaders.delete('x-middleware-override-headers');
|
||||
|
||||
// Delete headers.
|
||||
for (const key of Object.keys(reqHeaders)) {
|
||||
if (!NONOVERRIDABLE_HEADERS.has(key) && !overriddenKeys.has(key)) {
|
||||
delete reqHeaders[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add headers.
|
||||
for (const key of overriddenKeys.keys()) {
|
||||
if (NONOVERRIDABLE_HEADERS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const valueKey = 'x-middleware-request-' + key;
|
||||
const newValue = respHeaders.get(valueKey);
|
||||
const oldValue = reqHeaders[key];
|
||||
|
||||
if (oldValue !== newValue) {
|
||||
if (newValue) {
|
||||
reqHeaders[key] = newValue;
|
||||
} else {
|
||||
delete reqHeaders[key];
|
||||
}
|
||||
}
|
||||
|
||||
respHeaders.delete(valueKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,17 +85,16 @@ import {
|
||||
HttpHeadersConfig,
|
||||
EnvConfigs,
|
||||
} from './types';
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import exposeSystemEnvs from './expose-system-envs';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import { treeKill } from '../tree-kill';
|
||||
import { nodeHeadersToFetchHeaders } from './headers';
|
||||
import { applyOverriddenHeaders, nodeHeadersToFetchHeaders } from './headers';
|
||||
import { formatQueryString, parseQueryString } from './parse-query-string';
|
||||
import {
|
||||
errorToString,
|
||||
isErrnoException,
|
||||
isError,
|
||||
isSpawnError,
|
||||
} from '../is-error';
|
||||
} from '@vercel/error-utils';
|
||||
import isURL from './is-url';
|
||||
import { pickOverrides } from '../projects/project-settings';
|
||||
import { replaceLocalhost } from './parse-listen';
|
||||
@@ -168,15 +167,13 @@ export default class DevServer {
|
||||
private blockingBuildsPromise: Promise<void> | null;
|
||||
private startPromise: Promise<void> | null;
|
||||
|
||||
private systemEnvValues: string[];
|
||||
private projectEnvs: ProjectEnvVariable[];
|
||||
private envValues: Record<string, string>;
|
||||
|
||||
constructor(cwd: string, options: DevServerOptions) {
|
||||
this.cwd = cwd;
|
||||
this.output = options.output;
|
||||
this.envConfigs = { buildEnv: {}, runEnv: {}, allEnv: {} };
|
||||
this.systemEnvValues = options.systemEnvValues || [];
|
||||
this.projectEnvs = options.projectEnvs || [];
|
||||
this.envValues = options.envValues || {};
|
||||
this.files = {};
|
||||
this.originalProjectSettings = options.projectSettings;
|
||||
this.projectSettings = options.projectSettings;
|
||||
@@ -684,16 +681,13 @@ export default class DevServer {
|
||||
|
||||
// If no .env/.build.env is present, use cloud environment variables
|
||||
if (Object.keys(allEnv).length === 0) {
|
||||
const cloudEnv = exposeSystemEnvs(
|
||||
this.projectEnvs || [],
|
||||
this.systemEnvValues || [],
|
||||
this.projectSettings?.autoExposeSystemEnvs,
|
||||
this.address.host
|
||||
);
|
||||
|
||||
allEnv = { ...cloudEnv };
|
||||
runEnv = { ...cloudEnv };
|
||||
buildEnv = { ...cloudEnv };
|
||||
const envValues = { ...this.envValues };
|
||||
if (this.address.host) {
|
||||
envValues['VERCEL_URL'] = this.address.host;
|
||||
}
|
||||
allEnv = { ...envValues };
|
||||
runEnv = { ...envValues };
|
||||
buildEnv = { ...envValues };
|
||||
}
|
||||
|
||||
// legacy NOW_REGION env variable
|
||||
@@ -1478,6 +1472,9 @@ export default class DevServer {
|
||||
'content-length',
|
||||
'transfer-encoding',
|
||||
]);
|
||||
|
||||
applyOverriddenHeaders(req.headers, middlewareRes.headers);
|
||||
|
||||
for (const [name, value] of middlewareRes.headers) {
|
||||
if (name === 'x-middleware-next') {
|
||||
shouldContinue = value === '1';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { VercelConfig } from '@vercel/client';
|
||||
import { HandleValue, Route } from '@vercel/routing-utils';
|
||||
import { Output } from '../output';
|
||||
import { ProjectEnvVariable, ProjectSettings } from '../../types';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import { BuilderWithPkg } from '../build/import-builders';
|
||||
|
||||
export { VercelConfig };
|
||||
@@ -24,8 +24,7 @@ export { VercelConfig };
|
||||
export interface DevServerOptions {
|
||||
output: Output;
|
||||
projectSettings?: ProjectSettings;
|
||||
systemEnvValues?: string[];
|
||||
projectEnvs?: ProjectEnvVariable[];
|
||||
envValues?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface EnvConfigs {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Org, Project } from '../types';
|
||||
import Client from './client';
|
||||
import setupAndLink from './link/setup-and-link';
|
||||
import param from './output/param';
|
||||
import { getCommandName } from './pkg-name';
|
||||
import { getLinkedProject } from './projects/link';
|
||||
|
||||
type LinkResult = {
|
||||
org: Org;
|
||||
project: Project;
|
||||
};
|
||||
|
||||
export async function ensureLink(
|
||||
commandName: string,
|
||||
client: Client,
|
||||
cwd: string,
|
||||
yes: boolean
|
||||
): Promise<LinkResult | number> {
|
||||
let link = await getLinkedProject(client, cwd);
|
||||
if (link.status === 'not_linked') {
|
||||
link = await setupAndLink(client, cwd, {
|
||||
autoConfirm: yes,
|
||||
successEmoji: 'link',
|
||||
setupMsg: 'Set up',
|
||||
});
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
commandName
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
return { org: link.org, project: link.project };
|
||||
}
|
||||
38
packages/cli/src/util/env/get-env-records.ts
vendored
38
packages/cli/src/util/env/get-env-records.ts
vendored
@@ -2,6 +2,7 @@ import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
import { ProjectEnvVariable, ProjectEnvTarget } from '../../types';
|
||||
import { URLSearchParams } from 'url';
|
||||
import * as path from 'path';
|
||||
|
||||
/** The CLI command that was used that needs the environment variables. */
|
||||
export type EnvRecordsSource =
|
||||
@@ -49,3 +50,40 @@ export default async function getEnvRecords(
|
||||
|
||||
return client.fetch<{ envs: ProjectEnvVariable[] }>(url);
|
||||
}
|
||||
|
||||
interface PullEnvOptions {
|
||||
target?: ProjectEnvTarget | string;
|
||||
gitBranch?: string;
|
||||
}
|
||||
|
||||
export async function pullEnvRecords(
|
||||
output: Output,
|
||||
client: Client,
|
||||
projectId: string,
|
||||
source: EnvRecordsSource,
|
||||
{ target, gitBranch }: PullEnvOptions = {}
|
||||
) {
|
||||
output.debug(
|
||||
`Fetching Environment Variables of project ${projectId} and target ${target}`
|
||||
);
|
||||
const query = new URLSearchParams();
|
||||
|
||||
let url = `/v1/env/pull/${projectId}`;
|
||||
|
||||
if (target) {
|
||||
url = path.join(url, target, gitBranch ?? '');
|
||||
}
|
||||
|
||||
if (source) {
|
||||
query.set('source', source);
|
||||
}
|
||||
|
||||
if (Array.from(query).length > 0) {
|
||||
url += `?${query}`;
|
||||
}
|
||||
|
||||
return client.fetch<{
|
||||
env: Record<string, string>;
|
||||
buildEnv: Record<string, string>;
|
||||
}>(url);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Output } from '../output';
|
||||
import Client from '../client';
|
||||
|
||||
export default async function getSystemEnvValues(
|
||||
output: Output,
|
||||
client: Client,
|
||||
projectId: string
|
||||
) {
|
||||
output.debug(`Fetching System Environment Values of project ${projectId}`);
|
||||
const url = `/v6/projects/${projectId}/system-env-values`;
|
||||
return client.fetch<{ systemEnvValues: string[] }>(url);
|
||||
}
|
||||
2
packages/cli/src/util/env/known-error.ts
vendored
2
packages/cli/src/util/env/known-error.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { isErrnoException } from '../is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
const knownErrorsCodes = new Set([
|
||||
'PAYMENT_REQUIRED',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { NowError } from './now-error';
|
||||
import code from './output/code';
|
||||
import { getCommandName } from './pkg-name';
|
||||
import chalk from 'chalk';
|
||||
import { isError } from './is-error';
|
||||
import { isError } from '@vercel/error-utils';
|
||||
|
||||
/**
|
||||
* This error is thrown when there is an API error with a payload. The error
|
||||
|
||||
@@ -10,7 +10,7 @@ import humanizePath from './humanize-path';
|
||||
import readJSONFile from './read-json-file';
|
||||
import { VercelConfig } from './dev/types';
|
||||
import { Output } from './output';
|
||||
import { isErrnoException } from './is-error';
|
||||
import { isErrnoException } from '@vercel/error-utils';
|
||||
|
||||
let config: VercelConfig;
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import Client from './client';
|
||||
import { Output } from './output/create-output';
|
||||
import {
|
||||
ProjectEnvTarget,
|
||||
ProjectEnvType,
|
||||
ProjectEnvVariable,
|
||||
Secret,
|
||||
} from '../types';
|
||||
import getEnvRecords, { EnvRecordsSource } from './env/get-env-records';
|
||||
import { isAPIError } from './errors-ts';
|
||||
|
||||
export default async function getDecryptedEnvRecords(
|
||||
output: Output,
|
||||
client: Client,
|
||||
projectId: string,
|
||||
source: EnvRecordsSource,
|
||||
target?: ProjectEnvTarget
|
||||
): Promise<{ envs: ProjectEnvVariable[] }> {
|
||||
const { envs } = await getEnvRecords(output, client, projectId, source, {
|
||||
target: target || ProjectEnvTarget.Development,
|
||||
decrypt: true,
|
||||
});
|
||||
|
||||
const envsWithDecryptedSecrets = await Promise.all(
|
||||
envs.map(async ({ id, type, key, value }) => {
|
||||
// it's not possible to create secret env variables for development
|
||||
// anymore but we keep this because legacy env variables with "decryptable"
|
||||
// secret values still exit in our system
|
||||
if (type === ProjectEnvType.Secret) {
|
||||
try {
|
||||
const secretIdOrName = value;
|
||||
|
||||
if (!secretIdOrName) {
|
||||
return { id, type, key, value: '', found: true };
|
||||
}
|
||||
|
||||
output.debug(`Fetching decrypted secret ${secretIdOrName}`);
|
||||
const secret = await client.fetch<Secret>(
|
||||
`/v2/now/secrets/${secretIdOrName}?decrypt=true`
|
||||
);
|
||||
|
||||
return { id, type, key, value: secret.value, found: true };
|
||||
} catch (err: unknown) {
|
||||
if (isAPIError(err) && err.status === 404) {
|
||||
return { id, type, key, value: '', found: false };
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return { id, type, key, value, found: true };
|
||||
})
|
||||
);
|
||||
|
||||
for (let env of envsWithDecryptedSecrets) {
|
||||
if (!env.found) {
|
||||
output.print('');
|
||||
output.warn(
|
||||
`Unable to download variable ${env.key} because associated secret was deleted`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { envs: envsWithDecryptedSecrets };
|
||||
}
|
||||
58
packages/cli/src/util/link/ensure-link.ts
Normal file
58
packages/cli/src/util/link/ensure-link.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Org, Project } from '../../types';
|
||||
import Client from '../client';
|
||||
import setupAndLink from '../link/setup-and-link';
|
||||
import param from '../output/param';
|
||||
import { getCommandName } from '../pkg-name';
|
||||
import { getLinkedProject } from '../projects/link';
|
||||
import type { SetupAndLinkOptions } from '../link/setup-and-link';
|
||||
|
||||
type LinkResult = {
|
||||
org: Org;
|
||||
project: Project;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a project is already linked and if not, links the project and
|
||||
* validates the link response.
|
||||
*
|
||||
* @param commandName - The name of the current command to print in the
|
||||
* event of an error
|
||||
* @param client - The Vercel Node.js client instance
|
||||
* @param cwd - The current working directory
|
||||
* @param opts.forceDelete - When `true`, deletes the project's `.vercel`
|
||||
* directory
|
||||
* @param opts.projectName - The project name to use when linking, otherwise
|
||||
* the current directory
|
||||
* @returns {Promise<LinkResult|number>} Returns a numeric exit code when aborted or
|
||||
* error, otherwise an object containing the org an project
|
||||
*/
|
||||
export async function ensureLink(
|
||||
commandName: string,
|
||||
client: Client,
|
||||
cwd: string,
|
||||
opts: SetupAndLinkOptions
|
||||
): Promise<LinkResult | number> {
|
||||
let link = await getLinkedProject(client, cwd);
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
link = await setupAndLink(client, cwd, opts);
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.status === 'error') {
|
||||
if (link.reason === 'HEADLESS') {
|
||||
client.output.error(
|
||||
`Command ${getCommandName(
|
||||
commandName
|
||||
)} requires confirmation. Use option ${param('--yes')} to confirm.`
|
||||
);
|
||||
}
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
return { org: link.org, project: link.project };
|
||||
}
|
||||
@@ -32,8 +32,8 @@ import { isAPIError } from '../errors-ts';
|
||||
export interface SetupAndLinkOptions {
|
||||
forceDelete?: boolean;
|
||||
autoConfirm?: boolean;
|
||||
successEmoji: EmojiLabel;
|
||||
setupMsg: string;
|
||||
successEmoji?: EmojiLabel;
|
||||
setupMsg?: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ export default async function setupAndLink(
|
||||
{
|
||||
forceDelete = false,
|
||||
autoConfirm = false,
|
||||
successEmoji,
|
||||
setupMsg,
|
||||
successEmoji = 'link',
|
||||
setupMsg = 'Set up',
|
||||
projectName,
|
||||
}: SetupAndLinkOptions
|
||||
): Promise<ProjectLinkResult> {
|
||||
|
||||
@@ -7,7 +7,7 @@ import executeLogin from './login';
|
||||
import Client from '../client';
|
||||
import { LoginResult } from './types';
|
||||
import { isAPIError } from '../errors-ts';
|
||||
import { errorToString } from '../is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
export default async function doEmailLogin(
|
||||
client: Client,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Client from '../client';
|
||||
import { InvalidEmail, AccountNotFound, isAPIError } from '../errors-ts';
|
||||
import { errorToString } from '../is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
import { LoginData } from './types';
|
||||
|
||||
export default async function login(
|
||||
|
||||
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
||||
import renderLink from './link';
|
||||
import wait, { StopSpinner } from './wait';
|
||||
import type { WritableTTY } from '../../types';
|
||||
import { errorToString } from '../is-error';
|
||||
import { errorToString } from '@vercel/error-utils';
|
||||
|
||||
const IS_TEST = process.env.NODE_ENV === 'test';
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { prependEmoji, emoji, EmojiLabel } from '../emoji';
|
||||
import { isDirectory } from '../config/global-path';
|
||||
import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
|
||||
import outputCode from '../output/code';
|
||||
import { isErrnoException, isError } from '../is-error';
|
||||
import { isErrnoException, isError } from '@vercel/error-utils';
|
||||
|
||||
const readFile = promisify(fs.readFile);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Client from './client';
|
||||
import getScope from './get-scope';
|
||||
import getArgs from './get-args';
|
||||
import { isError } from './is-error';
|
||||
import { isError } from '@vercel/error-utils';
|
||||
import type { Team, User } from '../types';
|
||||
|
||||
export default async function reportError(
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default (req, res) => {
|
||||
res.json(req.headers);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export default () => {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
'x-middleware-next': '1',
|
||||
'x-middleware-override-headers':
|
||||
'x-from-client-a,x-from-client-b,x-from-middleware-a,x-from-middleware-b,transfer-encoding',
|
||||
// Headers to be preserved.
|
||||
'x-middleware-request-x-from-client-a': 'hello from client',
|
||||
// Headers to be modified by the middleware.
|
||||
'x-middleware-request-x-from-client-b': 'hello from middleware',
|
||||
// Headers to be added by the middleware.
|
||||
'x-middleware-request-x-from-middleware-a': 'hello a!',
|
||||
'x-middleware-request-x-from-middleware-b': 'hello b!',
|
||||
// Headers not allowed by the dev server: will be ignored.
|
||||
'transfer-encoding': 'gzip, chunked',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import ms from 'ms';
|
||||
import fs from 'fs-extra';
|
||||
import { isIP } from 'net';
|
||||
import { join } from 'path';
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
const {
|
||||
fetch,
|
||||
@@ -613,3 +614,72 @@ test(
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
);
|
||||
|
||||
test(
|
||||
'[vercel dev] Middleware can override request headers',
|
||||
testFixtureStdio(
|
||||
'middleware-request-headers-override',
|
||||
async (testPath: any) => {
|
||||
await testPath(
|
||||
200,
|
||||
'/api/dump-headers',
|
||||
(actual: string, res: Response) => {
|
||||
// Headers sent to the API route.
|
||||
const headers = JSON.parse(actual);
|
||||
|
||||
// Preserved headers.
|
||||
expect(headers).toHaveProperty(
|
||||
'x-from-client-a',
|
||||
'hello from client'
|
||||
);
|
||||
|
||||
// Headers added/modified by the middleware.
|
||||
expect(headers).toHaveProperty(
|
||||
'x-from-client-b',
|
||||
'hello from middleware'
|
||||
);
|
||||
expect(headers).toHaveProperty('x-from-middleware-a', 'hello a!');
|
||||
expect(headers).toHaveProperty('x-from-middleware-b', 'hello b!');
|
||||
|
||||
// Headers deleted by the middleware.
|
||||
expect(headers).not.toHaveProperty('x-from-client-c');
|
||||
|
||||
// Internal headers should not be visible from API routes.
|
||||
expect(headers).not.toHaveProperty('x-middleware-override-headers');
|
||||
expect(headers).not.toHaveProperty(
|
||||
'x-middleware-request-from-middleware-a'
|
||||
);
|
||||
expect(headers).not.toHaveProperty(
|
||||
'x-middleware-request-from-middleware-b'
|
||||
);
|
||||
|
||||
// Request headers should not be visible from clients.
|
||||
const respHeaders = Object.fromEntries(res.headers.entries());
|
||||
expect(respHeaders).not.toHaveProperty(
|
||||
'x-middleware-override-headers'
|
||||
);
|
||||
expect(respHeaders).not.toHaveProperty(
|
||||
'x-middleware-request-from-middleware-a'
|
||||
);
|
||||
expect(respHeaders).not.toHaveProperty(
|
||||
'x-middleware-request-from-middleware-b'
|
||||
);
|
||||
expect(respHeaders).not.toHaveProperty('from-middleware-a');
|
||||
expect(respHeaders).not.toHaveProperty('from-middleware-b');
|
||||
expect(respHeaders).not.toHaveProperty('x-from-client-a');
|
||||
expect(respHeaders).not.toHaveProperty('x-from-client-b');
|
||||
expect(respHeaders).not.toHaveProperty('x-from-client-c');
|
||||
},
|
||||
/*expectedHeaders=*/ {},
|
||||
{
|
||||
headers: {
|
||||
'x-from-client-a': 'hello from client',
|
||||
'x-from-client-b': 'hello from client',
|
||||
'x-from-client-c': 'hello from client',
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
{ skipDeploy: true }
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
import { client } from './client';
|
||||
import { Project } from '../../src/types';
|
||||
import {
|
||||
Project,
|
||||
ProjectEnvTarget,
|
||||
ProjectEnvType,
|
||||
ProjectEnvVariable,
|
||||
} from '../../src/types';
|
||||
import { formatProvider } from '../../src/util/git/connect-git-provider';
|
||||
import { parseEnvironment } from '../../src/commands/pull';
|
||||
import { Env } from '@vercel/build-utils/dist';
|
||||
|
||||
const envs = [
|
||||
const envs: ProjectEnvVariable[] = [
|
||||
{
|
||||
type: 'encrypted',
|
||||
type: ProjectEnvType.Encrypted,
|
||||
id: '781dt89g8r2h789g',
|
||||
key: 'REDIS_CONNECTION_STRING',
|
||||
value: 'redis://abc123@redis.example.com:6379',
|
||||
target: ['production', 'preview'],
|
||||
gitBranch: null,
|
||||
target: [ProjectEnvTarget.Production, ProjectEnvTarget.Preview],
|
||||
gitBranch: undefined,
|
||||
configurationId: null,
|
||||
updatedAt: 1557241361455,
|
||||
createdAt: 1557241361455,
|
||||
},
|
||||
{
|
||||
type: 'encrypted',
|
||||
type: ProjectEnvType.Encrypted,
|
||||
id: 'r124t6frtu25df16',
|
||||
key: 'SQL_CONNECTION_STRING',
|
||||
value: 'Server=sql.example.com;Database=app;Uid=root;Pwd=P455W0RD;',
|
||||
target: ['production'],
|
||||
gitBranch: null,
|
||||
target: [ProjectEnvTarget.Production],
|
||||
gitBranch: undefined,
|
||||
configurationId: null,
|
||||
updatedAt: 1557241361445,
|
||||
createdAt: 1557241361445,
|
||||
},
|
||||
{
|
||||
type: 'encrypted',
|
||||
type: ProjectEnvType.Encrypted,
|
||||
id: 'a235l6frtu25df32',
|
||||
key: 'SPECIAL_FLAG',
|
||||
value: '1',
|
||||
target: ['development'],
|
||||
gitBranch: null,
|
||||
target: [ProjectEnvTarget.Development],
|
||||
gitBranch: undefined,
|
||||
configurationId: null,
|
||||
updatedAt: 1557241361445,
|
||||
createdAt: 1557241361445,
|
||||
@@ -206,6 +213,43 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
||||
Object.assign(project, req.body);
|
||||
res.json(project);
|
||||
});
|
||||
client.scenario.get(
|
||||
`/v1/env/pull/${project.id}/:target?/:gitBranch?`,
|
||||
(req, res) => {
|
||||
const target: ProjectEnvTarget | undefined =
|
||||
typeof req.params.target === 'string'
|
||||
? parseEnvironment(req.params.target)
|
||||
: undefined;
|
||||
let projectEnvs = envs;
|
||||
if (target) {
|
||||
projectEnvs = projectEnvs.filter(env => {
|
||||
if (typeof env.target === 'string') {
|
||||
return env.target === target;
|
||||
}
|
||||
if (Array.isArray(env.target)) {
|
||||
return env.target.includes(target);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
const allEnvs = Object.entries(
|
||||
exposeSystemEnvs(
|
||||
projectEnvs,
|
||||
systemEnvs.map(env => env.key),
|
||||
project.autoExposeSystemEnvs,
|
||||
undefined,
|
||||
target
|
||||
)
|
||||
);
|
||||
|
||||
const env: Record<string, string> = {};
|
||||
|
||||
allEnvs.forEach(([k, v]) => {
|
||||
env[k] = v ?? '';
|
||||
});
|
||||
res.json({ env: env });
|
||||
}
|
||||
);
|
||||
client.scenario.get(
|
||||
`/v6/projects/${project.id}/system-env-values`,
|
||||
(_req, res) => {
|
||||
@@ -222,15 +266,26 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
||||
});
|
||||
}
|
||||
);
|
||||
client.scenario.get(`/v8/projects/${project.id}/env`, (_req, res) => {
|
||||
const target = _req.query.target;
|
||||
if (typeof target === 'string') {
|
||||
const targetEnvs = envs.filter(env => env.target.includes(target));
|
||||
res.json({ envs: targetEnvs });
|
||||
return;
|
||||
client.scenario.get(`/v8/projects/${project.id}/env`, (req, res) => {
|
||||
const target: ProjectEnvTarget | undefined =
|
||||
typeof req.query.target === 'string'
|
||||
? parseEnvironment(req.query.target)
|
||||
: undefined;
|
||||
|
||||
let targetEnvs = envs;
|
||||
if (target) {
|
||||
targetEnvs = targetEnvs.filter(env => {
|
||||
if (typeof env.target === 'string') {
|
||||
return env.target === target;
|
||||
}
|
||||
if (Array.isArray(env.target)) {
|
||||
return env.target.includes(target);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ envs });
|
||||
res.json({ envs: targetEnvs });
|
||||
});
|
||||
client.scenario.post(`/v8/projects/${project.id}/env`, (req, res) => {
|
||||
const envObj = req.body;
|
||||
@@ -310,3 +365,43 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
||||
|
||||
return { project, envs };
|
||||
}
|
||||
|
||||
function getSystemEnvValue(
|
||||
systemEnvRef: string,
|
||||
{ vercelUrl }: { vercelUrl?: string }
|
||||
) {
|
||||
if (systemEnvRef === 'VERCEL_URL') {
|
||||
return vercelUrl || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function exposeSystemEnvs(
|
||||
projectEnvs: ProjectEnvVariable[],
|
||||
systemEnvValues: string[],
|
||||
autoExposeSystemEnvs: boolean | undefined,
|
||||
vercelUrl?: string,
|
||||
target?: ProjectEnvTarget
|
||||
) {
|
||||
const envs: Env = {};
|
||||
|
||||
if (autoExposeSystemEnvs) {
|
||||
envs['VERCEL'] = '1';
|
||||
envs['VERCEL_ENV'] = target || 'development';
|
||||
|
||||
for (const key of systemEnvValues) {
|
||||
envs[key] = getSystemEnvValue(key, { vercelUrl });
|
||||
}
|
||||
}
|
||||
|
||||
for (let env of projectEnvs) {
|
||||
if (env.type === ProjectEnvType.System) {
|
||||
envs[env.key] = getSystemEnvValue(env.value, { vercelUrl });
|
||||
} else {
|
||||
envs[env.key] = env.value;
|
||||
}
|
||||
}
|
||||
|
||||
return envs;
|
||||
}
|
||||
|
||||
77
packages/cli/test/unit/util/dev/headers.test.ts
Normal file
77
packages/cli/test/unit/util/dev/headers.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Headers } from 'node-fetch';
|
||||
import { applyOverriddenHeaders } from '../../../../src/util/dev/headers';
|
||||
|
||||
describe('applyOverriddenHeaders', () => {
|
||||
it('do nothing if x-middleware-override-headers is not set', async () => {
|
||||
const reqHeaders = { a: '1' };
|
||||
const respHeaders = new Headers();
|
||||
|
||||
applyOverriddenHeaders(reqHeaders, respHeaders);
|
||||
expect(reqHeaders).toStrictEqual({ a: '1' });
|
||||
});
|
||||
|
||||
it('adds a new header', async () => {
|
||||
const reqHeaders = { a: '1' };
|
||||
const respHeaders = new Headers({
|
||||
// Define a new header 'b' and keep the existing header 'a'
|
||||
'x-middleware-override-headers': 'a,b',
|
||||
'x-middleware-request-a': '1',
|
||||
'x-middleware-request-b': '2',
|
||||
});
|
||||
|
||||
applyOverriddenHeaders(reqHeaders, respHeaders);
|
||||
expect(reqHeaders).toStrictEqual({ a: '1', b: '2' });
|
||||
});
|
||||
|
||||
it('delete the header if x-middleware-request-* is undefined', async () => {
|
||||
const reqHeaders = { a: '1', b: '2' };
|
||||
const respHeaders = new Headers({
|
||||
// Deletes a new header 'c' and keep the existing headers `a` and `b`
|
||||
'x-middleware-override-headers': 'a,b,c',
|
||||
'x-middleware-request-a': '1',
|
||||
'x-middleware-request-b': '2',
|
||||
});
|
||||
|
||||
applyOverriddenHeaders(reqHeaders, respHeaders);
|
||||
expect(reqHeaders).toStrictEqual({ a: '1', b: '2' });
|
||||
});
|
||||
|
||||
it('updates an existing header', async () => {
|
||||
const reqHeaders = { a: '1', b: '2' };
|
||||
const respHeaders = new Headers({
|
||||
// Modifies the header 'b' and keep the existing header 'a'
|
||||
'x-middleware-override-headers': 'a,b',
|
||||
'x-middleware-request-a': '1',
|
||||
'x-middleware-request-b': 'modified',
|
||||
});
|
||||
|
||||
applyOverriddenHeaders(reqHeaders, respHeaders);
|
||||
expect(reqHeaders).toStrictEqual({ a: '1', b: 'modified' });
|
||||
});
|
||||
|
||||
it('ignores headers listed in NONOVERRIDABLE_HEADERS', async () => {
|
||||
const reqHeaders = { a: '1', host: 'example.com' };
|
||||
const respHeaders = new Headers({
|
||||
// Define a new header 'b' and 'content-length'
|
||||
'x-middleware-override-headers': 'a,b,content-length',
|
||||
'x-middleware-request-a': '1',
|
||||
'x-middleware-request-b': '2',
|
||||
'x-middleware-request-content-length': '128',
|
||||
});
|
||||
|
||||
applyOverriddenHeaders(reqHeaders, respHeaders);
|
||||
expect(reqHeaders).toStrictEqual({ a: '1', b: '2', host: 'example.com' });
|
||||
});
|
||||
|
||||
it('deletes an existing header', async () => {
|
||||
const reqHeaders = { a: '1', b: '2' };
|
||||
const respHeaders = new Headers({
|
||||
// Deletes the header 'a' and keep the existing header 'b'
|
||||
'x-middleware-override-headers': 'b',
|
||||
'x-middleware-request-b': '2',
|
||||
});
|
||||
|
||||
applyOverriddenHeaders(reqHeaders, respHeaders);
|
||||
expect(reqHeaders).toStrictEqual({ b: '2' });
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
- [ExtraResponseInit](interfaces/ExtraResponseInit.md)
|
||||
- [Geo](interfaces/Geo.md)
|
||||
- [ModifiedRequest](interfaces/ModifiedRequest.md)
|
||||
|
||||
### Variables
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
- [LATITUDE_HEADER_NAME](README.md#latitude_header_name)
|
||||
- [LONGITUDE_HEADER_NAME](README.md#longitude_header_name)
|
||||
- [REGION_HEADER_NAME](README.md#region_header_name)
|
||||
- [REQUEST_ID_HEADER_NAME](README.md#request_id_header_name)
|
||||
|
||||
### Functions
|
||||
|
||||
@@ -89,11 +91,25 @@ Longitude of the original client IP as calculated by Vercel Proxy.
|
||||
|
||||
• `Const` **REGION_HEADER_NAME**: `"x-vercel-ip-country-region"`
|
||||
|
||||
Region of the original client IP as calculated by Vercel Proxy.
|
||||
Country region of the original client IP calculated by Vercel Proxy.
|
||||
|
||||
See [docs](https://vercel.com/docs/concepts/edge-network/headers#x-vercel-ip-country-region).
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:24](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L24)
|
||||
[src/edge-headers.ts:26](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L26)
|
||||
|
||||
---
|
||||
|
||||
### REQUEST_ID_HEADER_NAME
|
||||
|
||||
• `Const` **REQUEST_ID_HEADER_NAME**: `"x-vercel-id"`
|
||||
|
||||
The request ID for each request generated by Vercel Proxy.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:30](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L30)
|
||||
|
||||
## Functions
|
||||
|
||||
@@ -123,7 +139,7 @@ Returns the location information for the incoming request.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:80](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L80)
|
||||
[src/edge-headers.ts:106](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L106)
|
||||
|
||||
---
|
||||
|
||||
@@ -149,7 +165,7 @@ Returns the IP address of the request from the headers.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:66](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L66)
|
||||
[src/edge-headers.ts:77](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L77)
|
||||
|
||||
---
|
||||
|
||||
@@ -197,7 +213,7 @@ export default function middleware(_req: Request) {
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/middleware-helpers.ts:94](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L94)
|
||||
[src/middleware-helpers.ts:145](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L145)
|
||||
|
||||
---
|
||||
|
||||
@@ -259,4 +275,4 @@ export const config = { matcher: '/api/users/:path*' };
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/middleware-helpers.ts:53](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L53)
|
||||
[src/middleware-helpers.ts:101](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L101)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
### Properties
|
||||
|
||||
- [headers](ExtraResponseInit.md#headers)
|
||||
- [request](ExtraResponseInit.md#request)
|
||||
- [status](ExtraResponseInit.md#status)
|
||||
- [statusText](ExtraResponseInit.md#statustext)
|
||||
|
||||
@@ -25,7 +26,19 @@ along with the response headers from the origin.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/middleware-helpers.ts:6](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L6)
|
||||
[src/middleware-helpers.ts:31](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L31)
|
||||
|
||||
---
|
||||
|
||||
### request
|
||||
|
||||
• `Optional` **request**: [`ModifiedRequest`](ModifiedRequest.md)
|
||||
|
||||
Fields to rewrite for the upstream request.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/middleware-helpers.ts:35](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L35)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ The location information of a given request.
|
||||
|
||||
- [city](Geo.md#city)
|
||||
- [country](Geo.md#country)
|
||||
- [countryRegion](Geo.md#countryregion)
|
||||
- [latitude](Geo.md#latitude)
|
||||
- [longitude](Geo.md#longitude)
|
||||
- [region](Geo.md#region)
|
||||
@@ -22,7 +23,7 @@ The city that the request originated from.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:41](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L41)
|
||||
[src/edge-headers.ts:47](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L47)
|
||||
|
||||
---
|
||||
|
||||
@@ -34,7 +35,20 @@ The country that the request originated from.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:44](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L44)
|
||||
[src/edge-headers.ts:50](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L50)
|
||||
|
||||
---
|
||||
|
||||
### countryRegion
|
||||
|
||||
• `Optional` **countryRegion**: `string`
|
||||
|
||||
The region part of the ISO 3166-2 code of the client IP.
|
||||
See [docs](https://vercel.com/docs/concepts/edge-network/headers#x-vercel-ip-country-region).
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:58](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L58)
|
||||
|
||||
---
|
||||
|
||||
@@ -46,7 +60,7 @@ The latitude of the client.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:50](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L50)
|
||||
[src/edge-headers.ts:61](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L61)
|
||||
|
||||
---
|
||||
|
||||
@@ -58,7 +72,7 @@ The longitude of the client.
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:53](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L53)
|
||||
[src/edge-headers.ts:64](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L64)
|
||||
|
||||
---
|
||||
|
||||
@@ -70,4 +84,4 @@ The [Vercel Edge Network region](https://vercel.com/docs/concepts/edge-network/r
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/edge-headers.ts:47](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L47)
|
||||
[src/edge-headers.ts:53](https://github.com/vercel/vercel/blob/main/packages/edge/src/edge-headers.ts#L53)
|
||||
|
||||
38
packages/edge/docs/interfaces/ModifiedRequest.md
Normal file
38
packages/edge/docs/interfaces/ModifiedRequest.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Interface: ModifiedRequest
|
||||
|
||||
## Table of contents
|
||||
|
||||
### Properties
|
||||
|
||||
- [headers](ModifiedRequest.md#headers)
|
||||
|
||||
## Properties
|
||||
|
||||
### headers
|
||||
|
||||
• `Optional` **headers**: `Headers`
|
||||
|
||||
If set, overwrites the incoming headers to the origin request.
|
||||
|
||||
This is useful when you want to pass data between a Middleware and a
|
||||
Serverless or Edge Function.
|
||||
|
||||
**`Example`**
|
||||
|
||||
<caption>Add a `x-user-id` header and remove the `Authorization` header</caption>
|
||||
|
||||
```ts
|
||||
import { rewrite } from '@vercel/edge';
|
||||
export default async function middleware(request: Request): Promise<Response> {
|
||||
const newHeaders = new Headers(request.headers);
|
||||
newHeaders.set('x-user-id', 'user_123');
|
||||
newHeaders.delete('authorization');
|
||||
return rewrite(request.url, {
|
||||
request: { headers: newHeaders },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Defined in
|
||||
|
||||
[src/middleware-helpers.ts:23](https://github.com/vercel/vercel/blob/main/packages/edge/src/middleware-helpers.ts#L23)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/edge",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
@@ -12,7 +12,7 @@
|
||||
"build:docs": "typedoc && prettier --write docs/**/*.md docs/*.md"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edge-runtime/jest-environment": "1.1.0-beta.7",
|
||||
"@edge-runtime/jest-environment": "2.0.0",
|
||||
"@types/jest": "27.4.1",
|
||||
"ts-node": "8.9.1",
|
||||
"tsup": "6.1.2",
|
||||
|
||||
@@ -19,9 +19,15 @@ export const LATITUDE_HEADER_NAME = 'x-vercel-ip-latitude';
|
||||
*/
|
||||
export const LONGITUDE_HEADER_NAME = 'x-vercel-ip-longitude';
|
||||
/**
|
||||
* Region of the original client IP as calculated by Vercel Proxy.
|
||||
* Country region of the original client IP calculated by Vercel Proxy.
|
||||
*
|
||||
* See [docs](https://vercel.com/docs/concepts/edge-network/headers#x-vercel-ip-country-region).
|
||||
*/
|
||||
export const REGION_HEADER_NAME = 'x-vercel-ip-country-region';
|
||||
/**
|
||||
* The request ID for each request generated by Vercel Proxy.
|
||||
*/
|
||||
export const REQUEST_ID_HEADER_NAME = 'x-vercel-id';
|
||||
|
||||
/**
|
||||
* We define a new type so this function can be reused with
|
||||
@@ -46,6 +52,11 @@ export interface Geo {
|
||||
/** The [Vercel Edge Network region](https://vercel.com/docs/concepts/edge-network/regions) that received the request. */
|
||||
region?: string;
|
||||
|
||||
/** The region part of the ISO 3166-2 code of the client IP.
|
||||
* See [docs](https://vercel.com/docs/concepts/edge-network/headers#x-vercel-ip-country-region).
|
||||
*/
|
||||
countryRegion?: string;
|
||||
|
||||
/** The latitude of the client. */
|
||||
latitude?: string;
|
||||
|
||||
@@ -67,6 +78,21 @@ export function ipAddress(request: Request): string | undefined {
|
||||
return getHeader(request, IP_HEADER_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the Vercel Edge Network region name from the request ID.
|
||||
*
|
||||
* @param requestId The request ID (`x-vercel-id`).
|
||||
* @returns The first region received the client request.
|
||||
*/
|
||||
function getRegionFromRequestId(requestId?: string): string | undefined {
|
||||
if (!requestId) {
|
||||
return 'dev1';
|
||||
}
|
||||
|
||||
// The request ID is in the format of `region::id` or `region1:region2:...::id`.
|
||||
return requestId.split(':')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location information for the incoming request.
|
||||
*
|
||||
@@ -81,7 +107,8 @@ export function geolocation(request: Request): Geo {
|
||||
return {
|
||||
city: getHeader(request, CITY_HEADER_NAME),
|
||||
country: getHeader(request, COUNTRY_HEADER_NAME),
|
||||
region: getHeader(request, REGION_HEADER_NAME),
|
||||
countryRegion: getHeader(request, REGION_HEADER_NAME),
|
||||
region: getRegionFromRequestId(getHeader(request, REQUEST_ID_HEADER_NAME)),
|
||||
latitude: getHeader(request, LATITUDE_HEADER_NAME),
|
||||
longitude: getHeader(request, LONGITUDE_HEADER_NAME),
|
||||
};
|
||||
|
||||
@@ -1,9 +1,57 @@
|
||||
export interface ModifiedRequest {
|
||||
/**
|
||||
* If set, overwrites the incoming headers to the origin request.
|
||||
*
|
||||
* This is useful when you want to pass data between a Middleware and a
|
||||
* Serverless or Edge Function.
|
||||
*
|
||||
* @example
|
||||
* <caption>Add a `x-user-id` header and remove the `Authorization` header</caption>
|
||||
*
|
||||
* ```ts
|
||||
* import { rewrite } from '@vercel/edge';
|
||||
* export default async function middleware(request: Request): Promise<Response> {
|
||||
* const newHeaders = new Headers(request.headers);
|
||||
* newHeaders.set('x-user-id', 'user_123');
|
||||
* newHeaders.delete('authorization');
|
||||
* return rewrite(request.url, {
|
||||
* request: { headers: newHeaders }
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
headers?: Headers;
|
||||
}
|
||||
|
||||
export interface ExtraResponseInit extends Omit<ResponseInit, 'headers'> {
|
||||
/**
|
||||
* These headers will be sent to the user response
|
||||
* along with the response headers from the origin.
|
||||
*/
|
||||
headers?: HeadersInit;
|
||||
/**
|
||||
* Fields to rewrite for the upstream request.
|
||||
*/
|
||||
request?: ModifiedRequest;
|
||||
}
|
||||
|
||||
function handleMiddlewareField(
|
||||
init: ExtraResponseInit | undefined,
|
||||
headers: Headers
|
||||
) {
|
||||
if (init?.request?.headers) {
|
||||
if (!(init.request.headers instanceof Headers)) {
|
||||
throw new Error('request.headers must be an instance of Headers');
|
||||
}
|
||||
|
||||
const keys = [];
|
||||
for (const [key, value] of init.request.headers) {
|
||||
headers.set('x-middleware-request-' + key, value);
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
headers.set('x-middleware-override-headers', keys.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +104,9 @@ export function rewrite(
|
||||
): Response {
|
||||
const headers = new Headers(init?.headers ?? {});
|
||||
headers.set('x-middleware-rewrite', String(destination));
|
||||
|
||||
handleMiddlewareField(init, headers);
|
||||
|
||||
return new Response(null, {
|
||||
...init,
|
||||
headers,
|
||||
@@ -94,6 +145,9 @@ export function rewrite(
|
||||
export function next(init?: ExtraResponseInit): Response {
|
||||
const headers = new Headers(init?.headers ?? {});
|
||||
headers.set('x-middleware-next', '1');
|
||||
|
||||
handleMiddlewareField(init, headers);
|
||||
|
||||
return new Response(null, {
|
||||
...init,
|
||||
headers,
|
||||
|
||||
57
packages/edge/test/edge-headers.test.ts
vendored
57
packages/edge/test/edge-headers.test.ts
vendored
@@ -12,6 +12,7 @@ import {
|
||||
LATITUDE_HEADER_NAME,
|
||||
LONGITUDE_HEADER_NAME,
|
||||
REGION_HEADER_NAME,
|
||||
REQUEST_ID_HEADER_NAME,
|
||||
} from '../src';
|
||||
|
||||
test('`ipAddress` returns the value from the header', () => {
|
||||
@@ -24,9 +25,16 @@ test('`ipAddress` returns the value from the header', () => {
|
||||
});
|
||||
|
||||
describe('`geolocation`', () => {
|
||||
test('returns an empty object if headers are not found', () => {
|
||||
test('returns an object with lots of undefined if headers are not found', () => {
|
||||
const req = new Request('https://example.vercel.sh');
|
||||
expect(geolocation(req)).toEqual({});
|
||||
expect(geolocation(req)).toEqual({
|
||||
city: undefined,
|
||||
country: undefined,
|
||||
countryRegion: undefined,
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
region: 'dev1',
|
||||
});
|
||||
});
|
||||
|
||||
test('reads values from headers', () => {
|
||||
@@ -36,7 +44,8 @@ describe('`geolocation`', () => {
|
||||
[COUNTRY_HEADER_NAME]: 'Israel',
|
||||
[LATITUDE_HEADER_NAME]: '32.109333',
|
||||
[LONGITUDE_HEADER_NAME]: '34.855499',
|
||||
[REGION_HEADER_NAME]: 'fra1',
|
||||
[REGION_HEADER_NAME]: 'TA', // https://en.wikipedia.org/wiki/ISO_3166-2:IL
|
||||
[REQUEST_ID_HEADER_NAME]: 'fra1::kpwjx-123455678-c0ffee',
|
||||
},
|
||||
});
|
||||
expect(geolocation(req)).toEqual<Geo>({
|
||||
@@ -45,6 +54,48 @@ describe('`geolocation`', () => {
|
||||
latitude: '32.109333',
|
||||
longitude: '34.855499',
|
||||
region: 'fra1',
|
||||
countryRegion: 'TA',
|
||||
});
|
||||
});
|
||||
|
||||
test('reads values from headers (with a request ID containing two edge regions)', () => {
|
||||
const req = new Request('https://example.vercel.sh', {
|
||||
headers: {
|
||||
[CITY_HEADER_NAME]: 'Tokyo',
|
||||
[COUNTRY_HEADER_NAME]: 'Japan',
|
||||
[LATITUDE_HEADER_NAME]: '37.1233',
|
||||
[LONGITUDE_HEADER_NAME]: '30.733399',
|
||||
[REGION_HEADER_NAME]: '13',
|
||||
[REQUEST_ID_HEADER_NAME]: 'hnd1:iad1::kpwjx-123455678-c0ffee',
|
||||
},
|
||||
});
|
||||
expect(geolocation(req)).toEqual<Geo>({
|
||||
city: 'Tokyo',
|
||||
country: 'Japan',
|
||||
latitude: '37.1233',
|
||||
longitude: '30.733399',
|
||||
region: 'hnd1',
|
||||
countryRegion: '13',
|
||||
});
|
||||
});
|
||||
|
||||
test('reads values from headers (without a request ID)', () => {
|
||||
const req = new Request('https://example.vercel.sh', {
|
||||
headers: {
|
||||
[CITY_HEADER_NAME]: 'Tokyo',
|
||||
[COUNTRY_HEADER_NAME]: 'Japan',
|
||||
[LATITUDE_HEADER_NAME]: '37.1233',
|
||||
[LONGITUDE_HEADER_NAME]: '30.733399',
|
||||
[REGION_HEADER_NAME]: '13',
|
||||
},
|
||||
});
|
||||
expect(geolocation(req)).toEqual<Geo>({
|
||||
city: 'Tokyo',
|
||||
country: 'Japan',
|
||||
latitude: '37.1233',
|
||||
longitude: '30.733399',
|
||||
region: 'dev1',
|
||||
countryRegion: '13',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
52
packages/edge/test/middleware-helpers.test.ts
vendored
52
packages/edge/test/middleware-helpers.test.ts
vendored
@@ -22,6 +22,32 @@ describe('rewrite', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('receives new request headers', () => {
|
||||
const headers = new Headers();
|
||||
headers.set('x-from-middleware1', 'hello1');
|
||||
headers.set('x-from-middleware2', 'hello2');
|
||||
const resp = rewrite(new URL('https://example.vercel.sh/'), {
|
||||
headers: {
|
||||
'x-custom-header': 'custom-value',
|
||||
},
|
||||
request: { headers },
|
||||
});
|
||||
expect({
|
||||
status: resp.status,
|
||||
headers: Object.fromEntries(resp.headers),
|
||||
}).toMatchObject({
|
||||
status: 200,
|
||||
headers: {
|
||||
'x-middleware-rewrite': 'https://example.vercel.sh/',
|
||||
'x-custom-header': 'custom-value',
|
||||
'x-middleware-override-headers':
|
||||
'x-from-middleware1,x-from-middleware2',
|
||||
'x-middleware-request-x-from-middleware2': 'hello2',
|
||||
'x-middleware-request-x-from-middleware1': 'hello1',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('next', () => {
|
||||
@@ -42,4 +68,30 @@ describe('next', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('receives new request headers', () => {
|
||||
const headers = new Headers();
|
||||
headers.set('x-from-middleware1', 'hello1');
|
||||
headers.set('x-from-middleware2', 'hello2');
|
||||
const resp = next({
|
||||
headers: {
|
||||
'x-custom-header': 'custom-value',
|
||||
},
|
||||
request: { headers },
|
||||
});
|
||||
expect({
|
||||
status: resp.status,
|
||||
headers: Object.fromEntries(resp.headers),
|
||||
}).toMatchObject({
|
||||
status: 200,
|
||||
headers: {
|
||||
'x-middleware-next': '1',
|
||||
'x-custom-header': 'custom-value',
|
||||
'x-middleware-override-headers':
|
||||
'x-from-middleware1,x-from-middleware2',
|
||||
'x-middleware-request-x-from-middleware2': 'hello2',
|
||||
'x-middleware-request-x-from-middleware1': 'hello1',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
6
packages/error-utils/build.js
Normal file
6
packages/error-utils/build.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const outDir = path.join(__dirname, 'dist');
|
||||
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
13
packages/error-utils/jest.config.js
Normal file
13
packages/error-utils/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
statements: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
25
packages/error-utils/package.json
Normal file
25
packages/error-utils/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@vercel/error-utils",
|
||||
"private": "true",
|
||||
"version": "1.0.0",
|
||||
"description": "A collection of error utilities for vercel/vercel",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vercel/vercel.git",
|
||||
"directory": "packages/error-utils"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest --coverage --env node --verbose"
|
||||
},
|
||||
"author": "Ethan Arrowood <ethan.arrowood@vercel.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.2.1",
|
||||
"@types/node": "16.11.7",
|
||||
"jest": "29.2.2",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ export const isError = (error: unknown): error is Error => {
|
||||
// Walk the prototype tree until we find a matching object.
|
||||
while (error) {
|
||||
if (Object.prototype.toString.call(error) === '[object Error]') return true;
|
||||
// eslint-disable-next-line no-param-reassign -- TODO: Fix eslint error following @vercel/style-guide migration
|
||||
error = Object.getPrototypeOf(error);
|
||||
}
|
||||
|
||||
148
packages/error-utils/test/index.test.ts
vendored
Normal file
148
packages/error-utils/test/index.test.ts
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
import fs from 'node:fs';
|
||||
import {
|
||||
isObject,
|
||||
isError,
|
||||
isErrnoException,
|
||||
isErrorLike,
|
||||
normalizeError,
|
||||
isSpawnError,
|
||||
errorToString,
|
||||
} from '../src';
|
||||
|
||||
const ARRAY: any[] = [];
|
||||
const BIGINT = 1n;
|
||||
const BOOLEAN = true;
|
||||
const FUNCTION = () => {};
|
||||
const NULL = null;
|
||||
const NUMBER = 0;
|
||||
const OBJECT = {};
|
||||
const STRING = '';
|
||||
const SYMBOL = Symbol('');
|
||||
const UNDEFINED = undefined;
|
||||
|
||||
class CLASS {} // `CLASS` is a function and `new CLASS()` is an Object
|
||||
|
||||
test('isObject returns true for objects only', () => {
|
||||
for (const item of [ARRAY, new CLASS(), OBJECT]) {
|
||||
expect(isObject(item)).toBe(true);
|
||||
}
|
||||
for (const item of [
|
||||
BIGINT,
|
||||
BOOLEAN,
|
||||
CLASS,
|
||||
FUNCTION,
|
||||
NULL,
|
||||
NUMBER,
|
||||
STRING,
|
||||
SYMBOL,
|
||||
UNDEFINED,
|
||||
]) {
|
||||
expect(isObject(item)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('isError returns true for Error instances only', () => {
|
||||
for (const error of [
|
||||
new Error(),
|
||||
new EvalError(),
|
||||
new RangeError(),
|
||||
new ReferenceError(),
|
||||
new SyntaxError(),
|
||||
new TypeError(),
|
||||
new URIError(),
|
||||
]) {
|
||||
expect(isError(error)).toBe(true);
|
||||
}
|
||||
for (const item of [
|
||||
ARRAY,
|
||||
BIGINT,
|
||||
BOOLEAN,
|
||||
CLASS,
|
||||
new CLASS(),
|
||||
FUNCTION,
|
||||
NULL,
|
||||
NUMBER,
|
||||
OBJECT,
|
||||
STRING,
|
||||
SYMBOL,
|
||||
UNDEFINED,
|
||||
]) {
|
||||
expect(isError(item)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('isError returns true for objects with a nested Error prototype', () => {
|
||||
class Foo {}
|
||||
const err = new Error();
|
||||
Object.setPrototypeOf(err, Foo.prototype);
|
||||
expect(isError(err)).toBe(true);
|
||||
});
|
||||
|
||||
test('isErrnoException returns true for NodeJS.ErrnoException only', () => {
|
||||
try {
|
||||
fs.statSync('./i-definitely-do-not-exist');
|
||||
fail();
|
||||
} catch (err) {
|
||||
expect(isErrnoException(err)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('isErrorLike returns true when object is like an error', () => {
|
||||
expect(isErrorLike(new Error())).toBe(true);
|
||||
expect(isErrorLike({ message: '' })).toBe(true);
|
||||
expect(isErrorLike({})).toBe(false);
|
||||
});
|
||||
|
||||
describe('errorToString', () => {
|
||||
const message = 'message';
|
||||
test('return `message` when first argument is an error', () => {
|
||||
expect(errorToString(new Error(message))).toStrictEqual(message);
|
||||
});
|
||||
test('returns `message` when first argument is error like', () => {
|
||||
expect(errorToString({ message })).toStrictEqual(message);
|
||||
});
|
||||
test('returns first argument when it is a string', () => {
|
||||
expect(errorToString(message)).toStrictEqual(message);
|
||||
});
|
||||
test('returns second argument when first argument is not an error, error like, nor a string', () => {
|
||||
expect(errorToString(null, message)).toStrictEqual(message);
|
||||
});
|
||||
test('returns default fallback message when first argument is not an error, error like, nor a string, and the second argument is not provided', () => {
|
||||
expect(errorToString(null)).toStrictEqual('An unknown error has ocurred.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeError', () => {
|
||||
const message = 'message';
|
||||
test('returns first argument if it is an error', () => {
|
||||
expect(normalizeError(new Error(message))).toStrictEqual(
|
||||
new Error(message)
|
||||
);
|
||||
});
|
||||
test('returns a new error if argument is not error like', () => {
|
||||
expect(normalizeError(message)).toStrictEqual(new Error(message));
|
||||
});
|
||||
test('returns a new error if argument is not error like', () => {
|
||||
expect(normalizeError({ message })).toStrictEqual(new Error(message));
|
||||
});
|
||||
test('returns a new error with fallback message if argument is not error like nor a string.', () => {
|
||||
expect(normalizeError(null)).toStrictEqual(
|
||||
new Error('An unknown error has ocurred.')
|
||||
);
|
||||
});
|
||||
test('returns an Error with the input object assigned to it', () => {
|
||||
expect(normalizeError({ message, prop: 'value' })).toStrictEqual(
|
||||
Object.assign(new Error(message), { prop: 'value' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('isSpawnError', () => {
|
||||
const spawnError = new Error('spawn error');
|
||||
Object.assign(spawnError, {
|
||||
code: 'SPAWN_ERROR',
|
||||
spawnargs: ['a', 'b', 'c'],
|
||||
});
|
||||
expect(isSpawnError(spawnError)).toBe(true);
|
||||
expect(isSpawnError(new Error('not spawn error'))).toBe(false);
|
||||
});
|
||||
7
packages/error-utils/test/tsconfig.json
vendored
Normal file
7
packages/error-utils/test/tsconfig.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": ["*.test.ts"]
|
||||
}
|
||||
22
packages/error-utils/tsconfig.json
Normal file
22
packages/error-utils/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2020"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noEmitOnError": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "./dist",
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"target": "ES2020"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
"@types/jest": "27.5.1",
|
||||
"@types/node": "*",
|
||||
"@vercel/build-utils": "5.5.5",
|
||||
"@vercel/static-config": "2.0.3",
|
||||
"typescript": "4.6.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
scanParentDirs,
|
||||
} from '@vercel/build-utils';
|
||||
import type { BuildV2, PackageJson } from '@vercel/build-utils';
|
||||
import { getConfig } from '@vercel/static-config';
|
||||
import { Project } from 'ts-morph';
|
||||
|
||||
export const build: BuildV2 = async ({
|
||||
entrypoint,
|
||||
@@ -118,6 +120,15 @@ export const build: BuildV2 = async ({
|
||||
deploymentTarget: 'v8-worker',
|
||||
entrypoint: 'index.js',
|
||||
files: edgeFunctionFiles,
|
||||
regions: (() => {
|
||||
try {
|
||||
const project = new Project();
|
||||
const config = getConfig(project, edgeFunctionFiles['index.js'].fsPath);
|
||||
return config?.regions;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})(),
|
||||
});
|
||||
|
||||
// The `index.html` file is a template, but we want to serve the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/next",
|
||||
"version": "3.2.5",
|
||||
"version": "3.2.6",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Files,
|
||||
BuildResultV2Typical as BuildResult,
|
||||
} from '@vercel/build-utils';
|
||||
import { Route, RouteWithHandle, RouteWithSrc } from '@vercel/routing-utils';
|
||||
import { Route, RouteWithHandle } from '@vercel/routing-utils';
|
||||
import { MAX_AGE_ONE_YEAR } from '.';
|
||||
import {
|
||||
NextRequiredServerFilesManifest,
|
||||
@@ -218,7 +218,8 @@ export async function serverBuild({
|
||||
nonLambdaSsgPages,
|
||||
route,
|
||||
routesManifest.pages404,
|
||||
routesManifest
|
||||
routesManifest,
|
||||
appDir
|
||||
);
|
||||
|
||||
if (result && result.static404Page) {
|
||||
@@ -963,61 +964,6 @@ export async function serverBuild({
|
||||
const { staticFiles, publicDirectoryFiles, staticDirectoryFiles } =
|
||||
await getStaticFiles(entryPath, entryDirectory, outputDirectory);
|
||||
|
||||
const notFoundPreviewRoutes: RouteWithSrc[] = [];
|
||||
|
||||
if (prerenderManifest.notFoundRoutes?.length > 0 && canUsePreviewMode) {
|
||||
// we combine routes into one src here to reduce the number of needed
|
||||
// routes since only the status is being modified and we don't want
|
||||
// to exceed the routes limit
|
||||
const starterRouteSrc = `^${
|
||||
entryDirectory !== '.'
|
||||
? `${path.posix.join('/', entryDirectory)}()`
|
||||
: '()'
|
||||
}`;
|
||||
let currentRouteSrc = starterRouteSrc;
|
||||
|
||||
const pushRoute = (src: string) => {
|
||||
notFoundPreviewRoutes.push({
|
||||
src,
|
||||
missing: [
|
||||
{
|
||||
type: 'cookie',
|
||||
key: '__prerender_bypass',
|
||||
value: prerenderManifest.bypassToken || undefined,
|
||||
},
|
||||
{
|
||||
type: 'cookie',
|
||||
key: '__next_preview_data',
|
||||
},
|
||||
],
|
||||
status: 404,
|
||||
});
|
||||
};
|
||||
|
||||
for (let i = 0; i < prerenderManifest.notFoundRoutes.length; i++) {
|
||||
const route = prerenderManifest.notFoundRoutes[i];
|
||||
const isLastRoute = i === prerenderManifest.notFoundRoutes.length - 1;
|
||||
|
||||
if (prerenderManifest.staticRoutes[route]?.initialRevalidate === false) {
|
||||
if (currentRouteSrc.length + route.length + 1 >= 4000) {
|
||||
pushRoute(currentRouteSrc);
|
||||
currentRouteSrc = starterRouteSrc;
|
||||
}
|
||||
// add to existing route src if src length limit isn't reached
|
||||
currentRouteSrc = `${currentRouteSrc.substring(
|
||||
0,
|
||||
currentRouteSrc.length - 1
|
||||
)}${
|
||||
currentRouteSrc[currentRouteSrc.length - 2] === '(' ? '' : '|'
|
||||
}${route}/?)`;
|
||||
|
||||
if (isLastRoute) {
|
||||
pushRoute(currentRouteSrc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeNextDataRoute = (isOverride = false) => {
|
||||
return isNextDataServerResolving
|
||||
? [
|
||||
@@ -1329,10 +1275,6 @@ export async function serverBuild({
|
||||
|
||||
...beforeFilesRewrites,
|
||||
|
||||
// ensure prerender's for notFound: true static routes
|
||||
// have 404 status code when not in preview mode
|
||||
...notFoundPreviewRoutes,
|
||||
|
||||
// Make sure to 404 for the /404 path itself
|
||||
...(i18n
|
||||
? [
|
||||
@@ -1647,18 +1589,6 @@ export async function serverBuild({
|
||||
continue: true,
|
||||
important: true,
|
||||
},
|
||||
...(appDir
|
||||
? [
|
||||
{
|
||||
src: path.posix.join('/', entryDirectory, '/(.*).rsc$'),
|
||||
headers: {
|
||||
'content-type': 'application/octet-stream',
|
||||
},
|
||||
continue: true,
|
||||
important: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
// TODO: remove below workaround when `/` is allowed to be output
|
||||
// different than `/index`
|
||||
|
||||
@@ -1601,16 +1601,19 @@ export const onPrerenderRouteInitial = (
|
||||
nonLambdaSsgPages: Set<string>,
|
||||
routeKey: string,
|
||||
hasPages404: boolean,
|
||||
routesManifest?: RoutesManifest
|
||||
routesManifest?: RoutesManifest,
|
||||
appDir?: string | null
|
||||
) => {
|
||||
let static404Page: string | undefined;
|
||||
let static500Page: string | undefined;
|
||||
|
||||
// Get the route file as it'd be mounted in the builder output
|
||||
const pr = prerenderManifest.staticRoutes[routeKey];
|
||||
const { initialRevalidate, srcRoute } = pr;
|
||||
const { initialRevalidate, srcRoute, dataRoute } = pr;
|
||||
const route = srcRoute || routeKey;
|
||||
|
||||
const isAppPathRoute = appDir && dataRoute?.endsWith('.rsc');
|
||||
|
||||
const routeNoLocale = routesManifest?.i18n
|
||||
? normalizeLocalePath(routeKey, routesManifest.i18n.locales).pathname
|
||||
: routeKey;
|
||||
@@ -1626,6 +1629,9 @@ export const onPrerenderRouteInitial = (
|
||||
}
|
||||
|
||||
if (
|
||||
// App paths must be Prerenders to ensure Vary header is
|
||||
// correctly added
|
||||
!isAppPathRoute &&
|
||||
initialRevalidate === false &&
|
||||
(!canUsePreviewMode || (hasPages404 && routeNoLocale === '/404')) &&
|
||||
!prerenderManifest.fallbackRoutes[route] &&
|
||||
@@ -1836,14 +1842,26 @@ export const onPrerenderRoute =
|
||||
),
|
||||
});
|
||||
|
||||
const outputPathPage = normalizeIndexOutput(
|
||||
path.posix.join(entryDirectory, routeFileNoExt),
|
||||
isServerMode
|
||||
);
|
||||
if (isAppPathRoute) {
|
||||
// for literal index routes we need to append an additional /index
|
||||
// due to the proxy's normalizing for /index routes
|
||||
if (routeKey !== '/index' && routeKey.endsWith('/index')) {
|
||||
routeKey = `${routeKey}/index`;
|
||||
routeFileNoExt = routeKey;
|
||||
origRouteFileNoExt = routeKey;
|
||||
}
|
||||
}
|
||||
|
||||
let outputPathPage = path.posix.join(entryDirectory, routeFileNoExt);
|
||||
|
||||
if (!isAppPathRoute) {
|
||||
outputPathPage = normalizeIndexOutput(outputPathPage, isServerMode);
|
||||
}
|
||||
const outputPathPageOrig = path.posix.join(
|
||||
entryDirectory,
|
||||
origRouteFileNoExt
|
||||
);
|
||||
|
||||
let lambda: undefined | Lambda;
|
||||
let outputPathData = path.posix.join(entryDirectory, dataRoute);
|
||||
|
||||
@@ -1887,7 +1905,7 @@ export const onPrerenderRoute =
|
||||
lambda = lambdas[outputSrcPathPage];
|
||||
}
|
||||
|
||||
if (!isNotFound && initialRevalidate === false) {
|
||||
if (!isAppPathRoute && !isNotFound && initialRevalidate === false) {
|
||||
if (htmlFsRef == null || jsonFsRef == null) {
|
||||
throw new NowBuildError({
|
||||
code: 'NEXT_HTMLFSREF_JSONFSREF',
|
||||
@@ -1973,6 +1991,19 @@ export const onPrerenderRoute =
|
||||
fallback: htmlFsRef,
|
||||
group: prerenderGroup,
|
||||
bypassToken: prerenderManifest.bypassToken,
|
||||
...(isNotFound
|
||||
? {
|
||||
initialStatus: 404,
|
||||
}
|
||||
: {}),
|
||||
|
||||
...(isAppPathRoute
|
||||
? {
|
||||
initialHeaders: {
|
||||
vary: '__rsc__, __next_router_state_tree__, __next_router_prefetch__',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
prerenders[outputPathData] = new Prerender({
|
||||
expiration: initialRevalidate,
|
||||
@@ -1981,6 +2012,21 @@ export const onPrerenderRoute =
|
||||
fallback: jsonFsRef,
|
||||
group: prerenderGroup,
|
||||
bypassToken: prerenderManifest.bypassToken,
|
||||
|
||||
...(isNotFound
|
||||
? {
|
||||
initialStatus: 404,
|
||||
}
|
||||
: {}),
|
||||
|
||||
...(isAppPathRoute
|
||||
? {
|
||||
initialHeaders: {
|
||||
'content-type': 'application/octet-stream',
|
||||
vary: '__rsc__, __next_router_state_tree__, __next_router_prefetch__',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
++prerenderGroup;
|
||||
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/(newroot)/dashboard/another/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/(newroot)/dashboard/another/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function AnotherPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from newroot/dashboard/another</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
10
packages/next/test/fixtures/00-app-dir-static/app/(newroot)/layout.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-static/app/(newroot)/layout.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function Root({ children }) {
|
||||
return (
|
||||
<html className="this-is-another-document-html">
|
||||
<head>
|
||||
<title>{`hello world`}</title>
|
||||
</head>
|
||||
<body className="this-is-another-document-body">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/(rootonly)/dashboard/changelog/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/(rootonly)/dashboard/changelog/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ChangelogPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard/changelog</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/(rootonly)/dashboard/hello/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/(rootonly)/dashboard/hello/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function HelloPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard/rootonly/hello</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
20
packages/next/test/fixtures/00-app-dir-static/app/client-component-route/page.js
vendored
Normal file
20
packages/next/test/fixtures/00-app-dir-static/app/client-component-route/page.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import style from './style.module.css';
|
||||
import './style.css';
|
||||
|
||||
export default function ClientComponentRoute() {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
setCount(1);
|
||||
}, [count]);
|
||||
return (
|
||||
<>
|
||||
<p className={style.red}>
|
||||
hello from app/client-component-route. <b>count: {count}</b>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-static/app/client-component-route/style.css
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-static/app/client-component-route/style.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
b {
|
||||
color: blue;
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-static/app/client-component-route/style.module.css
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-static/app/client-component-route/style.module.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
20
packages/next/test/fixtures/00-app-dir-static/app/client-nested/layout.js
vendored
Normal file
20
packages/next/test/fixtures/00-app-dir-static/app/client-nested/layout.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import styles from './style.module.css';
|
||||
import './style.css';
|
||||
|
||||
export default function ClientNestedLayout({ children }) {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
setCount(1);
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<h1 className={styles.red}>Client Nested. Count: {count}</h1>
|
||||
<button onClick={() => setCount(count + 1)}>{count}</button>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/client-nested/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/client-nested/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ClientPage() {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/client-nested</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-static/app/client-nested/style.css
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-static/app/client-nested/style.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
button {
|
||||
color: red;
|
||||
}
|
||||
3
packages/next/test/fixtures/00-app-dir-static/app/client-nested/style.module.css
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir-static/app/client-nested/style.module.css
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.red {
|
||||
color: red;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function DeploymentsBreakdownPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard/(custom)/deployments/breakdown</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
8
packages/next/test/fixtures/00-app-dir-static/app/dashboard/(custom)/layout.js
vendored
Normal file
8
packages/next/test/fixtures/00-app-dir-static/app/dashboard/(custom)/layout.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function CustomDashboardRootLayout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<h2>Custom dashboard</h2>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/deployments/[id]/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/deployments/[id]/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function DeploymentsPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard/deployments/[id]. ID is: {props.params.id}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/deployments/info/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/deployments/info/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function DeploymentsInfoPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard/deployments/info</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
8
packages/next/test/fixtures/00-app-dir-static/app/dashboard/deployments/layout.js
vendored
Normal file
8
packages/next/test/fixtures/00-app-dir-static/app/dashboard/deployments/layout.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function DeploymentsLayout({ message, children }) {
|
||||
return (
|
||||
<>
|
||||
<h2>Deployments hello</h2>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
packages/next/test/fixtures/00-app-dir-static/app/dashboard/index/lazy.js
vendored
Normal file
9
packages/next/test/fixtures/00-app-dir-static/app/dashboard/index/lazy.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client';
|
||||
|
||||
export default function LazyComponent() {
|
||||
return (
|
||||
<>
|
||||
<p>hello from lazy</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
10
packages/next/test/fixtures/00-app-dir-static/app/dashboard/index/page.js
vendored
Normal file
10
packages/next/test/fixtures/00-app-dir-static/app/dashboard/index/page.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ClientComponent } from './test.js';
|
||||
|
||||
export default function DashboardIndexPage() {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard/index</p>
|
||||
<ClientComponent />
|
||||
</>
|
||||
);
|
||||
}
|
||||
15
packages/next/test/fixtures/00-app-dir-static/app/dashboard/index/test.js
vendored
Normal file
15
packages/next/test/fixtures/00-app-dir-static/app/dashboard/index/test.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useState, lazy } from 'react';
|
||||
|
||||
const Lazy = lazy(() => import('./lazy.js'));
|
||||
|
||||
export function ClientComponent() {
|
||||
let [state] = useState('use client');
|
||||
return (
|
||||
<>
|
||||
<Lazy />
|
||||
<p className="hi">hello from modern the {state}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/integrations/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/integrations/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function IntegrationsPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard/integrations</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
8
packages/next/test/fixtures/00-app-dir-static/app/dashboard/layout.js
vendored
Normal file
8
packages/next/test/fixtures/00-app-dir-static/app/dashboard/layout.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function DashboardLayout(props) {
|
||||
return (
|
||||
<>
|
||||
<h1>Dashboard</h1>
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/page.js
vendored
Normal file
7
packages/next/test/fixtures/00-app-dir-static/app/dashboard/page.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function DashboardPage(props) {
|
||||
return (
|
||||
<>
|
||||
<p>hello from app/dashboard</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
11
packages/next/test/fixtures/00-app-dir-static/app/dynamic/[category]/[id]/layout.js
vendored
Normal file
11
packages/next/test/fixtures/00-app-dir-static/app/dynamic/[category]/[id]/layout.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function IdLayout({ children, params }) {
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
Id Layout. Params:{' '}
|
||||
<span id="id-layout-params">{JSON.stringify(params)}</span>
|
||||
</h3>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
11
packages/next/test/fixtures/00-app-dir-static/app/dynamic/[category]/[id]/page.js
vendored
Normal file
11
packages/next/test/fixtures/00-app-dir-static/app/dynamic/[category]/[id]/page.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function IdPage({ children, params }) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
Id Page. Params:{' '}
|
||||
<span id="id-page-params">{JSON.stringify(params)}</span>
|
||||
</p>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
11
packages/next/test/fixtures/00-app-dir-static/app/dynamic/[category]/layout.js
vendored
Normal file
11
packages/next/test/fixtures/00-app-dir-static/app/dynamic/[category]/layout.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function CategoryLayout({ children, params }) {
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
Category Layout. Params:{' '}
|
||||
<span id="category-layout-params">{JSON.stringify(params)}</span>{' '}
|
||||
</h2>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user