Compare commits

..

30 Commits

Author SHA1 Message Date
JJ Kasper
b190f2e118 Publish Stable
- @vercel/next@2.6.38
2020-11-07 12:11:05 -06:00
JJ Kasper
2389d3e936 Publish Canary
- @vercel/next@2.6.38-canary.0
2020-11-07 11:55:08 -06:00
JJ Kasper
0f4ed1965a [next] Correct data routes with basePath (#5381) 2020-11-06 23:40:32 -05:00
luc
e1e38ee536 Publish Canary
- vercel@20.1.3-canary.5
2020-11-06 16:50:13 +01:00
Luc Leray
dc1ff00610 [cli] Show error when removing env variable from no target environment (#5378)
* error if no env target is selected

* improve prompt ui

* add test
2020-11-06 16:49:34 +01:00
luc
5f31736603 Publish Canary
- vercel@20.1.3-canary.4
2020-11-05 23:17:08 +01:00
Luc Leray
9a57cc72dd [cli] (major) Update vercel env (#5372)
* prompt to select env type

* improve copy

* create env variable with type

* remove nanoid dependency

* use encodeURIComponent

* refactor envType -> type

* refactor envName -> key

* do not hide value input

* adjust value prompt depending on type

* handle std input for plaintext env variables

* show custom error when secret is not found

* handle secret name input starting with `@`

* fix system env value question

* improve ui of prompts

* expand env list in vc rm

* adjust tests

* environment -> environments

* list multiple targets in vc env ls

* show value for system env vars

* adjust tests

* capitalize targets

* add <type> to arguments

* always use stdInput for envValue

* fix number of arguments error

* adjust tests

* refactor ProjectEnvType

* refactor SYSTEM_ENV_VALUES

* fix typo for typePlaceholder

* use getCommandName

* use title

* remove @ts-ignore

* improve types

* show key controls for checkbox prompt
2020-11-05 22:58:58 +01:00
JJ Kasper
9d9c5f3753 Publish Stable
- @vercel/next@2.6.37
2020-11-05 12:44:43 -06:00
JJ Kasper
deeefc0c93 Publish Canary
- @vercel/next@2.6.37-canary.0
2020-11-05 11:50:05 -06:00
JJ Kasper
5c23b08bc1 Ensure auto-export dynamic routes are routed properly (#5377) 2020-11-05 11:48:29 -06:00
dependabot[bot]
7c4e25ccce Bump bl from 1.2.2 to 1.2.3 (#5363)
Bumps [bl](https://github.com/rvagg/bl) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/rvagg/bl/releases)
- [Commits](https://github.com/rvagg/bl/compare/v1.2.2...v1.2.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-05 10:24:17 -05:00
Joe Haddad
1bfa310945 Publish Stable
- @vercel/static-build@0.17.11
2020-11-04 23:29:30 -05:00
Joe Haddad
c96062266b Publish Canary
- @vercel/frameworks@0.1.2-canary.1
 - @vercel/build-utils@2.5.5-canary.1
 - vercel@20.1.3-canary.3
 - @vercel/client@9.0.4-canary.1
 - @vercel/static-build@0.17.11-canary.0
 - @vercel/redwood@0.1.2-canary.1
2020-11-04 23:12:46 -05:00
Joe Haddad
5bea99c1d9 [static-build] zero-config gatsby should support export default (#5375) 2020-11-04 23:11:40 -05:00
Luc Leray
358be773a2 [tests] Cleanup fixture folder before running CLI test (#5374) 2020-11-05 02:09:36 +01:00
Nathan Rajlich
9ebf4e531d [frameworks] Add installCommand placeholders (#5373) 2020-11-04 16:24:06 -08:00
Joe Haddad
71e79258b7 Publish Stable
- @vercel/next@2.6.36
 - @vercel/static-build@0.17.10
2020-11-03 10:49:41 -05:00
Joe Haddad
1dfafe7040 Publish Canary
- @vercel/next@2.6.36-canary.0
2020-11-03 10:40:07 -05:00
JJ Kasper
78ed452a99 [next] Ensure localeDetection: false is honored (#5365)
This disables adding the locale redirects when `localeDetection` is set to `false` 

x-ref: https://github.com/vercel/next.js/issues/18482
2020-11-03 01:58:40 +00:00
Joe Haddad
d408e2ef1a Publish Canary
- @vercel/static-build@0.17.10-canary.0
2020-11-02 15:04:48 -05:00
Joe Haddad
8ebb1fd9ce [static-build] add Vercel Analytics Support for Gatsby (#5366) 2020-11-02 15:03:45 -05:00
JJ Kasper
4eb5ad625c Publish Stable
- @vercel/next@2.6.35
2020-11-02 10:26:52 -06:00
luc
7164f6e58e Publish Canary
- vercel@20.1.3-canary.2
2020-11-02 15:43:54 +01:00
Naoyuki Kanezawa
a36d084b3e [cli] Add support for plaintext env variables (#5345)
* fix plain envs can not be retrieved

* add test

* add test for `vc dev`

* use v6 to retrieve env variables

* fix v6 return type

* use v6 to display env vars

* add test for vc env ls

* expand env vars with multiple targets

* minor type fixes

* always use v6 for getEnvVariables

Co-authored-by: luc <luc.leray@gmail.com>
2020-11-02 15:42:28 +01:00
JJ Kasper
8a16447fed Publish Canary
- vercel@20.1.3-canary.1
 - @vercel/next@2.6.35-canary.1
2020-11-01 12:24:51 -06:00
Leo Lamprecht
efda4ab6b9 Updated Next.js Template to use latest version of Next.js and React (#5358) 2020-11-01 17:13:16 +01:00
JJ Kasper
16060a71a9 [next] Add additional tests for i18n revalidate params (#5356) 2020-11-01 15:15:20 +01:00
Luc Leray
b18e0a7415 [cli] Replace util/prompt-bool by util/input/confirm (#5359) 2020-10-31 22:07:35 +01:00
JJ Kasper
1251f11a97 Publish Canary
- @vercel/next@2.6.35-canary.0
2020-10-31 00:34:03 -05:00
JJ Kasper
07235e22f6 [next] Fix root-most optional-catchall with i18n (#5354)
* Fix root-most optional-catchall with i18n

* Ensure dynamic prerenders revalidation

* Add test delay
2020-10-30 17:40:39 -05:00
109 changed files with 72852 additions and 329 deletions

View File

@@ -34,6 +34,7 @@ packages/now-node-bridge/bridge.*
# now-static-build
packages/now-static-build/test/fixtures
packages/now-static-build/test/build-fixtures
# redwood
packages/redwood/test/fixtures

View File

@@ -8,8 +8,8 @@
"start": "next start"
},
"dependencies": {
"next": "9.5.4",
"react": "16.13.1",
"react-dom": "16.13.1"
"next": "10.0.0",
"react": "17.0.1",
"react-dom": "17.0.1"
}
}

View File

@@ -17,6 +17,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `blitz build`"
},
@@ -47,6 +50,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `next build`"
},
@@ -82,6 +88,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `gatsby build`"
},
@@ -111,6 +120,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `hexo generate`"
},
@@ -140,6 +152,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `npx @11ty/eleventy`"
},
@@ -168,6 +183,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `docusaurus build`"
},
@@ -196,6 +214,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `docusaurus-build`"
},
@@ -224,6 +245,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `preact build`"
},
@@ -255,6 +279,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `dojo build`"
},
@@ -283,6 +310,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `ember build`"
},
@@ -311,6 +341,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `vue-cli-service build`"
},
@@ -339,6 +372,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `ng build && scully`"
},
@@ -367,6 +403,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `ng build`"
},
@@ -395,6 +434,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `ng build`"
},
@@ -423,6 +465,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `polymer build`"
},
@@ -451,6 +496,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `rollup -c`"
},
@@ -479,6 +527,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `react-scripts build`"
},
@@ -511,6 +562,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `react-scripts build`"
},
@@ -539,6 +593,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `gridsome build`"
},
@@ -567,6 +624,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `umi build`"
},
@@ -595,6 +655,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `sapper export`"
},
@@ -623,6 +686,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `saber build`"
},
@@ -651,6 +717,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `stencil build`"
},
@@ -679,6 +748,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `nuxt generate`"
},
@@ -709,6 +781,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"value": "yarn rw build && yarn rw db up --no-db-client --auto-approve && yarn rw dataMigrate up"
},
@@ -743,6 +818,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "None"
},
"buildCommand": {
"placeholder": "`npm run build` or `hugo -D --gc`"
},
@@ -770,6 +848,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`bundle install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `jekyll build`"
},
@@ -797,6 +878,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run build` or `brunch build --production`"
},
@@ -824,6 +908,9 @@
]
},
"settings": {
"installCommand": {
"placeholder": "`bundle install`"
},
"buildCommand": {
"value": "`npm run build` or `bundle exec middleman build`"
},
@@ -841,6 +928,9 @@
"logo": "https://raw.githubusercontent.com/vercel/vercel/master/packages/frameworks/logos/other.svg",
"description": "No framework or a unoptimized framework.",
"settings": {
"installCommand": {
"placeholder": "`yarn install` or `npm install`"
},
"buildCommand": {
"placeholder": "`npm run vercel-build` or `npm run build`"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "0.1.2-canary.0",
"version": "0.1.2-canary.1",
"main": "frameworks.json",
"license": "UNLICENSED",
"scripts": {

View File

@@ -89,9 +89,15 @@ const Schema = {
},
settings: {
type: 'object',
required: ['buildCommand', 'devCommand', 'outputDirectory'],
required: [
'installCommand',
'buildCommand',
'devCommand',
'outputDirectory',
],
additionalProperties: false,
properties: {
installCommand: SchemaSettings,
buildCommand: SchemaSettings,
devCommand: SchemaSettings,
outputDirectory: SchemaSettings,

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/build-utils",
"version": "2.5.5-canary.0",
"version": "2.5.5-canary.1",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -29,7 +29,7 @@
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"@vercel/frameworks": "0.1.2-canary.0",
"@vercel/frameworks": "0.1.2-canary.1",
"@vercel/ncc": "0.24.0",
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "20.1.3-canary.0",
"version": "20.1.3-canary.5",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -13,7 +13,7 @@
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js test/dev-validate.unit.js --serial --fail-fast --verbose",
"test-integration-cli": "ava test/integration.js --serial --fail-fast --verbose",
"test-integration-cli": "rimraf test/fixtures/integration && ava test/integration.js --serial --fail-fast --verbose",
"test-integration-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
"prepublishOnly": "yarn build",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
@@ -61,7 +61,7 @@
"node": ">= 10"
},
"dependencies": {
"@vercel/build-utils": "2.5.5-canary.0",
"@vercel/build-utils": "2.5.5-canary.1",
"@vercel/go": "1.1.6",
"@vercel/node": "1.8.4",
"@vercel/python": "1.2.3",
@@ -100,7 +100,7 @@
"@types/universal-analytics": "0.4.2",
"@types/which": "1.3.2",
"@types/write-json-file": "2.2.1",
"@vercel/frameworks": "0.1.2-canary.0",
"@vercel/frameworks": "0.1.2-canary.1",
"@vercel/ncc": "0.24.0",
"@zeit/fun": "0.11.2",
"@zeit/source-map-support": "0.6.2",
@@ -146,7 +146,6 @@
"minimatch": "3.0.4",
"mri": "1.1.5",
"ms": "2.1.2",
"nanoid": "3.0.2",
"node-fetch": "2.6.1",
"npm-package-arg": "6.1.0",
"nyc": "13.2.0",
@@ -158,6 +157,7 @@
"psl": "1.1.31",
"qr-image": "3.2.0",
"raw-body": "2.4.1",
"rimraf": "3.0.2",
"semver": "5.5.0",
"serve-handler": "6.1.1",
"sinon": "4.4.2",

View File

@@ -7,7 +7,7 @@ import getScope from '../../util/get-scope.ts';
import removeAliasById from '../../util/alias/remove-alias-by-id';
import stamp from '../../util/output/stamp.ts';
import strlen from '../../util/strlen.ts';
import promptBool from '../../util/prompt-bool';
import confirm from '../../util/input/confirm';
import { isValidName } from '../../util/is-valid-name';
import findAliasByAliasOrId from '../../util/alias/find-alias-by-alias-or-id';
import { getCommandName } from '../../util/pkg-name.ts';
@@ -108,5 +108,5 @@ async function confirmAliasRemove(output, alias) {
output.log(`The following alias will be removed permanently`);
output.print(` ${tbl}\n`);
return promptBool(output, chalk.red('Are you sure?'));
return confirm(chalk.red('Are you sure?'), false);
}

View File

@@ -1,6 +1,6 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { ProjectEnvTarget, Project } from '../../types';
import { ProjectEnvTarget, Project, Secret, ProjectEnvType } from '../../types';
import { Output } from '../../util/output';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
@@ -11,12 +11,14 @@ import {
getEnvTargetPlaceholder,
getEnvTargetChoices,
} from '../../util/env/env-target';
import { isValidEnvType, getEnvTypePlaceholder } from '../../util/env/env-type';
import readStandardInput from '../../util/input/read-standard-input';
import param from '../../util/output/param';
import withSpinner from '../../util/with-spinner';
import { emoji, prependEmoji } from '../../util/emoji';
import { isKnownError } from '../../util/env/known-error';
import { getCommandName } from '../../util/pkg-name';
import { SYSTEM_ENV_VALUES } from '../../util/env/system-env';
type Options = {
'--debug': boolean;
@@ -29,38 +31,71 @@ export default async function add(
args: string[],
output: Output
) {
const stdInput = await readStandardInput();
let [envName, envTarget] = args;
// improve the way we show inquirer prompts
require('../../util/input/patch-inquirer');
if (args.length > 2) {
const stdInput = await readStandardInput();
let [envTypeArg, envName, envTargetArg] = args;
if (args.length > 3) {
output.error(
`Invalid number of arguments. Usage: ${getCommandName(
`env add <name> ${getEnvTargetPlaceholder()}`
`env add ${getEnvTypePlaceholder()} <name> ${getEnvTargetPlaceholder()}`
)}`
);
return 1;
}
if (stdInput && (!envName || !envTarget)) {
if (stdInput && (!envTypeArg || !envName || !envTargetArg)) {
output.error(
`Invalid number of arguments. Usage: ${getCommandName(
`env add <name> <target> < <file>`
`env add ${getEnvTypePlaceholder()} <name> <target> < <file>`
)}`
);
return 1;
}
let envTargets: ProjectEnvTarget[] = [];
if (envTarget) {
if (!isValidEnvTarget(envTarget)) {
if (envTargetArg) {
if (!isValidEnvTarget(envTargetArg)) {
output.error(
`The Environment ${param(
envTarget
envTargetArg
)} is invalid. It must be one of: ${getEnvTargetPlaceholder()}.`
);
return 1;
}
envTargets.push(envTarget);
envTargets.push(envTargetArg);
}
let envType: ProjectEnvType;
if (envTypeArg) {
if (!isValidEnvType(envTypeArg)) {
output.error(
`The Environment Variable type ${param(
envTypeArg
)} is invalid. It must be one of: ${getEnvTypePlaceholder()}.`
);
return 1;
}
envType = envTypeArg;
} else {
const answers = (await inquirer.prompt({
name: 'inputEnvType',
type: 'list',
message: `Which type of Environment Variable do you want to add?`,
choices: [
{ name: 'Plaintext', value: ProjectEnvType.Plaintext },
{
name: `Secret (can be created using ${getCommandName('secret add')})`,
value: ProjectEnvType.Secret,
},
{ name: 'Provided by System', value: ProjectEnvType.System },
],
})) as { inputEnvType: ProjectEnvType };
envType = answers.inputEnvType;
}
while (!envName) {
@@ -77,7 +112,7 @@ export default async function add(
}
}
const envs = await getEnvVariables(output, client, project.id, 4);
const { envs } = await getEnvVariables(output, client, project.id);
const existing = new Set(
envs.filter(r => r.key === envName).map(r => r.target)
);
@@ -98,15 +133,59 @@ export default async function add(
if (stdInput) {
envValue = stdInput;
} else if (isSystemEnvVariable(envName)) {
envValue = '';
} else {
} else if (envType === ProjectEnvType.Plaintext) {
const { inputValue } = await inquirer.prompt({
type: 'password',
type: 'input',
name: 'inputValue',
message: `Whats the value of ${envName}?`,
});
envValue = inputValue || '';
} else if (envType === ProjectEnvType.Secret) {
let secretId: string | null = null;
while (!secretId) {
let { secretName } = await inquirer.prompt({
type: 'input',
name: 'secretName',
message: `Whats the value of ${envName}?`,
});
secretName = secretName || '';
if (secretName[0] === '@') {
secretName = secretName.slice(1);
}
try {
const secret = await client.fetch<Secret>(
`/v2/now/secrets/${encodeURIComponent(secretName)}`
);
secretId = secret.uid;
} catch (error) {
if (error.status === 404) {
output.error(
`Please enter the name of an existing Secret (can be created with ${getCommandName(
'secret add'
)}).`
);
} else {
throw error;
}
}
}
envValue = secretId;
} else {
const { systemEnvValue } = await inquirer.prompt({
name: 'systemEnvValue',
type: 'list',
message: `Whats the value of ${envName}?`,
choices: SYSTEM_ENV_VALUES.map(value => ({ name: value, value })),
});
envValue = systemEnvValue;
}
while (envTargets.length === 0) {
@@ -127,7 +206,15 @@ export default async function add(
const addStamp = stamp();
try {
await withSpinner('Saving', () =>
addEnvRecord(output, client, project.id, envName, envValue, envTargets)
addEnvRecord(
output,
client,
project.id,
envType,
envName,
envValue,
envTargets
)
);
} catch (error) {
if (isKnownError(error) && error.serverMessage) {
@@ -148,7 +235,3 @@ export default async function add(
return 0;
}
function isSystemEnvVariable(envName: string) {
return envName.startsWith('VERCEL_');
}

View File

@@ -6,6 +6,7 @@ import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand';
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
import { getEnvTargetPlaceholder } from '../../util/env/env-target';
import { getEnvTypePlaceholder } from '../../util/env/env-type';
import { getLinkedProject } from '../../util/projects/link';
import Client from '../../util/client';
import handleError from '../../util/handle-error';
@@ -18,16 +19,17 @@ import ls from './ls';
import rm from './rm';
const help = () => {
const placeholder = getEnvTargetPlaceholder();
const typePlaceholder = getEnvTypePlaceholder();
const targetPlaceholder = getEnvTargetPlaceholder();
console.log(`
${chalk.bold(`${logo} ${getPkgName()} env`)} [options] <command>
${chalk.dim('Commands:')}
ls [environment] List all variables for the specified Environment
add [name] [environment] Add an Environment Variable (see examples below)
rm [name] [environment] Remove an Environment Variable (see examples below)
pull [filename] Pull all Development Environment Variables from the cloud and write to a file [.env]
ls [environment] List all variables for the specified Environment
add [type] [name] [environment] Add an Environment Variable (see examples below)
rm [name] [environment] Remove an Environment Variable (see examples below)
pull [filename] Pull all Development Environment Variables from the cloud and write to a file [.env]
${chalk.dim('Options:')}
@@ -42,27 +44,32 @@ const help = () => {
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-N, --next Show next page of results
${chalk.dim('Examples:')}
${chalk.gray('')} Add a new variable to multiple Environments
${chalk.cyan(`$ ${getPkgName()} env add <name>`)}
${chalk.cyan(`$ ${getPkgName()} env add API_TOKEN`)}
${chalk.cyan(`$ ${getPkgName()} env add ${typePlaceholder} <name>`)}
${chalk.cyan(`$ ${getPkgName()} env add secret API_TOKEN`)}
${chalk.gray('')} Add a new variable for a specific Environment
${chalk.cyan(`$ ${getPkgName()} env add <name> ${placeholder}`)}
${chalk.cyan(`$ ${getPkgName()} env add DB_CONNECTION production`)}
${chalk.cyan(
`$ ${getPkgName()} env add ${typePlaceholder} <name> ${targetPlaceholder}`
)}
${chalk.cyan(`$ ${getPkgName()} env add secret DB_PASS production`)}
${chalk.gray('')} Add a new Environment Variable from stdin
${chalk.cyan(
`$ cat <file> | ${getPkgName()} env add <name> ${placeholder}`
`$ cat <file> | ${getPkgName()} env add ${typePlaceholder} <name> ${targetPlaceholder}`
)}
${chalk.cyan(
`$ cat ~/.npmrc | ${getPkgName()} env add plain NPM_RC preview`
)}
${chalk.cyan(
`$ ${getPkgName()} env add plain API_URL production < url.txt`
)}
${chalk.cyan(`$ cat ~/.npmrc | ${getPkgName()} env add NPM_RC preview`)}
${chalk.cyan(`$ ${getPkgName()} env add DB_PASS production < secret.txt`)}
${chalk.gray('')} Remove an variable from multiple Environments
@@ -71,14 +78,8 @@ const help = () => {
${chalk.gray('')} Remove a variable from a specific Environment
${chalk.cyan(`$ ${getPkgName()} env rm <name> ${placeholder}`)}
${chalk.cyan(`$ ${getPkgName()} env rm <name> ${targetPlaceholder}`)}
${chalk.cyan(`$ ${getPkgName()} env rm NPM_RC preview`)}
${chalk.gray('')} Paginate results, where ${chalk.dim(
'`1584722256178`'
)} is the time in milliseconds since the UNIX epoch.
${chalk.cyan(`$ ${getPkgName()} env ls --next 1584722256178`)}
`);
};
@@ -96,8 +97,6 @@ export default async function main(ctx: NowContext) {
argv = getArgs(ctx.argv.slice(2), {
'--yes': Boolean,
'-y': '--yes',
'--next': Number,
'-N': '--next',
});
} catch (error) {
handleError(error);

View File

@@ -1,7 +1,12 @@
import chalk from 'chalk';
import ms from 'ms';
import { Output } from '../../util/output';
import { ProjectEnvVariable, ProjectEnvTarget, Project } from '../../types';
import {
ProjectEnvTarget,
Project,
ProjectEnvVariable,
ProjectEnvType,
} from '../../types';
import Client from '../../util/client';
import formatTable from '../../util/format-table';
import getEnvVariables from '../../util/env/get-env-records';
@@ -11,12 +16,13 @@ import {
} from '../../util/env/env-target';
import stamp from '../../util/output/stamp';
import param from '../../util/output/param';
import getCommandFlags from '../../util/get-command-flags';
import { getCommandName } from '../../util/pkg-name';
import ellipsis from '../../util/output/ellipsis';
// @ts-ignore
import title from 'title';
type Options = {
'--debug': boolean;
'--next'?: number;
};
export default async function ls(
@@ -26,8 +32,6 @@ export default async function ls(
args: string[],
output: Output
) {
const { '--next': nextTimestamp } = opts;
if (args.length > 1) {
output.error(
`Invalid number of arguments. Usage: ${getCommandName(
@@ -50,42 +54,21 @@ export default async function ls(
const lsStamp = stamp();
if (typeof nextTimestamp !== 'undefined' && Number.isNaN(nextTimestamp)) {
output.error('Please provide a number for flag --next');
return 1;
}
const { envs } = await getEnvVariables(output, client, project.id, envTarget);
const data = await getEnvVariables(
output,
client,
project.id,
5,
envTarget,
nextTimestamp
);
const { envs: records, pagination } = data;
output.log(
`${
records.length > 0 ? 'Environment Variables' : 'No Environment Variables'
envs.length > 0 ? 'Environment Variables' : 'No Environment Variables'
} found in Project ${chalk.bold(project.name)} ${chalk.gray(lsStamp())}`
);
console.log(getTable(records));
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(opts, ['_', '--next']);
output.log(
`To display the next page run ${getCommandName(
`env ls${flags} --next ${pagination.next}`
)}`
);
}
console.log(getTable(envs));
return 0;
}
function getTable(records: ProjectEnvVariable[]) {
return formatTable(
['name', 'value', 'environment', 'created'],
['name', 'value', 'environments', 'created'],
['l', 'l', 'l', 'l', 'l'],
[
{
@@ -96,17 +79,27 @@ function getTable(records: ProjectEnvVariable[]) {
);
}
function getRow({
key,
system = false,
target,
createdAt = 0,
}: ProjectEnvVariable) {
function getRow(env: ProjectEnvVariable) {
let value: string;
if (env.type === ProjectEnvType.Plaintext) {
// replace space characters (line-break, etc.) with simple spaces
// to make sure the displayed value is a single line
const singleLineValue = env.value.replace(/\s/g, ' ');
value = chalk.gray(ellipsis(singleLineValue, 19));
} else if (env.type === ProjectEnvType.System) {
value = chalk.gray.italic(env.value);
} else {
value = chalk.gray.italic('Encrypted');
}
const now = Date.now();
return [
chalk.bold(key),
chalk.gray(chalk.italic(system ? 'Populated by System' : 'Encrypted')),
target || '',
`${ms(now - createdAt)} ago`,
chalk.bold(env.key),
value,
(Array.isArray(env.target) ? env.target : [env.target || ''])
.map(title)
.join(', '),
env.createdAt ? `${ms(now - env.createdAt)} ago` : '',
];
}

View File

@@ -1,7 +1,7 @@
import chalk from 'chalk';
import { ProjectEnvTarget, Project } from '../../types';
import { Output } from '../../util/output';
import promptBool from '../../util/prompt-bool';
import confirm from '../../util/input/confirm';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import getDecryptedEnvRecords from '../../util/get-decrypted-env-records';
@@ -68,9 +68,9 @@ export default async function pull(
} else if (
exists &&
!skipConfirmation &&
!(await promptBool(
output,
`Found existing file ${param(filename)}. Do you want to overwrite?`
!(await confirm(
`Found existing file ${param(filename)}. Do you want to overwrite?`,
false
))
) {
output.log('Aborted');

View File

@@ -1,8 +1,8 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { ProjectEnvTarget, Project } from '../../types';
import { ProjectEnvTarget, Project, ProjectEnvVariableV5 } from '../../types';
import { Output } from '../../util/output';
import promptBool from '../../util/prompt-bool';
import confirm from '../../util/input/confirm';
import removeEnvRecord from '../../util/env/remove-env-record';
import getEnvVariables from '../../util/env/get-env-records';
import {
@@ -30,6 +30,9 @@ export default async function rm(
args: string[],
output: Output
) {
// improve the way we show inquirer prompts
require('../../util/input/patch-inquirer');
if (args.length > 2) {
output.error(
`Invalid number of arguments. Usage: ${getCommandName(
@@ -69,7 +72,20 @@ export default async function rm(
envName = inputName;
}
const envs = await getEnvVariables(output, client, project.id, 4);
const data = await getEnvVariables(output, client, project.id);
// we expand env vars with multiple targets
const envs: ProjectEnvVariableV5[] = [];
for (let env of data.envs) {
if (Array.isArray(env.target)) {
for (let target of env.target) {
envs.push({ ...env, target });
}
} else {
envs.push({ ...env, target: env.target });
}
}
const existing = new Set(
envs.filter(r => r.key === envName).map(r => r.target)
);
@@ -79,7 +95,7 @@ export default async function rm(
return 1;
}
if (envTargets.length === 0) {
while (envTargets.length === 0) {
const choices = getEnvTargetChoices().filter(c => existing.has(c.value));
if (choices.length === 0) {
output.error(
@@ -97,6 +113,13 @@ export default async function rm(
message: `Remove ${envName} from which Environments (select multiple)?`,
choices,
});
if (inputTargets.length === 0) {
output.error(
'Please select an Environment to remove the Environment Variable from.'
);
}
envTargets = inputTargets;
}
}
@@ -104,11 +127,11 @@ export default async function rm(
const skipConfirmation = opts['--yes'];
if (
!skipConfirmation &&
!(await promptBool(
output,
!(await confirm(
`Removing Environment Variable ${param(
envName
)} from Project ${chalk.bold(project.name)}. Are you sure?`
)} from Project ${chalk.bold(project.name)}. Are you sure?`,
false
))
) {
output.log('Aborted');

View File

@@ -203,12 +203,23 @@ export enum ProjectEnvTarget {
Development = 'development',
}
export enum ProjectEnvType {
Plaintext = 'plain',
Secret = 'secret',
System = 'system',
}
export interface ProjectEnvVariable {
key: string;
value: string;
type: ProjectEnvType;
configurationId?: string | null;
createdAt?: number;
updatedAt?: number;
target?: ProjectEnvTarget | ProjectEnvTarget[];
}
export interface ProjectEnvVariableV5 extends ProjectEnvVariable {
target?: ProjectEnvTarget;
system?: boolean;
}

View File

@@ -1,60 +1,39 @@
import { Output } from '../output';
import Client from '../client';
import { Secret, ProjectEnvTarget, ProjectEnvVariable } from '../../types';
import { customAlphabet } from 'nanoid';
import slugify from '@sindresorhus/slugify';
import {
Secret,
ProjectEnvTarget,
ProjectEnvVariableV5,
ProjectEnvType,
} from '../../types';
export default async function addEnvRecord(
output: Output,
client: Client,
projectId: string,
envName: string,
envValue: string | undefined,
type: ProjectEnvType,
key: string,
envValue: string,
targets: ProjectEnvTarget[]
): Promise<void> {
output.debug(
`Adding Environment Variable ${envName} to ${targets.length} targets`
`Adding ${type} Environment Variable ${key} to ${targets.length} targets`
);
let values: string[] | undefined;
let value = envValue;
if (envValue) {
const secrets = await Promise.all(
targets.map(target =>
client.fetch<Secret>('/v2/now/secrets', {
method: 'POST',
body: JSON.stringify({
name: generateSecretName(envName, target),
value: envValue,
projectId: projectId,
decryptable: target === ProjectEnvTarget.Development,
}),
})
)
if (type === ProjectEnvType.Secret) {
const secret = await client.fetch<Secret>(
`/v2/now/secrets/${encodeURIComponent(envValue)}`
);
values = secrets.map(secret => secret.uid);
value = secret.uid;
}
const body = targets.map((target, i) => ({
key: envName,
value: values ? values[i] : '',
target,
}));
const body = { type, key, value, target: targets };
const urlProject = `/v4/projects/${projectId}/env`;
await client.fetch<ProjectEnvVariable>(urlProject, {
const urlProject = `/v6/projects/${projectId}/env`;
await client.fetch<ProjectEnvVariableV5>(urlProject, {
method: 'POST',
body: JSON.stringify(body),
});
}
const randomSecretSuffix = customAlphabet(
'123456789abcdefghijklmnopqrstuvwxyz',
4
);
function generateSecretName(envName: string, target: ProjectEnvTarget) {
return `${
slugify(envName).substring(0, 80) // we truncate because the max secret length is 100
}-${target}-${randomSecretSuffix()}`;
}

View File

@@ -0,0 +1,15 @@
import { ProjectEnvType } from '../../types';
function envTypes(): string[] {
return Object.values(ProjectEnvType);
}
export function isValidEnvType(
type?: string
): type is ProjectEnvType | undefined {
return typeof type === 'undefined' || envTypes().includes(type);
}
export function getEnvTypePlaceholder() {
return `<${envTypes().join(' | ')}>`;
}

View File

@@ -1,69 +1,24 @@
import { Output } from '../output';
import Client from '../client';
import {
ProjectEnvVariable,
ProjectEnvTarget,
PaginationOptions,
} from '../../types';
import { ProjectEnvVariable, ProjectEnvTarget } from '../../types';
import { URLSearchParams } from 'url';
type ApiVersion = 4 | 5;
type APIV4Response = ProjectEnvVariable[];
interface APIV5Response {
pagination: PaginationOptions;
envs: ProjectEnvVariable[];
}
export default async function getEnvVariables(
output: Output,
client: Client,
projectId: string,
apiVersion: 4,
target?: ProjectEnvTarget
): Promise<APIV4Response>;
export default async function getEnvVariables(
output: Output,
client: Client,
projectId: string,
apiVersion: 5,
target?: ProjectEnvTarget,
next?: number
): Promise<APIV5Response>;
export default async function getEnvVariables<V extends ApiVersion>(
output: Output,
client: Client,
projectId: string,
apiVersion: V,
target?: ProjectEnvTarget,
next?: number
) {
output.debug(
`Fetching Environment Variables of project ${projectId} and target ${target}`
);
const query = new URLSearchParams();
if (apiVersion >= 5) {
query.set('limit', String(20));
}
if (target) {
query.set('target', target);
}
if (next) {
query.set('until', String(next));
}
const url = `/v6/projects/${projectId}/env?${query}`;
const url = `/v${apiVersion}/projects/${projectId}/env?${query}`;
if (apiVersion === 5) {
return client.fetch<APIV5Response>(url);
} else if (apiVersion === 4) {
return client.fetch<APIV4Response>(url);
} else {
throw new Error('Unknown version: ' + apiVersion);
}
return client.fetch<{ envs: ProjectEnvVariable[] }>(url);
}

View File

@@ -1,6 +1,6 @@
import { Output } from '../output';
import Client from '../client';
import { ProjectEnvTarget, Secret, ProjectEnvVariable } from '../../types';
import { ProjectEnvTarget, Secret, ProjectEnvVariableV5 } from '../../types';
export default async function removeEnvRecord(
output: Output,
@@ -18,7 +18,7 @@ export default async function removeEnvRecord(
envName
)}${qs}`;
const env = await client.fetch<ProjectEnvVariable>(urlProject, {
const env = await client.fetch<ProjectEnvVariableV5>(urlProject, {
method: 'DELETE',
});

View File

@@ -0,0 +1,32 @@
export const SYSTEM_ENV_VALUES = [
'VERCEL_URL',
'VERCEL_GITHUB_COMMIT_ORG',
'VERCEL_GITHUB_COMMIT_REF',
'VERCEL_GITHUB_ORG',
'VERCEL_GITHUB_DEPLOYMENT',
'VERCEL_GITHUB_COMMIT_REPO',
'VERCEL_GITHUB_REPO',
'VERCEL_GITHUB_COMMIT_AUTHOR_LOGIN',
'VERCEL_GITHUB_COMMIT_AUTHOR_NAME',
'VERCEL_GITHUB_COMMIT_SHA',
'VERCEL_GITLAB_DEPLOYMENT',
'VERCEL_GITLAB_PROJECT_NAMESPACE',
'VERCEL_GITLAB_PROJECT_NAME',
'VERCEL_GITLAB_PROJECT_ID',
'VERCEL_GITLAB_PROJECT_PATH',
'VERCEL_GITLAB_COMMIT_REF',
'VERCEL_GITLAB_COMMIT_SHA',
'VERCEL_GITLAB_COMMIT_MESSAGE',
'VERCEL_GITLAB_COMMIT_AUTHOR_LOGIN',
'VERCEL_GITLAB_COMMIT_AUTHOR_NAME',
'VERCEL_BITBUCKET_DEPLOYMENT',
'VERCEL_BITBUCKET_REPO_OWNER',
'VERCEL_BITBUCKET_REPO_SLUG',
'VERCEL_BITBUCKET_REPO_NAME',
'VERCEL_BITBUCKET_COMMIT_REF',
'VERCEL_BITBUCKET_COMMIT_SHA',
'VERCEL_BITBUCKET_COMMIT_MESSAGE',
'VERCEL_BITBUCKET_COMMIT_AUTHOR_NAME',
'VERCEL_BITBUCKET_COMMIT_AUTHOR_URL',
'VERCEL_BITBUCKET_COMMIT_AUTHOR_AVATAR',
];

View File

@@ -2,7 +2,7 @@ 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 { ProjectEnvTarget, Project, ProjectEnvType } from '../types';
import { Env } from '@vercel/build-utils';
@@ -12,9 +12,15 @@ export default async function getDecryptedEnvRecords(
project: Project,
target: ProjectEnvTarget
): Promise<Env> {
const envs = await getEnvVariables(output, client, project.id, 4, target);
const { envs } = await getEnvVariables(output, client, project.id, target);
const decryptedValues = await Promise.all(
envs.map(async env => {
if (env.type === ProjectEnvType.System) {
return { value: '', found: true };
} else if (env.type === ProjectEnvType.Plaintext) {
return { value: env.value, found: true };
}
try {
const value = await getDecryptedSecret(output, client, env.value);
return { value, found: true };

View File

@@ -10,7 +10,7 @@ import chalk from 'chalk';
*/
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/base.js#L126
const getQuestion = function() {
const getQuestion = function () {
let message = `${chalk.gray('?')} ${this.opt.message} `;
if (this.opt.type === 'confirm') {
@@ -35,7 +35,7 @@ inquirer.prompt.prompts.input.prototype.getQuestion = getQuestion;
inquirer.prompt.prompts.confirm.prototype.getQuestion = getQuestion;
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/list.js#L80
inquirer.prompt.prompts.list.prototype.render = function() {
inquirer.prompt.prompts.list.prototype.render = function () {
// Render question
let message = this.getQuestion();
@@ -89,11 +89,22 @@ function listRender(choices, pointer) {
}
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/checkbox.js#L84
inquirer.prompt.prompts.checkbox.prototype.render = function(error) {
inquirer.prompt.prompts.checkbox.prototype.render = function (error) {
// Render question
let message = this.getQuestion();
let bottomContent = '';
if (!this.spaceKeyPressed) {
message +=
'(Press ' +
chalk.cyan.bold('<space>') +
' to select, ' +
chalk.cyan.bold('<a>') +
' to toggle all, ' +
chalk.cyan.bold('<i>') +
' to invert selection)';
}
// Render choices or answer depending on the state
if (this.status === 'answered') {
message += this.selection.length > 0 ? this.selection.join(', ') : 'None';
@@ -118,7 +129,7 @@ function renderChoices(choices, pointer) {
let output = '';
let separatorOffset = 0;
choices.forEach(function(choice, i) {
choices.forEach(function (choice, i) {
if (choice.type === 'separator') {
separatorOffset++;
output += '' + choice + '\n';
@@ -151,7 +162,7 @@ function renderChoices(choices, pointer) {
}
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/input.js#L44
inquirer.prompt.prompts.input.prototype.render = function(error) {
inquirer.prompt.prompts.input.prototype.render = function (error) {
let bottomContent = '';
let appendContent = '';
let message = this.getQuestion();
@@ -178,7 +189,7 @@ inquirer.prompt.prompts.input.prototype.render = function(error) {
};
// adjusted from https://github.com/SBoudrias/Inquirer.js/blob/942908f17319343d1acc7b876f990797c5695918/packages/inquirer/lib/prompts/confirm.js#L64
inquirer.prompt.prompts.confirm.prototype.render = function(answer) {
inquirer.prompt.prompts.confirm.prototype.render = function (answer) {
let message = this.getQuestion();
if (this.status === 'answered') {

View File

@@ -0,0 +1,3 @@
export default function ellipsis(str: string, length: number) {
return str.length > length ? `${str.slice(0, length - 1)}` : str;
}

View File

@@ -1,21 +0,0 @@
import chalk from 'chalk';
import { Output } from './output';
async function promptBool(output: Output, message: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
output.print(`${chalk.gray('>')} ${message} ${chalk.gray('[y/N] ')}`);
process.stdin
.on('data', d => {
process.stdin.pause();
resolve(
d
.toString()
.trim()
.toLowerCase() === 'y'
);
})
.resume();
});
}
export default promptBool;

View File

@@ -422,6 +422,19 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function createSecret() {
const name = `my-secret${Math.floor(Math.random() * 10000)}`;
const res = await apiFetch('/v2/now/secrets', {
method: 'POST',
body: JSON.stringify({ name, value: 'my secret' }),
});
t.is(res.status, 200);
return name;
}
async function nowEnvLsIsEmpty() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
@@ -436,26 +449,34 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.regex(stderr, /No Environment Variables found in Project/gm);
}
async function nowEnvAdd() {
async function nowEnvAddPlaintext() {
const now = execa(binaryPath, ['env', 'add', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(now, chunk =>
chunk.includes('Which type of Environment Variable do you want to add?')
);
now.stdin.write('\n'); // select plaintext
await waitForPrompt(now, chunk =>
chunk.includes('Whats the name of the variable?')
);
now.stdin.write('MY_ENV_VAR\n');
now.stdin.write('MY_PLAINTEXT_ENV_VAR\n');
await waitForPrompt(
now,
chunk =>
chunk.includes('Whats the value of') && chunk.includes('MY_ENV_VAR')
chunk.includes('Whats the value of') &&
chunk.includes('MY_PLAINTEXT_ENV_VAR')
);
now.stdin.write('MY_VALUE\n');
now.stdin.write('my plaintext value\n');
await waitForPrompt(
now,
chunk =>
chunk.includes('which Environments') && chunk.includes('MY_ENV_VAR')
chunk.includes('which Environments') &&
chunk.includes('MY_PLAINTEXT_ENV_VAR')
);
now.stdin.write('a\n'); // select all
@@ -464,10 +485,47 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function nowEnvAddSecret(secretName) {
const now = execa(binaryPath, ['env', 'add', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(now, chunk =>
chunk.includes('Which type of Environment Variable do you want to add?')
);
now.stdin.write('j\n'); // select secret
await waitForPrompt(now, chunk =>
chunk.includes('Whats the name of the variable?')
);
now.stdin.write('MY_SECRET_ENV_VAR\n');
await waitForPrompt(
now,
chunk =>
chunk.includes('Whats the value of') &&
chunk.includes('MY_SECRET_ENV_VAR')
);
now.stdin.write(`@${secretName}\n`);
await waitForPrompt(
now,
chunk =>
chunk.includes('which Environments') &&
chunk.includes('MY_SECRET_ENV_VAR')
);
now.stdin.write('j \n'); // select preview
const { exitCode, stderr, stdout } = await now;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function nowEnvAddFromStdin() {
const now = execa(
binaryPath,
['env', 'add', 'MY_STDIN_VAR', 'development', ...defaultArgs],
['env', 'add', 'plain', 'MY_STDIN_VAR', 'development', ...defaultArgs],
{
reject: false,
cwd: target,
@@ -481,13 +539,20 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
async function nowEnvAddSystemEnv() {
const now = execa(
binaryPath,
['env', 'add', 'VERCEL_URL', ...defaultArgs],
['env', 'add', 'system', 'VERCEL_URL', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
await waitForPrompt(
now,
chunk =>
chunk.includes('Whats the value of') && chunk.includes('VERCEL_URL')
);
now.stdin.write(`\n`); // select VERCEL_URL
await waitForPrompt(
now,
chunk =>
@@ -513,23 +578,28 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Environment Variables found in Project/gm);
console.log(stdout);
const lines = stdout.split('\n');
const myEnvVars = lines.filter(line => line.includes('MY_ENV_VAR'));
t.is(myEnvVars.length, 3);
t.regex(myEnvVars.join('\n'), /development/gm);
t.regex(myEnvVars.join('\n'), /preview/gm);
t.regex(myEnvVars.join('\n'), /production/gm);
const plaintextEnvs = lines.filter(line =>
line.includes('MY_PLAINTEXT_ENV_VAR')
);
t.is(plaintextEnvs.length, 1);
t.regex(plaintextEnvs[0], /Production, Preview, Development/gm);
const myStdinVars = lines.filter(line => line.includes('MY_STDIN_VAR'));
t.is(myStdinVars.length, 1);
t.regex(myStdinVars.join('\n'), /development/gm);
const secretEnvs = lines.filter(line => line.includes('MY_SECRET_ENV_VAR'));
t.is(secretEnvs.length, 1);
t.regex(secretEnvs[0], /Preview/gm);
const vercelVars = lines.filter(line => line.includes('VERCEL_URL'));
t.is(vercelVars.length, 3);
t.regex(vercelVars.join('\n'), /development/gm);
t.regex(vercelVars.join('\n'), /preview/gm);
t.regex(vercelVars.join('\n'), /production/gm);
const stdinEnvs = lines.filter(line => line.includes('MY_STDIN_VAR'));
t.is(stdinEnvs.length, 1);
t.regex(stdinEnvs[0], /Development/gm);
const systemEnvs = lines.filter(line => line.includes('VERCEL_URL'));
t.is(systemEnvs.length, 1);
t.regex(systemEnvs[0], /VERCEL_URL/gm);
t.regex(systemEnvs[0], /Production, Preview, Development/gm);
}
async function nowEnvPull() {
@@ -549,7 +619,7 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
t.true(contents.startsWith('# Created by Vercel CLI\n'));
const lines = new Set(contents.split('\n'));
t.true(lines.has('MY_ENV_VAR="MY_VALUE"'));
t.true(lines.has('MY_PLAINTEXT_ENV_VAR="my plaintext value"'));
t.true(lines.has('MY_STDIN_VAR="{"expect":"quotes"}"'));
t.true(lines.has('VERCEL_URL=""'));
}
@@ -603,7 +673,8 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
const apiRes = await fetch(apiUrl);
t.is(apiRes.status, 200, formatOutput({ stderr, stdout }));
const apiJson = await apiRes.json();
t.is(apiJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(apiJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
t.is(apiJson['MY_SECRET_ENV_VAR'], 'my secret');
t.is(apiJson['VERCEL_URL'], host);
const homeUrl = `https://${host}`;
@@ -611,7 +682,8 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
const homeRes = await fetch(homeUrl);
t.is(homeRes.status, 200, formatOutput({ stderr, stdout }));
const homeJson = await homeRes.json();
t.is(homeJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(homeJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
t.is(homeJson['MY_SECRET_ENV_VAR'], 'my secret');
t.is(homeJson['VERCEL_URL'], host);
}
@@ -639,14 +711,14 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
const apiJson = await apiRes.json();
t.is(apiJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(apiJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext 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['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
@@ -680,13 +752,15 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
const apiJson = await apiRes.json();
t.is(apiJson['VERCEL_URL'], localhostNoProtocol);
t.is(apiJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(apiJson['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
t.is(apiJson['MY_STDIN_VAR'], '{"expect":"quotes"}');
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['MY_PLAINTEXT_ENV_VAR'], 'my plaintext value');
t.is(homeJson['VERCEL_URL'], localhostNoProtocol);
t.is(homeJson['MY_STDIN_VAR'], '{"expect":"quotes"}');
vc.kill('SIGTERM', { forceKillAfterTimeout: 2000 });
@@ -702,12 +776,27 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
await waitForPrompt(now, chunk =>
chunk.includes('Whats the name of the variable?')
);
now.stdin.write('MY_ENV_VAR\n');
now.stdin.write('MY_PLAINTEXT_ENV_VAR\n');
// expect error if no environment is selected
await waitForPrompt(
now,
chunk =>
chunk.includes('which Environments') &&
chunk.includes('MY_PLAINTEXT_ENV_VAR')
);
now.stdin.write('\n'); // select none
await waitForPrompt(now, chunk =>
chunk.includes(
'Please select an Environment to remove the Environment Variable from.'
)
);
await waitForPrompt(
now,
chunk =>
chunk.includes('which Environments') && chunk.includes('MY_ENV_VAR')
chunk.includes('which Environments') &&
chunk.includes('MY_PLAINTEXT_ENV_VAR')
);
now.stdin.write('a\n'); // select all
@@ -719,7 +808,7 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
async function nowEnvRemoveWithArgs() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'rm', 'MY_STDIN_VAR', 'development', '-y', ...defaultArgs],
['env', 'rm', 'MY_SECRET_ENV_VAR', 'preview', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
@@ -727,6 +816,21 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const {
exitCode: exitCode2,
stderr: stderr2,
stdout: stdout2,
} = await execa(
binaryPath,
['env', 'rm', 'MY_STDIN_VAR', 'development', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode2, 0, formatOutput({ stderr2, stdout2 }));
}
async function nowEnvRemoveWithNameOnly() {
@@ -751,8 +855,10 @@ test('Deploy `api-env` fixture and test `vercel env` command', async t => {
}
await nowDeploy();
const secretName = await createSecret();
await nowEnvLsIsEmpty();
await nowEnvAdd();
await nowEnvAddPlaintext();
await nowEnvAddSecret(secretName);
await nowEnvAddFromStdin();
await nowEnvAddSystemEnv();
await nowEnvLsIncludesVar();

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/client",
"version": "9.0.4-canary.0",
"version": "9.0.4-canary.1",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://vercel.com",
@@ -37,7 +37,7 @@
]
},
"dependencies": {
"@vercel/build-utils": "2.5.5-canary.0",
"@vercel/build-utils": "2.5.5-canary.1",
"@zeit/fetch": "5.2.0",
"async-retry": "1.2.3",
"async-sema": "3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "2.6.34",
"version": "2.6.38",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",

View File

@@ -421,7 +421,7 @@ export const build = async ({
});
}
const appMountPrefixNoTrailingSlash = path.posix
let appMountPrefixNoTrailingSlash = path.posix
.join('/', entryDirectory)
.replace(/\/+$/, '');
@@ -470,6 +470,30 @@ export const build = async ({
headers.push(...convertHeaders(routesManifest.headers));
}
if (routesManifest.basePath && routesManifest.basePath !== '/') {
const nextBasePath = routesManifest.basePath;
if (!nextBasePath.startsWith('/')) {
throw new NowBuildError({
code: 'NEXT_BASEPATH_STARTING_SLASH',
message:
'basePath must start with `/`. Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
if (nextBasePath.endsWith('/')) {
throw new NowBuildError({
code: 'NEXT_BASEPATH_TRAILING_SLASH',
message:
'basePath must not end with `/`. Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
entryDirectory = path.join(entryDirectory, nextBasePath);
appMountPrefixNoTrailingSlash = path.posix
.join('/', entryDirectory)
.replace(/\/+$/, '');
}
if (routesManifest.dataRoutes) {
// Load the /_next/data routes for both dynamic SSG and SSP pages.
// These must be combined and sorted to prevent conflicts
@@ -511,6 +535,7 @@ export const build = async ({
const { i18n } = routesManifest;
if (i18n) {
const origSrc = route.src;
route.src = route.src.replace(
// we need to double escape the build ID here
// to replace it properly
@@ -522,6 +547,21 @@ export const build = async ({
.join('|')})/`
);
// optional-catchall routes don't have slash between
// build-id and the regex
if (route.src === origSrc) {
route.src = route.src.replace(
// we need to double escape the build ID here
// to replace it properly
`/${escapedBuildId}`,
`/${escapedBuildId}/(?${
ssgDataRoute ? '<nextLocale>' : ':'
}${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})[/]?`
);
}
// make sure to route to the correct prerender output
if (ssgDataRoute) {
route.dest = route.dest.replace(
@@ -538,26 +578,6 @@ export const build = async ({
hasPages404 = true;
}
if (routesManifest.basePath && routesManifest.basePath !== '/') {
const nextBasePath = routesManifest.basePath;
if (!nextBasePath.startsWith('/')) {
throw new NowBuildError({
code: 'NEXT_BASEPATH_STARTING_SLASH',
message:
'basePath must start with `/`. Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
if (nextBasePath.endsWith('/')) {
throw new NowBuildError({
code: 'NEXT_BASEPATH_TRAILING_SLASH',
message:
'basePath must not end with `/`. Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
});
}
entryDirectory = path.join(entryDirectory, nextBasePath);
}
break;
}
default: {
@@ -1395,17 +1415,23 @@ export const build = async ({
const isFallback = prerenderManifest.fallbackRoutes[pathname!];
const isBlocking =
prerenderManifest.blockingFallbackRoutes[pathname!];
const isAutoExport =
staticPages[
addLocaleOrDefault(pathname!, routesManifest).substr(1)
];
const isLocalePrefixed = isFallback || isBlocking || isAutoExport;
route.src = route.src.replace(
'^',
`^${dynamicPrefix ? `${dynamicPrefix}[/]?` : '[/]?'}(?${
isFallback || isBlocking ? '<nextLocale>' : ':'
isLocalePrefixed ? '<nextLocale>' : ':'
}${i18n.locales
.map(locale => escapeStringRegexp(locale))
.join('|')})?`
);
if (isFallback || isBlocking) {
if (isLocalePrefixed) {
// ensure destination has locale prefix to match prerender output
// path so that the prerender object is used
route.dest = route.dest!.replace(
@@ -1544,8 +1570,9 @@ export const build = async ({
if (!currentPage) {
console.error(
"Failed to find matching page for", {toRender, header: req.headers['x-nextjs-page'], url: req.url, pages: Object.keys(pages) }, "in lambda"
"Failed to find matching page for", {toRender, header: req.headers['x-nextjs-page'], url: req.url }, "in lambda"
)
console.error('pages in lambda', Object.keys(pages))
res.statusCode = 500
return res.end('internal server error')
}
@@ -2063,7 +2090,7 @@ export const build = async ({
},
// Handle redirecting to locale specific domains
...(i18n.domains
...(i18n.domains && i18n.localeDetection !== false
? [
{
// TODO: enable redirecting between domains, will require
@@ -2100,26 +2127,29 @@ export const build = async ({
: []),
// Handle redirecting to locale paths
{
// TODO: enable redirecting between paths, will require
// updating the src with the desired locales to redirect.
// if default locale is included in this src it won't be visitable
// by users who prefer another language since the cookie isn't set
// on redirect currently like in `next start`
src: '/',
locale: {
redirect: i18n.locales.reduce(
(prev: Record<string, string>, locale) => {
prev[locale] =
locale === i18n.defaultLocale ? `/` : `/${locale}`;
return prev;
...(i18n.localeDetection !== false
? [
{
// TODO: if default locale is included in this src it won't be
// visitable by users who prefer another language since a
// cookie isn't set signaling the default locale is preferred
// on redirect currently, investigate adding this
src: '/',
locale: {
redirect: i18n.locales.reduce(
(prev: Record<string, string>, locale) => {
prev[locale] =
locale === i18n.defaultLocale ? `/` : `/${locale}`;
return prev;
},
{}
),
cookie: 'NEXT_LOCALE',
},
continue: true,
},
{}
),
cookie: 'NEXT_LOCALE',
},
continue: true,
},
]
: []),
{
src: `^${path.join('/', entryDirectory)}$`,
@@ -2128,6 +2158,10 @@ export const build = async ({
},
// Auto-prefix non-locale path with default locale
// note for prerendered pages this will cause
// x-now-route-matches to contain the path minus the locale
// e.g. for /de/posts/[slug] x-now-route-matches would have
// 1=posts%2Fpost-1
{
src: `^${path.join(
'/',
@@ -2170,10 +2204,22 @@ export const build = async ({
{ handle: 'filesystem' },
// map pages to their lambda
...pageLambdaRoutes,
...pageLambdaRoutes.filter(route => {
// filter out any SSG pages as they are already present in output
if ('headers' in route) {
let page = route.headers?.['x-nextjs-page']!;
page = page === '/index' ? '/' : page;
// map /blog/[post] to correct lambda for iSSG
...dynamicPageLambdaRoutes,
if (
prerenderManifest.staticRoutes[page] ||
prerenderManifest.fallbackRoutes[page] ||
prerenderManifest.blockingFallbackRoutes[page]
) {
return false;
}
}
return true;
}),
// These need to come before handle: miss or else they are grouped
// with that routing section

View File

@@ -327,6 +327,7 @@ export type RoutesManifest = {
routeKeys?: { [named: string]: string };
}>;
i18n?: {
localeDetection?: boolean;
defaultLocale: string;
locales: string[];
domains?: Array<{

View File

@@ -0,0 +1,22 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
i18n: {
localeDetection: false,
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
defaultLocale: 'en-US',
// TODO: testing locale domains support, will require custom
// testing set-up as test accounts are used currently
domains: [
{
domain: 'example.be',
defaultLocale: 'nl-BE',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
],
},
};

View File

@@ -0,0 +1,517 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"probes": [
{
"path": "/",
"headers": {
"accept-language": "en;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "\"en-US\""
},
{
"path": "/",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "\"en-US\""
},
{
"path": "/",
"headers": {
"accept-language": "nl-NL;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "\"en-US\""
},
{
"path": "/",
"headers": {
"accept-language": "fr;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "\"en-US\""
},
{
"path": "/",
"headers": {
"accept-language": "en-US;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "index page"
},
{
"path": "/en-US",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "index page"
},
{
"path": "/",
"status": 200,
"mustContain": "index page"
},
{
"path": "/",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en",
"status": 200,
"mustContain": "index page"
},
{
"path": "/en",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/fr",
"status": 200,
"mustContain": "index page"
},
{
"path": "/fr",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/nl",
"status": 200,
"mustContain": "index page"
},
{
"path": "/nl",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": "index page"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": ">nl-NL<"
},
{
"path": "/non-existent",
"status": 404
},
{
"path": "/fr/non-existent",
"status": 404,
"mustContain": "lang=\"fr\""
},
{
"path": "/en/non-existent",
"status": 404,
"mustContain": "lang=\"en\""
},
{
"path": "/en-US/non-existent",
"status": 404,
"mustContain": "lang=\"en-US\""
},
{
"path": "/nl/non-existent",
"status": 404,
"mustContain": "lang=\"nl\""
},
{
"path": "/nl-NL/non-existent",
"status": 404,
"mustContain": "lang=\"nl-NL\""
},
{
"path": "/hello.txt",
"status": 200,
"mustContain": "hello world!"
},
{
"path": "/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/dynamic/hello",
"status": 200,
"mustContain": "\"en-US\""
},
{
"path": "/en/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/en/dynamic/hello",
"status": 200,
"mustContain": "\"en\""
},
{
"path": "/nl/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/nl/dynamic/hello",
"status": 200,
"mustContain": "\"nl\""
},
{
"path": "/fr/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/fr/dynamic/hello",
"status": 200,
"mustContain": "\"fr\""
},
{
"path": "/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/gsp",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/en/gsp",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/nl/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/nl/gsp",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/fr/gsp",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/fr/gsp",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/gssp",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/en/gssp",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/nl/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/nl/gssp",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/fr/gssp",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/fr/gssp",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/en/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": "gssp page"
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/fr/gssp/first",
"status": 200,
"mustContain": "slug\":\"first\""
},
// TODO: update when directory listing is disabled
// and these are proper 404s
{
"path": "/en/not-found",
"status": 200,
"mustContain": "Index of"
},
{
"path": "/nl/not-found",
"status": 200,
"mustContain": "Index of"
},
{
"path": "/en-US/not-found",
"status": 200,
"mustContain": "lang=\"en-US\""
},
{
"path": "/nl-NL/not-found",
"status": 200,
"mustContain": "lang=\"nl-NL\""
},
{
"path": "/fr/not-found",
"status": 200,
"mustContain": "lang=\"fr\""
},
// this will always be a 200 unless fallback: blocking is used
// since the static fallback page is served before the 404
// page is rendered
{
"path": "/en/not-found/fallback/first",
"status": 200,
"mustContain": "lang=\"en\""
},
{
"delay": 2000
},
{
"path": "/en/not-found/fallback/first",
"status": 200,
"mustNotContain": "gsp page"
},
{
"path": "/_next/data/testing-build-id/en/not-found/fallback/first.json",
"status": 404
},
{
"path": "/en/not-found/fallback/first",
"status": 200,
"mustContain": "lang=\"en\""
},
{
"path": "/en/not-found/fallback/first",
"status": 200,
"mustNotContain": "gsp page"
},
{
"path": "/fr/not-found/fallback/first",
"status": 200,
"mustContain": "lang=\"fr\""
},
{
"path": "/_next/data/testing-build-id/fr/not-found/fallback/first.json",
"status": 200
},
{
"path": "/fr/not-found/fallback/first",
"status": 200,
"mustContain": "lang=\"fr\""
},
{
"delay": 2000
},
{
"path": "/fr/not-found/fallback/first",
"status": 200,
"mustContain": "gsp page"
},
{
"path": "/_next/data/testing-build-id/en-US/index.json",
"status": 200,
"mustContain": "\"locale\":\"en-US\""
},
{
"path": "/_next/data/testing-build-id/en/index.json",
"status": 200,
"mustContain": "\"locale\":\"en\""
},
{
"path": "/_next/data/testing-build-id/fr/index.json",
"status": 200,
"mustContain": "\"locale\":\"fr\""
},
{
"path": "/_next/data/testing-build-id/nl/index.json",
"status": 200,
"mustContain": "\"locale\":\"nl\""
},
{
"path": "/_next/data/testing-build-id/en-US/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"en-US\""
},
{
"path": "/_next/data/testing-build-id/en/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"en\""
},
{
"path": "/_next/data/testing-build-id/fr/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"fr\""
},
{
"path": "/_next/data/testing-build-id/nl/gsp.json",
"status": 200,
"mustContain": "\"locale\":\"nl\""
},
{
"path": "/gsp/blocking/first",
"status": 200,
"mustContain": "catchall"
},
{
"path": "/gsp/blocking/first",
"status": 200,
"mustContain": "lang=\"en-US\""
},
{
"path": "/_next/data/testing-build-id/en-US/gsp/blocking/first.json",
"status": 200,
"mustContain": "\"catchall\":\"yes\""
},
{
"path": "/nl-NL/gsp/blocking/first",
"status": 200,
"mustContain": "catchall"
},
{
"path": "/nl-NL/gsp/blocking/first",
"status": 200,
"mustContain": "lang=\"nl-NL\""
},
{
"path": "/_next/data/testing-build-id/nl-NL/gsp/blocking/first.json",
"status": 200,
"mustContain": "\"catchall\":\"yes\""
},
{
"path": "/fr/gsp/blocking/first",
"status": 200,
"mustContain": "catchall"
},
{
"path": "/fr/gsp/blocking/first",
"status": 200,
"mustContain": "lang=\"fr\""
},
{
"path": "/_next/data/testing-build-id/fr/gsp/blocking/first.json",
"status": 200,
"mustContain": "\"catchall\":\"yes\""
}
]
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1,31 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="another">another page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,21 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="auto-export">auto-export page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
</>
);
}

View File

@@ -0,0 +1,12 @@
import { useRouter } from 'next/router';
export default function Dynamic(props) {
const router = useRouter();
return (
<>
<p>dynamic page</p>
<p id="query">{JSON.stringify(router.query)}</p>
</>
);
}

View File

@@ -0,0 +1,43 @@
import Link from 'next/link';
const Slug = props => {
return (
<div>
<p id="props">{JSON.stringify(props)}</p>
<Link href="/gsp/blocking/hallo-wereld" locale={'nl-NL'}>
<a>/nl-NL/gsp/blocking/hallo-wereld</a>
</Link>
<br />
<Link href="/gsp/blocking/42" locale={'nl-NL'}>
<a>/nl-NL/gsp/blocking/42</a>
</Link>
<br />
<Link href="/gsp/blocking/hallo-welt" locale={'fr'}>
<a>/fr/gsp/blocking/hallo-welt</a>
</Link>
<br />
<Link href="/gsp/blocking/42" locale={'fr'}>
<a>/fr/gsp/blocking/42</a>
</Link>
</div>
);
};
export const getStaticProps = () => {
return {
props: {
random: Math.random(),
catchall: 'yes',
},
revalidate: 1,
};
};
export const getStaticPaths = () => {
return {
paths: [],
fallback: 'blocking',
};
};
export default Slug;

View File

@@ -0,0 +1,51 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
if (router.isFallback) return 'Loading...';
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
random: Math.random(),
params,
locale,
locales,
},
revalidate: 1,
};
};
export const getStaticPaths = ({ locales }) => {
const paths = [];
for (const locale of locales) {
paths.push({ params: { slug: 'first' }, locale });
paths.push({ params: { slug: 'second' }, locale });
}
return {
// the default locale will be used since one isn't defined here
paths,
fallback: true,
};
};

View File

@@ -0,0 +1,32 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
// TODO: should non-dynamic GSP pages pre-render for each locale?
export const getStaticProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,49 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
if (router.isFallback) return 'Loading...';
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ params, locale, locales }) => {
return {
props: {
random: Math.random(),
params,
locale,
locales,
},
revalidate: 1,
};
};
export const getStaticPaths = () => {
return {
paths: [
{ params: { slug: 'first' } },
'/gsp/no-fallback/second',
{ params: { slug: 'first' }, locale: 'en-US' },
'/nl-NL/gsp/no-fallback/second',
'/fr/gsp/no-fallback/first',
],
fallback: false,
};
};

View File

@@ -0,0 +1,32 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gssp">gssp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ params, locale, locales }) => {
return {
props: {
params,
locale,
locales,
},
};
};

View File

@@ -0,0 +1,31 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gssp">gssp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getServerSideProps = ({ locale, locales }) => {
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1,57 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="index">index page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/another">
<a id="to-another">to /another</a>
</Link>
<br />
<Link href="/gsp">
<a id="to-gsp">to /gsp</a>
</Link>
<br />
<Link href="/gsp/fallback/first">
<a id="to-fallback-first">to /gsp/fallback/first</a>
</Link>
<br />
<Link href="/gsp/fallback/hello">
<a id="to-fallback-hello">to /gsp/fallback/hello</a>
</Link>
<br />
<Link href="/gsp/no-fallback/first">
<a id="to-no-fallback-first">to /gsp/no-fallback/first</a>
</Link>
<br />
<Link href="/gssp">
<a id="to-gssp">to /gssp</a>
</Link>
<br />
<Link href="/gssp/first">
<a id="to-gssp-slug">to /gssp/first</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ locale, locales }) => {
return {
props: {
random: Math.random(),
locale,
locales,
},
revalidate: 1,
};
};

View File

@@ -0,0 +1,54 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
const { nextLocale } = router.query;
return (
<>
<p id="links">links page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/another" locale={nextLocale}>
<a id="to-another">to /another</a>
</Link>
<br />
<Link href="/gsp" locale={nextLocale}>
<a id="to-gsp">to /gsp</a>
</Link>
<br />
<Link href="/gsp/fallback/first" locale={nextLocale}>
<a id="to-fallback-first">to /gsp/fallback/first</a>
</Link>
<br />
<Link href="/gsp/fallback/hello" locale={nextLocale}>
<a id="to-fallback-hello">to /gsp/fallback/hello</a>
</Link>
<br />
<Link href="/gsp/no-fallback/first" locale={nextLocale}>
<a id="to-no-fallback-first">to /gsp/no-fallback/first</a>
</Link>
<br />
<Link href="/gssp" locale={nextLocale}>
<a id="to-gssp">to /gssp</a>
</Link>
<br />
<Link href="/gssp/first" locale={nextLocale}>
<a id="to-gssp-slug">to /gssp/first</a>
</Link>
<br />
</>
);
}
// make SSR page so we have query values immediately
export const getServerSideProps = () => {
return {
props: {},
};
};

View File

@@ -0,0 +1,50 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
if (router.isFallback) return 'Loading...';
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ params, locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
notFound: true,
};
}
return {
props: {
params,
locale,
locales,
},
};
};
export const getStaticPaths = () => {
return {
// the default locale will be used since one isn't defined here
paths: ['first', 'second'].map(slug => ({
params: { slug },
})),
fallback: true,
};
};

View File

@@ -0,0 +1,37 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Page(props) {
const router = useRouter();
return (
<>
<p id="gsp">gsp page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/">
<a id="to-index">to /</a>
</Link>
<br />
</>
);
}
export const getStaticProps = ({ locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
notFound: true,
};
}
return {
props: {
locale,
locales,
},
};
};

View File

@@ -0,0 +1 @@
hello world!

View File

@@ -172,6 +172,47 @@
"mustContain": "hello world!"
},
{
"path": "/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/dynamic/hello",
"status": 200,
"mustContain": "\"en-US\""
},
{
"path": "/en/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/en/dynamic/hello",
"status": 200,
"mustContain": "\"en\""
},
{
"path": "/nl/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/nl/dynamic/hello",
"status": 200,
"mustContain": "\"nl\""
},
{
"path": "/fr/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/fr/dynamic/hello",
"status": 200,
"mustContain": "\"fr\""
},
{
"path": "/gsp",
"status": 200,

View File

@@ -0,0 +1,12 @@
import { useRouter } from 'next/router';
export default function Dynamic(props) {
const router = useRouter();
return (
<>
<p>dynamic page</p>
<p id="query">{JSON.stringify(router.query)}</p>
</>
);
}

View File

@@ -0,0 +1,182 @@
/* eslint-env jest */
const fetch = require('node-fetch');
const cheerio = require('cheerio');
module.exports = function (ctx) {
it('should revalidate content properly from /', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
const res = await fetch(`${ctx.deploymentUrl}/`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
});
it('should revalidate content properly from /fr', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
const res = await fetch(`${ctx.deploymentUrl}/fr`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/fr`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
});
it('should revalidate content properly from /nl-NL', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
const res = await fetch(`${ctx.deploymentUrl}/nl-NL`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
});
it('should revalidate content properly from /second', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/second`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/second`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
});
it('should revalidate content properly from /fr/second', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/fr/second`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/fr/second`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
});
it('should revalidate content properly from /nl-NL/second', async () => {
// we have to hit the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/nl-NL/second`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL/second`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
});
};

View File

@@ -0,0 +1,21 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
i18n: {
locales: ['nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en-US', 'en'],
defaultLocale: 'en-US',
// TODO: testing locale domains support, will require custom
// testing set-up as test accounts are used currently
domains: [
{
domain: 'example.be',
defaultLocale: 'nl-BE',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
],
},
};

View File

@@ -0,0 +1,192 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"probes": [
{
"path": "/hello.txt",
"status": 200,
"mustContain": "hello world!"
},
{
"path": "/",
"headers": {
"accept-language": "en;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//en/"
}
},
{
"path": "/",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//nl/"
}
},
{
"path": "/",
"headers": {
"accept-language": "nl-NL;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//nl-NL/"
}
},
{
"path": "/",
"headers": {
"accept-language": "fr;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 307,
"responseHeaders": {
"location": "//fr/"
}
},
{
"path": "/",
"headers": {
"accept-language": "en-US;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/en-US",
"headers": {
"accept-language": "nl;q=0.9"
},
"fetchOptions": {
"redirect": "manual"
},
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/en",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/fr",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/fr",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/nl",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl-NL",
"status": 200,
"mustContain": ">nl-NL<"
},
{
"path": "/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/first",
"status": 200,
"mustContain": ">en-US<"
},
{
"path": "/en/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/en/first",
"status": 200,
"mustContain": ">en<"
},
{
"path": "/fr/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/fr/first",
"status": 200,
"mustContain": ">fr<"
},
{
"path": "/nl/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl/first",
"status": 200,
"mustContain": ">nl<"
},
{
"path": "/nl-NL/first",
"status": 200,
"mustContain": "catchall page"
},
{
"path": "/nl-NL/first",
"status": 200,
"mustContain": ">nl-NL<"
}
]
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1,69 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
const Slug = props => {
const router = useRouter();
// invariant ensuring fallback is never accidentally flipped
if (router.isFallback) {
return 'Loading...';
}
return (
<div>
<p>catchall page</p>
<p id="props">{JSON.stringify(props)}</p>
<p id="router-locale">{router.locale}</p>
<p id="router-locales">{JSON.stringify(router.locales)}</p>
<p id="router-query">{JSON.stringify(router.query)}</p>
<p id="router-pathname">{router.pathname}</p>
<p id="router-as-path">{router.asPath}</p>
<Link href="/gsp/blocking/hallo-wereld" locale={'nl-NL'}>
<a>/nl-NL/gsp/blocking/hallo-wereld</a>
</Link>
<br />
<Link href="/gsp/blocking/42" locale={'nl-NL'}>
<a>/nl-NL/gsp/blocking/42</a>
</Link>
<br />
<Link href="/gsp/blocking/hallo-welt" locale={'fr'}>
<a>/fr/gsp/blocking/hallo-welt</a>
</Link>
<br />
<Link href="/gsp/blocking/42" locale={'fr'}>
<a>/fr/gsp/blocking/42</a>
</Link>
<br />
<Link href="/">
<a>/</a>
</Link>
</div>
);
};
export const getStaticProps = ({ params }) => {
return {
props: {
params,
random: Math.random(),
catchall: 'yes',
},
revalidate: 1,
};
};
export const getStaticPaths = ({ locales }) => {
const paths = [];
for (const locale of locales) {
paths.push({ params: { slug: ['first'] }, locale });
paths.push({ params: { slug: ['first'] }, locale });
}
return {
paths,
fallback: 'blocking',
};
};
export default Slug;

View File

@@ -0,0 +1 @@
hello world!

View File

@@ -4,6 +4,14 @@ const cheerio = require('cheerio');
module.exports = function (ctx) {
it('should revalidate content properly from /', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/`);
expect(res.status).toBe(200);
@@ -11,6 +19,7 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
expect(JSON.parse($('#router-query').text())).toEqual({});
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -22,9 +31,18 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
expect(JSON.parse($('#router-query').text())).toEqual({});
});
it('should revalidate content properly from /fr', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/fr`);
expect(res.status).toBe(200);
@@ -32,6 +50,7 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
expect(JSON.parse($('#router-query').text())).toEqual({});
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -43,9 +62,18 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
expect(JSON.parse($('#router-query').text())).toEqual({});
});
it('should revalidate content properly from /nl-NL', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/index.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/nl-NL`);
expect(res.status).toBe(200);
@@ -53,6 +81,7 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
expect(JSON.parse($('#router-query').text())).toEqual({});
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -64,9 +93,122 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
expect(JSON.parse($('#router-query').text())).toEqual({});
});
it('should revalidate content properly from /gsp/fallback/first', async () => {
// check the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/gsp/fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/gsp/fallback/first`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/gsp/fallback/first`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /fr/gsp/fallback/first', async () => {
// check the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/gsp/fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/first`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/fr/gsp/fallback/first`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /nl-NL/gsp/fallback/first', async () => {
// check the _next/data URL first
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/gsp/fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/fallback/first`);
expect(res.status).toBe(200);
const html = await res.text();
let $ = cheerio.load(html);
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/nl-NL/gsp/fallback/first`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
//
it('should revalidate content properly from /gsp/fallback/new-page', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/gsp/fallback/new-page.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
const initRes = await fetch(`${ctx.deploymentUrl}/gsp/fallback/new-page`);
expect(initRes.status).toBe(200);
@@ -80,6 +222,8 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
expect(props.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -91,6 +235,8 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
expect(props2.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
});
it('should revalidate content properly from /fr/gsp/fallback/new-page', async () => {
@@ -110,6 +256,8 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
expect(props.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -140,6 +288,8 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
expect(props.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -153,9 +303,19 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
expect(props2.params).toEqual({ slug: 'new-page' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'new-page' });
});
it('should revalidate content properly from /gsp/no-fallback/first', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/en-US/gsp/no-fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/gsp/no-fallback/first`);
expect(res.status).toBe(200);
@@ -163,6 +323,8 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('en-US');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -174,9 +336,19 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('en-US');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /fr/gsp/no-fallback/first', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/fr/gsp/no-fallback/first.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(`${ctx.deploymentUrl}/fr/gsp/no-fallback/first`);
expect(res.status).toBe(200);
@@ -184,6 +356,8 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('fr');
expect(props.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -195,9 +369,19 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('fr');
expect(props2.params).toEqual({ slug: 'first' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'first' });
});
it('should revalidate content properly from /nl-NL/gsp/no-fallback/second', async () => {
const dataRes = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/nl-NL/gsp/no-fallback/second.json`
);
expect(dataRes.status).toBe(200);
await dataRes.json();
await new Promise(resolve => setTimeout(resolve, 2000));
const res = await fetch(
`${ctx.deploymentUrl}/nl-NL/gsp/no-fallback/second`
);
@@ -207,6 +391,8 @@ module.exports = function (ctx) {
const props = JSON.parse($('#props').text());
const initialRandom = props.random;
expect($('#router-locale').text()).toBe('nl-NL');
expect(props.params).toEqual({ slug: 'second' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'second' });
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -220,5 +406,7 @@ module.exports = function (ctx) {
const props2 = JSON.parse($('#props').text());
expect(initialRandom).not.toBe(props2.random);
expect($('#router-locale').text()).toBe('nl-NL');
expect(props2.params).toEqual({ slug: 'second' });
expect(JSON.parse($('#router-query').text())).toEqual({ slug: 'second' });
});
};

View File

@@ -169,6 +169,47 @@
"mustContain": "hello world!"
},
{
"path": "/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/dynamic/hello",
"status": 200,
"mustContain": "\"en-US\""
},
{
"path": "/en/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/en/dynamic/hello",
"status": 200,
"mustContain": "\"en\""
},
{
"path": "/nl/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/nl/dynamic/hello",
"status": 200,
"mustContain": "\"nl\""
},
{
"path": "/fr/dynamic/hello",
"status": 200,
"mustContain": "dynamic page"
},
{
"path": "/fr/dynamic/hello",
"status": 200,
"mustContain": "\"fr\""
},
{
"path": "/gsp",
"status": 200,
@@ -348,6 +389,9 @@
"status": 200,
"mustContain": "lang=\"en\""
},
{
"delay": 2000
},
{
"path": "/en/not-found/fallback/first",
"status": 200,
@@ -381,6 +425,9 @@
"status": 200,
"mustContain": "lang=\"fr\""
},
{
"delay": 2000
},
{
"path": "/fr/not-found/fallback/first",
"status": 200,

View File

@@ -0,0 +1,12 @@
import { useRouter } from 'next/router';
export default function Dynamic(props) {
const router = useRouter();
return (
<>
<p>dynamic page</p>
<p id="query">{JSON.stringify(router.query)}</p>
</>
);
}

View File

@@ -35,12 +35,17 @@ export const getStaticProps = ({ params, locale, locales }) => {
};
};
export const getStaticPaths = () => {
export const getStaticPaths = ({ locales }) => {
const paths = [];
for (const locale of locales) {
paths.push({ params: { slug: 'first' }, locale });
paths.push({ params: { slug: 'second' }, locale });
}
return {
// the default locale will be used since one isn't defined here
paths: ['first', 'second'].map(slug => ({
params: { slug },
})),
paths,
fallback: true,
};
};

View File

@@ -55,6 +55,26 @@
{
"path": "/docs/blog/post-1/comments",
"mustContain": "comments post: post-1"
},
{
"path": "/docs/_next/data/testing-build-id/blog/post-1.json",
"status": 200,
"mustContain": "\"post\""
},
{
"path": "/docs/_next/data/testing-build-id/blog/post-2/comments.json",
"status": 200,
"mustContain": "\"post\""
},
{
"path": "/docs/_next/data/testing-build-id/blog-ssg/post-1.json",
"status": 200,
"mustContain": "\"post\""
},
{
"path": "/docs/_next/data/testing-build-id/blog-ssg/post-2/comments.json",
"status": 200,
"mustContain": "\"post\""
}
]
}

View File

@@ -0,0 +1,16 @@
export const getStaticProps = ({ params }) => ({
props: {
post: params.post,
},
});
export const getStaticPaths = () => {
return {
paths: [{ params: { post: 'post-1' } }, { params: { post: 'post-2' } }],
fallback: true,
};
};
export default function Comment({ post }) {
return `comments post: ${post}`;
}

View File

@@ -0,0 +1,16 @@
export const getStaticProps = ({ params }) => ({
props: {
post: params.post,
},
});
export const getStaticPaths = () => {
return {
paths: [{ params: { post: 'post-1' } }, { params: { post: 'post-2' } }],
fallback: true,
};
};
export default function Post({ post }) {
return `index post: ${post}`;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "0.17.9",
"version": "0.17.11",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step",

View File

@@ -33,6 +33,7 @@ const {
NowBuildError,
} = buildUtils;
import { Route, Source } from '@vercel/routing-utils';
import * as GatsbyUtils from './utils/gatsby';
const sleep = (n: number) => new Promise(resolve => setTimeout(resolve, n));
@@ -327,6 +328,22 @@ export async function build({
debug(
`Detected ${framework.name} framework. Optimizing your deployment...`
);
if (process.env.VERCEL_ANALYTICS_ID) {
const frameworkDirectory = path.join(
workPath,
path.dirname(entrypoint)
);
switch (framework.slug) {
case 'gatsby': {
await GatsbyUtils.injectVercelAnalyticsPlugin(frameworkDirectory);
break;
}
default: {
break;
}
}
}
}
const nodeVersion = await getNodeVersion(

View File

@@ -0,0 +1,40 @@
import { PackageJson } from '@vercel/build-utils';
import { constants, PathLike, promises as fs } from 'fs';
import * as path from 'path';
export type DeepWriteable<T> = {
-readonly [P in keyof T]: DeepWriteable<T[P]>;
};
export async function fileExists(path: PathLike): Promise<boolean> {
return fs.access(path, constants.F_OK).then(
() => true,
() => false
);
}
/**
* Read package.json from files
*/
export async function readPackageJson(entryPath: string): Promise<PackageJson> {
const packagePath = path.join(entryPath, 'package.json');
try {
return JSON.parse(await fs.readFile(packagePath, 'utf8'));
} catch (err) {
return {};
}
}
/**
* Write package.json
*/
export async function writePackageJson(
workPath: string,
packageJson: PackageJson
) {
await fs.writeFile(
path.join(workPath, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
}

View File

@@ -0,0 +1,86 @@
import { PackageJson } from '@vercel/build-utils';
import { promises as fs } from 'fs';
import * as path from 'path';
import {
fileExists,
readPackageJson,
DeepWriteable,
writePackageJson,
} from './_shared';
const defaultConfig = {
plugins: [
{
resolve: 'gatsby-plugin-vercel',
options: {},
},
],
};
export async function injectVercelAnalyticsPlugin(dir: string) {
// Gatsby requires a special variable name for environment variables to be
// exposed to the client-side JavaScript bundles:
process.env.GATSBY_VERCEL_ANALYTICS_ID = process.env.VERCEL_ANALYTICS_ID;
const gatsbyConfigName = 'gatsby-config.js';
const gatsbyPluginPackageName = 'gatsby-plugin-vercel';
const gatsbyConfigPath = path.join(dir, gatsbyConfigName);
const pkgJson: DeepWriteable<PackageJson> = (await readPackageJson(
dir
)) as DeepWriteable<PackageJson>;
if (!pkgJson.dependencies) {
pkgJson.dependencies = {};
}
if (!pkgJson.dependencies[gatsbyPluginPackageName]) {
pkgJson.dependencies[gatsbyPluginPackageName] = 'latest';
await writePackageJson(dir, pkgJson);
}
if (await fileExists(gatsbyConfigPath)) {
await fs.rename(
gatsbyConfigPath,
gatsbyConfigPath + '.__vercel_builder_backup__.js'
);
await fs.writeFile(
gatsbyConfigPath,
`const userConfig = require("./gatsby-config.js.__vercel_builder_backup__.js");
// https://github.com/gatsbyjs/gatsby/blob/354003fb2908e02ff12109ca3a02978a5a6e608c/packages/gatsby/src/bootstrap/prefer-default.ts
const preferDefault = m => (m && m.default) || m;
const vercelConfig = Object.assign(
{},
// https://github.com/gatsbyjs/gatsby/blob/a6ecfb2b01d761e8a3612b8ea132c698659923d9/packages/gatsby/src/services/initialize.ts#L113-L117
preferDefault(userConfig)
);
if (!vercelConfig.plugins) {
vercelConfig.plugins = [];
}
const hasPlugin = vercelConfig.plugins.find(
(p) =>
p && (p === "gatsby-plugin-vercel" || p.resolve === "gatsby-plugin-vercel")
);
if (!hasPlugin) {
vercelConfig.plugins = vercelConfig.plugins.slice();
vercelConfig.plugins.push({
resolve: "gatsby-plugin-vercel",
options: {},
});
}
module.exports = vercelConfig;
`
);
} else {
await fs.writeFile(
gatsbyConfigPath,
`module.exports = ${JSON.stringify(defaultConfig)}`
);
}
}

View File

@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

View File

@@ -0,0 +1,19 @@
{
"name": "gatsby-starter-default",
"private": true,
"description": "A simple starter to get up and developing quickly with Gatsby",
"version": "0.1.0",
"dependencies": {
"gatsby": "^2.24.91",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean"
}
}

View File

@@ -0,0 +1,5 @@
import React from 'react';
const NotFoundPage = () => <h1>404: Not Found</h1>;
export default NotFoundPage;

View File

@@ -0,0 +1,5 @@
import React from 'react';
const IndexPage = () => <h1>Hello World people</h1>;
export default IndexPage;

View File

@@ -0,0 +1,13 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"zeroConfig": true,
"framework": "gatsby"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

View File

@@ -0,0 +1,7 @@
module.exports = {
siteMetadata: {
title: `Gatsby Default Starter`,
description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
author: `@gatsbyjs`,
},
};

View File

@@ -0,0 +1,19 @@
{
"name": "gatsby-starter-default",
"private": true,
"description": "A simple starter to get up and developing quickly with Gatsby",
"version": "0.1.0",
"dependencies": {
"gatsby": "^2.24.91",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean"
}
}

View File

@@ -0,0 +1,5 @@
import React from 'react';
const NotFoundPage = () => <h1>404: Not Found</h1>;
export default NotFoundPage;

View File

@@ -0,0 +1,5 @@
import React from 'react';
const IndexPage = () => <h1>Hello World people</h1>;
export default IndexPage;

View File

@@ -0,0 +1,13 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"zeroConfig": true,
"framework": "gatsby"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

View File

@@ -0,0 +1,8 @@
module.exports = {
siteMetadata: {
title: `Gatsby Default Starter`,
description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
author: `@gatsbyjs`,
},
plugins: [`gatsby-plugin-react-helmet`],
};

View File

@@ -0,0 +1,21 @@
{
"name": "gatsby-starter-default",
"private": true,
"description": "A simple starter to get up and developing quickly with Gatsby",
"version": "0.1.0",
"dependencies": {
"gatsby": "^2.24.91",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"gatsby-plugin-react-helmet": "3.3.14",
"react-helmet": "^6.0.0"
},
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean"
}
}

View File

@@ -0,0 +1,5 @@
import React from 'react';
const NotFoundPage = () => <h1>404: Not Found</h1>;
export default NotFoundPage;

View File

@@ -0,0 +1,5 @@
import React from 'react';
const IndexPage = () => <h1>Hello World people</h1>;
export default IndexPage;

View File

@@ -0,0 +1,13 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"zeroConfig": true,
"framework": "gatsby"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

View File

@@ -0,0 +1,8 @@
module.exports = {
siteMetadata: {
title: `Gatsby Default Starter`,
description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
author: `@gatsbyjs`,
},
plugins: [`gatsby-plugin-vercel`],
};

View File

@@ -0,0 +1,19 @@
{
"name": "gatsby-starter-default",
"private": true,
"description": "A simple starter to get up and developing quickly with Gatsby",
"version": "0.1.0",
"dependencies": {
"gatsby": "^2.24.91",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean"
}
}

View File

@@ -0,0 +1,5 @@
import React from 'react';
const NotFoundPage = () => <h1>404: Not Found</h1>;
export default NotFoundPage;

View File

@@ -0,0 +1,5 @@
import React from 'react';
const IndexPage = () => <h1>Hello World people</h1>;
export default IndexPage;

View File

@@ -0,0 +1,13 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"zeroConfig": true,
"framework": "gatsby"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

View File

@@ -0,0 +1,8 @@
module.exports = {
siteMetadata: {
title: `Gatsby Default Starter`,
description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
author: `@gatsbyjs`,
},
plugins: [{ resolve: `gatsby-plugin-vercel`, options: {} }],
};

View File

@@ -0,0 +1,19 @@
{
"name": "gatsby-starter-default",
"private": true,
"description": "A simple starter to get up and developing quickly with Gatsby",
"version": "0.1.0",
"dependencies": {
"gatsby": "^2.24.91",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean"
}
}

View File

@@ -0,0 +1,5 @@
import React from 'react';
const NotFoundPage = () => <h1>404: Not Found</h1>;
export default NotFoundPage;

View File

@@ -0,0 +1,5 @@
import React from 'react';
const IndexPage = () => <h1>Hello World people</h1>;
export default IndexPage;

View File

@@ -0,0 +1,13 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"zeroConfig": true,
"framework": "gatsby"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

Some files were not shown because too many files have changed in this diff Show More