mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-29 19:00:09 +00:00
Compare commits
35 Commits
@vercel/ne
...
loopless-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dabbed9e60 | ||
|
|
e519d49d7b | ||
|
|
27683818ba | ||
|
|
e016e38229 | ||
|
|
5db1c5e610 | ||
|
|
24c228569f | ||
|
|
963de9b64f | ||
|
|
ab7fd52305 | ||
|
|
0fdb0dac91 | ||
|
|
bb0b632dcf | ||
|
|
ced9495143 | ||
|
|
fadc3f2588 | ||
|
|
a1d548dfef | ||
|
|
754090a8ab | ||
|
|
8269a48ee0 | ||
|
|
9f05a1865c | ||
|
|
8d1afc026f | ||
|
|
130f36aad6 | ||
|
|
dd87c9b0c6 | ||
|
|
f813b3340b | ||
|
|
976b02e895 | ||
|
|
843be9658c | ||
|
|
ad501a4cd0 | ||
|
|
727ae587db | ||
|
|
536b15079b | ||
|
|
887882697b | ||
|
|
e2db7c7734 | ||
|
|
022504787c | ||
|
|
0f424de406 | ||
|
|
4819c3ac61 | ||
|
|
c28ca7ef2d | ||
|
|
068ea00615 | ||
|
|
7f8145ab40 | ||
|
|
1a12715096 | ||
|
|
5b6d565360 |
@@ -2,16 +2,14 @@
|
||||
|
||||
#### Why This Error Occurred
|
||||
|
||||
The domain you supplied cannot be verified using either the intended set of nameservers or the given verification TXT record.
|
||||
The domain you supplied cannot be verified using the intended nameservers.
|
||||
|
||||
#### Possible Ways to Fix It
|
||||
#### Possible Way to Fix It
|
||||
|
||||
Apply the intended set of nameservers to your domain or add the given TXT verification record through your domain provider.
|
||||
Apply the intended set of nameservers to your domain.
|
||||
|
||||
You can retrieve both the intended nameservers and TXT verification record for the domain you wish to verify by running `vercel domains inspect <domain>`.
|
||||
|
||||
When you have added either verification method to your domain, you can run `vercel domains verify <domain>` again to complete verification for your domain.
|
||||
|
||||
Vercel will also automatically check periodically that your domain has been verified and automatically mark it as such if we detect either verification method on the domain.
|
||||
|
||||
If you would not like to verify your domain, you can remove it from your account using `vercel domains rm <domain>`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
This is a [Blitz.js](https://blitzjs.com/) project bootstrapped with `blitz new`.
|
||||
This is a [Blitz.js](https://blitzjs.com/) project bootstrapped with `npx blitz new`.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"demo": "https://blitzjs.now-examples.now.sh",
|
||||
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/blitz.svg",
|
||||
"tagline": "Blitz.js: The Fullstack React Framework",
|
||||
"description": "A Blitz.js app, created with the `blitz new` command.",
|
||||
"description": "A brand new Blitz.js app - the result of running `npx blitz new`.",
|
||||
"website": "https://blitzjs.com",
|
||||
"detectors": {
|
||||
"every": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/frameworks",
|
||||
"version": "0.0.17-canary.1",
|
||||
"version": "0.0.17",
|
||||
"main": "frameworks.json",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
out="dist"
|
||||
|
||||
rm -rf "$out"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/build-utils",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -8,6 +8,8 @@ import { isOfficialRuntime } from './';
|
||||
interface ErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
action?: string;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
@@ -608,20 +610,22 @@ function checkUnusedFunctions(
|
||||
} else {
|
||||
return {
|
||||
code: 'unused_function',
|
||||
message: `The function for ${fnKey} can't be handled by any builder`,
|
||||
message: `The pattern "${fnKey}" defined in \`functions\` doesn't match any Serverless Functions.`,
|
||||
action: 'Learn More',
|
||||
link: 'https://vercel.link/unmatched-function-pattern',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unusedFunctions.size) {
|
||||
const [unusedFunction] = Array.from(unusedFunctions);
|
||||
const [fnKey] = Array.from(unusedFunctions);
|
||||
|
||||
return {
|
||||
code: 'unused_function',
|
||||
message:
|
||||
`The function for ${unusedFunction} can't be handled by any builder. ` +
|
||||
`Make sure it is inside the api/ directory.`,
|
||||
message: `The pattern "${fnKey}" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.`,
|
||||
action: 'Learn More',
|
||||
link: 'https://vercel.link/unmatched-function-pattern',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -349,20 +349,35 @@ export async function runPipInstall(
|
||||
);
|
||||
}
|
||||
|
||||
export function getScriptName(
|
||||
pkg: Pick<PackageJson, 'scripts'> | null | undefined,
|
||||
possibleNames: Iterable<string>
|
||||
): string | null {
|
||||
if (pkg && pkg.scripts) {
|
||||
for (const name of possibleNames) {
|
||||
if (name in pkg.scripts) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function runPackageJsonScript(
|
||||
destPath: string,
|
||||
scriptName: string,
|
||||
scriptNames: string | Iterable<string>,
|
||||
spawnOpts?: SpawnOptions
|
||||
) {
|
||||
assert(path.isAbsolute(destPath));
|
||||
const { packageJson, cliType } = await scanParentDirs(destPath, true);
|
||||
const hasScript = Boolean(
|
||||
packageJson &&
|
||||
packageJson.scripts &&
|
||||
scriptName &&
|
||||
packageJson.scripts[scriptName]
|
||||
const scriptName = getScriptName(
|
||||
packageJson,
|
||||
typeof scriptNames === 'string' ? [scriptNames] : scriptNames
|
||||
);
|
||||
if (!hasScript) return false;
|
||||
if (!scriptName) return false;
|
||||
|
||||
debug('Running user script...');
|
||||
const runScriptTime = Date.now();
|
||||
|
||||
if (cliType === 'npm') {
|
||||
const prettyCommand = `npm run ${scriptName}`;
|
||||
@@ -382,6 +397,7 @@ export async function runPackageJsonScript(
|
||||
});
|
||||
}
|
||||
|
||||
debug(`Script complete [${Date.now() - runScriptTime}ms]`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
execCommand,
|
||||
spawnCommand,
|
||||
walkParentDirs,
|
||||
getScriptName,
|
||||
installDependencies,
|
||||
runPackageJsonScript,
|
||||
runNpmInstall,
|
||||
@@ -47,6 +48,7 @@ export {
|
||||
rename,
|
||||
execAsync,
|
||||
spawnAsync,
|
||||
getScriptName,
|
||||
installDependencies,
|
||||
runPackageJsonScript,
|
||||
execCommand,
|
||||
|
||||
@@ -775,8 +775,9 @@ describe('Test `detectBuilders`', () => {
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
code: 'unused_function',
|
||||
message:
|
||||
"The function for server/**/*.ts can't be handled by any builder. Make sure it is inside the api/ directory.",
|
||||
message: `The pattern "server/**/*.ts" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.`,
|
||||
action: 'Learn More',
|
||||
link: 'https://vercel.link/unmatched-function-pattern',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -1878,8 +1879,9 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
code: 'unused_function',
|
||||
message:
|
||||
"The function for server/**/*.ts can't be handled by any builder. Make sure it is inside the api/ directory.",
|
||||
message: `The pattern "server/**/*.ts" defined in \`functions\` doesn't match any Serverless Functions inside the \`api\` directory.`,
|
||||
action: 'Learn More',
|
||||
link: 'https://vercel.link/unmatched-function-pattern',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
72
packages/now-build-utils/test/unit.get-script-name.test.ts
vendored
Normal file
72
packages/now-build-utils/test/unit.get-script-name.test.ts
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
import assert from 'assert';
|
||||
import { getScriptName } from '../src';
|
||||
|
||||
describe('Test `getScriptName()`', () => {
|
||||
it('should return "vercel-*"', () => {
|
||||
const pkg = {
|
||||
scripts: {
|
||||
'vercel-dev': '',
|
||||
'vercel-build': '',
|
||||
dev: '',
|
||||
build: '',
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
getScriptName(pkg, ['vercel-dev', 'now-dev', 'dev']),
|
||||
'vercel-dev'
|
||||
);
|
||||
assert.equal(
|
||||
getScriptName(pkg, ['vercel-build', 'now-build', 'build']),
|
||||
'vercel-build'
|
||||
);
|
||||
assert.equal(getScriptName(pkg, ['dev']), 'dev');
|
||||
assert.equal(getScriptName(pkg, ['build']), 'build');
|
||||
});
|
||||
|
||||
it('should return "now-*"', () => {
|
||||
const pkg = {
|
||||
scripts: {
|
||||
'now-dev': '',
|
||||
'now-build': '',
|
||||
dev: '',
|
||||
build: '',
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
getScriptName(pkg, ['vercel-dev', 'now-dev', 'dev']),
|
||||
'now-dev'
|
||||
);
|
||||
assert.equal(
|
||||
getScriptName(pkg, ['vercel-build', 'now-build', 'build']),
|
||||
'now-build'
|
||||
);
|
||||
assert.equal(getScriptName(pkg, ['dev']), 'dev');
|
||||
assert.equal(getScriptName(pkg, ['build']), 'build');
|
||||
});
|
||||
|
||||
it('should return base script name', () => {
|
||||
const pkg = {
|
||||
scripts: {
|
||||
dev: '',
|
||||
build: '',
|
||||
},
|
||||
};
|
||||
assert.equal(getScriptName(pkg, ['dev']), 'dev');
|
||||
assert.equal(getScriptName(pkg, ['build']), 'build');
|
||||
});
|
||||
|
||||
it('should return `null`', () => {
|
||||
assert.equal(getScriptName(undefined, ['build']), null);
|
||||
assert.equal(getScriptName({}, ['build']), null);
|
||||
assert.equal(getScriptName({ scripts: {} }, ['build']), null);
|
||||
|
||||
const pkg = {
|
||||
scripts: {
|
||||
dev: '',
|
||||
build: '',
|
||||
},
|
||||
};
|
||||
assert.equal(getScriptName(pkg, ['vercel-dev', 'now-dev']), null);
|
||||
assert.equal(getScriptName(pkg, ['vercel-build', 'now-build']), null);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vercel",
|
||||
"version": "19.1.3-canary.1",
|
||||
"version": "20.0.0-canary.1",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Vercel",
|
||||
@@ -62,13 +62,14 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.4.1",
|
||||
"@vercel/go": "1.1.4-canary.0",
|
||||
"@vercel/next": "2.6.12",
|
||||
"@vercel/node": "1.7.2",
|
||||
"@vercel/build-utils": "2.4.2",
|
||||
"@vercel/go": "1.1.5-canary.0",
|
||||
"@vercel/next": "2.6.14-canary.1",
|
||||
"@vercel/node": "1.7.4-canary.0",
|
||||
"@vercel/python": "1.2.2",
|
||||
"@vercel/ruby": "1.2.3",
|
||||
"@vercel/static-build": "0.17.6-canary.1"
|
||||
"@vercel/static-build": "0.17.7-canary.0",
|
||||
"update-notifier": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/node": "5.5.0",
|
||||
@@ -119,7 +120,7 @@
|
||||
"chalk": "2.4.2",
|
||||
"chokidar": "3.3.1",
|
||||
"clipboardy": "2.1.0",
|
||||
"codecov": "3.6.5",
|
||||
"codecov": "3.7.1",
|
||||
"cpy": "7.2.0",
|
||||
"credit-card": "3.0.1",
|
||||
"date-fns": "1.29.0",
|
||||
@@ -185,7 +186,6 @@
|
||||
"ts-node": "8.3.0",
|
||||
"typescript": "3.9.3",
|
||||
"universal-analytics": "0.4.20",
|
||||
"update-check": "1.5.3",
|
||||
"utility-types": "2.1.0",
|
||||
"which": "2.0.2",
|
||||
"which-promise": "1.0.0",
|
||||
|
||||
@@ -49,7 +49,13 @@ async function main() {
|
||||
// Do the initial `ncc` build
|
||||
console.log();
|
||||
const src = join(dirRoot, 'src');
|
||||
const args = ['@zeit/ncc', 'build', '--source-map'];
|
||||
const args = [
|
||||
'@zeit/ncc',
|
||||
'build',
|
||||
'--source-map',
|
||||
'--external',
|
||||
'update-notifier',
|
||||
];
|
||||
if (!isDev) {
|
||||
args.push('--minify');
|
||||
}
|
||||
@@ -86,7 +92,7 @@ async function main() {
|
||||
// A bunch of source `.ts` files from CLI's `util` directory
|
||||
await remove(join(dirRoot, 'dist', 'util'));
|
||||
|
||||
console.log('Finished building `now-cli`');
|
||||
console.log('Finished building Vercel CLI');
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
||||
|
||||
@@ -8,12 +8,15 @@ import Client from '../../util/client';
|
||||
import { getLinkedProject } from '../../util/projects/link';
|
||||
import { getFrameworks } from '../../util/get-frameworks';
|
||||
import { isSettingValue } from '../../util/is-setting-value';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { ProjectSettings } from '../../types';
|
||||
import { ProjectSettings, ProjectEnvTarget } from '../../types';
|
||||
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
|
||||
import { Env } from '@vercel/build-utils';
|
||||
import setupAndLink from '../../util/link/setup-and-link';
|
||||
|
||||
type Options = {
|
||||
'--debug'?: boolean;
|
||||
'--listen'?: string;
|
||||
'--confirm': boolean;
|
||||
};
|
||||
|
||||
export default async function dev(
|
||||
@@ -35,25 +38,39 @@ export default async function dev(
|
||||
});
|
||||
|
||||
// retrieve dev command
|
||||
const [link, frameworks] = await Promise.all([
|
||||
let [link, frameworks] = await Promise.all([
|
||||
getLinkedProject(output, client, cwd),
|
||||
getFrameworks(client),
|
||||
]);
|
||||
|
||||
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
|
||||
const autoConfirm = opts['--confirm'];
|
||||
const forceDelete = false;
|
||||
|
||||
link = await setupAndLink(
|
||||
ctx,
|
||||
output,
|
||||
cwd,
|
||||
forceDelete,
|
||||
autoConfirm,
|
||||
'link',
|
||||
'Set up and develop'
|
||||
);
|
||||
|
||||
if (link.status === 'not_linked') {
|
||||
// User aborted project linking questions
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (link.status === 'error') {
|
||||
return link.exitCode;
|
||||
}
|
||||
|
||||
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
|
||||
output.error(
|
||||
`Your codebase isn’t linked to a project on Vercel. Run ${getCommandName()} to link it.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
let devCommand: string | undefined;
|
||||
let frameworkSlug: string | undefined;
|
||||
let projectSettings: ProjectSettings | undefined;
|
||||
let environmentVars: Env | undefined;
|
||||
if (link.status === 'linked') {
|
||||
const { project, org } = link;
|
||||
client.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
@@ -80,6 +97,13 @@ export default async function dev(
|
||||
if (project.rootDirectory) {
|
||||
cwd = join(cwd, project.rootDirectory);
|
||||
}
|
||||
|
||||
environmentVars = await getDecryptedEnvRecords(
|
||||
output,
|
||||
client,
|
||||
project,
|
||||
ProjectEnvTarget.Development
|
||||
);
|
||||
}
|
||||
|
||||
const devServer = new DevServer(cwd, {
|
||||
@@ -88,6 +112,7 @@ export default async function dev(
|
||||
devCommand,
|
||||
frameworkSlug,
|
||||
projectSettings,
|
||||
environmentVars,
|
||||
});
|
||||
|
||||
process.once('SIGINT', () => devServer.stop());
|
||||
|
||||
@@ -32,6 +32,7 @@ const help = () => {
|
||||
-d, --debug Debug mode [off]
|
||||
-l, --listen [uri] Specify a URI endpoint on which to listen [0.0.0.0:3000]
|
||||
-t, --token [token] Specify an Authorization Token
|
||||
--confirm Skip questions and use defaults when setting up a new project
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
@@ -56,6 +57,7 @@ export default async function main(ctx: NowContext) {
|
||||
argv = getArgs(ctx.argv.slice(2), {
|
||||
'--listen': String,
|
||||
'-l': '--listen',
|
||||
'--confirm': Boolean,
|
||||
|
||||
// Deprecated
|
||||
'--port': Number,
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import chalk from 'chalk';
|
||||
import psl from 'psl';
|
||||
|
||||
import { NowContext } from '../../types';
|
||||
import { Output } from '../../util/output';
|
||||
import * as ERRORS from '../../util/errors-ts';
|
||||
import addDomain from '../../util/domains/add-domain';
|
||||
import Client from '../../util/client';
|
||||
import cmd from '../../util/output/cmd';
|
||||
import formatDnsTable from '../../util/format-dns-table';
|
||||
import formatNSTable from '../../util/format-ns-table';
|
||||
import getScope from '../../util/get-scope';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import param from '../../util/output/param';
|
||||
import { getCommandName, getTitleName } from '../../util/pkg-name';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { getDomain } from '../../util/domains/get-domain';
|
||||
import { getLinkedProject } from '../../util/projects/link';
|
||||
import { isPublicSuffix } from '../../util/domains/is-public-suffix';
|
||||
import { getDomainConfig } from '../../util/domains/get-domain-config';
|
||||
import { addDomainToProject } from '../../util/projects/add-domain-to-project';
|
||||
import { removeDomainFromProject } from '../../util/projects/remove-domain-from-project';
|
||||
import code from '../../util/output/code';
|
||||
|
||||
type Options = {
|
||||
'--cdn': boolean;
|
||||
'--debug': boolean;
|
||||
'--no-cdn': boolean;
|
||||
'--force': boolean;
|
||||
};
|
||||
|
||||
export default async function add(
|
||||
@@ -33,6 +34,7 @@ export default async function add(
|
||||
const { currentTeam } = config;
|
||||
const { apiUrl } = ctx;
|
||||
const debug = opts['--debug'];
|
||||
const force = opts['--force'];
|
||||
const client = new Client({ apiUrl, token, currentTeam, debug });
|
||||
let contextName = null;
|
||||
|
||||
@@ -47,105 +49,116 @@ export default async function add(
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (opts['--cdn'] !== undefined || opts['--no-cdn'] !== undefined) {
|
||||
output.error(`Toggling CF from ${getTitleName()} CLI is deprecated.`);
|
||||
return 1;
|
||||
}
|
||||
const project = await getLinkedProject(output, client).then(result => {
|
||||
if (result.status === 'linked') {
|
||||
return result.project;
|
||||
}
|
||||
|
||||
if (args.length !== 1) {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (project && args.length !== 1) {
|
||||
output.error(
|
||||
`${getCommandName('domains add <domain>')} expects one argument`
|
||||
`${getCommandName('domains add <domain>')} expects one argument.`
|
||||
);
|
||||
return 1;
|
||||
} else if (!project && args.length !== 2) {
|
||||
output.error(
|
||||
`${getCommandName(
|
||||
'domains add <domain> <project>'
|
||||
)} expects two arguments.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const domainName = String(args[0]);
|
||||
const parsedDomain = psl.parse(domainName);
|
||||
if (parsedDomain.error) {
|
||||
output.error(`The provided domain name ${param(domainName)} is invalid`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const { domain, subdomain } = parsedDomain;
|
||||
if (!domain) {
|
||||
output.error(`The provided domain '${param(domainName)}' is not valid.`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (subdomain) {
|
||||
output.error(
|
||||
`You are adding '${domainName}' as a domain name containing a subdomain part '${subdomain}'\n` +
|
||||
` This feature is deprecated, please add just the root domain: ${chalk.cyan(
|
||||
`${getCommandName(`domain add ${domain}`)}`
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const projectName = project ? project.name : String(args[1]);
|
||||
|
||||
const addStamp = stamp();
|
||||
const addedDomain = await addDomain(client, domainName, contextName);
|
||||
|
||||
if (addedDomain instanceof ERRORS.InvalidDomain) {
|
||||
output.error(
|
||||
`The provided domain name "${addedDomain.meta.domain}" is invalid`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
let aliasTarget = await addDomainToProject(client, projectName, domainName);
|
||||
|
||||
if (addedDomain instanceof ERRORS.DomainAlreadyExists) {
|
||||
output.error(
|
||||
`The domain ${chalk.underline(
|
||||
addedDomain.meta.domain
|
||||
)} is already registered by a different account.\n` +
|
||||
` If this seems like a mistake, please contact us at support@vercel.com`
|
||||
);
|
||||
return 1;
|
||||
if (aliasTarget instanceof Error) {
|
||||
if (
|
||||
aliasTarget instanceof ERRORS.APIError &&
|
||||
aliasTarget.code === 'ALIAS_DOMAIN_EXIST' &&
|
||||
aliasTarget.project &&
|
||||
aliasTarget.project.id
|
||||
) {
|
||||
if (force) {
|
||||
const removeResponse = await removeDomainFromProject(
|
||||
client,
|
||||
aliasTarget.project.id,
|
||||
domainName
|
||||
);
|
||||
|
||||
if (removeResponse instanceof Error) {
|
||||
output.prettyError(removeResponse);
|
||||
return 1;
|
||||
}
|
||||
|
||||
aliasTarget = await addDomainToProject(client, projectName, domainName);
|
||||
}
|
||||
}
|
||||
|
||||
if (aliasTarget instanceof Error) {
|
||||
output.prettyError(aliasTarget);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// We can cast the information because we've just added the domain and it should be there
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
|
||||
addedDomain.name
|
||||
)} added correctly. ${addStamp()}\n`
|
||||
domainName
|
||||
)} added to project ${chalk.bold(projectName)}. ${addStamp()}`
|
||||
);
|
||||
|
||||
if (!addedDomain.verified) {
|
||||
if (isPublicSuffix(domainName)) {
|
||||
output.log(
|
||||
`The domain will automatically get assigned to your latest production deployment.`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const domainResponse = await getDomain(client, contextName, domainName);
|
||||
|
||||
if (domainResponse instanceof Error) {
|
||||
output.prettyError(domainResponse);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const domainConfig = await getDomainConfig(client, contextName, domainName);
|
||||
|
||||
if (domainConfig.misconfigured) {
|
||||
output.warn(
|
||||
`The domain was added but it is not verified. To verify it, you should either:`
|
||||
`This domain is not configured properly. To configure it you should either:`
|
||||
);
|
||||
output.print(
|
||||
` ${chalk.gray(
|
||||
'a)'
|
||||
)} Change your domain nameservers to the following intended set: ${chalk.gray(
|
||||
'[recommended]'
|
||||
)}\n`
|
||||
` ${chalk.grey('a)')} ` +
|
||||
`Set the following record on your DNS provider to continue: ` +
|
||||
`${code(`A ${domainName} 76.76.21.21`)} ` +
|
||||
`${chalk.grey('[recommended]')}\n`
|
||||
);
|
||||
output.print(
|
||||
` ${chalk.grey('b)')} ` +
|
||||
`Change your domain nameservers to the intended set`
|
||||
);
|
||||
output.print(
|
||||
`\n${formatNSTable(
|
||||
addedDomain.intendedNameservers,
|
||||
addedDomain.nameservers,
|
||||
domainResponse.intendedNameservers,
|
||||
domainResponse.nameservers,
|
||||
{ extraSpace: ' ' }
|
||||
)}\n\n`
|
||||
);
|
||||
output.print(
|
||||
` ${chalk.gray(
|
||||
'b)'
|
||||
)} Add a DNS TXT record with the name and value shown below.\n`
|
||||
);
|
||||
output.print(
|
||||
`\n${formatDnsTable([['_now', 'TXT', addedDomain.verificationRecord]], {
|
||||
extraSpace: ' ',
|
||||
})}\n\n`
|
||||
);
|
||||
output.print(
|
||||
` We will run a verification for you and you will receive an email upon completion.\n`
|
||||
);
|
||||
output.print(
|
||||
` If you want to force running a verification, you can run ${cmd(
|
||||
`${getCommandName('domains verify <domain>')}`
|
||||
)}\n`
|
||||
output.print(' Read more: https://vercel.link/domain-configuration\n\n');
|
||||
} else {
|
||||
output.log(
|
||||
`The domain will automatically get assigned to your latest production deployment.`
|
||||
);
|
||||
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -13,7 +13,6 @@ import transferIn from './transfer-in';
|
||||
import inspect from './inspect';
|
||||
import ls from './ls';
|
||||
import rm from './rm';
|
||||
import verify from './verify';
|
||||
import move from './move';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
|
||||
@@ -25,17 +24,17 @@ const help = () => {
|
||||
|
||||
ls Show all domains in a list
|
||||
inspect [name] Displays information related to a domain
|
||||
add [name] Add a new domain that you already own
|
||||
add [name] [project] Add a new domain that you already own
|
||||
rm [name] Remove a domain
|
||||
buy [name] Buy a domain that you don't yet own
|
||||
move [name] [destination] Move a domain to another user or team.
|
||||
transfer-in [name] Transfer in a domain to Vercel
|
||||
verify [name] Run a verification for a domain
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-d, --debug Debug mode [off]
|
||||
-f, --force Force a domain on a project and remove it from an existing one
|
||||
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
|
||||
'FILE'
|
||||
)} Path to the local ${'`vercel.json`'} file
|
||||
@@ -82,7 +81,6 @@ const COMMAND_CONFIG = {
|
||||
move: ['move'],
|
||||
rm: ['rm', 'remove'],
|
||||
transferIn: ['transfer-in'],
|
||||
verify: ['verify'],
|
||||
};
|
||||
|
||||
export default async function main(ctx: NowContext) {
|
||||
@@ -90,10 +88,9 @@ export default async function main(ctx: NowContext) {
|
||||
|
||||
try {
|
||||
argv = getArgs(ctx.argv.slice(2), {
|
||||
'--cdn': Boolean,
|
||||
'--code': String,
|
||||
'--no-cdn': Boolean,
|
||||
'--yes': Boolean,
|
||||
'--force': Boolean,
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
});
|
||||
@@ -122,8 +119,6 @@ export default async function main(ctx: NowContext) {
|
||||
return rm(ctx, argv, args, output);
|
||||
case 'transferIn':
|
||||
return transferIn(ctx, argv, args, output);
|
||||
case 'verify':
|
||||
return verify(ctx, argv, args, output);
|
||||
default:
|
||||
return ls(ctx, argv, args, output);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,16 @@ import { NowContext } from '../../types';
|
||||
import { Output } from '../../util/output';
|
||||
import Client from '../../util/client';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import dnsTable from '../../util/format-dns-table';
|
||||
import formatDate from '../../util/format-date';
|
||||
import formatNSTable from '../../util/format-ns-table';
|
||||
import getDomainByName from '../../util/domains/get-domain-by-name';
|
||||
import getScope from '../../util/get-scope';
|
||||
import formatTable from '../../util/format-table';
|
||||
import { findProjectsForDomain } from '../../util/projects/find-projects-for-domain';
|
||||
import getDomainPrice from '../../util/domains/get-domain-price';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import { getDomainConfig } from '../../util/domains/get-domain-config';
|
||||
import code from '../../util/output/code';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
@@ -70,7 +73,7 @@ export default async function inspect(
|
||||
.then(res => (res instanceof Error ? null : res.price))
|
||||
.catch(() => null),
|
||||
]);
|
||||
if (domain instanceof DomainNotFound) {
|
||||
if (!domain || domain instanceof DomainNotFound) {
|
||||
output.error(
|
||||
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
|
||||
);
|
||||
@@ -88,6 +91,15 @@ export default async function inspect(
|
||||
return 1;
|
||||
}
|
||||
|
||||
const projects = await findProjectsForDomain(client, domainName);
|
||||
|
||||
if (projects instanceof Error) {
|
||||
output.prettyError(projects);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const domainConfig = await getDomainConfig(client, contextName, domainName);
|
||||
|
||||
output.log(
|
||||
`Domain ${domainName} found under ${chalk.bold(contextName)} ${chalk.gray(
|
||||
inspectStamp()
|
||||
@@ -129,6 +141,7 @@ export default async function inspect(
|
||||
domain.txtVerifiedAt
|
||||
)}\n`
|
||||
);
|
||||
|
||||
if (renewalPrice && domain.boughtAt) {
|
||||
output.print(
|
||||
` ${chalk.cyan('Renewal Price')}\t\t$${renewalPrice} USD\n`
|
||||
@@ -145,37 +158,57 @@ export default async function inspect(
|
||||
);
|
||||
output.print('\n');
|
||||
|
||||
output.print(chalk.bold(' Verification Record\n\n'));
|
||||
output.print(
|
||||
`${dnsTable([['_now', 'TXT', domain.verificationRecord]], {
|
||||
extraSpace: ' ',
|
||||
})}\n`
|
||||
);
|
||||
output.print('\n');
|
||||
|
||||
if (!domain.verified) {
|
||||
output.warn(`This domain is not verified. To verify it you should either:`);
|
||||
output.print(
|
||||
` ${chalk.gray(
|
||||
'a)'
|
||||
)} Change your domain nameservers to the intended set detailed above. ${chalk.gray(
|
||||
'[recommended]'
|
||||
)}\n`
|
||||
if (domainConfig.misconfigured) {
|
||||
output.warn(
|
||||
`This domain is not configured properly. To configure it you should either:`
|
||||
);
|
||||
output.print(
|
||||
` ${chalk.gray(
|
||||
'b)'
|
||||
)} Add a DNS TXT record with the name and value shown above.\n\n`
|
||||
` ${chalk.grey('a)')} ` +
|
||||
`Set the following record on your DNS provider to continue: ` +
|
||||
`${code(`A ${domainName} 76.76.21.21`)} ` +
|
||||
`${chalk.grey('[recommended]')}\n`
|
||||
);
|
||||
output.print(
|
||||
` ${chalk.grey('b)')} ` +
|
||||
`Change your domain nameservers to the intended set detailed above.\n\n`
|
||||
);
|
||||
output.print(
|
||||
` We will run a verification for you and you will receive an email upon completion.\n`
|
||||
);
|
||||
output.print(
|
||||
` If you want to force running a verification, you can run ${getCommandName(
|
||||
`domains verify <domain>`
|
||||
)}\n`
|
||||
output.print(' Read more: https://vercel.link/domain-configuration\n\n');
|
||||
}
|
||||
|
||||
if (Array.isArray(projects) && projects.length > 0) {
|
||||
output.print(chalk.bold(' Projects\n'));
|
||||
|
||||
const table = formatTable(
|
||||
['Project', 'Domains'],
|
||||
['l', 'l'],
|
||||
[
|
||||
{
|
||||
rows: projects.map(project => {
|
||||
const name = project.name;
|
||||
|
||||
const domains = (project.alias || [])
|
||||
.map(target => target.domain)
|
||||
.filter(alias => alias.endsWith(domainName));
|
||||
|
||||
const cols = domains.length ? domains.join(', ') : '-';
|
||||
|
||||
return [name, cols];
|
||||
}),
|
||||
},
|
||||
]
|
||||
);
|
||||
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
|
||||
|
||||
output.print(
|
||||
table
|
||||
.split('\n')
|
||||
.map(line => ` ${line}`)
|
||||
.join('\n')
|
||||
);
|
||||
|
||||
output.print('\n\n');
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import ms from 'ms';
|
||||
import psl from 'psl';
|
||||
import chalk from 'chalk';
|
||||
import table from 'text-table';
|
||||
import plural from 'pluralize';
|
||||
|
||||
import Client from '../../util/client';
|
||||
import getDomains from '../../util/domains/get-domains';
|
||||
import getScope from '../../util/get-scope';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import strlen from '../../util/strlen';
|
||||
import { Output } from '../../util/output';
|
||||
import { Domain, NowContext } from '../../types';
|
||||
import formatTable from '../../util/format-table';
|
||||
import { formatDateWithoutTime } from '../../util/format-date';
|
||||
import { Domain, Project, NowContext } from '../../types';
|
||||
import { getProjectsWithDomains } from '../../util/projects/get-projects-with-domains';
|
||||
import getCommandFlags from '../../util/get-command-flags';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import isDomainExternal from '../../util/domains/is-domain-external';
|
||||
import { isPublicSuffix } from '../../util/domains/is-public-suffix';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
'--next': number;
|
||||
};
|
||||
|
||||
interface DomainInfo {
|
||||
domain: string;
|
||||
apexDomain: string;
|
||||
projectName: string | null;
|
||||
dns: 'Vercel' | 'External';
|
||||
configured: boolean;
|
||||
expiresAt: number | null;
|
||||
createdAt: number | null;
|
||||
}
|
||||
|
||||
export default async function ls(
|
||||
ctx: NowContext,
|
||||
opts: Options,
|
||||
@@ -60,16 +75,31 @@ export default async function ls(
|
||||
return 1;
|
||||
}
|
||||
|
||||
const { domains, pagination } = await getDomains(
|
||||
client,
|
||||
contextName,
|
||||
nextTimestamp
|
||||
);
|
||||
const [{ domains, pagination }, projects] = await Promise.all([
|
||||
getDomains(client, contextName),
|
||||
getProjectsWithDomains(client),
|
||||
] as const);
|
||||
|
||||
if (projects instanceof Error) {
|
||||
output.prettyError(projects);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const domainsInfo = createDomainsInfo(domains, projects);
|
||||
|
||||
output.log(
|
||||
`Domains found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}\n`
|
||||
`${plural(
|
||||
'project domain',
|
||||
domainsInfo.length,
|
||||
true
|
||||
)} found under ${chalk.bold(contextName)} ${chalk.gray(lsStamp())}`
|
||||
);
|
||||
if (domains.length > 0) {
|
||||
console.log(`${formatDomainsTable(domains)}\n`);
|
||||
|
||||
if (domainsInfo.length > 0) {
|
||||
output.print(
|
||||
formatDomainsTable(domainsInfo).replace(/^(.*)/gm, `${' '.repeat(3)}$1`)
|
||||
);
|
||||
output.print('\n\n');
|
||||
}
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
@@ -84,28 +114,92 @@ export default async function ls(
|
||||
return 0;
|
||||
}
|
||||
|
||||
function formatDomainsTable(domains: Domain[]) {
|
||||
const current = new Date();
|
||||
return table(
|
||||
[
|
||||
[
|
||||
'',
|
||||
chalk.gray('domain'),
|
||||
chalk.gray('serviceType'),
|
||||
chalk.gray('verified'),
|
||||
chalk.gray('cdn'),
|
||||
chalk.gray('age'),
|
||||
].map(s => chalk.dim(s)),
|
||||
...domains.map(domain => {
|
||||
const url = chalk.bold(domain.name);
|
||||
const time = chalk.gray(ms(current.getTime() - domain.createdAt));
|
||||
return ['', url, domain.serviceType, domain.verified, true, time];
|
||||
}),
|
||||
],
|
||||
{
|
||||
align: ['l', 'l', 'l', 'l', 'l'],
|
||||
hsep: ' '.repeat(4),
|
||||
stringLength: strlen,
|
||||
function createDomainsInfo(domains: Domain[], projects: Project[]) {
|
||||
const info = new Map<string, DomainInfo>();
|
||||
|
||||
domains.forEach(domain => {
|
||||
info.set(domain.name, {
|
||||
domain: domain.name,
|
||||
apexDomain: domain.name,
|
||||
projectName: null,
|
||||
expiresAt: domain.expiresAt || null,
|
||||
createdAt: domain.createdAt,
|
||||
configured: Boolean(domain.verified),
|
||||
dns: isDomainExternal(domain) ? 'External' : 'Vercel',
|
||||
});
|
||||
|
||||
projects.forEach(project => {
|
||||
(project.alias || []).forEach(target => {
|
||||
if (!target.domain.endsWith(domain.name)) return;
|
||||
|
||||
info.set(target.domain, {
|
||||
domain: target.domain,
|
||||
apexDomain: domain.name,
|
||||
projectName: project.name,
|
||||
expiresAt: domain.expiresAt || null,
|
||||
createdAt: domain.createdAt || target.createdAt || null,
|
||||
configured: Boolean(domain.verified),
|
||||
dns: isDomainExternal(domain) ? 'External' : 'Vercel',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
projects.forEach(project => {
|
||||
(project.alias || []).forEach(target => {
|
||||
if (info.has(target.domain)) return;
|
||||
|
||||
const { domain: apexDomain } = psl.parse(
|
||||
target.domain
|
||||
) as psl.ParsedDomain;
|
||||
|
||||
info.set(target.domain, {
|
||||
domain: target.domain,
|
||||
apexDomain: apexDomain || target.domain,
|
||||
projectName: project.name,
|
||||
expiresAt: null,
|
||||
createdAt: target.createdAt || null,
|
||||
configured: isPublicSuffix(target.domain),
|
||||
dns: isPublicSuffix(target.domain) ? 'Vercel' : 'External',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const list = Array.from(info.values());
|
||||
|
||||
return list.sort((a, b) => {
|
||||
if (a.apexDomain === b.apexDomain) {
|
||||
if (a.apexDomain === a.domain) return -1;
|
||||
if (b.apexDomain === b.domain) return 1;
|
||||
return a.domain.localeCompare(b.domain);
|
||||
}
|
||||
);
|
||||
|
||||
return a.apexDomain.localeCompare(b.apexDomain);
|
||||
});
|
||||
}
|
||||
|
||||
function formatDomainsTable(domainsInfo: DomainInfo[]) {
|
||||
const current = Date.now();
|
||||
|
||||
const rows: string[][] = domainsInfo.map(info => {
|
||||
const expiration = formatDateWithoutTime(info.expiresAt);
|
||||
const age = info.createdAt ? ms(current - info.createdAt) : '-';
|
||||
|
||||
return [
|
||||
info.domain,
|
||||
info.projectName || '-',
|
||||
info.dns,
|
||||
expiration,
|
||||
info.configured.toString(),
|
||||
chalk.gray(age),
|
||||
];
|
||||
});
|
||||
|
||||
const table = formatTable(
|
||||
['domain', 'project', 'dns', 'expiration', 'configured', 'age'],
|
||||
['l', 'l', 'l', 'l', 'l', 'l'],
|
||||
[{ rows }]
|
||||
);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as ERRORS from '../../util/errors-ts';
|
||||
import param from '../../util/output/param';
|
||||
import promptBool from '../../util/input/prompt-bool';
|
||||
import setCustomSuffix from '../../util/domains/set-custom-suffix';
|
||||
import { findProjectsForDomain } from '../../util/projects/find-projects-for-domain';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
type Options = {
|
||||
@@ -67,7 +68,7 @@ export default async function rm(
|
||||
}
|
||||
|
||||
const domain = await getDomainByName(client, contextName, domainName);
|
||||
if (domain instanceof DomainNotFound) {
|
||||
if (domain instanceof DomainNotFound || domain.name !== domainName) {
|
||||
output.error(
|
||||
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
|
||||
);
|
||||
@@ -85,6 +86,18 @@ export default async function rm(
|
||||
return 1;
|
||||
}
|
||||
|
||||
const projects = await findProjectsForDomain(client, domain.name);
|
||||
|
||||
if (Array.isArray(projects) && projects.length > 0) {
|
||||
output.warn(
|
||||
`The domain is currently used by ${plural(
|
||||
'project',
|
||||
projects.length,
|
||||
true
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
const skipConfirmation = opts['--yes'];
|
||||
if (
|
||||
!skipConfirmation &&
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import { NowContext } from '../../types';
|
||||
import { Output } from '../../util/output';
|
||||
import * as ERRORS from '../../util/errors-ts';
|
||||
import Client from '../../util/client';
|
||||
import formatDnsTable from '../../util/format-dns-table';
|
||||
import formatNSTable from '../../util/format-ns-table';
|
||||
import getDomainByName from '../../util/domains/get-domain-by-name';
|
||||
import getScope from '../../util/get-scope';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import verifyDomain from '../../util/domains/verify-domain';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
type Options = {
|
||||
'--debug': boolean;
|
||||
};
|
||||
|
||||
export default async function verify(
|
||||
ctx: NowContext,
|
||||
opts: Options,
|
||||
args: string[],
|
||||
output: Output
|
||||
) {
|
||||
const {
|
||||
authConfig: { token },
|
||||
config,
|
||||
} = ctx;
|
||||
const { currentTeam } = config;
|
||||
const { apiUrl } = ctx;
|
||||
const debug = opts['--debug'];
|
||||
const client = new Client({ apiUrl, token, currentTeam, debug });
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const [domainName] = args;
|
||||
|
||||
if (!domainName) {
|
||||
output.error(
|
||||
`${getCommandName(`domains verify <domain>`)} expects one argument`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (args.length !== 1) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('domains verify <domain>')}`
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const domain = await getDomainByName(client, contextName, domainName);
|
||||
if (domain instanceof ERRORS.DomainNotFound) {
|
||||
output.error(
|
||||
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
|
||||
);
|
||||
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (domain instanceof ERRORS.DomainPermissionDenied) {
|
||||
output.error(
|
||||
`You don't have access to the domain ${domainName} under ${chalk.bold(
|
||||
contextName
|
||||
)}`
|
||||
);
|
||||
output.log(`Run ${getCommandName(`domains ls`)} to see your domains.`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const verifyStamp = stamp();
|
||||
const result = await verifyDomain(client, domain.name, contextName);
|
||||
if (result instanceof ERRORS.DomainVerificationFailed) {
|
||||
const { nsVerification, txtVerification } = result.meta;
|
||||
output.error(
|
||||
`The domain ${
|
||||
domain.name
|
||||
} could not be verified due to the following reasons: ${verifyStamp()}\n`
|
||||
);
|
||||
output.print(
|
||||
` ${chalk.gray(
|
||||
'a)'
|
||||
)} Nameservers verification failed since we see a different set than the intended set:`
|
||||
);
|
||||
output.print(
|
||||
`\n${formatNSTable(
|
||||
nsVerification.intendedNameservers,
|
||||
nsVerification.nameservers,
|
||||
{ extraSpace: ' ' }
|
||||
)}\n\n`
|
||||
);
|
||||
output.print(
|
||||
` ${chalk.gray(
|
||||
'b)'
|
||||
)} DNS TXT verification failed since found no matching records.`
|
||||
);
|
||||
output.print(
|
||||
`\n${formatDnsTable(
|
||||
[['_now', 'TXT', txtVerification.verificationRecord]],
|
||||
{ extraSpace: ' ' }
|
||||
)}\n\n`
|
||||
);
|
||||
output.print(
|
||||
` Once your domain uses either the nameservers or the TXT DNS record from above, run again ${getCommandName(
|
||||
`domains verify <domain>`
|
||||
)}.\n`
|
||||
);
|
||||
output.print(
|
||||
` We will also periodically run a verification check for you and you will receive an email once your domain is verified.\n`
|
||||
);
|
||||
output.print(' Read more: https://err.sh/now/domain-verification\n\n');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (result.nsVerifiedAt) {
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
|
||||
domain.name
|
||||
)} was verified using nameservers. ${verifyStamp()}`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Domain ${chalk.bold(
|
||||
domain.name
|
||||
)} was verified using DNS TXT record. ${verifyStamp()}`
|
||||
);
|
||||
output.print(
|
||||
` You can verify with nameservers too. Run ${getCommandName(
|
||||
`domains inspect ${domain.name}`
|
||||
)} to find out the intended set.\n`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
4
packages/now-cli/src/commands/env/index.ts
vendored
4
packages/now-cli/src/commands/env/index.ts
vendored
@@ -124,7 +124,9 @@ export default async function main(ctx: NowContext) {
|
||||
return link.exitCode;
|
||||
} else if (link.status === 'not_linked') {
|
||||
output.error(
|
||||
`Your codebase isn’t linked to a project on Vercel. Run ${getCommandName()} to link it.`
|
||||
`Your codebase isn’t linked to a project on Vercel. Run ${getCommandName(
|
||||
'link'
|
||||
)} to begin.`
|
||||
);
|
||||
return 1;
|
||||
} else {
|
||||
|
||||
60
packages/now-cli/src/commands/env/pull.ts
vendored
60
packages/now-cli/src/commands/env/pull.ts
vendored
@@ -4,8 +4,7 @@ import { Output } from '../../util/output';
|
||||
import promptBool from '../../util/prompt-bool';
|
||||
import Client from '../../util/client';
|
||||
import stamp from '../../util/output/stamp';
|
||||
import getEnvVariables from '../../util/env/get-env-records';
|
||||
import getDecryptedSecret from '../../util/env/get-decrypted-secret';
|
||||
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
|
||||
import param from '../../util/output/param';
|
||||
import withSpinner from '../../util/with-spinner';
|
||||
import { join } from 'path';
|
||||
@@ -13,6 +12,7 @@ import { promises, openSync, closeSync, readSync } from 'fs';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
const { writeFile } = promises;
|
||||
import { Env } from '@vercel/build-utils';
|
||||
|
||||
const CONTENTS_PREFIX = '# Created by Vercel CLI\n';
|
||||
|
||||
@@ -84,45 +84,21 @@ export default async function pull(
|
||||
);
|
||||
const pullStamp = stamp();
|
||||
|
||||
const records = await withSpinner('Downloading', async () => {
|
||||
const dev = ProjectEnvTarget.Development;
|
||||
const envs = await getEnvVariables(output, client, project.id, 4, dev);
|
||||
const decryptedValues = await Promise.all(
|
||||
envs.map(async env => {
|
||||
try {
|
||||
const value = await getDecryptedSecret(output, client, env.value);
|
||||
return { value, found: true };
|
||||
} catch (error) {
|
||||
if (error && error.status === 404) {
|
||||
return { value: '', found: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
const results: { key: string; value: string; found: boolean }[] = [];
|
||||
for (let i = 0; i < decryptedValues.length; i++) {
|
||||
const { key } = envs[i];
|
||||
const { value, found } = decryptedValues[i];
|
||||
results.push({ key, value, found });
|
||||
}
|
||||
return results;
|
||||
});
|
||||
const records: Env = await withSpinner(
|
||||
'Downloading',
|
||||
async () =>
|
||||
await getDecryptedEnvRecords(
|
||||
output,
|
||||
client,
|
||||
project,
|
||||
ProjectEnvTarget.Development
|
||||
)
|
||||
);
|
||||
|
||||
const contents =
|
||||
CONTENTS_PREFIX +
|
||||
records
|
||||
.filter(obj => {
|
||||
if (!obj.found) {
|
||||
output.print('');
|
||||
output.warn(
|
||||
`Unable to download variable ${obj.key} because associated secret was deleted`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(({ key, value }) => `${key}="${escapeValue(value)}"`)
|
||||
Object.entries(records)
|
||||
.map(([key, value]) => `${key}="${escapeValue(value)}"`)
|
||||
.join('\n') +
|
||||
'\n';
|
||||
|
||||
@@ -139,8 +115,10 @@ export default async function pull(
|
||||
return 0;
|
||||
}
|
||||
|
||||
function escapeValue(value: string) {
|
||||
function escapeValue(value: string | undefined) {
|
||||
return value
|
||||
.replace(new RegExp('\n', 'g'), '\\n') // combine newlines (unix) into one line
|
||||
.replace(new RegExp('\r', 'g'), '\\r'); // combine newlines (windows) into one line
|
||||
? value
|
||||
.replace(new RegExp('\n', 'g'), '\\n') // combine newlines (unix) into one line
|
||||
.replace(new RegExp('\r', 'g'), '\\r') // combine newlines (windows) into one line
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default new Map([
|
||||
['help', 'help'],
|
||||
['init', 'init'],
|
||||
['inspect', 'inspect'],
|
||||
['link', 'link'],
|
||||
['list', 'list'],
|
||||
['ln', 'alias'],
|
||||
['log', 'logs'],
|
||||
|
||||
98
packages/now-cli/src/commands/link/index.ts
Normal file
98
packages/now-cli/src/commands/link/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import chalk from 'chalk';
|
||||
import { NowContext } from '../../types';
|
||||
import createOutput from '../../util/output';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getSubcommand from '../../util/get-subcommand';
|
||||
import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import setupAndLink from '../../util/link/setup-and-link';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} link`)} [options]
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
|
||||
'FILE'
|
||||
)} Path to the local ${'`vercel.json`'} file
|
||||
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
|
||||
'DIR'
|
||||
)} Path to the global ${'`.vercel`'} directory
|
||||
-d, --debug Debug mode [off]
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
--confirm Confirm default options and skip questions
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Link current directory to a Vercel Project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} link`)}
|
||||
|
||||
${chalk.gray(
|
||||
'–'
|
||||
)} Link current directory with default options and skip questions
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} link --confirm`)}
|
||||
|
||||
${chalk.gray('–')} Link a specific directory to a Vercel Project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} link /usr/src/project`)}
|
||||
`);
|
||||
};
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
// No subcommands yet
|
||||
};
|
||||
|
||||
export default async function main(ctx: NowContext) {
|
||||
let argv;
|
||||
|
||||
try {
|
||||
argv = getArgs(ctx.argv.slice(2), {
|
||||
'--confirm': Boolean,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
const debug = argv['--debug'];
|
||||
const output = createOutput({ debug });
|
||||
const { args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG);
|
||||
const path = args[0] || process.cwd();
|
||||
const autoConfirm = argv['--confirm'];
|
||||
const forceDelete = true;
|
||||
|
||||
const link = await setupAndLink(
|
||||
ctx,
|
||||
output,
|
||||
path,
|
||||
forceDelete,
|
||||
autoConfirm,
|
||||
'success',
|
||||
'Set up'
|
||||
);
|
||||
|
||||
if (link.status === 'error') {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,7 @@ import sourceMap from '@zeit/source-map-support';
|
||||
import { mkdirp } from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import epipebomb from 'epipebomb';
|
||||
import checkForUpdate from 'update-check';
|
||||
import ms from 'ms';
|
||||
import updateNotifier from 'update-notifier';
|
||||
import { URL } from 'url';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { NowBuildError } from '@vercel/build-utils';
|
||||
@@ -52,6 +51,14 @@ import getUpdateCommand from './util/get-update-command';
|
||||
import { metrics, shouldCollectMetrics } from './util/metrics.ts';
|
||||
import { getCommandName, getTitleName } from './util/pkg-name.ts';
|
||||
|
||||
const isCanary = pkg.version.includes('canary');
|
||||
|
||||
// Checks for available update and returns an instance
|
||||
const notifier = updateNotifier({
|
||||
pkg,
|
||||
distTag: isCanary ? 'canary' : 'latest',
|
||||
});
|
||||
|
||||
const VERCEL_DIR = getGlobalPathConfig();
|
||||
const VERCEL_CONFIG_PATH = configFiles.getConfigFilePath();
|
||||
const VERCEL_AUTH_CONFIG_PATH = configFiles.getAuthConfigFilePath();
|
||||
@@ -66,7 +73,7 @@ sourceMap.install();
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
release: `vercel-cli@${pkg.version}`,
|
||||
environment: pkg.version.includes('canary') ? 'canary' : 'stable',
|
||||
environment: isCanary ? 'canary' : 'stable',
|
||||
});
|
||||
|
||||
let debug = () => {};
|
||||
@@ -128,38 +135,20 @@ const main = async argv_ => {
|
||||
// (as in: `vercel ls`)
|
||||
const targetOrSubcommand = argv._[2];
|
||||
|
||||
let update = null;
|
||||
|
||||
try {
|
||||
if (targetOrSubcommand !== 'update') {
|
||||
update = await checkForUpdate(pkg, {
|
||||
interval: ms('1d'),
|
||||
distTag: pkg.version.includes('canary') ? 'canary' : 'latest',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
error(`Checking for updates failed${isDebugging ? ':' : ''}`)
|
||||
);
|
||||
|
||||
if (isDebugging) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (update && isTTY) {
|
||||
if (notifier.update && isTTY) {
|
||||
const { latest } = notifier.update;
|
||||
console.log(
|
||||
info(
|
||||
`${chalk.bgRed('UPDATE AVAILABLE')} ` +
|
||||
`Run ${cmd(
|
||||
await getUpdateCommand()
|
||||
)} to install ${getTitleName()} CLI ${update.latest}`
|
||||
)} to install ${getTitleName()} CLI ${latest}`
|
||||
)
|
||||
);
|
||||
|
||||
console.log(
|
||||
info(
|
||||
`Changelog: https://github.com/vercel/vercel/releases/tag/vercel@${update.latest}`
|
||||
`Changelog: https://github.com/vercel/vercel/releases/tag/vercel@${latest}`
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -169,7 +158,7 @@ const main = async argv_ => {
|
||||
`${getTitleName()} CLI ${pkg.version}${
|
||||
targetOrSubcommand === 'dev' ? ' dev (beta)' : ''
|
||||
}${
|
||||
pkg.version.includes('canary') || targetOrSubcommand === 'dev'
|
||||
isCanary || targetOrSubcommand === 'dev'
|
||||
? ' — https://vercel.com/feedback'
|
||||
: ''
|
||||
}`
|
||||
@@ -191,9 +180,7 @@ const main = async argv_ => {
|
||||
} catch (err) {
|
||||
console.error(
|
||||
error(
|
||||
`An unexpected error occurred while trying to find the global directory: ${
|
||||
err.message
|
||||
}`
|
||||
`An unexpected error occurred while trying to find the global directory: ${err.message}`
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -83,6 +83,16 @@ export type Domain = {
|
||||
};
|
||||
};
|
||||
|
||||
export type DomainConfig = {
|
||||
configuredBy: null | 'CNAME' | 'A' | 'http';
|
||||
misconfigured: boolean;
|
||||
serviceType: 'zeit.world' | 'external' | 'na';
|
||||
nameservers: string[];
|
||||
cnames: string[] & { traceString?: string };
|
||||
aValues: string[] & { traceString?: string };
|
||||
dnssecEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type Cert = {
|
||||
uid: string;
|
||||
autoRenew: boolean;
|
||||
@@ -217,6 +227,16 @@ export type DNSRecordData =
|
||||
| SRVRecordData
|
||||
| MXRecordData;
|
||||
|
||||
export interface ProjectAliasTarget {
|
||||
createdAt?: number;
|
||||
domain: string;
|
||||
redirect?: string | null;
|
||||
target: 'PRODUCTION' | 'STAGING';
|
||||
configuredBy?: null | 'CNAME' | 'A';
|
||||
configuredChangedAt?: null | number;
|
||||
configuredChangeAttempts?: [number, number];
|
||||
}
|
||||
|
||||
export interface Secret {
|
||||
uid: string;
|
||||
name: string;
|
||||
@@ -258,6 +278,10 @@ export interface Project extends ProjectSettings {
|
||||
accountId: string;
|
||||
updatedAt: number;
|
||||
createdAt: number;
|
||||
alias?: ProjectAliasTarget[];
|
||||
devCommand?: string | null;
|
||||
framework?: string | null;
|
||||
rootDirectory?: string | null;
|
||||
latestDeployments?: Partial<Deployment>[];
|
||||
}
|
||||
|
||||
@@ -277,3 +301,8 @@ export interface PaginationOptions {
|
||||
count: number;
|
||||
next?: number;
|
||||
}
|
||||
|
||||
export type ProjectLinkResult =
|
||||
| { status: 'linked'; org: Org; project: Project }
|
||||
| { status: 'not_linked'; org: null; project: null }
|
||||
| { status: 'error'; exitCode: number };
|
||||
|
||||
@@ -67,8 +67,6 @@ async function createBuildProcess(
|
||||
...process.env,
|
||||
PATH,
|
||||
...envConfigs.allEnv,
|
||||
NOW_REGION: 'dev1',
|
||||
VERCEL_REGION: 'dev1',
|
||||
};
|
||||
|
||||
const buildProcess = fork(builderWorkerPath, [], {
|
||||
@@ -365,8 +363,6 @@ export async function executeBuild(
|
||||
...nowConfig.env,
|
||||
...asset.environment,
|
||||
...envConfigs.runEnv,
|
||||
NOW_REGION: 'dev1',
|
||||
VERCEL_REGION: 'dev1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -140,12 +140,16 @@ export default class DevServer {
|
||||
private blockingBuildsPromise: Promise<void> | null;
|
||||
private updateBuildersPromise: Promise<void> | null;
|
||||
private updateBuildersTimeout: NodeJS.Timeout | undefined;
|
||||
private startPromise: Promise<void> | null;
|
||||
|
||||
private environmentVars: Env | undefined;
|
||||
|
||||
constructor(cwd: string, options: DevServerOptions) {
|
||||
this.cwd = cwd;
|
||||
this.debug = options.debug;
|
||||
this.output = options.output;
|
||||
this.envConfigs = { buildEnv: {}, runEnv: {}, allEnv: {} };
|
||||
this.environmentVars = options.environmentVars;
|
||||
this.files = {};
|
||||
this.address = '';
|
||||
this.devCommand = options.devCommand;
|
||||
@@ -169,12 +173,13 @@ export default class DevServer {
|
||||
this.getNowConfigPromise = null;
|
||||
this.blockingBuildsPromise = null;
|
||||
this.updateBuildersPromise = null;
|
||||
this.startPromise = null;
|
||||
|
||||
this.watchAggregationId = null;
|
||||
this.watchAggregationEvents = [];
|
||||
this.watchAggregationTimeout = 500;
|
||||
|
||||
this.filter = (path) => Boolean(path);
|
||||
this.filter = path => Boolean(path);
|
||||
this.podId = Math.random().toString(32).slice(-5);
|
||||
|
||||
this.devServerPids = new Set();
|
||||
@@ -213,8 +218,8 @@ export default class DevServer {
|
||||
}
|
||||
}
|
||||
|
||||
events = events.filter((event) =>
|
||||
distPaths.every((distPath) => !event.path.startsWith(distPath))
|
||||
events = events.filter(event =>
|
||||
distPaths.every(distPath => !event.path.startsWith(distPath))
|
||||
);
|
||||
|
||||
// First, update the `files` mapping of source files
|
||||
@@ -370,7 +375,7 @@ export default class DevServer {
|
||||
this,
|
||||
fileList
|
||||
);
|
||||
const sources = matches.map((m) => m.src);
|
||||
const sources = matches.map(m => m.src);
|
||||
|
||||
if (isInitial && fileList.length === 0) {
|
||||
this.output.warn('There are no files inside your deployment.');
|
||||
@@ -479,13 +484,16 @@ export default class DevServer {
|
||||
const dotenv = await fs.readFile(filePath, 'utf8');
|
||||
this.output.debug(`Using local env: ${filePath}`);
|
||||
env = parseDotenv(dotenv);
|
||||
env = this.populateVercelEnvVars(env);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return this.validateEnvConfig(fileName, base || {}, env);
|
||||
return {
|
||||
...this.validateEnvConfig(fileName, base || {}, env),
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof MissingDotenvVarsError) {
|
||||
this.output.error(err.message);
|
||||
@@ -543,7 +551,7 @@ export default class DevServer {
|
||||
const { projectSettings, cleanUrls, trailingSlash } = config;
|
||||
|
||||
const opts = { output: this.output, isBuilds: true };
|
||||
const files = (await getFiles(this.cwd, config, opts)).map((f) =>
|
||||
const files = (await getFiles(this.cwd, config, opts)).map(f =>
|
||||
relative(this.cwd, f)
|
||||
);
|
||||
|
||||
@@ -570,7 +578,7 @@ export default class DevServer {
|
||||
}
|
||||
|
||||
if (warnings && warnings.length > 0) {
|
||||
warnings.forEach((warning) => this.output.warn(warning.message));
|
||||
warnings.forEach(warning => this.output.warn(warning.message));
|
||||
}
|
||||
|
||||
if (builders) {
|
||||
@@ -620,13 +628,20 @@ export default class DevServer {
|
||||
this.apiExtensions = detectApiExtensions(config.builds || []);
|
||||
|
||||
// Update the env vars configuration
|
||||
const [runEnv, buildEnv] = await Promise.all([
|
||||
let [runEnv, buildEnv] = await Promise.all([
|
||||
this.getLocalEnv('.env', config.env),
|
||||
this.getLocalEnv('.env.build', config.build?.env),
|
||||
]);
|
||||
const allEnv = { ...buildEnv, ...runEnv };
|
||||
this.envConfigs = { buildEnv, runEnv, allEnv };
|
||||
|
||||
let allEnv = { ...buildEnv, ...runEnv };
|
||||
|
||||
// If no .env/.build.env is present, fetch and use cloud environment variables
|
||||
if (Object.keys(allEnv).length === 0) {
|
||||
const cloudEnv = this.populateVercelEnvVars(this.environmentVars);
|
||||
allEnv = runEnv = buildEnv = cloudEnv;
|
||||
}
|
||||
|
||||
this.envConfigs = { buildEnv, runEnv, allEnv };
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -731,6 +746,26 @@ export default class DevServer {
|
||||
return merged;
|
||||
}
|
||||
|
||||
populateVercelEnvVars(env: Env | undefined): Env {
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const name of Object.keys(env)) {
|
||||
if (name === 'VERCEL_URL') {
|
||||
const host = new URL(this.address).host;
|
||||
env['VERCEL_URL'] = host;
|
||||
} else if (name === 'VERCEL_REGION') {
|
||||
env['VERCEL_REGION'] = 'dev1';
|
||||
}
|
||||
}
|
||||
|
||||
// Always set NOW_REGION to match production
|
||||
env['NOW_REGION'] = 'dev1';
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of from builder inputs
|
||||
* and filter them
|
||||
@@ -739,10 +774,20 @@ export default class DevServer {
|
||||
return Object.keys(files).filter(this.filter);
|
||||
}
|
||||
|
||||
start(...listenSpec: ListenSpec): Promise<void> {
|
||||
if (!this.startPromise) {
|
||||
this.startPromise = this._start(...listenSpec).catch(err => {
|
||||
this.stop();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the `vercel dev` server.
|
||||
*/
|
||||
async start(...listenSpec: ListenSpec): Promise<void> {
|
||||
async _start(...listenSpec: ListenSpec): Promise<void> {
|
||||
if (!fs.existsSync(this.cwd)) {
|
||||
throw new Error(`${chalk.bold(this.cwd)} doesn't exist`);
|
||||
}
|
||||
@@ -754,7 +799,39 @@ export default class DevServer {
|
||||
const { ig } = await getVercelIgnore(this.cwd);
|
||||
this.filter = ig.createFilter();
|
||||
|
||||
let address: string | null = null;
|
||||
while (typeof address !== 'string') {
|
||||
try {
|
||||
address = await listen(this.server, ...listenSpec);
|
||||
} catch (err) {
|
||||
this.output.debug(`Got listen error: ${err.code}`);
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
if (typeof listenSpec[0] === 'number') {
|
||||
// Increase port and try again
|
||||
this.output.note(
|
||||
`Requested port ${chalk.yellow(
|
||||
String(listenSpec[0])
|
||||
)} is already in use`
|
||||
);
|
||||
listenSpec[0]++;
|
||||
} else {
|
||||
this.output.error(
|
||||
`Requested socket ${chalk.cyan(listenSpec[0])} is already in use`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.address = address
|
||||
.replace('[::]', 'localhost')
|
||||
.replace('127.0.0.1', 'localhost');
|
||||
|
||||
const nowConfig = await this.getNowConfig();
|
||||
const devCommandPromise = this.runDevCommand();
|
||||
|
||||
const opts = { output: this.output, isBuilds: true };
|
||||
const files = await getFiles(this.cwd, nowConfig, opts);
|
||||
@@ -782,11 +859,11 @@ export default class DevServer {
|
||||
// get their "build matches" invalidated so that the new version is used.
|
||||
this.updateBuildersTimeout = setTimeout(() => {
|
||||
this.updateBuildersPromise = updateBuilders(builders, this.output)
|
||||
.then((updatedBuilders) => {
|
||||
.then(updatedBuilders => {
|
||||
this.updateBuildersPromise = null;
|
||||
this.invalidateBuildMatches(nowConfig, updatedBuilders);
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
this.updateBuildersPromise = null;
|
||||
this.output.error(`Failed to update builders: ${err.message}`);
|
||||
this.output.debug(err.stack);
|
||||
@@ -846,39 +923,6 @@ export default class DevServer {
|
||||
this.proxy.ws(req, socket, head, { target });
|
||||
});
|
||||
|
||||
const devCommandPromise = this.runDevCommand();
|
||||
|
||||
let address: string | null = null;
|
||||
while (typeof address !== 'string') {
|
||||
try {
|
||||
address = await listen(this.server, ...listenSpec);
|
||||
} catch (err) {
|
||||
this.output.debug(`Got listen error: ${err.code}`);
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
if (typeof listenSpec[0] === 'number') {
|
||||
// Increase port and try again
|
||||
this.output.note(
|
||||
`Requested port ${chalk.yellow(
|
||||
String(listenSpec[0])
|
||||
)} is already in use`
|
||||
);
|
||||
listenSpec[0]++;
|
||||
} else {
|
||||
this.output.error(
|
||||
`Requested socket ${chalk.cyan(listenSpec[0])} is already in use`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.address = address
|
||||
.replace('[::]', 'localhost')
|
||||
.replace('127.0.0.1', 'localhost');
|
||||
|
||||
await devCommandPromise;
|
||||
|
||||
this.output.ready(`Available at ${link(this.address)}`);
|
||||
@@ -1087,10 +1131,7 @@ export default class DevServer {
|
||||
const allHeaders = {
|
||||
'cache-control': 'public, max-age=0, must-revalidate',
|
||||
...headers,
|
||||
server: 'now',
|
||||
'x-now-trace': 'dev1',
|
||||
'x-now-id': nowRequestId,
|
||||
'x-now-cache': 'MISS',
|
||||
server: 'Vercel',
|
||||
'x-vercel-id': nowRequestId,
|
||||
'x-vercel-cache': 'MISS',
|
||||
};
|
||||
@@ -1104,23 +1145,24 @@ export default class DevServer {
|
||||
*/
|
||||
getNowProxyHeaders(
|
||||
req: http.IncomingMessage,
|
||||
nowRequestId: string
|
||||
nowRequestId: string,
|
||||
xfwd: boolean
|
||||
): http.IncomingHttpHeaders {
|
||||
const ip = this.getRequestIp(req);
|
||||
const { host } = req.headers;
|
||||
return {
|
||||
...req.headers,
|
||||
Connection: 'close',
|
||||
'x-forwarded-host': host,
|
||||
'x-forwarded-proto': 'http',
|
||||
'x-forwarded-for': ip,
|
||||
const headers: http.IncomingHttpHeaders = {
|
||||
connection: 'close',
|
||||
'x-real-ip': ip,
|
||||
'x-now-trace': 'dev1',
|
||||
'x-now-deployment-url': host,
|
||||
'x-now-id': nowRequestId,
|
||||
'x-now-log-id': nowRequestId.split('-')[2],
|
||||
'x-zeit-co-forwarded-for': ip,
|
||||
'x-vercel-deployment-url': host,
|
||||
'x-vercel-forwarded-for': ip,
|
||||
'x-vercel-id': nowRequestId,
|
||||
};
|
||||
if (xfwd) {
|
||||
headers['x-forwarded-host'] = host;
|
||||
headers['x-forwarded-proto'] = 'http';
|
||||
headers['x-forwarded-for'] = ip;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async triggerBuild(
|
||||
@@ -1202,6 +1244,8 @@ export default class DevServer {
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse
|
||||
) => {
|
||||
await this.startPromise;
|
||||
|
||||
let nowRequestId = generateRequestId(this.podId);
|
||||
|
||||
if (this.stopping) {
|
||||
@@ -1468,9 +1512,9 @@ export default class DevServer {
|
||||
const { dest, headers, uri_args } = routeResult;
|
||||
|
||||
// Set any headers defined in the matched `route` config
|
||||
Object.entries(headers).forEach(([name, value]) => {
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
res.setHeader(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
if (statusCode) {
|
||||
// Set the `statusCode` as read-only so that `http-proxy`
|
||||
@@ -1493,6 +1537,13 @@ export default class DevServer {
|
||||
if (this.devProcessPort) {
|
||||
const upstream = `http://localhost:${this.devProcessPort}`;
|
||||
debug(`Proxying to frontend dev server: ${upstream}`);
|
||||
|
||||
// Add the Vercel platform proxy request headers
|
||||
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
req.headers[name] = value;
|
||||
}
|
||||
|
||||
this.setResponseHeaders(res, nowRequestId);
|
||||
const origUrl = url.parse(req.url || '/', true);
|
||||
delete origUrl.search;
|
||||
@@ -1612,6 +1663,12 @@ export default class DevServer {
|
||||
query: parsed.query,
|
||||
});
|
||||
|
||||
// Add the Vercel platform proxy request headers
|
||||
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
req.headers[name] = value;
|
||||
}
|
||||
|
||||
this.setResponseHeaders(res, nowRequestId);
|
||||
return proxyPass(
|
||||
req,
|
||||
@@ -1642,6 +1699,13 @@ export default class DevServer {
|
||||
(!foundAsset || (foundAsset && foundAsset.asset.type !== 'Lambda'))
|
||||
) {
|
||||
debug('Proxying to frontend dev server');
|
||||
|
||||
// Add the Vercel platform proxy request headers
|
||||
const headers = this.getNowProxyHeaders(req, nowRequestId, false);
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
req.headers[name] = value;
|
||||
}
|
||||
|
||||
this.setResponseHeaders(res, nowRequestId);
|
||||
return proxyPass(
|
||||
req,
|
||||
@@ -1724,7 +1788,10 @@ export default class DevServer {
|
||||
method: req.method || 'GET',
|
||||
host: req.headers.host,
|
||||
path,
|
||||
headers: this.getNowProxyHeaders(req, nowRequestId),
|
||||
headers: {
|
||||
...req.headers,
|
||||
...this.getNowProxyHeaders(req, nowRequestId, true),
|
||||
},
|
||||
encoding: 'base64',
|
||||
body: body.toString('base64'),
|
||||
};
|
||||
@@ -1781,7 +1848,7 @@ export default class DevServer {
|
||||
|
||||
const dirs: Set<string> = new Set();
|
||||
const files = Array.from(this.buildMatches.keys())
|
||||
.filter((p) => {
|
||||
.filter(p => {
|
||||
const base = basename(p);
|
||||
if (
|
||||
base === 'now.json' ||
|
||||
@@ -1802,7 +1869,7 @@ export default class DevServer {
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((p) => {
|
||||
.map(p => {
|
||||
let base = basename(p);
|
||||
let ext = '';
|
||||
let type = 'file';
|
||||
@@ -1894,8 +1961,6 @@ export default class DevServer {
|
||||
...(this.frameworkSlug === 'create-react-app' ? { BROWSER: 'none' } : {}),
|
||||
...process.env,
|
||||
...this.envConfigs.allEnv,
|
||||
NOW_REGION: 'dev1',
|
||||
VERCEL_REGION: 'dev1',
|
||||
PORT: `${port}`,
|
||||
};
|
||||
|
||||
@@ -2214,7 +2279,7 @@ function isIndex(path: string): boolean {
|
||||
|
||||
function minimatches(files: string[], pattern: string): boolean {
|
||||
return files.some(
|
||||
(file) => file === pattern || minimatch(file, pattern, { dot: true })
|
||||
file => file === pattern || minimatch(file, pattern, { dot: true })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2244,7 +2309,7 @@ function needsBlockingBuild(buildMatch: BuildMatch): boolean {
|
||||
}
|
||||
|
||||
async function sleep(n: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, n));
|
||||
return new Promise(resolve => setTimeout(resolve, n));
|
||||
}
|
||||
|
||||
async function checkForPort(
|
||||
|
||||
@@ -27,11 +27,12 @@ export interface DevServerOptions {
|
||||
devCommand?: string;
|
||||
frameworkSlug?: string;
|
||||
projectSettings?: ProjectSettings;
|
||||
environmentVars?: Env;
|
||||
}
|
||||
|
||||
export interface EnvConfigs {
|
||||
/**
|
||||
* environment variables from `.env.build` file (deprecated)
|
||||
* environment variables from `.env.build` file
|
||||
*/
|
||||
buildEnv: Env;
|
||||
|
||||
|
||||
29
packages/now-cli/src/util/domains/get-domain-config.ts
Normal file
29
packages/now-cli/src/util/domains/get-domain-config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../client';
|
||||
import wait from '../output/wait';
|
||||
import { DomainConfig } from '../../types';
|
||||
|
||||
export async function getDomainConfig(
|
||||
client: Client,
|
||||
contextName: string,
|
||||
domainName: string
|
||||
) {
|
||||
const cancelWait = wait(
|
||||
`Fetching domain config ${domainName} under ${chalk.bold(contextName)}`
|
||||
);
|
||||
try {
|
||||
const config = await client.fetch<DomainConfig>(
|
||||
`/v4/domains/${domainName}/config`
|
||||
);
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (error.status < 500) {
|
||||
return error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
cancelWait();
|
||||
}
|
||||
}
|
||||
33
packages/now-cli/src/util/domains/get-domain.ts
Normal file
33
packages/now-cli/src/util/domains/get-domain.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../client';
|
||||
import wait from '../output/wait';
|
||||
import { Domain } from '../../types';
|
||||
|
||||
type Response = {
|
||||
domain: Domain;
|
||||
};
|
||||
|
||||
export async function getDomain(
|
||||
client: Client,
|
||||
contextName: string,
|
||||
domainName: string
|
||||
) {
|
||||
const cancelWait = wait(
|
||||
`Fetching domain ${domainName} under ${chalk.bold(contextName)}`
|
||||
);
|
||||
try {
|
||||
const { domain } = await client.fetch<Response>(
|
||||
`/v4/domains/${domainName}`
|
||||
);
|
||||
|
||||
return domain;
|
||||
} catch (error) {
|
||||
if (error.status < 500) {
|
||||
return error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
cancelWait();
|
||||
}
|
||||
}
|
||||
3
packages/now-cli/src/util/domains/is-public-suffix.ts
Normal file
3
packages/now-cli/src/util/domains/is-public-suffix.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isPublicSuffix(domainName: string) {
|
||||
return domainName.endsWith('.vercel.app') || domainName.endsWith('.now.sh');
|
||||
}
|
||||
@@ -18,3 +18,18 @@ export default function formatDate(dateStrOrNumber?: number | string | null) {
|
||||
`[in ${ms(diff)}]`
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function formatDateWithoutTime(
|
||||
dateStrOrNumber?: number | string | null
|
||||
) {
|
||||
if (!dateStrOrNumber) {
|
||||
return chalk.gray('-');
|
||||
}
|
||||
|
||||
const date = new Date(dateStrOrNumber);
|
||||
const diff = date.getTime() - Date.now();
|
||||
|
||||
return diff < 0
|
||||
? `${format(date, 'MMM DD YYYY')} ${chalk.gray(`[${ms(-diff)} ago]`)}`
|
||||
: `${format(date, 'MMM DD YYYY')} ${chalk.gray(`[in ${ms(diff)}]`)}`;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import strlen from './strlen';
|
||||
export default function formatTable(
|
||||
header: string[],
|
||||
align: Array<'l' | 'r' | 'c' | '.'>,
|
||||
blocks: { name: string; rows: string[][] }[],
|
||||
blocks: { name?: string; rows: string[][] }[],
|
||||
hsep = ' '
|
||||
) {
|
||||
const nrCols = header.length;
|
||||
@@ -50,8 +50,10 @@ export default function formatTable(
|
||||
for (let j = 0; j < nrCols; j++) {
|
||||
const col = `${row[j]}`;
|
||||
const al = align[j] || 'l';
|
||||
const spaces = Math.max(padding[j] * 8 - strlen(col), 0);
|
||||
const pad = ' '.repeat(spaces);
|
||||
|
||||
const repeat = padding[j] > 1 ? padding[j] * 8 - strlen(col) : 0;
|
||||
const pad = repeat > 0 ? ' '.repeat(repeat) : '';
|
||||
|
||||
rows[i][j] = al === 'l' ? col + pad : pad + col;
|
||||
}
|
||||
}
|
||||
|
||||
46
packages/now-cli/src/util/get-decrypted-env-records.ts
Normal file
46
packages/now-cli/src/util/get-decrypted-env-records.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import getEnvVariables from './env/get-env-records';
|
||||
import getDecryptedSecret from './env/get-decrypted-secret';
|
||||
import Client from './client';
|
||||
import { Output } from './output/create-output';
|
||||
import { ProjectEnvTarget, Project } from '../types';
|
||||
|
||||
import { Env } from '@vercel/build-utils';
|
||||
|
||||
export default async function getDecryptedEnvRecords(
|
||||
output: Output,
|
||||
client: Client,
|
||||
project: Project,
|
||||
target: ProjectEnvTarget
|
||||
): Promise<Env> {
|
||||
const envs = await getEnvVariables(output, client, project.id, 4, target);
|
||||
const decryptedValues = await Promise.all(
|
||||
envs.map(async env => {
|
||||
try {
|
||||
const value = await getDecryptedSecret(output, client, env.value);
|
||||
return { value, found: true };
|
||||
} catch (error) {
|
||||
if (error && error.status === 404) {
|
||||
return { value: '', found: false };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const results: Env = {};
|
||||
for (let i = 0; i < decryptedValues.length; i++) {
|
||||
const { key } = envs[i];
|
||||
const { value, found } = decryptedValues[i];
|
||||
|
||||
if (!found) {
|
||||
output.print('');
|
||||
output.warn(
|
||||
`Unable to download variable ${key} because associated secret was deleted`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
results[key] = value ? value : '';
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@@ -13,6 +13,6 @@ export default async function getTeamById(
|
||||
team = await client.fetch<Team>(`/teams/${teamId}`);
|
||||
teamCache.set(teamId, team);
|
||||
}
|
||||
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,15 @@ import chalk from 'chalk';
|
||||
import { Output } from '../output';
|
||||
import { Framework } from '@vercel/frameworks';
|
||||
import { isSettingValue } from '../is-setting-value';
|
||||
import { ProjectSettings } from '../../types';
|
||||
|
||||
export interface ProjectSettings {
|
||||
export interface PartialProjectSettings {
|
||||
buildCommand: string | null;
|
||||
outputDirectory: string | null;
|
||||
devCommand: string | null;
|
||||
}
|
||||
|
||||
export interface ProjectSettingsWithFramework extends ProjectSettings {
|
||||
framework: string | null;
|
||||
}
|
||||
|
||||
const fields: { name: string; value: keyof ProjectSettings }[] = [
|
||||
const fields: { name: string; value: keyof PartialProjectSettings }[] = [
|
||||
{ name: 'Build Command', value: 'buildCommand' },
|
||||
{ name: 'Output Directory', value: 'outputDirectory' },
|
||||
{ name: 'Development Command', value: 'devCommand' },
|
||||
@@ -23,13 +20,15 @@ const fields: { name: string; value: keyof ProjectSettings }[] = [
|
||||
|
||||
export default async function editProjectSettings(
|
||||
output: Output,
|
||||
projectSettings: ProjectSettings | null,
|
||||
framework: Framework | null
|
||||
) {
|
||||
projectSettings: PartialProjectSettings | null,
|
||||
framework: Framework | null,
|
||||
autoConfirm: boolean
|
||||
): Promise<ProjectSettings> {
|
||||
// create new settings object, missing values will be filled with `null`
|
||||
const settings: Partial<ProjectSettingsWithFramework> = {
|
||||
...projectSettings,
|
||||
};
|
||||
const settings: ProjectSettings = Object.assign(
|
||||
{ framework: null },
|
||||
projectSettings
|
||||
);
|
||||
|
||||
for (let field of fields) {
|
||||
settings[field.value] =
|
||||
@@ -64,7 +63,10 @@ export default async function editProjectSettings(
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await confirm(`Want to override the settings?`, false))) {
|
||||
if (
|
||||
autoConfirm ||
|
||||
!(await confirm(`Want to override the settings?`, false))
|
||||
) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -75,7 +77,7 @@ export default async function editProjectSettings(
|
||||
choices: fields,
|
||||
});
|
||||
|
||||
for (let setting of settingFields as (keyof ProjectSettings)[]) {
|
||||
for (let setting of settingFields as (keyof PartialProjectSettings)[]) {
|
||||
const field = fields.find(f => f.value === setting);
|
||||
const name = `${Date.now()}`;
|
||||
const answers = await inquirer.prompt({
|
||||
|
||||
@@ -15,10 +15,6 @@ export default async function inputProject(
|
||||
detectedProjectName: string,
|
||||
autoConfirm: boolean
|
||||
): Promise<Project | string> {
|
||||
if (autoConfirm) {
|
||||
return detectedProjectName;
|
||||
}
|
||||
|
||||
const slugifiedName = slugify(detectedProjectName);
|
||||
|
||||
// attempt to auto-detect a project to link
|
||||
@@ -42,6 +38,10 @@ export default async function inputProject(
|
||||
} catch (error) {}
|
||||
existingProjectSpinner();
|
||||
|
||||
if (autoConfirm) {
|
||||
return detectedProject || detectedProjectName;
|
||||
}
|
||||
|
||||
let shouldLinkProject;
|
||||
|
||||
if (!detectedProject) {
|
||||
|
||||
229
packages/now-cli/src/util/link/setup-and-link.ts
Normal file
229
packages/now-cli/src/util/link/setup-and-link.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { join, basename } from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { remove } from 'fs-extra';
|
||||
import { NowContext, ProjectLinkResult } from '../../types';
|
||||
import { NowConfig } from '../dev/types';
|
||||
import { Output } from '../output';
|
||||
import {
|
||||
getLinkedProject,
|
||||
linkFolderToProject,
|
||||
getVercelDirectory,
|
||||
} from '../projects/link';
|
||||
import createProject from '../projects/create-project';
|
||||
import updateProject from '../projects/update-project';
|
||||
import Client from '../client';
|
||||
import handleError from '../handle-error';
|
||||
import confirm from '../input/confirm';
|
||||
import toHumanPath from '../humanize-path';
|
||||
import { isDirectory } from '../config/global-path';
|
||||
import selectOrg from '../input/select-org';
|
||||
import inputProject from '../input/input-project';
|
||||
import { validateRootDirectory } from '../validate-paths';
|
||||
import { inputRootDirectory } from '../input/input-root-directory';
|
||||
import editProjectSettings from '../input/edit-project-settings';
|
||||
import stamp from '../output/stamp';
|
||||
import { EmojiLabel } from '../emoji';
|
||||
//@ts-expect-error
|
||||
import createDeploy from '../deploy/create-deploy';
|
||||
//@ts-expect-error
|
||||
import Now from '../index';
|
||||
|
||||
export default async function setupAndLink(
|
||||
ctx: NowContext,
|
||||
output: Output,
|
||||
path: string,
|
||||
forceDelete: boolean,
|
||||
autoConfirm: boolean,
|
||||
successEmoji: EmojiLabel,
|
||||
setupMsg: string
|
||||
): Promise<ProjectLinkResult> {
|
||||
const {
|
||||
authConfig: { token },
|
||||
config,
|
||||
} = ctx;
|
||||
const { apiUrl } = ctx;
|
||||
const debug = output.isDebugEnabled();
|
||||
const client = new Client({
|
||||
apiUrl,
|
||||
token,
|
||||
currentTeam: config.currentTeam,
|
||||
debug,
|
||||
});
|
||||
|
||||
const isFile = !isDirectory(path);
|
||||
if (isFile) {
|
||||
output.error(`Expected directory but found file: ${path}`);
|
||||
return { status: 'error', exitCode: 1 };
|
||||
}
|
||||
const link = await getLinkedProject(output, client, path);
|
||||
const isTTY = process.stdout.isTTY;
|
||||
const quiet = !isTTY;
|
||||
let rootDirectory: string | null = null;
|
||||
let newProjectName: string;
|
||||
let org;
|
||||
|
||||
if (!forceDelete && link.status === 'linked') {
|
||||
return link;
|
||||
}
|
||||
|
||||
if (forceDelete) {
|
||||
const vercelDir = getVercelDirectory(path);
|
||||
remove(vercelDir);
|
||||
}
|
||||
|
||||
const shouldStartSetup =
|
||||
autoConfirm ||
|
||||
(await confirm(
|
||||
`${setupMsg} ${chalk.cyan(`“${toHumanPath(path)}”`)}?`,
|
||||
true
|
||||
));
|
||||
|
||||
if (!shouldStartSetup) {
|
||||
output.print(`Aborted. Project not set up.\n`);
|
||||
return { status: 'not_linked', org: null, project: null };
|
||||
}
|
||||
|
||||
try {
|
||||
org = await selectOrg(
|
||||
output,
|
||||
'Which scope should contain your project?',
|
||||
client,
|
||||
config.currentTeam,
|
||||
autoConfirm
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.prettyError(err);
|
||||
return { status: 'error', exitCode: 1 };
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const detectedProjectName = basename(path);
|
||||
|
||||
const projectOrNewProjectName = await inputProject(
|
||||
output,
|
||||
client,
|
||||
org,
|
||||
detectedProjectName,
|
||||
autoConfirm
|
||||
);
|
||||
|
||||
if (typeof projectOrNewProjectName === 'string') {
|
||||
newProjectName = projectOrNewProjectName;
|
||||
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
|
||||
} else {
|
||||
const project = projectOrNewProjectName;
|
||||
|
||||
await linkFolderToProject(
|
||||
output,
|
||||
path,
|
||||
{
|
||||
projectId: project.id,
|
||||
orgId: org.id,
|
||||
},
|
||||
project.name,
|
||||
org.slug,
|
||||
successEmoji
|
||||
);
|
||||
return { status: 'linked', org, project };
|
||||
}
|
||||
const sourcePath = rootDirectory ? join(path, rootDirectory) : path;
|
||||
|
||||
if (
|
||||
rootDirectory &&
|
||||
!(await validateRootDirectory(output, path, sourcePath, ''))
|
||||
) {
|
||||
return { status: 'error', exitCode: 1 };
|
||||
}
|
||||
|
||||
let localConfig: NowConfig = {};
|
||||
if (ctx.localConfig && !(ctx.localConfig instanceof Error)) {
|
||||
localConfig = ctx.localConfig;
|
||||
}
|
||||
client.currentTeam = org.type === 'team' ? org.id : undefined;
|
||||
const now = new Now({
|
||||
apiUrl,
|
||||
token,
|
||||
debug,
|
||||
currentTeam: client.currentTeam,
|
||||
});
|
||||
let deployment = null;
|
||||
|
||||
try {
|
||||
const createArgs: any = {
|
||||
name: newProjectName,
|
||||
env: {},
|
||||
build: { env: {} },
|
||||
forceNew: undefined,
|
||||
withCache: undefined,
|
||||
quiet,
|
||||
wantsPublic: localConfig.public,
|
||||
isFile,
|
||||
type: null,
|
||||
nowConfig: localConfig,
|
||||
regions: undefined,
|
||||
meta: {},
|
||||
deployStamp: stamp(),
|
||||
target: undefined,
|
||||
skipAutoDetectionConfirmation: false,
|
||||
};
|
||||
|
||||
deployment = await createDeploy(
|
||||
output,
|
||||
now,
|
||||
client.currentTeam || 'current user',
|
||||
[sourcePath],
|
||||
createArgs,
|
||||
org,
|
||||
!isFile,
|
||||
path
|
||||
);
|
||||
|
||||
if (
|
||||
!deployment ||
|
||||
!('code' in deployment) ||
|
||||
deployment.code !== 'missing_project_settings'
|
||||
) {
|
||||
output.error('Failed to detect project settings. Please try again.');
|
||||
if (output.isDebugEnabled()) {
|
||||
console.log(deployment);
|
||||
}
|
||||
return { status: 'error', exitCode: 1 };
|
||||
}
|
||||
|
||||
const { projectSettings, framework } = deployment;
|
||||
|
||||
if (rootDirectory) {
|
||||
projectSettings.rootDirectory = rootDirectory;
|
||||
}
|
||||
|
||||
const settings = await editProjectSettings(
|
||||
output,
|
||||
projectSettings,
|
||||
framework,
|
||||
autoConfirm
|
||||
);
|
||||
const project = await createProject(client, newProjectName);
|
||||
await updateProject(client, project.id, settings);
|
||||
Object.assign(project, settings);
|
||||
|
||||
await linkFolderToProject(
|
||||
output,
|
||||
path,
|
||||
{
|
||||
projectId: project.id,
|
||||
orgId: org.id,
|
||||
},
|
||||
project.name,
|
||||
org.slug,
|
||||
successEmoji
|
||||
);
|
||||
|
||||
return { status: 'linked', org, project };
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
return { status: 'error', exitCode: 1 };
|
||||
}
|
||||
}
|
||||
46
packages/now-cli/src/util/projects/add-domain-to-project.ts
Normal file
46
packages/now-cli/src/util/projects/add-domain-to-project.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../client';
|
||||
import wait from '../output/wait';
|
||||
import { ProjectAliasTarget } from '../../types';
|
||||
|
||||
export async function addDomainToProject(
|
||||
client: Client,
|
||||
projectNameOrId: string,
|
||||
domain: string
|
||||
) {
|
||||
const cancelWait = wait(
|
||||
`Adding domain ${domain} to project ${chalk.bold(projectNameOrId)}`
|
||||
);
|
||||
try {
|
||||
const response = await client.fetch<ProjectAliasTarget[]>(
|
||||
`/projects/${encodeURIComponent(projectNameOrId)}/alias`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
target: 'PRODUCTION',
|
||||
domain,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const aliasTarget: ProjectAliasTarget | undefined = response.find(
|
||||
aliasTarget => aliasTarget.domain === domain
|
||||
);
|
||||
|
||||
if (!aliasTarget) {
|
||||
throw new Error(
|
||||
`Unexpected error when adding the domain "${domain}" to project "${projectNameOrId}".`
|
||||
);
|
||||
}
|
||||
|
||||
return aliasTarget;
|
||||
} catch (err) {
|
||||
if (err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
cancelWait();
|
||||
}
|
||||
}
|
||||
13
packages/now-cli/src/util/projects/create-project.ts
Normal file
13
packages/now-cli/src/util/projects/create-project.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import Client from '../client';
|
||||
import { Project } from '../../types';
|
||||
|
||||
export default async function createProject(
|
||||
client: Client,
|
||||
projectName: string
|
||||
) {
|
||||
const project = await client.fetch<Project>('/v1/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: projectName }),
|
||||
});
|
||||
return project;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../client';
|
||||
import wait from '../output/wait';
|
||||
import { Project } from '../../types';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
export async function findProjectsForDomain(
|
||||
client: Client,
|
||||
domainName: string
|
||||
): Promise<Project[] | Error> {
|
||||
const cancelWait = wait(
|
||||
`Searching project for domain ${chalk.bold(domainName)}`
|
||||
);
|
||||
try {
|
||||
const limit = 50;
|
||||
let result: Project[] = [];
|
||||
|
||||
const query = new URLSearchParams({
|
||||
hasProductionDomains: '1',
|
||||
limit: limit.toString(),
|
||||
domain: domainName,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const response = await client.fetch<Project[]>(`/v2/projects/?${query}`);
|
||||
result.push(...response);
|
||||
|
||||
if (response.length !== limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [latest] = response.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
query.append('from', latest.updatedAt.toString());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
cancelWait();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Client from '../client';
|
||||
import wait from '../output/wait';
|
||||
import { Project } from '../../types';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
export async function getProjectsWithDomains(
|
||||
client: Client
|
||||
): Promise<Project[] | Error> {
|
||||
const cancelWait = wait(`Fetching projects with domains`);
|
||||
try {
|
||||
const limit = 50;
|
||||
let result: Project[] = [];
|
||||
|
||||
const query = new URLSearchParams({
|
||||
hasProductionDomains: '1',
|
||||
limit: limit.toString(),
|
||||
});
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const response = await client.fetch<Project[]>(`/v2/projects/?${query}`);
|
||||
result.push(...response);
|
||||
|
||||
const [latest] = response.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
query.append('from', latest.updatedAt.toString());
|
||||
|
||||
if (response.length !== limit) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
cancelWait();
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,14 @@ import { ProjectNotFound } from '../errors-ts';
|
||||
import getUser from '../get-user';
|
||||
import getTeamById from '../get-team-by-id';
|
||||
import { Output } from '../output';
|
||||
import { Project } from '../../types';
|
||||
import { Project, ProjectLinkResult } from '../../types';
|
||||
import { Org, ProjectLink } from '../../types';
|
||||
import chalk from 'chalk';
|
||||
import { prependEmoji, emoji } from '../emoji';
|
||||
import { prependEmoji, emoji, EmojiLabel } from '../emoji';
|
||||
import AJV from 'ajv';
|
||||
import { isDirectory } from '../config/global-path';
|
||||
import { NowBuildError, getPlatformEnv } from '@vercel/build-utils';
|
||||
import outputCode from '../output/code';
|
||||
|
||||
const readFile = promisify(fs.readFile);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
@@ -111,11 +112,7 @@ export async function getLinkedProject(
|
||||
output: Output,
|
||||
client: Client,
|
||||
path?: string
|
||||
): Promise<
|
||||
| { status: 'linked'; org: Org; project: Project }
|
||||
| { status: 'not_linked'; org: null; project: null }
|
||||
| { status: 'error'; exitCode: number }
|
||||
> {
|
||||
): Promise<ProjectLinkResult> {
|
||||
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
|
||||
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
|
||||
const shouldUseEnv = Boolean(VERCEL_ORG_ID && VERCEL_PROJECT_ID);
|
||||
@@ -152,7 +149,9 @@ export async function getLinkedProject(
|
||||
if (err?.status === 403) {
|
||||
spinner();
|
||||
throw new NowBuildError({
|
||||
message: `Could not retrieve Project Settings. To link your project, remove the .vercel directory and deploy again.`,
|
||||
message: `Could not retrieve Project Settings. To link your Project, remove the ${outputCode(
|
||||
VERCEL_DIR
|
||||
)} directory and deploy again.`,
|
||||
code: 'PROJECT_UNAUTHORIZED',
|
||||
link: 'https://vercel.link/cannot-load-project-settings',
|
||||
});
|
||||
@@ -193,7 +192,8 @@ export async function linkFolderToProject(
|
||||
path: string,
|
||||
projectLink: ProjectLink,
|
||||
projectName: string,
|
||||
orgSlug: string
|
||||
orgSlug: string,
|
||||
successEmoji: EmojiLabel = 'link'
|
||||
) {
|
||||
const VERCEL_ORG_ID = getPlatformEnv('ORG_ID');
|
||||
const VERCEL_PROJECT_ID = getPlatformEnv('PROJECT_ID');
|
||||
@@ -263,7 +263,7 @@ export async function linkFolderToProject(
|
||||
)} (created ${VERCEL_DIR}${
|
||||
isGitIgnoreUpdated ? ' and added it to .gitignore' : ''
|
||||
})`,
|
||||
emoji('link')
|
||||
emoji(successEmoji)
|
||||
) + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../client';
|
||||
import wait from '../output/wait';
|
||||
import { ProjectAliasTarget } from '../../types';
|
||||
|
||||
export async function removeDomainFromProject(
|
||||
client: Client,
|
||||
projectNameOrId: string,
|
||||
domain: string
|
||||
) {
|
||||
const cancelWait = wait(
|
||||
`Removing domain ${domain} from project ${chalk.bold(projectNameOrId)}`
|
||||
);
|
||||
try {
|
||||
const response = await client.fetch<ProjectAliasTarget[]>(
|
||||
`/projects/${encodeURIComponent(
|
||||
projectNameOrId
|
||||
)}/alias?domain=${encodeURIComponent(domain)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (err.status < 500) {
|
||||
return err;
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
cancelWait();
|
||||
}
|
||||
}
|
||||
24
packages/now-cli/src/util/projects/update-project.ts
Normal file
24
packages/now-cli/src/util/projects/update-project.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Client from '../client';
|
||||
import { ProjectSettings } from '../../types';
|
||||
|
||||
interface ProjectSettingsResponse extends ProjectSettings {
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export default async function updateProject(
|
||||
client: Client,
|
||||
prjNameOrId: string,
|
||||
settings: ProjectSettings
|
||||
) {
|
||||
const res = await client.fetch<ProjectSettingsResponse>(
|
||||
`/v2/projects/${encodeURIComponent(prjNameOrId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(settings),
|
||||
}
|
||||
);
|
||||
return res;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default async function responseError(
|
||||
) {
|
||||
let bodyError;
|
||||
|
||||
if (res.status >= 400 && res.status < 500) {
|
||||
if (!res.ok) {
|
||||
let body;
|
||||
|
||||
try {
|
||||
|
||||
6
packages/now-cli/test/dev-router.unit.js
vendored
6
packages/now-cli/test/dev-router.unit.js
vendored
@@ -3,7 +3,11 @@ import { devRouter } from '../src/util/dev/router';
|
||||
|
||||
test('[dev-router] 301 redirection', async t => {
|
||||
const routesConfig = [
|
||||
{ src: '/redirect', status: 301, headers: { Location: 'https://vercel.com' } },
|
||||
{
|
||||
src: '/redirect',
|
||||
status: 301,
|
||||
headers: { Location: 'https://vercel.com' },
|
||||
},
|
||||
];
|
||||
const result = await devRouter('/redirect', 'GET', routesConfig);
|
||||
|
||||
|
||||
2
packages/now-cli/test/dev-server.unit.js
vendored
2
packages/now-cli/test/dev-server.unit.js
vendored
@@ -71,7 +71,7 @@ function testFixture(name, fn) {
|
||||
}
|
||||
|
||||
function validateResponseHeaders(t, res, podId = null) {
|
||||
t.is(res.headers.get('server'), 'now');
|
||||
t.is(res.headers.get('server'), 'Vercel');
|
||||
t.truthy(res.headers.get('cache-control').length > 0);
|
||||
t.truthy(
|
||||
/^dev1::(dev1::)?[0-9a-z]{5}-[1-9][0-9]+-[a-f0-9]{12}$/.test(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/user.sh": {
|
||||
"runtime": "vercel-bash@3.0.7"
|
||||
"runtime": "vercel-bash@3.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Created by Vercel CLI
|
||||
VERCEL_REGION=""
|
||||
VERCEL_URL=""
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = (req, res) => {
|
||||
res.send({
|
||||
env: process.env,
|
||||
headers: req.headers,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
from flask import Flask, Response, request
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def headers(path):
|
||||
url = request.headers.get('x-vercel-deployment-url')
|
||||
return Response(url, mimetype='text/plain')
|
||||
@@ -2,6 +2,7 @@ import ms from 'ms';
|
||||
import os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import test from 'ava';
|
||||
import { isIP } from 'net';
|
||||
import { join, resolve, delimiter } from 'path';
|
||||
import _execa from 'execa';
|
||||
import fetch from 'node-fetch';
|
||||
@@ -118,24 +119,9 @@ async function runNpmInstall(fixturePath) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getPackedBuilderPath(builderDirName) {
|
||||
const packagePath = join(__dirname, '..', '..', '..', builderDirName);
|
||||
const output = await execa('npm', ['pack'], {
|
||||
cwd: packagePath,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
if (output.exitCode !== 0 || output.stdout.trim() === '') {
|
||||
throw new Error(
|
||||
`Failed to pack ${builderDirName}: ${formatOutput(output)}`
|
||||
);
|
||||
}
|
||||
|
||||
return join(packagePath, output.stdout.trim());
|
||||
}
|
||||
|
||||
async function testPath(
|
||||
t,
|
||||
isDev,
|
||||
origin,
|
||||
status,
|
||||
path,
|
||||
@@ -153,6 +139,9 @@ async function testPath(
|
||||
if (typeof expectedText === 'string') {
|
||||
const actualText = await res.text();
|
||||
t.is(actualText.trim(), expectedText.trim(), msg);
|
||||
} else if (typeof expectedText === 'function') {
|
||||
const actualText = await res.text();
|
||||
await expectedText(t, actualText, res, isDev);
|
||||
} else if (expectedText instanceof RegExp) {
|
||||
const actualText = await res.text();
|
||||
expectedText.lastIndex = 0; // reset since we test twice
|
||||
@@ -342,9 +331,9 @@ function testFixtureStdio(
|
||||
|
||||
const helperTestPath = async (...args) => {
|
||||
if (!skipDeploy) {
|
||||
await testPath(t, `https://${deploymentUrl}`, ...args);
|
||||
await testPath(t, false, `https://${deploymentUrl}`, ...args);
|
||||
}
|
||||
await testPath(t, `http://localhost:${port}`, ...args);
|
||||
await testPath(t, true, `http://localhost:${port}`, ...args);
|
||||
};
|
||||
await fn(helperTestPath, t, port);
|
||||
} finally {
|
||||
@@ -1480,22 +1469,6 @@ test('[vercel dev] render warning for empty cwd dir', async t => {
|
||||
test('[vercel dev] do not rebuild for changes in the output directory', async t => {
|
||||
const directory = fixture('output-is-source');
|
||||
|
||||
// Pack the builder and set it in the `vercel.json`
|
||||
const builder = await getPackedBuilderPath('now-static-build');
|
||||
|
||||
await fs.writeFile(
|
||||
join(directory, 'vercel.json'),
|
||||
JSON.stringify({
|
||||
builds: [
|
||||
{
|
||||
src: 'package.json',
|
||||
use: `file://${builder}`,
|
||||
config: { zeroConfig: true },
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const { dev, port } = await testFixture(directory, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
@@ -1581,6 +1554,10 @@ test(
|
||||
await testPath(200, `/api/user?name=${name}`, new RegExp(`Hello ${name}`));
|
||||
await testPath(200, `/api/date`, new RegExp(`Current date is ${year}`));
|
||||
await testPath(200, `/api/date.py`, new RegExp(`Current date is ${year}`));
|
||||
await testPath(200, `/api/headers`, (t, body, res) => {
|
||||
const { host } = new URL(res.url);
|
||||
t.is(body, host);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1647,5 +1624,25 @@ test(
|
||||
`/api/array`,
|
||||
'{"months":[1,2,3,4,5,6,7,8,9,10,11,12]}'
|
||||
);
|
||||
|
||||
await testPath(200, `/api/dump`, (t, body, res, isDev) => {
|
||||
const { host } = new URL(res.url);
|
||||
const { env, headers } = JSON.parse(body);
|
||||
|
||||
// Test that the API endpoint receives the Vercel proxy request headers
|
||||
t.is(headers['x-forwarded-host'], host);
|
||||
t.is(headers['x-vercel-deployment-url'], host);
|
||||
t.truthy(isIP(headers['x-real-ip']));
|
||||
t.truthy(isIP(headers['x-forwarded-for']));
|
||||
t.truthy(isIP(headers['x-vercel-forwarded-for']));
|
||||
|
||||
// Test that the API endpoint has the Vercel platform env vars defined.
|
||||
t.regex(env.NOW_REGION, /^[a-z]{3}\d$/);
|
||||
if (isDev) {
|
||||
// Only dev is tested because in production these are opt-in.
|
||||
t.is(env.VERCEL_URL, host);
|
||||
t.is(env.VERCEL_REGION, 'dev1');
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"version": 2,
|
||||
"name": "nodejs",
|
||||
"builds": [
|
||||
{ "src": "*.js", "use": "@now/node" },
|
||||
{ "src": "statics/*", "use": "@now/static" }
|
||||
],
|
||||
"routes": [
|
||||
{ "src": "/api/(.*)", "dest": "/api.js?topic=$1" },
|
||||
{ "src": "/help.js", "dest": "/index.js" },
|
||||
{ "src": "/help", "dest": "/help.js" },
|
||||
{ "src": "/proxy_pass", "dest": "https://vercel.com" },
|
||||
{ "src": "/redirect", "status": 301, "headers": { "Location": "https://vercel.com" }}
|
||||
]
|
||||
"version": 2,
|
||||
"name": "nodejs",
|
||||
"builds": [
|
||||
{ "src": "*.js", "use": "@now/node" },
|
||||
{ "src": "statics/*", "use": "@now/static" }
|
||||
],
|
||||
"routes": [
|
||||
{ "src": "/api/(.*)", "dest": "/api.js?topic=$1" },
|
||||
{ "src": "/help.js", "dest": "/index.js" },
|
||||
{ "src": "/help", "dest": "/help.js" },
|
||||
{ "src": "/proxy_pass", "dest": "https://vercel.com" },
|
||||
{
|
||||
"src": "/redirect",
|
||||
"status": 301,
|
||||
"headers": { "Location": "https://vercel.com" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const getConfigFile = builds =>
|
||||
]
|
||||
}`
|
||||
: `{
|
||||
"version": 1
|
||||
"version": 2
|
||||
}`;
|
||||
|
||||
const getIndexHTMLFile = session => `
|
||||
@@ -365,7 +365,7 @@ CMD ["node", "index.js"]`,
|
||||
'package.json': JSON.stringify({
|
||||
private: true,
|
||||
scripts: {
|
||||
build: 'mkdir public && node print.js > public/index.json',
|
||||
build: 'mkdir -p public && node print.js > public/index.json',
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -503,6 +503,18 @@ CMD ["node", "index.js"]`,
|
||||
'project-link': {
|
||||
'package.json': JSON.stringify({}),
|
||||
},
|
||||
'project-link-confirm': {
|
||||
'package.json': JSON.stringify({}),
|
||||
},
|
||||
'project-link-dev': {
|
||||
'package.json': JSON.stringify({}),
|
||||
},
|
||||
'dev-proxy-headers-and-env': {
|
||||
'package.json': JSON.stringify({}),
|
||||
'server.js': `require('http').createServer((req, res) => {
|
||||
res.end(JSON.stringify({ headers: req.headers, env: process.env }));
|
||||
}).listen(process.env.PORT);`,
|
||||
},
|
||||
'project-root-directory': {
|
||||
'src/index.html': '<h1>I am a website.</h1>',
|
||||
'src/now.json': JSON.stringify({
|
||||
|
||||
547
packages/now-cli/test/integration.js
vendored
547
packages/now-cli/test/integration.js
vendored
@@ -29,8 +29,19 @@ function execa(file, args, options) {
|
||||
return _execa(file, args, options);
|
||||
}
|
||||
|
||||
function fixture(name) {
|
||||
const directory = path.join(__dirname, 'fixtures', 'integration', name);
|
||||
const config = path.join(directory, 'project.json');
|
||||
|
||||
// We need to remove it, otherwise we can't re-use fixtures
|
||||
if (fs.existsSync(config)) {
|
||||
fs.unlinkSync(config);
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
const binaryPath = path.resolve(__dirname, `../scripts/start.js`);
|
||||
const fixture = name => path.join(__dirname, 'fixtures', 'integration', name);
|
||||
const example = name =>
|
||||
path.join(__dirname, '..', '..', '..', 'examples', name);
|
||||
const deployHelpMessage = `${logo} vercel [options] <command | path>`;
|
||||
@@ -95,7 +106,19 @@ function fetchTokenInformation(token, retries = 3) {
|
||||
}
|
||||
|
||||
function formatOutput({ stderr, stdout }) {
|
||||
return `Received:\n"${stderr}"\n"${stdout}"`;
|
||||
return `
|
||||
-----
|
||||
|
||||
Stderr:
|
||||
${stderr}
|
||||
|
||||
-----
|
||||
|
||||
Stdout:
|
||||
${stdout}
|
||||
|
||||
-----
|
||||
`;
|
||||
}
|
||||
|
||||
// AVA's `t.context` can only be set before the tests,
|
||||
@@ -279,10 +302,6 @@ test('login', async t => {
|
||||
...defaultArgs,
|
||||
]);
|
||||
|
||||
console.log(loginOutput.stderr);
|
||||
console.log(loginOutput.stdout);
|
||||
console.log(loginOutput.exitCode);
|
||||
|
||||
t.is(loginOutput.exitCode, 0, formatOutput(loginOutput));
|
||||
t.regex(
|
||||
loginOutput.stdout,
|
||||
@@ -593,7 +612,86 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
t.is(homeRes.status, 200, formatOutput({ stderr, stdout }));
|
||||
const homeJson = await homeRes.json();
|
||||
t.is(homeJson['MY_ENV_VAR'], 'MY_VALUE');
|
||||
t.is(apiJson['VERCEL_URL'], host);
|
||||
t.is(homeJson['VERCEL_URL'], host);
|
||||
}
|
||||
|
||||
async function nowDevWithEnv() {
|
||||
const vc = execa(binaryPath, ['dev', ...defaultArgs], {
|
||||
reject: false,
|
||||
cwd: target,
|
||||
});
|
||||
|
||||
let localhost = undefined;
|
||||
await waitForPrompt(vc, chunk => {
|
||||
if (chunk.includes('Ready! Available at')) {
|
||||
localhost = /(https?:[^\s]+)/g.exec(chunk);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const localhostNoProtocol = localhost[0].slice('http://'.length);
|
||||
|
||||
const apiUrl = `${localhost[0]}/api/get-env`;
|
||||
const apiRes = await fetch(apiUrl);
|
||||
|
||||
t.is(apiRes.status, 200);
|
||||
|
||||
const apiJson = await apiRes.json();
|
||||
|
||||
t.is(apiJson['MY_ENV_VAR'], 'MY_VALUE');
|
||||
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
|
||||
|
||||
const homeUrl = localhost[0];
|
||||
|
||||
const homeRes = await fetch(homeUrl);
|
||||
const homeJson = await homeRes.json();
|
||||
t.is(homeJson['MY_ENV_VAR'], 'MY_VALUE');
|
||||
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
|
||||
|
||||
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
|
||||
|
||||
const { exitCode, stderr, stdout } = await vc;
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
}
|
||||
|
||||
async function nowDevAndFetchCloudVars() {
|
||||
const vc = execa(binaryPath, ['dev', ...defaultArgs], {
|
||||
reject: false,
|
||||
cwd: target,
|
||||
});
|
||||
|
||||
let localhost = undefined;
|
||||
await waitForPrompt(vc, chunk => {
|
||||
if (chunk.includes('Ready! Available at')) {
|
||||
localhost = /(https?:[^\s]+)/g.exec(chunk);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const apiUrl = `${localhost[0]}/api/get-env`;
|
||||
const apiRes = await fetch(apiUrl);
|
||||
|
||||
const localhostNoProtocol = localhost[0].slice('http://'.length);
|
||||
|
||||
t.is(apiRes.status, 200);
|
||||
|
||||
const apiJson = await apiRes.json();
|
||||
|
||||
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
|
||||
t.is(apiJson['MY_ENV_VAR'], 'MY_VALUE');
|
||||
|
||||
const homeUrl = localhost[0];
|
||||
const homeRes = await fetch(homeUrl);
|
||||
const homeJson = await homeRes.json();
|
||||
t.is(homeJson['MY_ENV_VAR'], 'MY_VALUE');
|
||||
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
|
||||
|
||||
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
|
||||
|
||||
const { exitCode, stderr, stdout } = await vc;
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
}
|
||||
|
||||
async function nowEnvRemove() {
|
||||
@@ -662,11 +760,13 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
|
||||
await nowEnvPullOverwrite();
|
||||
await nowEnvPullConfirm();
|
||||
await nowDeployWithVar();
|
||||
await nowDevWithEnv();
|
||||
fs.unlinkSync(path.join(target, '.env'));
|
||||
await nowDevAndFetchCloudVars();
|
||||
await nowEnvRemove();
|
||||
await nowEnvRemoveWithArgs();
|
||||
await nowEnvRemoveWithNameOnly();
|
||||
await nowEnvLsIsEmpty();
|
||||
fs.unlinkSync(path.join(target, '.env'));
|
||||
});
|
||||
|
||||
test('deploy with metadata containing "=" in the value', async t => {
|
||||
@@ -945,16 +1045,35 @@ test('list the payment methods', async t => {
|
||||
});
|
||||
|
||||
test('domains inspect', async t => {
|
||||
const domainName = `inspect-${contextName}.org`;
|
||||
const domainName = `inspect-${contextName}-${Math.random()
|
||||
.toString()
|
||||
.slice(2, 8)}.org`;
|
||||
|
||||
const addRes = await execa(
|
||||
binaryPath,
|
||||
[`domains`, `add`, domainName, ...defaultArgs],
|
||||
{ reject: false }
|
||||
);
|
||||
t.is(addRes.exitCode, 0);
|
||||
const directory = fixture('static-multiple-files');
|
||||
const projectName = Math.random().toString().slice(2);
|
||||
|
||||
const { stderr, exitCode } = await execa(
|
||||
const output = await execute([
|
||||
directory,
|
||||
`-V`,
|
||||
`2`,
|
||||
`--name=${projectName}`,
|
||||
'--confirm',
|
||||
'--public',
|
||||
]);
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
|
||||
{
|
||||
// Add a domain that can be inspected
|
||||
const result = await execa(
|
||||
binaryPath,
|
||||
[`domains`, `add`, domainName, projectName, ...defaultArgs],
|
||||
{ reject: false }
|
||||
);
|
||||
|
||||
t.is(result.exitCode, 0, formatOutput(result));
|
||||
}
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
['domains', 'inspect', domainName, ...defaultArgs],
|
||||
{
|
||||
@@ -962,18 +1081,30 @@ test('domains inspect', async t => {
|
||||
}
|
||||
);
|
||||
|
||||
const rmRes = await execa(
|
||||
binaryPath,
|
||||
[`domains`, `rm`, domainName, ...defaultArgs],
|
||||
{ reject: false, input: 'y' }
|
||||
);
|
||||
t.is(rmRes.exitCode, 0);
|
||||
|
||||
t.is(exitCode, 0);
|
||||
t.true(!stderr.includes(`Renewal Price`));
|
||||
t.is(exitCode, 0, formatOutput({ stdout, stderr }));
|
||||
|
||||
{
|
||||
// Remove the domain again
|
||||
const result = await execa(
|
||||
binaryPath,
|
||||
[`domains`, `rm`, domainName, ...defaultArgs],
|
||||
{ reject: false, input: 'y' }
|
||||
);
|
||||
|
||||
t.is(result.exitCode, 0, formatOutput(result));
|
||||
}
|
||||
});
|
||||
|
||||
test('try to purchase a domain', async t => {
|
||||
if (process.env.VERCEL_TOKEN || process.env.NOW_TOKEN) {
|
||||
console.log(
|
||||
'Skipping test `try to purchase a domain` because a personal VERCEL_TOKEN was provided.'
|
||||
);
|
||||
t.pass();
|
||||
return;
|
||||
}
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
['domains', 'buy', `${session}-test.org`, ...defaultArgs],
|
||||
@@ -2431,11 +2562,10 @@ test('fail to deploy a Lambda with a specific runtime but without a locked versi
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure `github` and `scope` are not sent to the API', async t => {
|
||||
const directory = fixture('github-and-scope-config');
|
||||
const output = await execute([directory, '--confirm']);
|
||||
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
test('fail to add a domain without a project', async t => {
|
||||
const output = await execute(['domains', 'add', 'my-domain.now.sh']);
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.regex(output.stderr, /expects two arguments/gm, formatOutput(output));
|
||||
});
|
||||
|
||||
test('change user', async t => {
|
||||
@@ -2465,6 +2595,68 @@ test('change user', async t => {
|
||||
t.not(prevUser, nextUser, JSON.stringify({ prevUser, nextUser }));
|
||||
});
|
||||
|
||||
test('assign a domain to a project', async t => {
|
||||
const domain = `project-domain.${contextName}.now.sh`;
|
||||
const directory = fixture('static-deployment');
|
||||
|
||||
const deploymentOutput = await execute([directory, '--public', '--confirm']);
|
||||
t.is(deploymentOutput.exitCode, 0, formatOutput(deploymentOutput));
|
||||
|
||||
const host = deploymentOutput.stdout.trim().replace('https://', '');
|
||||
const deployment = await apiFetch(
|
||||
`/v10/now/deployments/unknown?url=${host}`
|
||||
).then(resp => resp.json());
|
||||
|
||||
t.is(typeof deployment.name, 'string', JSON.stringify(deployment, null, 2));
|
||||
const project = deployment.name;
|
||||
|
||||
const output = await execute(['domains', 'add', domain, project, '--force']);
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
|
||||
const removeResponse = await execute(['rm', project, '-y']);
|
||||
t.is(removeResponse.exitCode, 0, formatOutput(removeResponse));
|
||||
});
|
||||
|
||||
test('list project domains', async t => {
|
||||
const domain = `project-domain.${contextName}.now.sh`;
|
||||
const directory = fixture('static-deployment');
|
||||
|
||||
const deploymentOutput = await execute([directory, '--public', '--confirm']);
|
||||
t.is(deploymentOutput.exitCode, 0, formatOutput(deploymentOutput));
|
||||
|
||||
const host = deploymentOutput.stdout.trim().replace('https://', '');
|
||||
const deployment = await apiFetch(
|
||||
`/v10/now/deployments/unknown?url=${host}`
|
||||
).then(resp => resp.json());
|
||||
|
||||
t.is(typeof deployment.name, 'string', JSON.stringify(deployment, null, 2));
|
||||
const project = deployment.name;
|
||||
|
||||
const addOutput = await execute([
|
||||
'domains',
|
||||
'add',
|
||||
domain,
|
||||
project,
|
||||
'--force',
|
||||
]);
|
||||
t.is(addOutput.exitCode, 0, formatOutput(addOutput));
|
||||
|
||||
const output = await execute(['domains', 'ls']);
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
t.regex(output.stderr, new RegExp(domain), formatOutput(output));
|
||||
t.regex(output.stderr, new RegExp(project), formatOutput(output));
|
||||
|
||||
const removeResponse = await execute(['rm', project, '-y']);
|
||||
t.is(removeResponse.exitCode, 0, formatOutput(removeResponse));
|
||||
});
|
||||
|
||||
test('ensure `github` and `scope` are not sent to the API', async t => {
|
||||
const directory = fixture('github-and-scope-config');
|
||||
const output = await execute([directory, '--confirm']);
|
||||
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test('should show prompts to set up project', async t => {
|
||||
const directory = fixture('project-link');
|
||||
const projectName = `project-link-${
|
||||
@@ -2514,7 +2706,9 @@ test('should show prompts to set up project', async t => {
|
||||
await waitForPrompt(now, chunk =>
|
||||
chunk.includes(`What's your Build Command?`)
|
||||
);
|
||||
now.stdin.write(`mkdir o && echo '<h1>custom hello</h1>' > o/index.html\n`);
|
||||
now.stdin.write(
|
||||
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
|
||||
);
|
||||
|
||||
await waitForPrompt(now, chunk =>
|
||||
chunk.includes(`What's your Output Directory?`)
|
||||
@@ -3083,8 +3277,299 @@ test('reject deploying with wrong team .vercel config', async t => {
|
||||
t.is(exitCode, 1, formatOutput({ stderr, stdout }));
|
||||
t.true(
|
||||
stderr.includes(
|
||||
'Could not retrieve Project Settings. To link your project, remove the .vercel directory and deploy again.'
|
||||
'Could not retrieve Project Settings. To link your Project, remove the `.vercel` directory and deploy again.'
|
||||
),
|
||||
formatOutput({ stderr, stdout })
|
||||
);
|
||||
});
|
||||
|
||||
test('[vc link] should show prompts to set up project', async t => {
|
||||
const dir = fixture('project-link');
|
||||
const projectName = `project-link-${
|
||||
Math.random().toString(36).split('.')[1]
|
||||
}`;
|
||||
|
||||
// remove previously linked project if it exists
|
||||
await remove(path.join(dir, '.vercel'));
|
||||
|
||||
const vc = execa(binaryPath, ['link', ...defaultArgs], { cwd: dir });
|
||||
|
||||
await waitForPrompt(vc, chunk => /Set up [^?]+\?/.test(chunk));
|
||||
vc.stdin.write('yes\n');
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes('Which scope should contain your project?')
|
||||
);
|
||||
vc.stdin.write('\n');
|
||||
|
||||
await waitForPrompt(vc, chunk => chunk.includes('Link to existing project?'));
|
||||
vc.stdin.write('no\n');
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes('What’s your project’s name?')
|
||||
);
|
||||
vc.stdin.write(`${projectName}\n`);
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes('In which directory is your code located?')
|
||||
);
|
||||
vc.stdin.write('\n');
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes('Want to override the settings?')
|
||||
);
|
||||
vc.stdin.write('yes\n');
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes(
|
||||
'Which settings would you like to overwrite (select multiple)?'
|
||||
)
|
||||
);
|
||||
vc.stdin.write('a\n'); // 'a' means select all
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes(`What's your Build Command?`)
|
||||
);
|
||||
vc.stdin.write(`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`);
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes(`What's your Output Directory?`)
|
||||
);
|
||||
vc.stdin.write(`o\n`);
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes(`What's your Development Command?`)
|
||||
);
|
||||
vc.stdin.write(`\n`);
|
||||
|
||||
await waitForPrompt(vc, chunk => chunk.includes('Linked to'));
|
||||
|
||||
const output = await vc;
|
||||
|
||||
// Ensure the exit code is right
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
|
||||
// Ensure .gitignore is created
|
||||
t.is((await readFile(path.join(dir, '.gitignore'))).toString(), '.vercel');
|
||||
|
||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||
t.is(
|
||||
await exists(path.join(dir, '.vercel', 'project.json')),
|
||||
true,
|
||||
'project.json should be created'
|
||||
);
|
||||
t.is(
|
||||
await exists(path.join(dir, '.vercel', 'README.txt')),
|
||||
true,
|
||||
'README.txt should be created'
|
||||
);
|
||||
});
|
||||
|
||||
test('[vc link --confirm] should not show prompts and autolink', async t => {
|
||||
const dir = fixture('project-link-confirm');
|
||||
|
||||
// remove previously linked project if it exists
|
||||
await remove(path.join(dir, '.vercel'));
|
||||
|
||||
const { exitCode, stderr, stdout } = await execa(
|
||||
binaryPath,
|
||||
['link', '--confirm', ...defaultArgs],
|
||||
{ cwd: dir, reject: false }
|
||||
);
|
||||
|
||||
// Ensure the exit code is right
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
|
||||
// Ensure the message is correct pattern
|
||||
t.regex(stderr, /Linked to /m);
|
||||
|
||||
// Ensure .gitignore is created
|
||||
t.is((await readFile(path.join(dir, '.gitignore'))).toString(), '.vercel');
|
||||
|
||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||
t.is(
|
||||
await exists(path.join(dir, '.vercel', 'project.json')),
|
||||
true,
|
||||
'project.json should be created'
|
||||
);
|
||||
t.is(
|
||||
await exists(path.join(dir, '.vercel', 'README.txt')),
|
||||
true,
|
||||
'README.txt should be created'
|
||||
);
|
||||
});
|
||||
|
||||
test('[vc dev] should show prompts to set up project', async t => {
|
||||
const dir = fixture('project-link-dev');
|
||||
const port = 58352;
|
||||
const projectName = `project-link-dev-${
|
||||
Math.random().toString(36).split('.')[1]
|
||||
}`;
|
||||
|
||||
// remove previously linked project if it exists
|
||||
await remove(path.join(dir, '.vercel'));
|
||||
|
||||
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
|
||||
cwd: dir,
|
||||
});
|
||||
|
||||
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
|
||||
dev.stdin.write('yes\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('Which scope should contain your project?')
|
||||
);
|
||||
dev.stdin.write('\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('Link to existing project?')
|
||||
);
|
||||
dev.stdin.write('no\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('What’s your project’s name?')
|
||||
);
|
||||
dev.stdin.write(`${projectName}\n`);
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('In which directory is your code located?')
|
||||
);
|
||||
dev.stdin.write('\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('Want to override the settings?')
|
||||
);
|
||||
dev.stdin.write('yes\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(
|
||||
'Which settings would you like to overwrite (select multiple)?'
|
||||
)
|
||||
);
|
||||
dev.stdin.write('a\n'); // 'a' means select all
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(`What's your Build Command?`)
|
||||
);
|
||||
dev.stdin.write(
|
||||
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
|
||||
);
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(`What's your Output Directory?`)
|
||||
);
|
||||
dev.stdin.write(`o\n`);
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(`What's your Development Command?`)
|
||||
);
|
||||
dev.stdin.write(`\n`);
|
||||
|
||||
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
|
||||
|
||||
// Ensure .gitignore is created
|
||||
t.is((await readFile(path.join(dir, '.gitignore'))).toString(), '.vercel');
|
||||
|
||||
// Ensure .vercel/project.json and .vercel/README.txt are created
|
||||
t.is(
|
||||
await exists(path.join(dir, '.vercel', 'project.json')),
|
||||
true,
|
||||
'project.json should be created'
|
||||
);
|
||||
t.is(
|
||||
await exists(path.join(dir, '.vercel', 'README.txt')),
|
||||
true,
|
||||
'README.txt should be created'
|
||||
);
|
||||
|
||||
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
|
||||
|
||||
// Ensure that `vc dev` also works
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/`);
|
||||
const text = await response.text();
|
||||
t.is(text.includes('<h1>custom hello</h1>'), true, text);
|
||||
} finally {
|
||||
process.kill(dev.pid, 'SIGTERM');
|
||||
}
|
||||
});
|
||||
|
||||
test('[vc dev] should send the platform proxy request headers to frontend dev server ', async t => {
|
||||
const dir = fixture('dev-proxy-headers-and-env');
|
||||
const port = 58353;
|
||||
const projectName = `dev-proxy-headers-and-env-${
|
||||
Math.random().toString(36).split('.')[1]
|
||||
}`;
|
||||
|
||||
// remove previously linked project if it exists
|
||||
await remove(path.join(dir, '.vercel'));
|
||||
|
||||
const dev = execa(binaryPath, ['dev', '--listen', port, ...defaultArgs], {
|
||||
cwd: dir,
|
||||
});
|
||||
|
||||
await waitForPrompt(dev, chunk => /Set up and develop [^?]+\?/.test(chunk));
|
||||
dev.stdin.write('yes\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('Which scope should contain your project?')
|
||||
);
|
||||
dev.stdin.write('\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('Link to existing project?')
|
||||
);
|
||||
dev.stdin.write('no\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('What’s your project’s name?')
|
||||
);
|
||||
dev.stdin.write(`${projectName}\n`);
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('In which directory is your code located?')
|
||||
);
|
||||
dev.stdin.write('\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes('Want to override the settings?')
|
||||
);
|
||||
dev.stdin.write('yes\n');
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(
|
||||
'Which settings would you like to overwrite (select multiple)?'
|
||||
)
|
||||
);
|
||||
dev.stdin.write('a\n'); // 'a' means select all
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(`What's your Build Command?`)
|
||||
);
|
||||
dev.stdin.write(
|
||||
`mkdir -p o && echo '<h1>custom hello</h1>' > o/index.html\n`
|
||||
);
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(`What's your Output Directory?`)
|
||||
);
|
||||
dev.stdin.write(`o\n`);
|
||||
|
||||
await waitForPrompt(dev, chunk =>
|
||||
chunk.includes(`What's your Development Command?`)
|
||||
);
|
||||
dev.stdin.write(`node server.js\n`);
|
||||
|
||||
await waitForPrompt(dev, chunk => chunk.includes('Linked to'));
|
||||
await waitForPrompt(dev, chunk => chunk.includes('Ready! Available at'));
|
||||
|
||||
// Ensure that `vc dev` also works
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/`);
|
||||
const body = await response.json();
|
||||
t.is(body.headers['x-vercel-deployment-url'], `localhost:${port}`);
|
||||
t.is(body.env.NOW_REGION, 'dev1');
|
||||
} finally {
|
||||
process.kill(dev.pid, 'SIGTERM');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/client",
|
||||
"version": "8.2.0",
|
||||
"version": "8.2.2-canary.0",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://vercel.com",
|
||||
@@ -38,7 +38,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/build-utils": "2.4.1",
|
||||
"@vercel/build-utils": "2.4.2",
|
||||
"@zeit/fetch": "5.2.0",
|
||||
"async-retry": "1.2.3",
|
||||
"async-sema": "3.0.0",
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface NowConfig extends LegacyNowConfig {
|
||||
[fileNameSymbol]?: string;
|
||||
name?: string;
|
||||
version?: number;
|
||||
public?: boolean;
|
||||
env?: Dictionary<string>;
|
||||
build?: {
|
||||
env?: Dictionary<string>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/go",
|
||||
"version": "1.1.4-canary.0",
|
||||
"version": "1.1.5-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/go",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vercel/next",
|
||||
"version": "2.6.12",
|
||||
"version": "2.6.14-canary.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/resolve-from": "5.0.1",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "2.4.1",
|
||||
"@zeit/node-file-trace": "0.6.5",
|
||||
"@zeit/node-file-trace": "0.8.0",
|
||||
"async-sema": "3.0.1",
|
||||
"buffer-crc32": "0.2.13",
|
||||
"escape-string-regexp": "3.0.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
getLambdaOptionsFromFunction,
|
||||
getNodeVersion,
|
||||
getSpawnOptions,
|
||||
getScriptName,
|
||||
glob,
|
||||
runNpmInstall,
|
||||
runPackageJsonScript,
|
||||
@@ -32,16 +33,10 @@ import {
|
||||
convertRedirects,
|
||||
convertRewrites,
|
||||
} from '@vercel/routing-utils/dist/superstatic';
|
||||
import nodeFileTrace, { NodeFileTraceReasons } from '@zeit/node-file-trace';
|
||||
import { nodeFileTrace, NodeFileTraceReasons } from '@zeit/node-file-trace';
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
import {
|
||||
lstat,
|
||||
pathExists,
|
||||
readFile,
|
||||
unlink as unlinkFile,
|
||||
writeFile,
|
||||
} from 'fs-extra';
|
||||
import { lstat, pathExists, readFile, remove, writeFile } from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -91,7 +86,7 @@ export const version = 2;
|
||||
const htmlContentType = 'text/html; charset=utf-8';
|
||||
const nowDevChildProcesses = new Set<ChildProcess>();
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach((signal) => {
|
||||
['SIGINT', 'SIGTERM'].forEach(signal => {
|
||||
process.once(signal as NodeJS.Signals, () => {
|
||||
for (const child of nowDevChildProcesses) {
|
||||
debug(
|
||||
@@ -108,13 +103,7 @@ const MAX_AGE_ONE_YEAR = 31536000;
|
||||
/**
|
||||
* Read package.json from files
|
||||
*/
|
||||
async function readPackageJson(
|
||||
entryPath: string
|
||||
): Promise<{
|
||||
scripts?: { [key: string]: string };
|
||||
dependencies?: { [key: string]: string };
|
||||
devDependencies?: { [key: string]: string };
|
||||
}> {
|
||||
async function readPackageJson(entryPath: string): Promise<PackageJson> {
|
||||
const packagePath = path.join(entryPath, 'package.json');
|
||||
|
||||
try {
|
||||
@@ -329,49 +318,38 @@ export const build = async ({
|
||||
}
|
||||
|
||||
const isLegacy = nextVersionRange && isLegacyNext(nextVersionRange);
|
||||
let shouldRunScript = 'now-build';
|
||||
|
||||
debug(`MODE: ${isLegacy ? 'legacy' : 'serverless'}`);
|
||||
|
||||
if (isLegacy) {
|
||||
try {
|
||||
await unlinkFile(path.join(entryPath, 'yarn.lock'));
|
||||
} catch (err) {
|
||||
debug('no yarn.lock removed');
|
||||
}
|
||||
|
||||
try {
|
||||
await unlinkFile(path.join(entryPath, 'package-lock.json'));
|
||||
} catch (err) {
|
||||
debug('no package-lock.json removed');
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"WARNING: your application is being deployed in @vercel/next's legacy mode. http://err.sh/vercel/vercel/now-next-legacy-mode"
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
remove(path.join(entryPath, 'yarn.lock')),
|
||||
remove(path.join(entryPath, 'package-lock.json')),
|
||||
]);
|
||||
|
||||
debug('Normalizing package.json');
|
||||
const packageJson = normalizePackageJson(pkg);
|
||||
debug('Normalized package.json result: ', packageJson);
|
||||
await writePackageJson(entryPath, packageJson);
|
||||
} else if (pkg.scripts && pkg.scripts['now-build']) {
|
||||
debug('Found user `now-build` script');
|
||||
shouldRunScript = 'now-build';
|
||||
} else if (pkg.scripts && pkg.scripts['build']) {
|
||||
debug('Found user `build` script');
|
||||
shouldRunScript = 'build';
|
||||
} else if (!pkg.scripts || !pkg.scripts['now-build']) {
|
||||
debug(
|
||||
}
|
||||
|
||||
const buildScriptName = getScriptName(pkg, [
|
||||
'vercel-build',
|
||||
'now-build',
|
||||
'build',
|
||||
]);
|
||||
let { buildCommand } = config;
|
||||
|
||||
if (!buildScriptName && !buildCommand) {
|
||||
console.log(
|
||||
'Your application is being built using `next build`. ' +
|
||||
'If you need to define a different build step, please create a `now-build` script in your `package.json` ' +
|
||||
'(e.g. `{ "scripts": { "now-build": "npm run prepare && next build" } }`).'
|
||||
'If you need to define a different build step, please create a `vercel-build` script in your `package.json` ' +
|
||||
'(e.g. `{ "scripts": { "vercel-build": "npm run prepare && next build" } }`).'
|
||||
);
|
||||
pkg.scripts = {
|
||||
'now-build': 'next build',
|
||||
...(pkg.scripts || {}),
|
||||
};
|
||||
shouldRunScript = 'now-build';
|
||||
await writePackageJson(entryPath, pkg);
|
||||
buildCommand = 'next build';
|
||||
}
|
||||
|
||||
if (process.env.NPM_AUTH_TOKEN) {
|
||||
@@ -398,12 +376,11 @@ export const build = async ({
|
||||
await createServerlessConfig(workPath, entryPath, nextVersion);
|
||||
}
|
||||
|
||||
debug('Running user script...');
|
||||
const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128;
|
||||
const env: { [key: string]: string | undefined } = { ...spawnOpts.env };
|
||||
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
|
||||
|
||||
if (config.buildCommand) {
|
||||
if (buildCommand) {
|
||||
// Add `node_modules/.bin` to PATH
|
||||
const nodeBinPath = await getNodeBinPath({ cwd: entryPath });
|
||||
env.PATH = `${nodeBinPath}${path.delimiter}${env.PATH}`;
|
||||
@@ -412,14 +389,14 @@ export const build = async ({
|
||||
`Added "${nodeBinPath}" to PATH env because a build command was used.`
|
||||
);
|
||||
|
||||
console.log(`Running "${config.buildCommand}"`);
|
||||
await execCommand(config.buildCommand, {
|
||||
console.log(`Running "${buildCommand}"`);
|
||||
await execCommand(buildCommand, {
|
||||
...spawnOpts,
|
||||
cwd: entryPath,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
await runPackageJsonScript(entryPath, shouldRunScript, {
|
||||
} else if (buildScriptName) {
|
||||
await runPackageJsonScript(entryPath, buildScriptName, {
|
||||
...spawnOpts,
|
||||
env,
|
||||
});
|
||||
@@ -485,7 +462,7 @@ export const build = async ({
|
||||
`${(ssgDataRoute && ssgDataRoute.dataRoute) || dataRoute.page}${
|
||||
dataRoute.routeKeys
|
||||
? `?${Object.keys(dataRoute.routeKeys)
|
||||
.map((key) => `${dataRoute.routeKeys![key]}=$${key}`)
|
||||
.map(key => `${dataRoute.routeKeys![key]}=$${key}`)
|
||||
.join('&')}`
|
||||
: ''
|
||||
}`
|
||||
@@ -579,8 +556,6 @@ export const build = async ({
|
||||
return {
|
||||
output,
|
||||
routes: [
|
||||
// TODO: low priority: handle trailingSlash
|
||||
|
||||
// User headers
|
||||
...headers,
|
||||
|
||||
@@ -669,7 +644,7 @@ export const build = async ({
|
||||
}
|
||||
|
||||
if (process.env.NPM_AUTH_TOKEN) {
|
||||
await unlinkFile(path.join(entryPath, '.npmrc'));
|
||||
await remove(path.join(entryPath, '.npmrc'));
|
||||
}
|
||||
|
||||
const pageLambdaRoutes: Route[] = [];
|
||||
@@ -716,7 +691,7 @@ export const build = async ({
|
||||
);
|
||||
const nodeModules = excludeFiles(
|
||||
await glob('node_modules/**', entryPath),
|
||||
(file) => file.startsWith('node_modules/.cache')
|
||||
file => file.startsWith('node_modules/.cache')
|
||||
);
|
||||
const launcherFiles = {
|
||||
'now__bridge.js': new FileFsRef({
|
||||
@@ -745,7 +720,7 @@ export const build = async ({
|
||||
const launcherData = await readFile(launcherPath, 'utf8');
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(pages).map(async (page) => {
|
||||
Object.keys(pages).map(async page => {
|
||||
// These default pages don't have to be handled as they'd always 404
|
||||
if (['_app.js', '_error.js', '_document.js'].includes(page)) {
|
||||
return;
|
||||
@@ -840,7 +815,7 @@ export const build = async ({
|
||||
|
||||
// > 1 because _error is a lambda but isn't used if a static 404 is available
|
||||
const pageKeys = Object.keys(pages);
|
||||
const hasLambdas = !static404Page || pageKeys.length > 1;
|
||||
let hasLambdas = !static404Page || pageKeys.length > 1;
|
||||
|
||||
if (pageKeys.length === 0) {
|
||||
const nextConfig = await getNextConfig(workPath, entryPath);
|
||||
@@ -880,14 +855,38 @@ export const build = async ({
|
||||
[filePath: string]: FileFsRef;
|
||||
};
|
||||
|
||||
let canUsePreviewMode = false;
|
||||
const isApiPage = (page: string) =>
|
||||
page.replace(/\\/g, '/').match(/serverless\/pages\/api/);
|
||||
|
||||
const canUsePreviewMode = Object.keys(pages).some(page =>
|
||||
isApiPage(pages[page].fsPath)
|
||||
);
|
||||
|
||||
let pseudoLayerBytes = 0;
|
||||
let apiPseudoLayerBytes = 0;
|
||||
const pseudoLayers: PseudoLayer[] = [];
|
||||
const apiPseudoLayers: PseudoLayer[] = [];
|
||||
const nonLambdaSsgPages = new Set<string>();
|
||||
|
||||
const isApiPage = (page: string) =>
|
||||
page.replace(/\\/g, '/').match(/serverless\/pages\/api/);
|
||||
const onPrerenderRouteInitial = (routeKey: string) => {
|
||||
// Get the route file as it'd be mounted in the builder output
|
||||
const pr = prerenderManifest.staticRoutes[routeKey];
|
||||
const { initialRevalidate, srcRoute } = pr;
|
||||
const route = srcRoute || routeKey;
|
||||
|
||||
if (
|
||||
initialRevalidate === false &&
|
||||
!canUsePreviewMode &&
|
||||
!prerenderManifest.fallbackRoutes[route] &&
|
||||
!prerenderManifest.legacyBlockingRoutes[route]
|
||||
) {
|
||||
nonLambdaSsgPages.add(route);
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(prerenderManifest.staticRoutes).forEach(route =>
|
||||
onPrerenderRouteInitial(route)
|
||||
);
|
||||
|
||||
const tracedFiles: {
|
||||
[filePath: string]: FileFsRef;
|
||||
@@ -897,22 +896,31 @@ export const build = async ({
|
||||
} = {};
|
||||
|
||||
if (requiresTracing) {
|
||||
const tracingLabel =
|
||||
'Traced Next.js serverless functions for external files in';
|
||||
console.time(tracingLabel);
|
||||
|
||||
const apiPages: string[] = [];
|
||||
const nonApiPages: string[] = [];
|
||||
const allPagePaths = Object.keys(pages).map((page) => pages[page].fsPath);
|
||||
const pageKeys = Object.keys(pages);
|
||||
|
||||
for (const page of allPagePaths) {
|
||||
if (isApiPage(page)) {
|
||||
apiPages.push(page);
|
||||
canUsePreviewMode = true;
|
||||
} else {
|
||||
nonApiPages.push(page);
|
||||
for (const page of pageKeys) {
|
||||
const pagePath = pages[page].fsPath;
|
||||
const route = `/${page.replace(/\.js$/, '')}`;
|
||||
|
||||
if (route === '/_error' && static404Page) continue;
|
||||
|
||||
if (isApiPage(pagePath)) {
|
||||
apiPages.push(pagePath);
|
||||
} else if (!nonLambdaSsgPages.has(route)) {
|
||||
nonApiPages.push(pagePath);
|
||||
}
|
||||
}
|
||||
hasLambdas =
|
||||
!static404Page || apiPages.length > 0 || nonApiPages.length > 0;
|
||||
|
||||
const tracingLabel =
|
||||
'Traced Next.js serverless functions for external files in';
|
||||
|
||||
if (hasLambdas) {
|
||||
console.time(tracingLabel);
|
||||
}
|
||||
|
||||
const {
|
||||
fileList: apiFileList,
|
||||
@@ -962,10 +970,16 @@ export const build = async ({
|
||||
await Promise.all(
|
||||
apiFileList.map(collectTracedFiles(apiReasons, apiTracedFiles))
|
||||
);
|
||||
console.timeEnd(tracingLabel);
|
||||
|
||||
if (hasLambdas) {
|
||||
console.timeEnd(tracingLabel);
|
||||
}
|
||||
|
||||
const zippingLabel = 'Compressed shared serverless function files';
|
||||
console.time(zippingLabel);
|
||||
|
||||
if (hasLambdas) {
|
||||
console.time(zippingLabel);
|
||||
}
|
||||
|
||||
let pseudoLayer;
|
||||
let apiPseudoLayer;
|
||||
@@ -980,7 +994,9 @@ export const build = async ({
|
||||
pseudoLayers.push(pseudoLayer);
|
||||
apiPseudoLayers.push(apiPseudoLayer);
|
||||
|
||||
console.timeEnd(zippingLabel);
|
||||
if (hasLambdas) {
|
||||
console.timeEnd(zippingLabel);
|
||||
}
|
||||
} else {
|
||||
// An optional assets folder that is placed alongside every page
|
||||
// entrypoint.
|
||||
@@ -996,7 +1012,7 @@ export const build = async ({
|
||||
debug(
|
||||
'detected (legacy) assets to be bundled with serverless function:'
|
||||
);
|
||||
assetKeys.forEach((assetFile) => debug(`\t${assetFile}`));
|
||||
assetKeys.forEach(assetFile => debug(`\t${assetFile}`));
|
||||
debug(
|
||||
'\nPlease upgrade to Next.js 9.1 to leverage modern asset handling.'
|
||||
);
|
||||
@@ -1065,6 +1081,11 @@ export const build = async ({
|
||||
if (routeIsDynamic) {
|
||||
dynamicPages.push(normalizePage(pathname));
|
||||
}
|
||||
|
||||
if (nonLambdaSsgPages.has(`/${pathname}`)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const outputName = path.join('/', entryDirectory, pathname);
|
||||
|
||||
const lambdaGroups = routeIsApi ? apiLambdaGroups : pageLambdaGroups;
|
||||
@@ -1136,7 +1157,7 @@ export const build = async ({
|
||||
}
|
||||
} else {
|
||||
await Promise.all(
|
||||
pageKeys.map(async (page) => {
|
||||
pageKeys.map(async page => {
|
||||
// These default pages don't have to be handled as they'd always 404
|
||||
if (['_app.js', '_document.js'].includes(page)) {
|
||||
return;
|
||||
@@ -1217,8 +1238,8 @@ export const build = async ({
|
||||
false,
|
||||
routesManifest,
|
||||
new Set(prerenderManifest.omittedRoutes)
|
||||
).then((arr) =>
|
||||
arr.map((route) => {
|
||||
).then(arr =>
|
||||
arr.map(route => {
|
||||
route.src = route.src.replace('^', `^${dynamicPrefix}`);
|
||||
return route;
|
||||
})
|
||||
@@ -1236,8 +1257,8 @@ export const build = async ({
|
||||
dynamicPages,
|
||||
false,
|
||||
routesManifest
|
||||
).then((arr) =>
|
||||
arr.map((route) => {
|
||||
).then(arr =>
|
||||
arr.map(route => {
|
||||
route.src = route.src.replace('^', `^${dynamicPrefix}`);
|
||||
return route;
|
||||
})
|
||||
@@ -1257,7 +1278,7 @@ export const build = async ({
|
||||
const pages = {
|
||||
${groupPageKeys
|
||||
.map(
|
||||
(page) =>
|
||||
page =>
|
||||
`'${page}': require('./${path.join(
|
||||
'./',
|
||||
group.pages[page].pageFileName
|
||||
@@ -1302,7 +1323,7 @@ export const build = async ({
|
||||
// for prerendered dynamic routes (/blog/post-1) we need to
|
||||
// find the match since it won't match the page directly
|
||||
const dynamicRoutes = ${JSON.stringify(
|
||||
completeDynamicRoutes.map((route) => ({
|
||||
completeDynamicRoutes.map(route => ({
|
||||
src: route.src,
|
||||
dest: route.dest,
|
||||
}))
|
||||
@@ -1474,13 +1495,6 @@ export const build = async ({
|
||||
lambda = lambdas[outputSrcPathPage];
|
||||
}
|
||||
|
||||
if (lambda == null) {
|
||||
throw new NowBuildError({
|
||||
code: 'NEXT_MISSING_LAMBDA',
|
||||
message: `Unable to find lambda for route: ${routeFileNoExt}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (initialRevalidate === false) {
|
||||
if (htmlFsRef == null || jsonFsRef == null) {
|
||||
throw new NowBuildError({
|
||||
@@ -1497,6 +1511,13 @@ export const build = async ({
|
||||
}
|
||||
|
||||
if (prerenders[outputPathPage] == null) {
|
||||
if (lambda == null) {
|
||||
throw new NowBuildError({
|
||||
code: 'NEXT_MISSING_LAMBDA',
|
||||
message: `Unable to find lambda for route: ${routeFileNoExt}`,
|
||||
});
|
||||
}
|
||||
|
||||
prerenders[outputPathPage] = new Prerender({
|
||||
expiration: initialRevalidate,
|
||||
lambda,
|
||||
@@ -1516,13 +1537,13 @@ export const build = async ({
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(prerenderManifest.staticRoutes).forEach((route) =>
|
||||
Object.keys(prerenderManifest.staticRoutes).forEach(route =>
|
||||
onPrerenderRoute(route, { isBlocking: false, isFallback: false })
|
||||
);
|
||||
Object.keys(prerenderManifest.fallbackRoutes).forEach((route) =>
|
||||
Object.keys(prerenderManifest.fallbackRoutes).forEach(route =>
|
||||
onPrerenderRoute(route, { isBlocking: false, isFallback: true })
|
||||
);
|
||||
Object.keys(prerenderManifest.legacyBlockingRoutes).forEach((route) =>
|
||||
Object.keys(prerenderManifest.legacyBlockingRoutes).forEach(route =>
|
||||
onPrerenderRoute(route, { isBlocking: true, isFallback: false })
|
||||
);
|
||||
|
||||
@@ -1586,7 +1607,7 @@ export const build = async ({
|
||||
// We need to delete lambdas from output instead of omitting them from the
|
||||
// start since we rely on them for powering Preview Mode (read above in
|
||||
// onPrerenderRoute).
|
||||
prerenderManifest.omittedRoutes.forEach((routeKey) => {
|
||||
prerenderManifest.omittedRoutes.forEach(routeKey => {
|
||||
// Get the route file as it'd be mounted in the builder output
|
||||
const routeFileNoExt = path.posix.join(
|
||||
entryDirectory,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default () => 'hello from /[teamSlug]/[project]/[id]'
|
||||
export default () => 'hello from /[teamSlug]/[project]/[id]';
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export const getStaticProps = ({ params }) => {
|
||||
return {
|
||||
props: {
|
||||
id: params.id
|
||||
}
|
||||
}
|
||||
}
|
||||
id: params.id,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticPaths = () => ({
|
||||
paths: ['first', 'second'].map(id => ({ params: { id }})),
|
||||
fallback: true
|
||||
})
|
||||
paths: ['first', 'second'].map(id => ({ params: { id } })),
|
||||
fallback: true,
|
||||
});
|
||||
|
||||
export default ({ id }) => useRouter().isFallback
|
||||
? `loading...`
|
||||
: `hello from /groups/[id] ${id}`
|
||||
export default ({ id }) =>
|
||||
useRouter().isFallback ? `loading...` : `hello from /groups/[id] ${id}`;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export const getServerSideProps = ({ params }) => {
|
||||
return {
|
||||
props: {
|
||||
code: params.inviteCode
|
||||
}
|
||||
}
|
||||
}
|
||||
code: params.inviteCode,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default ({ code }) => `hello from /teams/invite/[inviteCode] ${code}`
|
||||
export default ({ code }) => `hello from /teams/invite/[inviteCode] ${code}`;
|
||||
|
||||
1
packages/now-next/test/fixtures/00-trailing-slash-add/next.config.js
vendored
Normal file
1
packages/now-next/test/fixtures/00-trailing-slash-add/next.config.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = { trailingSlash: true };
|
||||
38
packages/now-next/test/fixtures/00-trailing-slash-add/now.json
vendored
Normal file
38
packages/now-next/test/fixtures/00-trailing-slash-add/now.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
|
||||
"probes": [
|
||||
{ "path": "/foo/", "status": 200, "mustContain": "foo page" },
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/foo",
|
||||
"status": 308,
|
||||
"responseHeaders": {
|
||||
"refresh": "/url=/foo/$/"
|
||||
}
|
||||
},
|
||||
{ "path": "/abc/def/", "status": 200, "mustContain": "nested page" },
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/abc/def",
|
||||
"status": 308,
|
||||
"responseHeaders": {
|
||||
"refresh": "/url=/abc/def/$/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/test.txt/",
|
||||
"status": 308,
|
||||
"responseHeaders": {
|
||||
"refresh": "/url=/test\\.txt$/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/test.txt",
|
||||
"status": 200,
|
||||
"mustContain": "this is a file"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/now-next/test/fixtures/00-trailing-slash-add/package.json
vendored
Normal file
7
packages/now-next/test/fixtures/00-trailing-slash-add/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
3
packages/now-next/test/fixtures/00-trailing-slash-add/pages/abc/def.js
vendored
Normal file
3
packages/now-next/test/fixtures/00-trailing-slash-add/pages/abc/def.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <p>nested page</p>;
|
||||
}
|
||||
3
packages/now-next/test/fixtures/00-trailing-slash-add/pages/foo.js
vendored
Normal file
3
packages/now-next/test/fixtures/00-trailing-slash-add/pages/foo.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <p>foo page</p>;
|
||||
}
|
||||
1
packages/now-next/test/fixtures/00-trailing-slash-add/public/test.txt
vendored
Normal file
1
packages/now-next/test/fixtures/00-trailing-slash-add/public/test.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
this is a file
|
||||
1
packages/now-next/test/fixtures/00-trailing-slash-remove/next.config.js
vendored
Normal file
1
packages/now-next/test/fixtures/00-trailing-slash-remove/next.config.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = { trailingSlash: false };
|
||||
38
packages/now-next/test/fixtures/00-trailing-slash-remove/now.json
vendored
Normal file
38
packages/now-next/test/fixtures/00-trailing-slash-remove/now.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
|
||||
"probes": [
|
||||
{ "path": "/foo", "status": 200, "mustContain": "foo page" },
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/foo/",
|
||||
"status": 308,
|
||||
"responseHeaders": {
|
||||
"refresh": "/url=/foo$/"
|
||||
}
|
||||
},
|
||||
{ "path": "/abc/def", "status": 200, "mustContain": "nested page" },
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/abc/def/",
|
||||
"status": 308,
|
||||
"responseHeaders": {
|
||||
"refresh": "/url=/abc/def$/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/test.txt/",
|
||||
"status": 308,
|
||||
"responseHeaders": {
|
||||
"refresh": "/url=/test\\.txt$/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fetchOptions": { "redirect": "manual" },
|
||||
"path": "/test.txt",
|
||||
"status": 200,
|
||||
"mustContain": "this is a file"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/now-next/test/fixtures/00-trailing-slash-remove/package.json
vendored
Normal file
7
packages/now-next/test/fixtures/00-trailing-slash-remove/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
3
packages/now-next/test/fixtures/00-trailing-slash-remove/pages/abc/def.js
vendored
Normal file
3
packages/now-next/test/fixtures/00-trailing-slash-remove/pages/abc/def.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <p>nested page</p>;
|
||||
}
|
||||
3
packages/now-next/test/fixtures/00-trailing-slash-remove/pages/foo.js
vendored
Normal file
3
packages/now-next/test/fixtures/00-trailing-slash-remove/pages/foo.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <p>foo page</p>;
|
||||
}
|
||||
1
packages/now-next/test/fixtures/00-trailing-slash-remove/public/test.txt
vendored
Normal file
1
packages/now-next/test/fixtures/00-trailing-slash-remove/public/test.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
this is a file
|
||||
@@ -1 +1 @@
|
||||
export default () => 'hi from final route'
|
||||
export default () => 'hi from final route';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default () => 'hi from another route'
|
||||
export default () => 'hi from another route';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default () => 'hi from deployment route'
|
||||
export default () => 'hi from deployment route';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default () => 'hi from project route'
|
||||
export default () => 'hi from project route';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default () => 'hi from team route'
|
||||
export default () => 'hi from team route';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps () {
|
||||
export async function unstable_getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime()
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
revalidate: 5
|
||||
}
|
||||
revalidate: 5,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ world, time }) => {
|
||||
@@ -17,5 +17,5 @@ export default ({ world, time }) => {
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps () {
|
||||
export async function unstable_getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime()
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
revalidate: 5
|
||||
}
|
||||
revalidate: 5,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ world, time }) => {
|
||||
@@ -17,5 +17,5 @@ export default ({ world, time }) => {
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticPaths () {
|
||||
export async function unstable_getStaticPaths() {
|
||||
return [
|
||||
'/blog/post-1/comment-1',
|
||||
{ params: { post: 'post-2', comment: 'comment-2' } },
|
||||
@@ -10,7 +10,7 @@ export async function unstable_getStaticPaths () {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps ({ params }) {
|
||||
export async function unstable_getStaticProps({ params }) {
|
||||
return {
|
||||
props: {
|
||||
post: params.post,
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticPaths () {
|
||||
return [
|
||||
'/blog/post-1',
|
||||
{ params: { post: 'post-2' } },
|
||||
]
|
||||
export async function unstable_getStaticPaths() {
|
||||
return ['/blog/post-1', { params: { post: 'post-2' } }];
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps ({ params }) {
|
||||
export async function unstable_getStaticProps({ params }) {
|
||||
if (params.post === 'post-10') {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(() => resolve(), 1000)
|
||||
})
|
||||
setTimeout(() => resolve(), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
post: params.post,
|
||||
time: (await import('perf_hooks')).performance.now()
|
||||
time: (await import('perf_hooks')).performance.now(),
|
||||
},
|
||||
revalidate: 10
|
||||
}
|
||||
revalidate: 10,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ post, time }) => {
|
||||
@@ -32,5 +28,5 @@ export default ({ post, time }) => {
|
||||
<p>Post: {post}</p>
|
||||
<span>time: {time}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export async function unstable_getStaticProps () {
|
||||
export async function unstable_getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
world: 'world',
|
||||
time: new Date().getTime()
|
||||
time: new Date().getTime(),
|
||||
},
|
||||
revalidate: false
|
||||
}
|
||||
revalidate: false,
|
||||
};
|
||||
}
|
||||
|
||||
export default ({ world, time }) => {
|
||||
@@ -17,5 +17,5 @@ export default ({ world, time }) => {
|
||||
<p>hello: {world}</p>
|
||||
<span>time: {time}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Page = ({ data }) => <p>{data} world</p>
|
||||
const Page = ({ data }) => <p>{data} world</p>;
|
||||
|
||||
Page.getInitialProps = () => ({ data: 'hello' })
|
||||
Page.getInitialProps = () => ({ data: 'hello' });
|
||||
|
||||
export default Page
|
||||
export default Page;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function(req, res) {
|
||||
export default function (req, res) {
|
||||
res.end(`${process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE}`);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default () => 'hello world!'
|
||||
export default () => 'hello world!';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const Page = () => {
|
||||
const { query } = useRouter()
|
||||
return <p>{JSON.stringify(query)}</p>
|
||||
}
|
||||
const { query } = useRouter();
|
||||
return <p>{JSON.stringify(query)}</p>;
|
||||
};
|
||||
|
||||
Page.getInitialProps = () => ({ a: 'b' })
|
||||
Page.getInitialProps = () => ({ a: 'b' });
|
||||
|
||||
export default Page
|
||||
export default Page;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Page = () => 'hello world'
|
||||
const Page = () => 'hello world';
|
||||
|
||||
Page.getInitialProps = () => ({ hello: 'world' })
|
||||
Page.getInitialProps = () => ({ hello: 'world' });
|
||||
|
||||
export default Page
|
||||
export default Page;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const Page = () => {
|
||||
const { query } = useRouter()
|
||||
return <p>{JSON.stringify(query)}</p>
|
||||
}
|
||||
const { query } = useRouter();
|
||||
return <p>{JSON.stringify(query)}</p>;
|
||||
};
|
||||
|
||||
Page.getInitialProps = () => ({ a: 'b' })
|
||||
Page.getInitialProps = () => ({ a: 'b' });
|
||||
|
||||
export default Page
|
||||
export default Page;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "next build && next export"
|
||||
"now-build": "next build && next export"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "next build && next export"
|
||||
"vercel-build": "next build && next export"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default (req, res) => res.end(`another slug: ${req.query.slug}`)
|
||||
export default (req, res) => res.end(`another slug: ${req.query.slug}`);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default (req, res) => res.end(`index slug: ${req.query.slug}`)
|
||||
export default (req, res) => res.end(`index slug: ${req.query.slug}`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export const getServerSideProps = ({ params }) => ({
|
||||
props: {
|
||||
post: params.post
|
||||
}
|
||||
})
|
||||
post: params.post,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Comment({ post }) {
|
||||
return `comments post: ${post}`
|
||||
}
|
||||
return `comments post: ${post}`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export const getServerSideProps = ({ params }) => ({
|
||||
props: {
|
||||
post: params.post
|
||||
}
|
||||
})
|
||||
post: params.post,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Post({ post }) {
|
||||
return `index post: ${post}`
|
||||
}
|
||||
return `index post: ${post}`;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user