mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-31 03:39:11 +00:00
Compare commits
55 Commits
@now/frame
...
@now/next@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e09a418423 | ||
|
|
52d4464368 | ||
|
|
4dc635e5f2 | ||
|
|
510fb7ee7e | ||
|
|
243451e94b | ||
|
|
11bbda977d | ||
|
|
62c050f394 | ||
|
|
bd5a013312 | ||
|
|
1823cf452e | ||
|
|
c426d72ccf | ||
|
|
ddf59c052d | ||
|
|
1dcf6e7fb1 | ||
|
|
d4f4792988 | ||
|
|
7e1f2bd10e | ||
|
|
a80a1d0c1d | ||
|
|
8ff747b4d7 | ||
|
|
aa63b5a581 | ||
|
|
2094ec3c99 | ||
|
|
bf30d10211 | ||
|
|
ccc03c9c6e | ||
|
|
4b7367e2dc | ||
|
|
00aa56a095 | ||
|
|
56ae93a2a5 | ||
|
|
adb32a09d3 | ||
|
|
3358d8e44c | ||
|
|
c3bd2698e8 | ||
|
|
a7baa4761d | ||
|
|
5dd2daa970 | ||
|
|
dd36a489ed | ||
|
|
2e742209e3 | ||
|
|
8d13464cba | ||
|
|
20fdcfa0af | ||
|
|
fac004f83c | ||
|
|
5fee4bbad1 | ||
|
|
18e4b18839 | ||
|
|
b8627fd384 | ||
|
|
4e2db6f8a5 | ||
|
|
b685a3afdd | ||
|
|
aea3f58970 | ||
|
|
b0bb90dc11 | ||
|
|
2132c2463f | ||
|
|
60bf6e2420 | ||
|
|
9badc9048c | ||
|
|
a74c1921f8 | ||
|
|
e6044f2e8d | ||
|
|
78d1afa25e | ||
|
|
4792adf524 | ||
|
|
4d8a99141b | ||
|
|
7954ddc1d0 | ||
|
|
216a1fd9d2 | ||
|
|
e5add6750c | ||
|
|
b7533650e1 | ||
|
|
6272f5ce5c | ||
|
|
bde0538efe | ||
|
|
5e6775fca0 |
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -2,6 +2,7 @@
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
* @tootallnate @leo
|
||||
/packages/frameworks @AndyBitz
|
||||
/packages/now-cli/src/commands/dev/ @tootallnate @leo @styfle @AndyBitz
|
||||
/packages/now-cli/src/util/dev/ @tootallnate @leo @styfle @AndyBitz
|
||||
/packages/now-cli/src/commands/domains/ @javivelasco @mglagola @anatrajkovska
|
||||
@@ -10,13 +11,16 @@
|
||||
/packages/now-build-utils @styfle @AndyBitz
|
||||
/packages/now-node @styfle @tootallnate @lucleray
|
||||
/packages/now-node-bridge @styfle @tootallnate @lucleray
|
||||
/packages/now-next @Timer
|
||||
/packages/now-next @Timer @ijjk
|
||||
/packages/now-go @styfle @sophearak
|
||||
/packages/now-python @styfle @sophearak
|
||||
/packages/now-ruby @styfle @coetry @nathancahill
|
||||
/packages/now-static-build @styfle @AndyBitz
|
||||
/packages/now-routing-utils @dav-is
|
||||
/packages/now-routing-utils @styfle @dav-is @ijjk
|
||||
|
||||
/examples @msweeneydev @timothyis
|
||||
/examples/create-react-app @Timer
|
||||
/examples/nextjs @timneutkens
|
||||
/examples/hugo @msweeneydev @timothyis @styfle
|
||||
/examples/jekyll @msweeneydev @timothyis @sarupbanskota
|
||||
/examples/zola @msweeneydev @timothyis @styfle
|
||||
|
||||
@@ -26,17 +26,17 @@ You can deploy your new Jekyll project with a single command from your terminal
|
||||
$ now
|
||||
```
|
||||
|
||||
### Example Changes
|
||||
### Build Command
|
||||
|
||||
This example adds a `package.json` file with the following:
|
||||
The default build command is `jekyll build`.
|
||||
|
||||
If you wish to change the build command, add a `package.json` file with the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "jekyll build && mv _site public"
|
||||
"build": "jekyll build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This instructs ZEIT Now to build the Jekyll website and move the output to the public directory.
|
||||
|
||||
1
examples/nextjs/.gitignore
vendored
1
examples/nextjs/.gitignore
vendored
@@ -13,7 +13,6 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -580,7 +580,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"buildCommand": {
|
||||
"value": "hugo"
|
||||
"value": "hugo -D --gc"
|
||||
},
|
||||
"devCommand": {
|
||||
"value": "hugo server -D -w -p $PORT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/frameworks",
|
||||
"version": "0.0.6-canary.3",
|
||||
"version": "0.0.7",
|
||||
"main": "frameworks.json",
|
||||
"license": "UNLICENSED"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/build-utils",
|
||||
"version": "1.3.6-canary.0",
|
||||
"version": "1.3.7-canary.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parse as parsePath } from 'path';
|
||||
import { parse as parsePath, extname } from 'path';
|
||||
import { Route, Source } from '@now/routing-utils';
|
||||
import { Builder } from './types';
|
||||
import { getIgnoreApiFilter, sortFiles } from './detect-builders';
|
||||
@@ -331,8 +331,22 @@ export function detectOutputDirectory(builders: Builder[]): string | null {
|
||||
export function detectApiDirectory(builders: Builder[]): string | null {
|
||||
// TODO: We eventually want to save the api directory to
|
||||
// builder.config.apiDirectory so it is only detected once
|
||||
const isZeroConfig = builders.some(b => b.config && b.config.zeroConfig);
|
||||
return isZeroConfig ? 'api' : null;
|
||||
const found = builders.some(
|
||||
b => b.config && b.config.zeroConfig && b.src.startsWith('api/')
|
||||
);
|
||||
return found ? 'api' : null;
|
||||
}
|
||||
|
||||
export function detectApiExtensions(builders: Builder[]): Set<string> {
|
||||
return new Set<string>(
|
||||
builders
|
||||
.filter(
|
||||
b =>
|
||||
b.config && b.config.zeroConfig && b.src && b.src.startsWith('api/')
|
||||
)
|
||||
.map(b => extname(b.src))
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
export async function detectRoutes(
|
||||
@@ -361,12 +375,7 @@ export async function detectRoutes(
|
||||
);
|
||||
if (featHandleMiss) {
|
||||
defaultRoutes.push({ handle: 'miss' });
|
||||
const extSet = new Set(
|
||||
builders
|
||||
.filter(b => b.src && b.src.startsWith('api/'))
|
||||
.map(b => parsePath(b.src).ext)
|
||||
.filter(Boolean)
|
||||
);
|
||||
const extSet = detectApiExtensions(builders);
|
||||
if (extSet.size > 0) {
|
||||
const exts = Array.from(extSet)
|
||||
.map(ext => ext.slice(1))
|
||||
|
||||
@@ -66,6 +66,7 @@ export {
|
||||
detectRoutes,
|
||||
detectOutputDirectory,
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
} from './detect-routes';
|
||||
export { detectBuilders } from './detect-builders';
|
||||
export { detectFramework } from './detect-framework';
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Source, Route } from '@now/routing-utils';
|
||||
import { detectBuilders, detectRoutes } from '../src';
|
||||
import { detectOutputDirectory, detectApiDirectory } from '../';
|
||||
import {
|
||||
detectOutputDirectory,
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
} from '../';
|
||||
|
||||
describe('Test `detectBuilders`', () => {
|
||||
it('package.json + no build', async () => {
|
||||
@@ -1877,6 +1881,93 @@ describe('Test `detectApiDirectory`', () => {
|
||||
const result = detectApiDirectory(builders);
|
||||
expect(result).toBe('api');
|
||||
});
|
||||
|
||||
it('should be `null` with zero config but without api directory', async () => {
|
||||
const builders = [
|
||||
{
|
||||
use: '@now/next',
|
||||
src: 'package.json',
|
||||
config: { zeroConfig: true },
|
||||
},
|
||||
];
|
||||
const result = detectApiDirectory(builders);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test `detectApiExtensions`', () => {
|
||||
it('should have correct extensions', async () => {
|
||||
const builders = [
|
||||
{
|
||||
use: '@now/node',
|
||||
src: 'api/**/*.js',
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
use: '@now/python',
|
||||
src: 'api/**/*.py',
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
use: '@now/go',
|
||||
src: 'api/**/*.go',
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
use: '@now/ruby',
|
||||
src: 'api/**/*.rb',
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
use: 'now-bash',
|
||||
src: 'api/**/*.sh',
|
||||
// No zero config so it should not be added
|
||||
},
|
||||
{
|
||||
use: 'now-no-extension',
|
||||
src: 'api/executable',
|
||||
// No extension should not be added
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
use: '@now/next',
|
||||
src: 'package.json',
|
||||
// No api directory should not be added
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
use: 'now-rust@1.0.1',
|
||||
src: 'api/user.rs',
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
functions: {
|
||||
'api/**/*.rs': {
|
||||
runtime: 'now-rust@1.0.1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const result = detectApiExtensions(builders);
|
||||
expect(result.size).toBe(5);
|
||||
expect(result.has('.js')).toBe(true);
|
||||
expect(result.has('.py')).toBe(true);
|
||||
expect(result.has('.go')).toBe(true);
|
||||
expect(result.has('.rb')).toBe(true);
|
||||
expect(result.has('.rs')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
3
packages/now-build-utils/test/unit.test.js
vendored
3
packages/now-build-utils/test/unit.test.js
vendored
@@ -116,10 +116,7 @@ it('should throw for discontinued versions', async () => {
|
||||
const realDateNow = Date.now.bind(global.Date);
|
||||
global.Date.now = () => new Date('2020-02-14').getTime();
|
||||
|
||||
expect(getSupportedNodeVersion('', false)).rejects.toThrow();
|
||||
expect(getSupportedNodeVersion('8.10.x', false)).rejects.toThrow();
|
||||
|
||||
expect(getSupportedNodeVersion('', true)).rejects.toThrow();
|
||||
expect(getSupportedNodeVersion('8.10.x', true)).rejects.toThrow();
|
||||
|
||||
expect(getDiscontinuedNodeVersions().length).toBe(1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "now",
|
||||
"version": "17.0.0-canary.11",
|
||||
"version": "17.0.0-canary.19",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Now",
|
||||
|
||||
@@ -50,21 +50,23 @@ async function createBuildersTarball() {
|
||||
async function main() {
|
||||
const isDev = process.argv[2] === '--dev';
|
||||
|
||||
// Create a tarball from all the `@now` scoped builders which will be bundled
|
||||
// with Now CLI
|
||||
await createBuildersTarball();
|
||||
if (!isDev) {
|
||||
// Create a tarball from all the `@now` scoped builders which will be bundled
|
||||
// with Now CLI
|
||||
await createBuildersTarball();
|
||||
|
||||
// `now dev` uses chokidar to watch the filesystem, but opts-out of the
|
||||
// `fsevents` feature using `useFsEvents: false`, so delete the module here so
|
||||
// that it is not compiled by ncc, which makes the npm package size larger
|
||||
// than necessary.
|
||||
await remove(join(dirRoot, '../../node_modules/fsevents'));
|
||||
// `now dev` uses chokidar to watch the filesystem, but opts-out of the
|
||||
// `fsevents` feature using `useFsEvents: false`, so delete the module here so
|
||||
// that it is not compiled by ncc, which makes the npm package size larger
|
||||
// than necessary.
|
||||
await remove(join(dirRoot, '../../node_modules/fsevents'));
|
||||
|
||||
// Compile the `doT.js` template files for `now dev`
|
||||
console.log();
|
||||
await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
// Compile the `doT.js` template files for `now dev`
|
||||
console.log();
|
||||
await execa(process.execPath, [join(__dirname, 'compile-templates.js')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
// Do the initial `ncc` build
|
||||
console.log();
|
||||
|
||||
@@ -66,6 +66,7 @@ export const latestHelp = () => `
|
||||
-S, --scope Set a custom scope
|
||||
--regions Set default regions to enable the deployment on
|
||||
--prod Create a production deployment
|
||||
-c, --confirm Confirm default options and skip questions
|
||||
|
||||
${note(
|
||||
`To view the usage information for Now 1.0, run ${code(
|
||||
|
||||
@@ -155,7 +155,7 @@ const printDeploymentStatus = async (
|
||||
for (let indication of indications) {
|
||||
output.print(
|
||||
prependEmoji(
|
||||
`${chalk.grey(indication.payload)}`,
|
||||
`${chalk.dim(indication.payload)}`,
|
||||
emoji(indication.type)
|
||||
) + `\n`
|
||||
);
|
||||
@@ -232,6 +232,19 @@ export default async function main(
|
||||
return path;
|
||||
}
|
||||
|
||||
// check env variables options
|
||||
const { NOW_ORG_ID, NOW_PROJECT_ID } = process.env;
|
||||
if ((NOW_ORG_ID && !NOW_PROJECT_ID) || (!NOW_ORG_ID && NOW_PROJECT_ID)) {
|
||||
output.print(
|
||||
`${chalk.red('Error!')} You specified ${
|
||||
NOW_ORG_ID ? '`NOW_ORG_ID`' : '`NOW_PROJECT_ID`'
|
||||
} but you forgot to specify ${
|
||||
NOW_ORG_ID ? '`NOW_PROJECT_ID`' : '`NOW_ORG_ID`'
|
||||
}. You need to specify both to deploy to a custom project.\n`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// build `meta`
|
||||
const meta = Object.assign(
|
||||
{},
|
||||
@@ -347,10 +360,21 @@ export default async function main(
|
||||
});
|
||||
|
||||
// retrieve `project` and `org` from .now
|
||||
let [org, project] = await getLinkedProject(client, path);
|
||||
let [org, project] = await getLinkedProject(output, client, path);
|
||||
let newProjectName = null;
|
||||
|
||||
if (!org || !project) {
|
||||
const { NOW_PROJECT_ID, NOW_ORG_ID } = process.env;
|
||||
if (NOW_PROJECT_ID && NOW_ORG_ID) {
|
||||
output.print(
|
||||
`${chalk.red('Error!')} Project not found (${JSON.stringify({
|
||||
NOW_PROJECT_ID,
|
||||
NOW_ORG_ID,
|
||||
})})\n`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const shouldStartSetup =
|
||||
autoConfirm ||
|
||||
(await confirm(
|
||||
|
||||
@@ -896,10 +896,16 @@ async function sync({
|
||||
if (isTTY) {
|
||||
let inClipboard = '';
|
||||
const platformVersion = deployment.version || 1;
|
||||
const displayUrl =
|
||||
Array.isArray(deployment.alias) &&
|
||||
deployment.alias.length > 0 &&
|
||||
!deployment.aliasError
|
||||
? `https://${deployment.alias[0]}`
|
||||
: url;
|
||||
|
||||
if (clipboard) {
|
||||
try {
|
||||
await copy(url);
|
||||
await copy(displayUrl);
|
||||
inClipboard = chalk.gray(' [in clipboard]');
|
||||
} catch (err) {
|
||||
debug(`Error copying to clipboard: ${err}`);
|
||||
@@ -907,7 +913,7 @@ async function sync({
|
||||
}
|
||||
|
||||
log(
|
||||
chalk`{bold.cyan ${url}} {gray [v${platformVersion}]}${inClipboard}${dcs} ${deployStamp()}`
|
||||
chalk`{bold.cyan ${displayUrl}} {gray [v${platformVersion}]}${inClipboard}${dcs} ${deployStamp()}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -229,3 +229,10 @@ export interface Org {
|
||||
id: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface ProjectLink {
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
orgSlug?: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
@@ -21,15 +21,24 @@ function printInspectUrl(
|
||||
output: Output,
|
||||
deploymentUrl: string,
|
||||
deployStamp: () => string,
|
||||
orgName: string
|
||||
orgSlug: string
|
||||
) {
|
||||
const urlParts = deploymentUrl
|
||||
.replace(/\..*/, '')
|
||||
.replace('https://', '')
|
||||
.split('-');
|
||||
const deploymentShortId = urlParts.pop();
|
||||
const projectName = urlParts.join('-');
|
||||
const inspectUrl = `https://zeit.co/${orgName}/${projectName}/${deploymentShortId}`;
|
||||
const url = deploymentUrl.replace('https://', '');
|
||||
|
||||
// example urls:
|
||||
// lucim-fyulaijvg.now.sh
|
||||
// s-66p6vb23x.n8.io (custom domain suffix)
|
||||
const [sub, ...p] = url.split('.');
|
||||
const apex = p.join('.');
|
||||
|
||||
const q = sub.split('-');
|
||||
const deploymentShortId = q.pop();
|
||||
const projectName = q.join('-');
|
||||
|
||||
const inspectUrl = `https://zeit.co/${orgSlug}/${projectName}/${deploymentShortId}${
|
||||
apex !== 'now.sh' ? `/${apex}` : ''
|
||||
}`;
|
||||
|
||||
output.print(
|
||||
prependEmoji(
|
||||
`Inspect: ${chalk.bold(inspectUrl)} ${deployStamp()}`,
|
||||
@@ -121,7 +130,7 @@ export default async function processDeployment({
|
||||
indications.push(event);
|
||||
}
|
||||
|
||||
if (event.type === 'file_count') {
|
||||
if (event.type === 'file-count') {
|
||||
debug(
|
||||
`Total files ${event.payload.total.size}, ${event.payload.missing.length} changed`
|
||||
);
|
||||
@@ -181,10 +190,7 @@ export default async function processDeployment({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'build-state-changed' &&
|
||||
event.payload.readyState === 'BUILDING'
|
||||
) {
|
||||
if (event.type === 'building') {
|
||||
if (queuedSpinner) {
|
||||
queuedSpinner();
|
||||
}
|
||||
@@ -194,7 +200,7 @@ export default async function processDeployment({
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'all-builds-completed') {
|
||||
if (event.type === 'ready') {
|
||||
if (queuedSpinner) {
|
||||
queuedSpinner();
|
||||
}
|
||||
@@ -202,7 +208,7 @@ export default async function processDeployment({
|
||||
buildSpinner();
|
||||
}
|
||||
|
||||
deploySpinner = wait('Finalizing', 0);
|
||||
deploySpinner = wait('Completing', 0);
|
||||
}
|
||||
|
||||
// Handle error events
|
||||
|
||||
@@ -71,7 +71,7 @@ export default async function processLegacyDeployment({
|
||||
hashes = event.payload;
|
||||
}
|
||||
|
||||
if (event.type === 'file_count') {
|
||||
if (event.type === 'file-count') {
|
||||
debug(
|
||||
`Total files ${event.payload.total.size}, ${event.payload.missing.length} changed`
|
||||
);
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
import ms from 'ms';
|
||||
import bytes from 'bytes';
|
||||
import { promisify } from 'util';
|
||||
import { delimiter, dirname, join } from 'path';
|
||||
import { delimiter, dirname, extname, join } from 'path';
|
||||
import { fork, ChildProcess } from 'child_process';
|
||||
import { createFunction } from '@zeit/fun';
|
||||
import { Builder, File, Lambda, FileBlob, FileFsRef } from '@now/build-utils';
|
||||
import {
|
||||
Builder,
|
||||
File,
|
||||
Lambda,
|
||||
FileBlob,
|
||||
FileFsRef,
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
} from '@now/build-utils';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import chalk from 'chalk';
|
||||
import which from 'which';
|
||||
@@ -279,19 +287,20 @@ export async function executeBuild(
|
||||
|
||||
const { output } = result;
|
||||
|
||||
// Mimic fmeta-util and convert cleanUrls
|
||||
if (nowConfig.cleanUrls) {
|
||||
Object.entries(output)
|
||||
.filter(([name, value]) => name.endsWith('.html'))
|
||||
.forEach(([name, value]) => {
|
||||
const cleanName = name.slice(0, -5);
|
||||
delete output[name];
|
||||
output[cleanName] = value;
|
||||
if (value.type === 'FileBlob' || value.type === 'FileFsRef') {
|
||||
value.contentType = value.contentType || 'text/html; charset=utf-8';
|
||||
}
|
||||
});
|
||||
}
|
||||
const { cleanUrls } = nowConfig;
|
||||
// Mimic fmeta-util and perform file renaming
|
||||
Object.entries(output).forEach(([path, value]) => {
|
||||
if (cleanUrls && path.endsWith('.html')) {
|
||||
path = path.slice(0, -5);
|
||||
|
||||
if (value.type === 'FileBlob' || value.type === 'FileFsRef') {
|
||||
value.contentType = value.contentType || 'text/html; charset=utf-8';
|
||||
}
|
||||
}
|
||||
|
||||
delete output[path];
|
||||
output[path] = value;
|
||||
});
|
||||
|
||||
// Convert the JSON-ified output map back into their corresponding `File`
|
||||
// subclass type instances.
|
||||
@@ -410,6 +419,9 @@ export async function getBuildMatches(
|
||||
|
||||
const noMatches: Builder[] = [];
|
||||
const builds = nowConfig.builds || [{ src: '**', use: '@now/static' }];
|
||||
const apiDir = detectApiDirectory(builds || []);
|
||||
const apiExtensions = detectApiExtensions(builds || []);
|
||||
const apiMatch = apiDir + '/';
|
||||
|
||||
for (const buildConfig of builds) {
|
||||
let { src, use } = buildConfig;
|
||||
@@ -428,6 +440,11 @@ export async function getBuildMatches(
|
||||
// We need to escape brackets since `glob` will
|
||||
// try to find a group otherwise
|
||||
src = src.replace(/(\[|\])/g, '[$1]');
|
||||
const ext = extname(src);
|
||||
if (apiDir && src.startsWith(apiMatch) && apiExtensions.has(ext)) {
|
||||
// lambda function files are trimmed of their file extension
|
||||
src = src.slice(0, -ext.length);
|
||||
}
|
||||
|
||||
const files = fileList
|
||||
.filter(name => name === src || minimatch(name, src))
|
||||
|
||||
@@ -4,13 +4,8 @@ import PCRE from 'pcre-to-regexp';
|
||||
import isURL from './is-url';
|
||||
import DevServer from './server';
|
||||
|
||||
import {
|
||||
HttpHeadersConfig,
|
||||
RouteConfig,
|
||||
RouteResult,
|
||||
NowConfig,
|
||||
} from './types';
|
||||
import { isHandler } from '@now/routing-utils';
|
||||
import { HttpHeadersConfig, RouteConfig, RouteResult } from './types';
|
||||
import { isHandler, Route, HandleValue } from '@now/routing-utils';
|
||||
|
||||
export function resolveRouteParameters(
|
||||
str: string,
|
||||
@@ -31,27 +26,47 @@ export function resolveRouteParameters(
|
||||
});
|
||||
}
|
||||
|
||||
export default async function(
|
||||
export function getRoutesTypes(routes: Route[] = []) {
|
||||
const handleMap = new Map<HandleValue | null, Route[]>();
|
||||
let prevHandle: HandleValue | null = null;
|
||||
routes.forEach(route => {
|
||||
if (isHandler(route)) {
|
||||
prevHandle = route.handle;
|
||||
} else {
|
||||
const routes = handleMap.get(prevHandle);
|
||||
if (!routes) {
|
||||
handleMap.set(prevHandle, [route]);
|
||||
} else {
|
||||
routes.push(route);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return handleMap;
|
||||
}
|
||||
|
||||
export async function devRouter(
|
||||
reqUrl: string = '/',
|
||||
reqMethod?: string,
|
||||
routes?: RouteConfig[],
|
||||
devServer?: DevServer
|
||||
devServer?: DevServer,
|
||||
previousHeaders?: HttpHeadersConfig,
|
||||
missRoutes?: RouteConfig[],
|
||||
phase?: HandleValue | null
|
||||
): Promise<RouteResult> {
|
||||
let found: RouteResult | undefined;
|
||||
let { query, pathname: reqPathname = '/' } = url.parse(reqUrl, true);
|
||||
const combinedHeaders: HttpHeadersConfig = {};
|
||||
const combinedHeaders: HttpHeadersConfig = { ...previousHeaders };
|
||||
let status: number | undefined;
|
||||
|
||||
// Try route match
|
||||
if (routes) {
|
||||
let idx = -1;
|
||||
for (const routeConfig of routes) {
|
||||
idx++;
|
||||
|
||||
if (isHandler(routeConfig)) {
|
||||
if (routeConfig.handle === 'filesystem' && devServer) {
|
||||
if (await devServer.hasFilesystem(reqPathname)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We don't expect any Handle, only Source routes
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -74,41 +89,74 @@ export default async function(
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
// Create a clone of the `headers` object to not mutate the original one
|
||||
headers = { ...headers };
|
||||
for (const key of Object.keys(headers)) {
|
||||
headers[key] = resolveRouteParameters(headers[key], match, keys);
|
||||
for (const originalKey of Object.keys(headers)) {
|
||||
const lowerKey = originalKey.toLowerCase();
|
||||
if (
|
||||
previousHeaders &&
|
||||
Object.prototype.hasOwnProperty.call(previousHeaders, lowerKey) &&
|
||||
(phase === 'hit' || phase === 'miss')
|
||||
) {
|
||||
// don't override headers in the hit or miss phase
|
||||
} else {
|
||||
const originalValue = headers[originalKey];
|
||||
const value = resolveRouteParameters(originalValue, match, keys);
|
||||
combinedHeaders[lowerKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(combinedHeaders, headers);
|
||||
}
|
||||
|
||||
if (routeConfig.continue) {
|
||||
if (routeConfig.status) {
|
||||
status = routeConfig.status;
|
||||
}
|
||||
reqPathname = destPath;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (routeConfig.check && devServer) {
|
||||
if (routeConfig.check && devServer && phase !== 'hit') {
|
||||
const { pathname = '/' } = url.parse(destPath);
|
||||
const hasDestFile = await devServer.hasFilesystem(pathname);
|
||||
|
||||
if (!hasDestFile) {
|
||||
// If the file is not found, `check: true` will
|
||||
// behave the same as `continue: true`
|
||||
reqPathname = destPath;
|
||||
continue;
|
||||
if (routeConfig.status && phase !== 'miss') {
|
||||
// Equivalent to now-proxy exit_with_status() function
|
||||
} else if (missRoutes && missRoutes.length > 0) {
|
||||
// Trigger a 'miss'
|
||||
const missResult = await devRouter(
|
||||
destPath,
|
||||
reqMethod,
|
||||
missRoutes,
|
||||
devServer,
|
||||
previousHeaders,
|
||||
[],
|
||||
'miss'
|
||||
);
|
||||
if (missResult.found) {
|
||||
return missResult;
|
||||
}
|
||||
} else {
|
||||
if (routeConfig.status && phase === 'miss') {
|
||||
status = routeConfig.status;
|
||||
}
|
||||
reqPathname = destPath;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isURL(destPath)) {
|
||||
const isDestUrl = isURL(destPath);
|
||||
if (isDestUrl) {
|
||||
found = {
|
||||
found: true,
|
||||
dest: destPath,
|
||||
userDest: false,
|
||||
status: routeConfig.status,
|
||||
isDestUrl,
|
||||
status: routeConfig.status || status,
|
||||
headers: combinedHeaders,
|
||||
uri_args: query,
|
||||
matched_route: routeConfig,
|
||||
matched_route_idx: idx,
|
||||
phase,
|
||||
};
|
||||
break;
|
||||
} else {
|
||||
@@ -120,11 +168,13 @@ export default async function(
|
||||
found: true,
|
||||
dest: pathname || '/',
|
||||
userDest: Boolean(routeConfig.dest),
|
||||
status: routeConfig.status,
|
||||
isDestUrl,
|
||||
status: routeConfig.status || status,
|
||||
headers: combinedHeaders,
|
||||
uri_args: query,
|
||||
matched_route: routeConfig,
|
||||
matched_route_idx: idx,
|
||||
phase,
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -136,8 +186,11 @@ export default async function(
|
||||
found = {
|
||||
found: false,
|
||||
dest: reqPathname,
|
||||
status,
|
||||
isDestUrl: false,
|
||||
uri_args: query,
|
||||
headers: combinedHeaders,
|
||||
phase,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ import serveHandler from 'serve-handler';
|
||||
import { watch, FSWatcher } from 'chokidar';
|
||||
import { parse as parseDotenv } from 'dotenv';
|
||||
import { basename, dirname, extname, join } from 'path';
|
||||
import { getTransformedRoutes } from '@now/routing-utils';
|
||||
import {
|
||||
getTransformedRoutes,
|
||||
HandleValue,
|
||||
isHandler,
|
||||
} from '@now/routing-utils';
|
||||
import directoryTemplate from 'serve-handler/src/directory';
|
||||
|
||||
import {
|
||||
@@ -22,6 +26,8 @@ import {
|
||||
PackageJson,
|
||||
detectBuilders,
|
||||
detectRoutes,
|
||||
detectApiDirectory,
|
||||
detectApiExtensions,
|
||||
} from '@now/build-utils';
|
||||
|
||||
import { once } from '../once';
|
||||
@@ -48,8 +54,7 @@ import {
|
||||
validateNowConfigFunctions,
|
||||
} from './validate';
|
||||
|
||||
import isURL from './is-url';
|
||||
import devRouter from './router';
|
||||
import { devRouter, getRoutesTypes } from './router';
|
||||
import getMimeType from './mime-type';
|
||||
import { getYarnPath } from './yarn-installer';
|
||||
import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';
|
||||
@@ -81,6 +86,7 @@ import {
|
||||
ListenSpec,
|
||||
RouteConfig,
|
||||
RouteResult,
|
||||
HttpHeadersConfig,
|
||||
} from './types';
|
||||
|
||||
interface FSEvent {
|
||||
@@ -526,7 +532,8 @@ export default class DevServer {
|
||||
|
||||
// no builds -> zero config
|
||||
if (!config.builds || config.builds.length === 0) {
|
||||
const { projectSettings } = config;
|
||||
const featHandleMiss = true; // enable for zero config
|
||||
const { projectSettings, cleanUrls, trailingSlash } = config;
|
||||
|
||||
const { builders, warnings, errors } = await detectBuilders(files, pkg, {
|
||||
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
|
||||
@@ -548,7 +555,13 @@ export default class DevServer {
|
||||
defaultRoutes,
|
||||
redirectRoutes,
|
||||
error: routesError,
|
||||
} = await detectRoutes(files, builders);
|
||||
} = await detectRoutes(
|
||||
files,
|
||||
builders,
|
||||
featHandleMiss,
|
||||
cleanUrls,
|
||||
trailingSlash
|
||||
);
|
||||
|
||||
config.builds = config.builds || [];
|
||||
config.builds.push(...builders);
|
||||
@@ -720,9 +733,17 @@ export default class DevServer {
|
||||
const opts = { output: this.output, isBuilds: true };
|
||||
const files = await getFiles(this.cwd, nowConfig, opts);
|
||||
const results: { [filePath: string]: FileFsRef } = {};
|
||||
const apiDir = detectApiDirectory(nowConfig.builds || []);
|
||||
const apiExtensions = detectApiExtensions(nowConfig.builds || []);
|
||||
const apiMatch = apiDir + '/';
|
||||
for (const fsPath of files) {
|
||||
const path = relative(this.cwd, fsPath);
|
||||
let path = relative(this.cwd, fsPath);
|
||||
const { mode } = await fs.stat(fsPath);
|
||||
const ext = extname(path);
|
||||
if (apiDir && path.startsWith(apiMatch) && apiExtensions.has(ext)) {
|
||||
// lambda function files are trimmed of their file extension
|
||||
path = path.slice(0, -ext.length);
|
||||
}
|
||||
results[path] = new FileFsRef({ mode, fsPath });
|
||||
}
|
||||
this.files = results;
|
||||
@@ -1155,55 +1176,133 @@ export default class DevServer {
|
||||
await this.blockingBuildsPromise;
|
||||
}
|
||||
|
||||
const { dest, status, headers, uri_args } = await devRouter(
|
||||
req.url,
|
||||
req.method,
|
||||
routes,
|
||||
this
|
||||
);
|
||||
const handleMap = getRoutesTypes(routes);
|
||||
const missRoutes = handleMap.get('miss') || [];
|
||||
const hitRoutes = handleMap.get('hit') || [];
|
||||
handleMap.delete('miss');
|
||||
handleMap.delete('hit');
|
||||
const phases: (HandleValue | null)[] = [null, 'filesystem'];
|
||||
|
||||
let routeResult: RouteResult | null = null;
|
||||
let match: BuildMatch | null = null;
|
||||
let statusCode: number | undefined;
|
||||
|
||||
for (const phase of phases) {
|
||||
statusCode = undefined;
|
||||
const phaseRoutes = handleMap.get(phase) || [];
|
||||
routeResult = await devRouter(
|
||||
req.url,
|
||||
req.method,
|
||||
phaseRoutes,
|
||||
this,
|
||||
undefined,
|
||||
missRoutes,
|
||||
phase
|
||||
);
|
||||
|
||||
if (routeResult.isDestUrl) {
|
||||
// Mix the `routes` result dest query params into the req path
|
||||
const destParsed = url.parse(routeResult.dest, true);
|
||||
delete destParsed.search;
|
||||
Object.assign(destParsed.query, routeResult.uri_args);
|
||||
const destUrl = url.format(destParsed);
|
||||
|
||||
this.output.debug(`ProxyPass: ${destUrl}`);
|
||||
this.setResponseHeaders(res, nowRequestId);
|
||||
return proxyPass(req, res, destUrl, this.output);
|
||||
}
|
||||
|
||||
match = await findBuildMatch(
|
||||
this.buildMatches,
|
||||
this.files,
|
||||
routeResult.dest,
|
||||
this
|
||||
);
|
||||
|
||||
if (!match && missRoutes.length > 0) {
|
||||
// Since there was no build match, enter the miss phase
|
||||
routeResult = await devRouter(
|
||||
routeResult.dest || req.url,
|
||||
req.method,
|
||||
missRoutes,
|
||||
this,
|
||||
routeResult.headers,
|
||||
[],
|
||||
'miss'
|
||||
);
|
||||
|
||||
match = await findBuildMatch(
|
||||
this.buildMatches,
|
||||
this.files,
|
||||
routeResult.dest,
|
||||
this
|
||||
);
|
||||
} else if (match && hitRoutes.length > 0) {
|
||||
// Since there was a build match, enter the hit phase.
|
||||
// The hit phase must not set status code.
|
||||
const prevStatus = routeResult.status;
|
||||
routeResult = await devRouter(
|
||||
routeResult.dest || req.url,
|
||||
req.method,
|
||||
hitRoutes,
|
||||
this,
|
||||
routeResult.headers,
|
||||
[],
|
||||
'hit'
|
||||
);
|
||||
routeResult.status = prevStatus;
|
||||
}
|
||||
|
||||
statusCode = routeResult.status;
|
||||
|
||||
if (match && statusCode === 404 && routeResult.phase === 'miss') {
|
||||
statusCode = undefined;
|
||||
}
|
||||
|
||||
const location = routeResult.headers['location'] || routeResult.dest;
|
||||
|
||||
if (statusCode && location && (300 <= statusCode && statusCode <= 399)) {
|
||||
// Equivalent to now-proxy exit_with_status() function
|
||||
this.output.debug(
|
||||
`Route found with redirect status code ${statusCode}`
|
||||
);
|
||||
await this.sendRedirect(req, res, nowRequestId, location, statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!match && statusCode && routeResult.phase !== 'miss') {
|
||||
// Equivalent to now-proxy exit_with_status() function
|
||||
this.output.debug(`Route found with with status code ${statusCode}`);
|
||||
await this.sendError(req, res, nowRequestId, '', statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
// end the phase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!routeResult) {
|
||||
throw new Error('Expected Route Result but none was found.');
|
||||
}
|
||||
|
||||
const { dest, headers, uri_args } = routeResult;
|
||||
|
||||
// Set any headers defined in the matched `route` config
|
||||
Object.entries(headers).forEach(([name, value]) => {
|
||||
res.setHeader(name, value);
|
||||
});
|
||||
|
||||
if (isURL(dest)) {
|
||||
// Mix the `routes` result dest query params into the req path
|
||||
const destParsed = url.parse(dest, true);
|
||||
delete destParsed.search;
|
||||
Object.assign(destParsed.query, uri_args);
|
||||
const destUrl = url.format(destParsed);
|
||||
|
||||
this.output.debug(`ProxyPass: ${destUrl}`);
|
||||
this.setResponseHeaders(res, nowRequestId);
|
||||
return proxyPass(req, res, destUrl, this.output);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
res.statusCode = status;
|
||||
if (300 <= status && status <= 399) {
|
||||
await this.sendRedirect(
|
||||
req,
|
||||
res,
|
||||
nowRequestId,
|
||||
res.getHeader('location') as string,
|
||||
status
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (statusCode) {
|
||||
res.statusCode = statusCode;
|
||||
}
|
||||
|
||||
const requestPath = dest.replace(/^\//, '');
|
||||
const match = await findBuildMatch(
|
||||
this.buildMatches,
|
||||
this.files,
|
||||
requestPath,
|
||||
this
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
if (
|
||||
status === 404 ||
|
||||
(statusCode === 404 && routeResult.phase === 'miss') ||
|
||||
!this.renderDirectoryListing(req, res, requestPath, nowRequestId)
|
||||
) {
|
||||
await this.send404(req, res, nowRequestId);
|
||||
@@ -1349,7 +1448,7 @@ export default class DevServer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
if (!statusCode) {
|
||||
res.statusCode = result.statusCode;
|
||||
}
|
||||
this.setResponseHeaders(res, nowRequestId, result.headers);
|
||||
@@ -1453,16 +1552,7 @@ export default class DevServer {
|
||||
}
|
||||
|
||||
async hasFilesystem(dest: string): Promise<boolean> {
|
||||
const requestPath = dest.replace(/^\//, '');
|
||||
if (
|
||||
await findBuildMatch(
|
||||
this.buildMatches,
|
||||
this.files,
|
||||
requestPath,
|
||||
this,
|
||||
true
|
||||
)
|
||||
) {
|
||||
if (await findBuildMatch(this.buildMatches, this.files, dest, this, true)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1552,6 +1642,7 @@ async function findBuildMatch(
|
||||
devServer: DevServer,
|
||||
isFilesystem?: boolean
|
||||
): Promise<BuildMatch | null> {
|
||||
requestPath = requestPath.replace(/^\//, '');
|
||||
for (const match of matches.values()) {
|
||||
if (await shouldServe(match, files, requestPath, devServer, isFilesystem)) {
|
||||
return match;
|
||||
|
||||
@@ -3,11 +3,22 @@ import { BuilderParams, BuildResult, ShouldServeParams } from './types';
|
||||
|
||||
export const version = 2;
|
||||
|
||||
export function build({ files, entrypoint }: BuilderParams): BuildResult {
|
||||
export function build({
|
||||
files,
|
||||
entrypoint,
|
||||
config,
|
||||
}: BuilderParams): BuildResult {
|
||||
let path = entrypoint;
|
||||
const outputDir = config.zeroConfig ? config.outputDirectory : '';
|
||||
const outputMatch = outputDir + '/';
|
||||
if (outputDir && path.startsWith(outputMatch)) {
|
||||
// static output files are moved to the root directory
|
||||
path = path.slice(outputMatch.length);
|
||||
}
|
||||
const output = {
|
||||
[entrypoint]: files[entrypoint],
|
||||
[path]: files[entrypoint],
|
||||
};
|
||||
const watch = [entrypoint];
|
||||
const watch = [path];
|
||||
|
||||
return { output, routes: [], watch };
|
||||
}
|
||||
@@ -16,14 +27,25 @@ export function shouldServe({
|
||||
entrypoint,
|
||||
files,
|
||||
requestPath,
|
||||
config = {},
|
||||
}: ShouldServeParams) {
|
||||
let outputPrefix = '';
|
||||
const outputDir = config.zeroConfig ? config.outputDirectory : '';
|
||||
const outputMatch = outputDir + '/';
|
||||
if (outputDir && entrypoint.startsWith(outputMatch)) {
|
||||
// static output files are moved to the root directory
|
||||
entrypoint = entrypoint.slice(outputMatch.length);
|
||||
outputPrefix = outputMatch;
|
||||
}
|
||||
const isMatch = (f: string) => entrypoint === f && outputPrefix + f in files;
|
||||
|
||||
if (isIndex(entrypoint)) {
|
||||
const indexPath = join(requestPath, basename(entrypoint));
|
||||
if (entrypoint === indexPath && indexPath in files) {
|
||||
if (isMatch(indexPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return entrypoint === requestPath && requestPath in files;
|
||||
return isMatch(requestPath);
|
||||
}
|
||||
|
||||
function isIndex(path: string): boolean {
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
Lambda,
|
||||
PackageJson,
|
||||
BuilderFunctions,
|
||||
Config,
|
||||
} from '@now/build-utils';
|
||||
import { NowConfig } from 'now-client';
|
||||
import { NowRedirect, NowRewrite, NowHeader, Route } from '@now/routing-utils';
|
||||
import { HandleValue, Route } from '@now/routing-utils';
|
||||
import { Output } from '../output';
|
||||
|
||||
export { NowConfig };
|
||||
@@ -61,7 +62,7 @@ export interface CacheOutputs {
|
||||
export interface BuilderParamsBase {
|
||||
files: BuilderInputs;
|
||||
entrypoint: string;
|
||||
config: object;
|
||||
config: Config;
|
||||
meta?: {
|
||||
isDev?: boolean;
|
||||
requestPath?: string | null;
|
||||
@@ -124,7 +125,7 @@ export interface BuildResultV4 {
|
||||
export interface ShouldServeParams {
|
||||
files: BuilderInputs;
|
||||
entrypoint: string;
|
||||
config?: object;
|
||||
config?: Config;
|
||||
requestPath: string;
|
||||
workPath: string;
|
||||
}
|
||||
@@ -156,6 +157,10 @@ export interface RouteResult {
|
||||
matched_route_idx?: number;
|
||||
// "userDest": <boolean in case the destination was user defined>
|
||||
userDest?: boolean;
|
||||
// url as destination should end routing
|
||||
isDestUrl: boolean;
|
||||
// the phase that this route is defined in
|
||||
phase?: HandleValue | null;
|
||||
}
|
||||
|
||||
export interface InvokePayload {
|
||||
|
||||
@@ -1213,7 +1213,7 @@ export class BuildError extends NowError<'BUILD_ERROR', {}> {
|
||||
meta,
|
||||
}: {
|
||||
message: string;
|
||||
meta: { entrypoint: string };
|
||||
meta: { entrypoint?: string };
|
||||
}) {
|
||||
super({
|
||||
code: 'BUILD_ERROR',
|
||||
|
||||
@@ -319,6 +319,13 @@ export default class Now extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
if (error.errorCode && error.errorCode === 'BUILD_FAILED') {
|
||||
return new BuildError({
|
||||
message: error.errorMessage,
|
||||
meta: {},
|
||||
});
|
||||
}
|
||||
|
||||
return new Error(error.message);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export default async function editProjectSettings(
|
||||
const defaults = framework.settings[field.value];
|
||||
|
||||
output.print(
|
||||
chalk.gray(
|
||||
chalk.dim(
|
||||
`- ${chalk.bold(`${field.name}:`)} ${`${
|
||||
isSettingValue(defaults)
|
||||
? defaults.value
|
||||
|
||||
@@ -69,9 +69,17 @@ export default async function inputProject(
|
||||
});
|
||||
const projectName = answers.existingProjectName as string;
|
||||
|
||||
const loader = wait('Verifying project name…', 1000);
|
||||
project = await getProjectByIdOrName(client, projectName, org.id);
|
||||
loader();
|
||||
if (!projectName) {
|
||||
output.error(`Project name cannot be empty`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const spinner = wait('Verifying project name…', 1000);
|
||||
try {
|
||||
project = await getProjectByIdOrName(client, projectName, org.id);
|
||||
} finally {
|
||||
spinner();
|
||||
}
|
||||
|
||||
if (project instanceof ProjectNotFound) {
|
||||
output.print(`${chalk.red('Error!')} Project not found\n`);
|
||||
@@ -93,13 +101,22 @@ export default async function inputProject(
|
||||
});
|
||||
newProjectName = answers.newProjectName as string;
|
||||
|
||||
if (!newProjectName) {
|
||||
output.error(`Project name cannot be empty`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const spinner = wait('Verifying project name…', 1000);
|
||||
const existingProject = await getProjectByIdOrName(
|
||||
client,
|
||||
newProjectName,
|
||||
org.id
|
||||
);
|
||||
spinner();
|
||||
let existingProject: Project | ProjectNotFound;
|
||||
try {
|
||||
existingProject = await getProjectByIdOrName(
|
||||
client,
|
||||
newProjectName,
|
||||
org.id
|
||||
);
|
||||
} finally {
|
||||
spinner();
|
||||
}
|
||||
|
||||
if (existingProject && !(existingProject instanceof ProjectNotFound)) {
|
||||
output.print(`${chalk.red('Error!')} Project already exists\n`);
|
||||
|
||||
@@ -2,7 +2,7 @@ import Client from '../client';
|
||||
import inquirer from 'inquirer';
|
||||
import getUser from '../get-user';
|
||||
import getTeams from '../get-teams';
|
||||
import { Org } from '../../types';
|
||||
import { User, Team, Org } from '../../types';
|
||||
import wait from '../output/wait';
|
||||
|
||||
type Choice = { name: string; value: Org };
|
||||
@@ -16,8 +16,13 @@ export default async function selectProject(
|
||||
require('./patch-inquirer');
|
||||
|
||||
const spinner = wait('Loading scopes…', 1000);
|
||||
const [user, teams] = await Promise.all([getUser(client), getTeams(client)]);
|
||||
spinner();
|
||||
let user: User;
|
||||
let teams: Team[];
|
||||
try {
|
||||
[user, teams] = await Promise.all([getUser(client), getTeams(client)]);
|
||||
} finally {
|
||||
spinner();
|
||||
}
|
||||
|
||||
const choices: Choice[] = [
|
||||
{
|
||||
|
||||
@@ -52,9 +52,9 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
|
||||
}
|
||||
|
||||
function error(str: string, slug: string | null = null) {
|
||||
log(chalk`{red.bold Error!} ${str}`, chalk.red);
|
||||
print(`${chalk.red(`Error!`)} ${str}\n`);
|
||||
if (slug !== null) {
|
||||
log(`More details: https://err.sh/now/${slug}`);
|
||||
print(`More details: https://err.sh/now/${slug}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,5 +19,5 @@ export default function error(
|
||||
metric.exception(messages.join('\n')).send();
|
||||
}
|
||||
|
||||
return `${chalk.red('> Error!')} ${messages.join('\n')}`;
|
||||
return `${chalk.red('Error!')} ${messages.join('\n')}`;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import getUser from '../get-user';
|
||||
import getTeamById from '../get-team-by-id';
|
||||
import { Output } from '../output';
|
||||
import { Project } from '../../types';
|
||||
import { Org } from '../../types';
|
||||
import { Org, ProjectLink } from '../../types';
|
||||
import chalk from 'chalk';
|
||||
import { prependEmoji, emoji } from '../emoji';
|
||||
import wait from '../output/wait';
|
||||
|
||||
const readFile = promisify(fs.readFile);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
@@ -19,13 +20,6 @@ const writeFile = promisify(fs.writeFile);
|
||||
export const NOW_FOLDER = '.now';
|
||||
export const NOW_PROJECT_LINK_FILE = 'project.json';
|
||||
|
||||
interface ProjectFolderLink {
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
orgSlug?: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
async function getOrg(client: Client, orgId: string): Promise<Org | null> {
|
||||
if (orgId.startsWith('team_')) {
|
||||
const team = await getTeamById(client, orgId);
|
||||
@@ -39,22 +33,50 @@ async function getOrg(client: Client, orgId: string): Promise<Org | null> {
|
||||
}
|
||||
|
||||
export async function getLinkedProject(
|
||||
output: Output,
|
||||
client: Client,
|
||||
path: string
|
||||
): Promise<[Org | null, Project | null]> {
|
||||
try {
|
||||
const json = await readFile(join(path, NOW_FOLDER, NOW_PROJECT_LINK_FILE), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
let link: ProjectLink;
|
||||
|
||||
const link: ProjectFolderLink = JSON.parse(json);
|
||||
const { NOW_ORG_ID, NOW_PROJECT_ID } = process.env;
|
||||
if (NOW_ORG_ID && NOW_PROJECT_ID) {
|
||||
link = {
|
||||
orgId: NOW_ORG_ID,
|
||||
projectId: NOW_PROJECT_ID,
|
||||
};
|
||||
} else {
|
||||
const json = await readFile(
|
||||
join(path, NOW_FOLDER, NOW_PROJECT_LINK_FILE),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
|
||||
const [org, project] = await Promise.all([
|
||||
getOrg(client, link.orgId),
|
||||
getProjectByIdOrName(client, link.projectId, link.orgId),
|
||||
]);
|
||||
link = JSON.parse(json);
|
||||
}
|
||||
|
||||
const spinner = wait('Retrieving project…', 1000);
|
||||
let org: Org | null;
|
||||
let project: Project | ProjectNotFound | null;
|
||||
try {
|
||||
[org, project] = await Promise.all([
|
||||
getOrg(client, link.orgId),
|
||||
getProjectByIdOrName(client, link.projectId, link.orgId),
|
||||
]);
|
||||
} finally {
|
||||
spinner();
|
||||
}
|
||||
|
||||
if (project instanceof ProjectNotFound || org === null) {
|
||||
if (!(NOW_ORG_ID && NOW_PROJECT_ID)) {
|
||||
output.print(
|
||||
prependEmoji(
|
||||
'Your project was either removed from ZEIT Now or you’re not a member of it anymore.\n',
|
||||
emoji('warning')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
@@ -79,7 +101,7 @@ export async function getLinkedProject(
|
||||
export async function linkFolderToProject(
|
||||
output: Output,
|
||||
path: string,
|
||||
projectFolderLink: ProjectFolderLink,
|
||||
projectLink: ProjectLink,
|
||||
projectName: string,
|
||||
orgSlug: string
|
||||
) {
|
||||
@@ -96,18 +118,22 @@ export async function linkFolderToProject(
|
||||
|
||||
await writeFile(
|
||||
join(path, NOW_FOLDER, NOW_PROJECT_LINK_FILE),
|
||||
JSON.stringify(projectFolderLink),
|
||||
{
|
||||
encoding: 'utf8',
|
||||
}
|
||||
JSON.stringify(projectLink),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
|
||||
// update .nowignore
|
||||
// update .gitignore
|
||||
let isGitIgnoreUpdated = false;
|
||||
try {
|
||||
const gitIgnorePath = join(path, '.gitignore');
|
||||
const gitIgnore = (await readFile(gitIgnorePath)).toString();
|
||||
if (gitIgnore.split('\n').indexOf('.now') < 0) {
|
||||
await writeFile(gitIgnorePath, gitIgnore + '\n.now');
|
||||
|
||||
const gitIgnore = await readFile(gitIgnorePath)
|
||||
.then(buf => buf.toString())
|
||||
.catch(() => null);
|
||||
|
||||
if (!gitIgnore || !gitIgnore.split('\n').includes('.now')) {
|
||||
await writeFile(gitIgnorePath, gitIgnore ? `${gitIgnore}\n.now` : '.now');
|
||||
isGitIgnoreUpdated = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore errors since this is non-critical
|
||||
@@ -115,9 +141,9 @@ export async function linkFolderToProject(
|
||||
|
||||
output.print(
|
||||
prependEmoji(
|
||||
`Linked to ${chalk.bold(
|
||||
`${orgSlug}/${projectName}`
|
||||
)} (created .now and added it to .gitignore)`,
|
||||
`Linked to ${chalk.bold(`${orgSlug}/${projectName}`)} (created .now${
|
||||
isGitIgnoreUpdated ? ' and added it to .gitignore' : ''
|
||||
})`,
|
||||
emoji('link')
|
||||
) + '\n'
|
||||
);
|
||||
|
||||
33
packages/now-cli/test/dev-router.unit.js
vendored
33
packages/now-cli/test/dev-router.unit.js
vendored
@@ -1,5 +1,5 @@
|
||||
import test from 'ava';
|
||||
import devRouter from '../src/util/dev/router';
|
||||
import { devRouter } from '../src/util/dev/router';
|
||||
|
||||
test('[dev-router] 301 redirection', async t => {
|
||||
const routesConfig = [
|
||||
@@ -11,11 +11,13 @@ test('[dev-router] 301 redirection', async t => {
|
||||
found: true,
|
||||
dest: '/redirect',
|
||||
status: 301,
|
||||
headers: { Location: 'https://zeit.co' },
|
||||
headers: { location: 'https://zeit.co' },
|
||||
uri_args: {},
|
||||
matched_route: routesConfig[0],
|
||||
matched_route_idx: 0,
|
||||
userDest: false,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +34,8 @@ test('[dev-router] captured groups', async t => {
|
||||
matched_route: routesConfig[0],
|
||||
matched_route_idx: 0,
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +52,8 @@ test('[dev-router] named groups', async t => {
|
||||
matched_route: routesConfig[0],
|
||||
matched_route_idx: 0,
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +75,8 @@ test('[dev-router] optional named groups', async t => {
|
||||
matched_route: routesConfig[0],
|
||||
matched_route_idx: 0,
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,6 +94,8 @@ test('[dev-router] proxy_pass', async t => {
|
||||
matched_route: routesConfig[0],
|
||||
matched_route_idx: 0,
|
||||
userDest: false,
|
||||
isDestUrl: true,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,6 +115,8 @@ test('[dev-router] methods', async t => {
|
||||
matched_route: routesConfig[1],
|
||||
matched_route_idx: 1,
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
});
|
||||
|
||||
result = await devRouter('/', 'POST', routesConfig);
|
||||
@@ -117,6 +129,8 @@ test('[dev-router] methods', async t => {
|
||||
matched_route: routesConfig[0],
|
||||
matched_route_idx: 0,
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,6 +147,8 @@ test('[dev-router] match without prefix slash', async t => {
|
||||
matched_route: routesConfig[0],
|
||||
matched_route_idx: 0,
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,6 +165,8 @@ test('[dev-router] match with needed prefixed slash', async t => {
|
||||
found: true,
|
||||
dest: '/some/dest',
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
status: undefined,
|
||||
headers: {},
|
||||
uri_args: {},
|
||||
@@ -179,6 +197,9 @@ test('[dev-router] `continue: true` with fallthrough', async t => {
|
||||
t.deepEqual(result, {
|
||||
found: false,
|
||||
dest: '/_next/static/chunks/0.js',
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
status: undefined,
|
||||
uri_args: {},
|
||||
headers: {
|
||||
'cache-control': 'immutable,max-age=31536000',
|
||||
@@ -211,6 +232,8 @@ test('[dev-router] `continue: true` with match', async t => {
|
||||
dest: '/hi',
|
||||
status: undefined,
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
uri_args: {},
|
||||
headers: {
|
||||
'cache-control': 'immutable,max-age=31536000',
|
||||
@@ -231,6 +254,8 @@ test('[dev-router] match with catch-all with prefix slash', async t => {
|
||||
found: true,
|
||||
dest: '/www/',
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
status: undefined,
|
||||
headers: {},
|
||||
uri_args: {},
|
||||
@@ -247,6 +272,8 @@ test('[dev-router] match with catch-all with no prefix slash', async t => {
|
||||
found: true,
|
||||
dest: '/www/',
|
||||
userDest: true,
|
||||
isDestUrl: false,
|
||||
phase: undefined,
|
||||
status: undefined,
|
||||
headers: {},
|
||||
uri_args: {},
|
||||
@@ -274,5 +301,7 @@ test('[dev-router] `continue: true` with `dest`', async t => {
|
||||
matched_route: routesConfig[1],
|
||||
matched_route_idx: 1,
|
||||
userDest: false,
|
||||
isDestUrl: true,
|
||||
phase: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"functions": {
|
||||
"api/**/*.sh": {
|
||||
"api/user": {
|
||||
"runtime": "now-bash@1.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
"builds": [
|
||||
{
|
||||
"use": "@now/static-build",
|
||||
"src": "package.json",
|
||||
"config": {
|
||||
"zeroConfig": true
|
||||
}
|
||||
"src": "package.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Blog Post
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{
|
||||
"src": "/([^/]+)",
|
||||
"headers": { "override": "one" },
|
||||
"dest": "/blog/$1.html",
|
||||
"check": true
|
||||
},
|
||||
{ "handle": "hit" },
|
||||
{
|
||||
"src": "^/.*",
|
||||
"headers": { "test": "1", "override": "two" },
|
||||
"continue": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Blog Page
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{ "handle": "hit" },
|
||||
{
|
||||
"src": "^/.*",
|
||||
"headers": { "test": "1" },
|
||||
"continue": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Blog Post
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{ "handle": "filesystem" },
|
||||
{
|
||||
"src": "/([^/]+)",
|
||||
"headers": { "override": "one" },
|
||||
"dest": "/blog/$1.html",
|
||||
"check": true
|
||||
},
|
||||
{ "handle": "hit" },
|
||||
{
|
||||
"src": "^/.*",
|
||||
"headers": { "test": "1", "override": "two" },
|
||||
"continue": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
About Page
|
||||
@@ -0,0 +1 @@
|
||||
Index Page
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{
|
||||
"handle": "filesystem"
|
||||
},
|
||||
{
|
||||
"src": "/([^/]+)",
|
||||
"headers": { "override": "one" },
|
||||
"dest": "/blog/$1",
|
||||
"check": true
|
||||
},
|
||||
{
|
||||
"handle": "miss"
|
||||
},
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "/src/$1",
|
||||
"check": true
|
||||
},
|
||||
{
|
||||
"src": "/src/blog/([^/]+)",
|
||||
"headers": { "test": "1", "override": "two" },
|
||||
"dest": "/src/blog/$1.html",
|
||||
"check": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Blog Post Page
|
||||
@@ -0,0 +1 @@
|
||||
About Page
|
||||
@@ -0,0 +1 @@
|
||||
Index Page
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{
|
||||
"src": "/([^/]+)",
|
||||
"headers": { "override": "one" },
|
||||
"dest": "/blog/$1"
|
||||
},
|
||||
{
|
||||
"handle": "miss"
|
||||
},
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "/src/$1",
|
||||
"check": true
|
||||
},
|
||||
{
|
||||
"src": "/src/blog/([^/]+)",
|
||||
"headers": { "test": "1", "override": "two" },
|
||||
"dest": "/src/blog/$1.html",
|
||||
"check": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Blog Post Page
|
||||
@@ -0,0 +1 @@
|
||||
About Page
|
||||
@@ -0,0 +1 @@
|
||||
Index Page
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{
|
||||
"handle": "filesystem"
|
||||
},
|
||||
{
|
||||
"src": "/([^/]+)",
|
||||
"headers": { "override": "one" },
|
||||
"dest": "/blog/$1",
|
||||
"check": true
|
||||
},
|
||||
{
|
||||
"handle": "miss"
|
||||
},
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "/src/$1",
|
||||
"check": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
First Post
|
||||
@@ -0,0 +1 @@
|
||||
Index Page
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{ "handle": "filesystem" },
|
||||
{ "src": "/([^/]+/dir/.+)", "dest": "/$1.html", "check": true },
|
||||
{ "handle": "miss" },
|
||||
{ "src": "/pathA(?:/.+)?", "status": 404, "continue": true },
|
||||
{ "src": "/pathB(?:/.+)?", "status": 404, "continue": true }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Path A
|
||||
@@ -0,0 +1 @@
|
||||
Path B
|
||||
@@ -0,0 +1 @@
|
||||
Path C
|
||||
@@ -0,0 +1 @@
|
||||
About Page
|
||||
@@ -0,0 +1 @@
|
||||
Index Page
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{
|
||||
"handle": "filesystem"
|
||||
},
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"headers": { "override": "one" },
|
||||
"dest": "/blog/$1",
|
||||
"check": true
|
||||
},
|
||||
{
|
||||
"handle": "miss"
|
||||
},
|
||||
{
|
||||
"src": "/.*",
|
||||
"status": 404,
|
||||
"continue": true
|
||||
},
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "/src/$1",
|
||||
"check": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
First Post
|
||||
@@ -0,0 +1 @@
|
||||
Index Page
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 2,
|
||||
"routes": [
|
||||
{ "handle": "miss" },
|
||||
{ "src": "/pathA(?:/.+)?", "status": 404, "continue": true },
|
||||
{ "src": "/pathB(?:/.+)?", "status": 404, "continue": true }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Path A
|
||||
@@ -0,0 +1 @@
|
||||
Path B
|
||||
@@ -0,0 +1 @@
|
||||
Path C
|
||||
@@ -0,0 +1,3 @@
|
||||
export default (_req, res) => {
|
||||
res.end('current date: ' + new Date().toISOString());
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export default (_req, res) => {
|
||||
res.end('random number: ' + Math.random());
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "test-public-and-api"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
This is the about page
|
||||
@@ -0,0 +1 @@
|
||||
This is the home page.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "**/*", "use": "@now/static" }],
|
||||
"routes": [
|
||||
{ "src": "/secret", "status": 403, "dest": "/post", "check": true },
|
||||
{ "src": "/post", "dest": "/post.html" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
This is a post.
|
||||
@@ -319,6 +319,168 @@ test(
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] validate routes that use `check: true` and `status` code',
|
||||
testFixtureStdio('routes-check-true-status', async (t, port) => {
|
||||
const secret = await fetch(`http://localhost:${port}/secret`);
|
||||
t.is(secret.status, 403);
|
||||
t.regex(await secret.text(), /FORBIDDEN/gm);
|
||||
|
||||
const rewrite = await fetchWithRetry(`http://localhost:${port}/post`);
|
||||
t.is(rewrite.status, 200);
|
||||
t.regex(await rewrite.text(), /This is a post/gm);
|
||||
|
||||
const raw = await fetchWithRetry(`http://localhost:${port}/post.html`);
|
||||
t.is(raw.status, 200);
|
||||
t.regex(await raw.text(), /This is a post/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] handles miss after route',
|
||||
testFixtureStdio('handle-miss-after-route', async (t, port) => {
|
||||
const response = await fetchWithRetry(`http://localhost:${port}/post`);
|
||||
|
||||
const test = response.headers.get('test');
|
||||
const override = response.headers.get('override');
|
||||
t.is(test, '1', 'exected miss header to be added');
|
||||
t.is(override, 'one', 'exected override header to not override');
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Blog/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] handles miss after rewrite',
|
||||
testFixtureStdio('handle-miss-after-rewrite', async (t, port) => {
|
||||
const response = await fetchWithRetry(`http://localhost:${port}/post`);
|
||||
const test = response.headers.get('test');
|
||||
const override = response.headers.get('override');
|
||||
t.is(test, '1', 'expected miss header to be added');
|
||||
t.is(override, 'two', 'expected override header to not override');
|
||||
t.is(response.status, 200);
|
||||
const body = await response.text();
|
||||
t.regex(body, /Blog Post Page/gm);
|
||||
|
||||
const response1 = await fetchWithRetry(
|
||||
`http://localhost:${port}/blog/post`
|
||||
);
|
||||
const test1 = response.headers.get('test');
|
||||
const override1 = response.headers.get('override');
|
||||
t.is(test1, '1', 'expected miss header to be added');
|
||||
t.is(override1, 'two', 'expected override header to be added');
|
||||
t.is(response1.status, 200);
|
||||
t.regex(await response1.text(), /Blog Post Page/gm);
|
||||
|
||||
const response2 = await fetchWithRetry(
|
||||
`http://localhost:${port}/about.html`
|
||||
);
|
||||
const test2 = response2.headers.get('test');
|
||||
const override2 = response2.headers.get('override');
|
||||
t.is(test2, null, 'expected miss header to be not be added');
|
||||
t.is(override2, null, 'expected override header to not be added');
|
||||
t.is(response2.status, 200);
|
||||
t.regex(await response2.text(), /About Page/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] displays directory listing after miss',
|
||||
testFixtureStdio('handle-miss-display-dir-list', async (t, port) => {
|
||||
const response = await fetchWithRetry(`http://localhost:${port}/post`);
|
||||
const body = await response.text();
|
||||
t.regex(body, /one.html/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] does not display directory listing after 404',
|
||||
testFixtureStdio('handle-miss-hide-dir-list', async (t, port) => {
|
||||
const post = await fetch(`http://localhost:${port}/post`);
|
||||
t.is(post.status, 404);
|
||||
|
||||
const file = await fetch(`http://localhost:${port}/post/one.html`);
|
||||
t.is(file.status, 200);
|
||||
t.regex(await file.text(), /First Post/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] does not display directory listing after multiple 404',
|
||||
testFixtureStdio('handle-miss-multiple-404', async (t, port) => {
|
||||
t.is((await fetch(`http://localhost:${port}/pathA/dir`)).status, 404);
|
||||
t.is((await fetch(`http://localhost:${port}/pathB/dir`)).status, 404);
|
||||
t.is((await fetch(`http://localhost:${port}/pathC/dir`)).status, 200);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] does not display directory listing after `handle: miss` and 404',
|
||||
testFixtureStdio('handle-miss-handle-filesystem-404', async (t, port) => {
|
||||
t.is((await fetch(`http://localhost:${port}/pathA/dir`)).status, 404);
|
||||
t.is((await fetch(`http://localhost:${port}/pathB/dir`)).status, 404);
|
||||
t.is((await fetch(`http://localhost:${port}/pathC/dir`)).status, 200);
|
||||
|
||||
t.is((await fetch(`http://localhost:${port}/pathA/dir/one`)).status, 200);
|
||||
t.is((await fetch(`http://localhost:${port}/pathB/dir/two`)).status, 200);
|
||||
t.is((await fetch(`http://localhost:${port}/pathC/dir/three`)).status, 200);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] handles hit after handle: filesystem',
|
||||
testFixtureStdio('handle-hit-after-fs', async (t, port) => {
|
||||
const response = await fetchWithRetry(`http://localhost:${port}/blog.html`);
|
||||
const test = response.headers.get('test');
|
||||
t.is(test, '1', 'expected hit header to be added');
|
||||
const body = await response.text();
|
||||
t.regex(body, /Blog Page/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] handles hit after dest',
|
||||
testFixtureStdio('handle-hit-after-dest', async (t, port) => {
|
||||
const response = await fetchWithRetry(`http://localhost:${port}/post`);
|
||||
const test = response.headers.get('test');
|
||||
const override = response.headers.get('override');
|
||||
t.is(test, '1', 'expected hit header to be added');
|
||||
t.is(override, 'one', 'expected hit header to not override');
|
||||
const body = await response.text();
|
||||
t.regex(body, /Blog Post/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] handles hit after rewrite',
|
||||
testFixtureStdio('handle-hit-after-rewrite', async (t, port) => {
|
||||
const response = await fetchWithRetry(`http://localhost:${port}/post`);
|
||||
const test = response.headers.get('test');
|
||||
const override = response.headers.get('override');
|
||||
t.is(test, '1', 'expected hit header to be added');
|
||||
t.is(override, 'one', 'expected hit header to not override');
|
||||
const body = await response.text();
|
||||
t.regex(body, /Blog Post/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] should serve the public directory and api functions',
|
||||
testFixtureStdio('public-and-api', async (t, port) => {
|
||||
const index = await fetchWithRetry(`http://localhost:${port}`);
|
||||
t.regex(await index.text(), /home page/gm);
|
||||
const about = await fetchWithRetry(`http://localhost:${port}/about.html`);
|
||||
t.regex(await about.text(), /about page/gm);
|
||||
const date = await fetchWithRetry(`http://localhost:${port}/api/date`);
|
||||
t.regex(await date.text(), /current date/gm);
|
||||
const rand = await fetchWithRetry(`http://localhost:${port}/api/rand`);
|
||||
t.regex(await rand.text(), /random number/gm);
|
||||
const rand2 = await fetchWithRetry(`http://localhost:${port}/api/rand.js`);
|
||||
t.regex(await rand2.text(), /random number/gm);
|
||||
})
|
||||
);
|
||||
|
||||
test('[now dev] validate builds', async t => {
|
||||
const directory = fixture('invalid-builds');
|
||||
const output = await exec(directory);
|
||||
|
||||
175
packages/now-cli/test/integration.js
vendored
175
packages/now-cli/test/integration.js
vendored
@@ -9,7 +9,7 @@ import _execa from 'execa';
|
||||
import fetch from 'node-fetch';
|
||||
import tmp from 'tmp-promise';
|
||||
import retry from 'async-retry';
|
||||
import fs, { writeFile, readFile, remove, copy } from 'fs-extra';
|
||||
import fs, { writeFile, readFile, remove, copy, ensureDir } from 'fs-extra';
|
||||
import logo from '../src/util/output/logo';
|
||||
import sleep from '../src/util/sleep';
|
||||
import pkg from '../package';
|
||||
@@ -427,7 +427,7 @@ test('login with unregistered user', async t => {
|
||||
console.log(stdout);
|
||||
console.log(exitCode);
|
||||
|
||||
const goal = `> Error! Please sign up: https://zeit.co/signup`;
|
||||
const goal = `Error! Please sign up: https://zeit.co/signup`;
|
||||
const lines = stdout.trim().split('\n');
|
||||
const last = lines[lines.length - 1];
|
||||
|
||||
@@ -606,7 +606,7 @@ test('try to purchase a domain', async t => {
|
||||
t.is(exitCode, 1);
|
||||
t.true(
|
||||
stderr.includes(
|
||||
`> Error! Could not purchase domain. Please add a payment method using \`now billing add\`.`
|
||||
`Error! Could not purchase domain. Please add a payment method using \`now billing add\`.`
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -633,7 +633,7 @@ test('try to transfer-in a domain with "--code" option', async t => {
|
||||
|
||||
t.true(
|
||||
stderr.includes(
|
||||
`> Error! The domain "${session}-test.org" is not transferable.`
|
||||
`Error! The domain "${session}-test.org" is not transferable.`
|
||||
)
|
||||
);
|
||||
t.is(exitCode, 1);
|
||||
@@ -658,7 +658,7 @@ test('try to move an invalid domain', async t => {
|
||||
console.log(stdout);
|
||||
console.log(exitCode);
|
||||
|
||||
t.true(stderr.includes(`> Error! Domain not found under `));
|
||||
t.true(stderr.includes(`Error! Domain not found under `));
|
||||
t.is(exitCode, 1);
|
||||
});
|
||||
|
||||
@@ -703,7 +703,7 @@ test('try to remove a non-existing payment method', async t => {
|
||||
test('set platform version using `-V` to invalid number', async t => {
|
||||
const directory = fixture('builds');
|
||||
const goal =
|
||||
'> Error! The "--platform-version" option must be either `1` or `2`.';
|
||||
'Error! The "--platform-version" option must be either `1` or `2`.';
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
@@ -727,7 +727,7 @@ test('set platform version using `-V` to invalid number', async t => {
|
||||
test('set platform version using `--platform-version` to invalid number', async t => {
|
||||
const directory = fixture('builds');
|
||||
const goal =
|
||||
'> Error! The "--platform-version" option must be either `1` or `2`.';
|
||||
'Error! The "--platform-version" option must be either `1` or `2`.';
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
@@ -1144,7 +1144,7 @@ test('try to create a builds deployments with wrong config', async t => {
|
||||
t.is(exitCode, 1);
|
||||
t.true(
|
||||
stderr.includes(
|
||||
'> Error! The property `builder` is not allowed in now.json when using Now 2.0 – please remove it.'
|
||||
'Error! The property `builder` is not allowed in now.json when using Now 2.0 – please remove it.'
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -1624,7 +1624,7 @@ test('use `--debug` CLI flag', async t => {
|
||||
});
|
||||
|
||||
test('try to deploy non-existing path', async t => {
|
||||
const goal = `> Error! The specified file or directory "${session}" does not exist.`;
|
||||
const goal = `Error! The specified file or directory "${session}" does not exist.`;
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
@@ -1644,7 +1644,7 @@ test('try to deploy non-existing path', async t => {
|
||||
|
||||
test('try to deploy with non-existing team', async t => {
|
||||
const target = fixture('node');
|
||||
const goal = `> Error! The specified scope does not exist`;
|
||||
const goal = `Error! The specified scope does not exist`;
|
||||
|
||||
const { stderr, stdout, exitCode } = await execa(
|
||||
binaryPath,
|
||||
@@ -1760,7 +1760,7 @@ test('try to initialize example to existing directory', async t => {
|
||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
||||
const cwd = tmpDir.name;
|
||||
const goal =
|
||||
'> Error! Destination path "angular" already exists and is not an empty directory. You may use `--force` or `--f` to override it.';
|
||||
'Error! Destination path "angular" already exists and is not an empty directory. You may use `--force` or `--f` to override it.';
|
||||
|
||||
createDirectory(path.join(cwd, 'angular'));
|
||||
createFile(path.join(cwd, 'angular', '.gitignore'));
|
||||
@@ -1781,7 +1781,7 @@ test('try to initialize misspelled example (noce) in non-tty', async t => {
|
||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
||||
const cwd = tmpDir.name;
|
||||
const goal =
|
||||
'> Error! No example found for noce, run `now init` to see the list of available examples.';
|
||||
'Error! No example found for noce, run `now init` to see the list of available examples.';
|
||||
|
||||
const { stdout, stderr, exitCode } = await execute(['init', 'noce'], { cwd });
|
||||
|
||||
@@ -1797,7 +1797,7 @@ test('try to initialize example "example-404"', async t => {
|
||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
||||
const cwd = tmpDir.name;
|
||||
const goal =
|
||||
'> Error! No example found for example-404, run `now init` to see the list of available examples.';
|
||||
'Error! No example found for example-404, run `now init` to see the list of available examples.';
|
||||
|
||||
const { stdout, stderr, exitCode } = await execute(['init', 'example-404'], {
|
||||
cwd,
|
||||
@@ -2316,6 +2316,9 @@ test('should show prompts to set up project', async t => {
|
||||
// Ensure the exit code is right
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
|
||||
// Ensure .gitignore is created
|
||||
t.is((await readFile(path.join(directory, '.gitignore'))).toString(), '.now');
|
||||
|
||||
// Send a test request to the deployment
|
||||
const response = await fetch(new URL(output.stdout).href);
|
||||
const text = await response.text();
|
||||
@@ -2365,47 +2368,7 @@ test('should not prompt "project settings overwrite" for undetected projects', a
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test('should prefill "project name" prompt with detected project name', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
const projectName = `static-deployment-${
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.split('.')[1]
|
||||
}`;
|
||||
|
||||
// remove previously linked project if it exists
|
||||
await remove(path.join(directory, '.now'));
|
||||
|
||||
const now = execa(binaryPath, [
|
||||
directory,
|
||||
'--name',
|
||||
projectName,
|
||||
...defaultArgs,
|
||||
]);
|
||||
|
||||
await waitForPrompt(now, chunk => /Set up and deploy [^?]+\?/.test(chunk));
|
||||
now.stdin.write('yes\n');
|
||||
|
||||
await waitForPrompt(now, chunk =>
|
||||
chunk.includes('Which scope do you want to deploy to?')
|
||||
);
|
||||
now.stdin.write('\n');
|
||||
|
||||
await waitForPrompt(now, chunk =>
|
||||
chunk.includes('Link to existing project?')
|
||||
);
|
||||
now.stdin.write('no\n');
|
||||
|
||||
await waitForPrompt(now, chunk =>
|
||||
chunk.includes(`What’s your project’s name? (${projectName})`)
|
||||
);
|
||||
now.stdin.write(`\n`);
|
||||
|
||||
const output = await now;
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test.only('should prefill "project name" prompt with folder name', async t => {
|
||||
test('should prefill "project name" prompt with folder name', async t => {
|
||||
const projectName = `static-deployment-${
|
||||
Math.random()
|
||||
.toString(36)
|
||||
@@ -2444,7 +2407,7 @@ test.only('should prefill "project name" prompt with folder name', async t => {
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test.only('should prefill "project name" prompt with --name', async t => {
|
||||
test('should prefill "project name" prompt with --name', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
const projectName = `static-deployment-${
|
||||
Math.random()
|
||||
@@ -2484,7 +2447,7 @@ test.only('should prefill "project name" prompt with --name', async t => {
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test.only('should prefill "project name" prompt with now.json `name`', async t => {
|
||||
test('should prefill "project name" prompt with now.json `name`', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
const projectName = `static-deployment-${
|
||||
Math.random()
|
||||
@@ -2525,6 +2488,106 @@ test.only('should prefill "project name" prompt with now.json `name`', async t =
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test('deploy with unknown `NOW_ORG_ID` and `NOW_PROJECT_ID` should fail', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
|
||||
const output = await execute([directory], {
|
||||
env: {
|
||||
NOW_ORG_ID: 'asdf',
|
||||
NOW_PROJECT_ID: 'asdf',
|
||||
},
|
||||
});
|
||||
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.is(output.stderr.includes('Project not found'), true, formatOutput(output));
|
||||
});
|
||||
|
||||
test('deploy with `NOW_ORG_ID` but without `NOW_PROJECT_ID` should fail', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
|
||||
const output = await execute([directory], {
|
||||
env: { NOW_ORG_ID: 'asdf' },
|
||||
});
|
||||
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.is(
|
||||
output.stderr.includes(
|
||||
'You specified `NOW_ORG_ID` but you forgot to specify `NOW_PROJECT_ID`. You need to specify both to deploy to a custom project.'
|
||||
),
|
||||
true,
|
||||
formatOutput(output)
|
||||
);
|
||||
});
|
||||
|
||||
test('deploy with `NOW_PROJECT_ID` but without `NOW_ORG_ID` should fail', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
|
||||
const output = await execute([directory], {
|
||||
env: { NOW_PROJECT_ID: 'asdf' },
|
||||
});
|
||||
|
||||
t.is(output.exitCode, 1, formatOutput(output));
|
||||
t.is(
|
||||
output.stderr.includes(
|
||||
'You specified `NOW_PROJECT_ID` but you forgot to specify `NOW_ORG_ID`. You need to specify both to deploy to a custom project.'
|
||||
),
|
||||
true,
|
||||
formatOutput(output)
|
||||
);
|
||||
});
|
||||
|
||||
test('deploy with `NOW_ORG_ID` and `NOW_PROJECT_ID`', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
|
||||
// generate `.now`
|
||||
await execute([directory, '--confirm']);
|
||||
|
||||
const link = require(path.join(directory, '.now/project.json'));
|
||||
await remove(path.join(directory, '.now'));
|
||||
|
||||
const output = await execute([directory], {
|
||||
env: {
|
||||
NOW_ORG_ID: link.orgId,
|
||||
NOW_PROJECT_ID: link.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
t.is(output.exitCode, 0, formatOutput(output));
|
||||
t.is(output.stdout.includes('Linked to'), false);
|
||||
});
|
||||
|
||||
test('deploy shows notice when project in `.now` does not exists', async t => {
|
||||
const directory = fixture('static-deployment');
|
||||
|
||||
// overwrite .now with unexisting project
|
||||
await ensureDir(path.join(directory, '.now'));
|
||||
await writeFile(
|
||||
path.join(directory, '.now/project.json'),
|
||||
JSON.stringify({
|
||||
orgId: 'asdf',
|
||||
projectId: 'asdf',
|
||||
})
|
||||
);
|
||||
|
||||
const now = execute([directory]);
|
||||
|
||||
let detectedNotice = false;
|
||||
|
||||
// kill after first prompt
|
||||
await waitForPrompt(now, chunk => {
|
||||
detectedNotice =
|
||||
detectedNotice ||
|
||||
chunk.includes(
|
||||
'Your project was either removed from ZEIT Now or you’re not a member of it anymore'
|
||||
);
|
||||
|
||||
return /Set up and deploy [^?]+\?/.test(chunk);
|
||||
});
|
||||
now.stdin.write('no\n');
|
||||
|
||||
t.is(detectedNotice, true, 'did not detect notice');
|
||||
});
|
||||
|
||||
test.after.always(async () => {
|
||||
// Make sure the token gets revoked
|
||||
await execa(binaryPath, ['logout', ...defaultArgs]);
|
||||
|
||||
@@ -32,10 +32,10 @@ Then call inside a `for...of` loop to follow the progress with the following arg
|
||||
async function deploy() {
|
||||
let deployment;
|
||||
|
||||
for await (const event of createDeployment(
|
||||
'/Users/zeit-user/projects/front',
|
||||
{ token: process.env.TOKEN }
|
||||
)) {
|
||||
for await (const event of createDeployment({
|
||||
token: process.env.TOKEN,
|
||||
path: '/Users/zeit-user/projects/front',
|
||||
})) {
|
||||
if (event.type === 'ready') {
|
||||
deployment = event.payload;
|
||||
break;
|
||||
@@ -50,16 +50,18 @@ Full list of events:
|
||||
|
||||
```js
|
||||
[
|
||||
// File events (receive relevant data as payload)
|
||||
// File events
|
||||
'hashes-calculated',
|
||||
'file-count',
|
||||
'file-uploaded',
|
||||
'all-files-uploaded',
|
||||
// Deployment events (receive deployment object as payload)
|
||||
// Deployment events
|
||||
'created',
|
||||
'building',
|
||||
'ready',
|
||||
'alias-assigned',
|
||||
'warning',
|
||||
'error',
|
||||
// Build events (receive build object as payload)
|
||||
'build-state-changed'
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "now-client",
|
||||
"version": "6.0.2-canary.2",
|
||||
"version": "7.0.0-canary.1",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"homepage": "https://zeit.co",
|
||||
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
} from './utils/ready-state';
|
||||
import { createDebug } from './utils';
|
||||
import {
|
||||
Dictionary,
|
||||
Deployment,
|
||||
NowClientOptions,
|
||||
DeploymentBuild,
|
||||
DeploymentEventType,
|
||||
} from './types';
|
||||
|
||||
interface DeploymentStatus {
|
||||
type: string;
|
||||
type: DeploymentEventType;
|
||||
payload: Deployment | DeploymentBuild[];
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ export async function* checkDeploymentStatus(
|
||||
const debug = createDebug(clientOptions.debug);
|
||||
|
||||
let deploymentState = deployment;
|
||||
let allBuildsCompleted = false;
|
||||
const buildsState: Dictionary<DeploymentBuild> = {};
|
||||
|
||||
const apiDeployments = getApiDeploymentsUrl({
|
||||
version,
|
||||
@@ -52,87 +50,60 @@ export async function* checkDeploymentStatus(
|
||||
|
||||
// Build polling
|
||||
debug('Waiting for builds and the deployment to complete...');
|
||||
let readyEventFired = false;
|
||||
const finishedEvents = new Set();
|
||||
|
||||
while (true) {
|
||||
if (!allBuildsCompleted) {
|
||||
const buildsData = await fetch(
|
||||
`${apiDeployments}/${deployment.id}/builds${
|
||||
teamId ? `?teamId=${teamId}` : ''
|
||||
}`,
|
||||
token,
|
||||
{ apiUrl, userAgent }
|
||||
);
|
||||
// Deployment polling
|
||||
const deploymentData = await fetch(
|
||||
`${apiDeployments}/${deployment.id || deployment.deploymentId}${
|
||||
teamId ? `?teamId=${teamId}` : ''
|
||||
}`,
|
||||
token,
|
||||
{ apiUrl, userAgent }
|
||||
);
|
||||
const deploymentUpdate = await deploymentData.json();
|
||||
|
||||
const data = await buildsData.json();
|
||||
const { builds = [] } = data;
|
||||
if (deploymentUpdate.error) {
|
||||
debug('Deployment status check has errorred');
|
||||
return yield { type: 'error', payload: deploymentUpdate.error };
|
||||
}
|
||||
|
||||
for (const build of builds) {
|
||||
const prevState = buildsState[build.id];
|
||||
if (
|
||||
deploymentUpdate.readyState === 'BUILDING' &&
|
||||
!finishedEvents.has('building')
|
||||
) {
|
||||
debug('Deployment state changed to BUILDING');
|
||||
finishedEvents.add('building');
|
||||
yield { type: 'building', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
if (!prevState || prevState.readyState !== build.readyState) {
|
||||
debug(
|
||||
`Build state for '${build.entrypoint}' changed to ${build.readyState}`
|
||||
);
|
||||
yield { type: 'build-state-changed', payload: build };
|
||||
}
|
||||
if (isReady(deploymentUpdate) && !finishedEvents.has('ready')) {
|
||||
debug('Deployment state changed to READY');
|
||||
finishedEvents.add('ready');
|
||||
yield { type: 'ready', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
if (build.readyState.includes('ERROR')) {
|
||||
debug(`Build '${build.entrypoint}' has errorred`);
|
||||
return yield { type: 'error', payload: build };
|
||||
}
|
||||
if (isAliasAssigned(deploymentUpdate)) {
|
||||
debug('Deployment alias assigned');
|
||||
return yield { type: 'alias-assigned', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
buildsState[build.id] = build;
|
||||
}
|
||||
if (isAliasError(deploymentUpdate)) {
|
||||
return yield { type: 'error', payload: deploymentUpdate.aliasError };
|
||||
}
|
||||
|
||||
const readyBuilds = builds.filter((b: DeploymentBuild) => isDone(b));
|
||||
if (
|
||||
deploymentUpdate.readyState === 'ERROR' &&
|
||||
deploymentUpdate.errorCode === 'BUILD_FAILED'
|
||||
) {
|
||||
return yield { type: 'error', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
if (readyBuilds.length === builds.length) {
|
||||
debug('All builds completed');
|
||||
allBuildsCompleted = true;
|
||||
yield { type: 'all-builds-completed', payload: readyBuilds };
|
||||
}
|
||||
} else {
|
||||
// Deployment polling
|
||||
const deploymentData = await fetch(
|
||||
`${apiDeployments}/${deployment.id || deployment.deploymentId}${
|
||||
teamId ? `?teamId=${teamId}` : ''
|
||||
}`,
|
||||
token,
|
||||
{ apiUrl, userAgent }
|
||||
);
|
||||
const deploymentUpdate = await deploymentData.json();
|
||||
|
||||
if (deploymentUpdate.error) {
|
||||
debug('Deployment status check has errorred');
|
||||
return yield { type: 'error', payload: deploymentUpdate.error };
|
||||
}
|
||||
|
||||
if (isReady(deploymentUpdate) && !readyEventFired) {
|
||||
debug('Deployment state changed to READY 2');
|
||||
readyEventFired = true;
|
||||
yield { type: 'ready', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
if (isAliasAssigned(deploymentUpdate)) {
|
||||
debug('Deployment alias assigned');
|
||||
return yield { type: 'alias-assigned', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
const aliasError = isAliasError(deploymentUpdate);
|
||||
|
||||
if (isFailed(deploymentUpdate) || aliasError) {
|
||||
debug(
|
||||
aliasError
|
||||
? 'Alias assignment error has occurred'
|
||||
: 'Deployment has failed'
|
||||
);
|
||||
return yield {
|
||||
type: 'error',
|
||||
payload: aliasError
|
||||
? deploymentUpdate.aliasError
|
||||
: deploymentUpdate.error || deploymentUpdate,
|
||||
};
|
||||
}
|
||||
if (isFailed(deploymentUpdate)) {
|
||||
return yield {
|
||||
type: 'error',
|
||||
payload: deploymentUpdate.error || deploymentUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
await sleep(ms('1.5s'));
|
||||
|
||||
@@ -6,16 +6,19 @@ import hashes, { mapToObject } from './utils/hashes';
|
||||
import { upload } from './upload';
|
||||
import { getNowIgnore, createDebug, parseNowJSON } from './utils';
|
||||
import { DeploymentError } from './errors';
|
||||
import { NowConfig, NowClientOptions, DeploymentOptions } from './types';
|
||||
|
||||
export { EVENTS } from './utils';
|
||||
import {
|
||||
NowConfig,
|
||||
NowClientOptions,
|
||||
DeploymentOptions,
|
||||
DeploymentEventType,
|
||||
} from './types';
|
||||
|
||||
export default function buildCreateDeployment(version: number) {
|
||||
return async function* createDeployment(
|
||||
clientOptions: NowClientOptions,
|
||||
deploymentOptions: DeploymentOptions,
|
||||
deploymentOptions: DeploymentOptions = {},
|
||||
nowConfig: NowConfig = {}
|
||||
): AsyncIterableIterator<any> {
|
||||
): AsyncIterableIterator<{ type: DeploymentEventType; payload: any }> {
|
||||
const { path } = clientOptions;
|
||||
|
||||
const debug = createDebug(clientOptions.debug);
|
||||
|
||||
@@ -13,10 +13,9 @@ import {
|
||||
DeploymentOptions,
|
||||
NowConfig,
|
||||
NowClientOptions,
|
||||
DeploymentEventType,
|
||||
} from './types';
|
||||
|
||||
type DeploymentEventType = 'warning' | 'tip' | 'error' | 'notice' | 'created';
|
||||
|
||||
async function* createDeployment(
|
||||
files: Map<string, DeploymentFile>,
|
||||
clientOptions: NowClientOptions,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Builder, BuilderFunctions } from '@now/build-utils';
|
||||
import { NowHeader, Route, NowRedirect, NowRewrite } from '@now/routing-utils';
|
||||
|
||||
export { DeploymentEventType } from './utils';
|
||||
|
||||
export interface Dictionary<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function* upload(
|
||||
|
||||
const shas = missingFiles;
|
||||
|
||||
yield { type: 'file_count', payload: { total: files, missing: shas } };
|
||||
yield { type: 'file-count', payload: { total: files, missing: shas } };
|
||||
|
||||
const uploadList: { [key: string]: Promise<any> } = {};
|
||||
debug('Building an upload list...');
|
||||
|
||||
@@ -14,21 +14,25 @@ const semaphore = new Sema(10);
|
||||
export const API_FILES = '/v2/now/files';
|
||||
export const API_DELETE_DEPLOYMENTS_LEGACY = '/v2/now/deployments';
|
||||
|
||||
export const EVENTS = new Set([
|
||||
const EVENTS_ARRAY = [
|
||||
// File events
|
||||
'hashes-calculated',
|
||||
'file_count',
|
||||
'file-count',
|
||||
'file-uploaded',
|
||||
'all-files-uploaded',
|
||||
// Deployment events
|
||||
'created',
|
||||
'building',
|
||||
'ready',
|
||||
'alias-assigned',
|
||||
'warning',
|
||||
'error',
|
||||
// Build events
|
||||
'build-state-changed',
|
||||
]);
|
||||
'notice',
|
||||
'tip',
|
||||
] as const;
|
||||
|
||||
export type DeploymentEventType = (typeof EVENTS_ARRAY)[number];
|
||||
export const EVENTS = new Set(EVENTS_ARRAY);
|
||||
|
||||
export function getApiDeploymentsUrl(
|
||||
metadata?: Pick<DeploymentOptions, 'version' | 'builds' | 'functions'>
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('create v2 deployment', () => {
|
||||
name: 'now-client-tests-v2',
|
||||
}
|
||||
)) {
|
||||
if (event.type === 'file_count') {
|
||||
if (event.type === 'file-count') {
|
||||
expect(event.payload.total).toEqual(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/next",
|
||||
"version": "2.3.9",
|
||||
"version": "2.3.12",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/next-js",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/resolve-from": "5.0.1",
|
||||
"@types/semver": "6.0.0",
|
||||
"@types/yazl": "2.4.1",
|
||||
"@zeit/node-file-trace": "0.4.1",
|
||||
"@zeit/node-file-trace": "0.5.1",
|
||||
"async-sema": "3.0.1",
|
||||
"buffer-crc32": "0.2.13",
|
||||
"execa": "2.0.4",
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
runNpmInstall,
|
||||
runPackageJsonScript,
|
||||
} from '@now/build-utils';
|
||||
import { Route } from '@now/routing-utils';
|
||||
import { Route, Source } from '@now/routing-utils';
|
||||
import {
|
||||
convertRedirects,
|
||||
convertRewrites,
|
||||
convertHeaders,
|
||||
} from '@now/routing-utils/dist/superstatic';
|
||||
import nodeFileTrace, { NodeFileTraceReasons } from '@zeit/node-file-trace';
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
@@ -334,6 +335,7 @@ export const build = async ({
|
||||
await runPackageJsonScript(entryPath, shouldRunScript, { ...spawnOpts, env });
|
||||
|
||||
const routesManifest = await getRoutesManifest(entryPath, realNextVersion);
|
||||
const headers: Route[] = [];
|
||||
const rewrites: Route[] = [];
|
||||
const redirects: Route[] = [];
|
||||
const nextBasePathRoute: Route[] = [];
|
||||
@@ -345,6 +347,11 @@ export const build = async ({
|
||||
case 2: {
|
||||
redirects.push(...convertRedirects(routesManifest.redirects));
|
||||
rewrites.push(...convertRewrites(routesManifest.rewrites));
|
||||
|
||||
if (routesManifest.headers) {
|
||||
headers.push(...convertHeaders(routesManifest.headers));
|
||||
}
|
||||
|
||||
if (routesManifest.basePath && routesManifest.basePath !== '/') {
|
||||
nextBasePath = routesManifest.basePath;
|
||||
|
||||
@@ -425,8 +432,12 @@ export const build = async ({
|
||||
// Add top level rewrite for basePath if provided
|
||||
...nextBasePathRoute,
|
||||
|
||||
// redirects take the highest priority
|
||||
// User headers
|
||||
...headers,
|
||||
|
||||
// User redirects
|
||||
...redirects,
|
||||
|
||||
// Before we handle static files we need to set proper caching headers
|
||||
{
|
||||
// This ensures we only match known emitted-by-Next.js files and not
|
||||
@@ -447,10 +458,21 @@ export const build = async ({
|
||||
// Next.js pages, `static/` folder, reserved assets, and `public/`
|
||||
// folder
|
||||
{ handle: 'filesystem' },
|
||||
...rewrites,
|
||||
|
||||
...rewrites,
|
||||
// Dynamic routes
|
||||
// TODO: do we want to do this?: ...dynamicRoutes,
|
||||
|
||||
// 404
|
||||
...(output['404']
|
||||
? [
|
||||
{
|
||||
src: path.join('/', entryDirectory, '.*'),
|
||||
dest: path.join('/', entryDirectory, '404'),
|
||||
status: 404,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
watch: [],
|
||||
childProcesses: [],
|
||||
@@ -475,7 +497,7 @@ export const build = async ({
|
||||
const prerenders: { [key: string]: Prerender | FileFsRef } = {};
|
||||
const staticPages: { [key: string]: FileFsRef } = {};
|
||||
const dynamicPages: string[] = [];
|
||||
const dynamicDataRoutes: Array<{ src: string; dest: string }> = [];
|
||||
const dynamicDataRoutes: Array<Source> = [];
|
||||
|
||||
const appMountPrefixNoTrailingSlash = path.posix
|
||||
.join('/', entryDirectory)
|
||||
@@ -612,6 +634,8 @@ export const build = async ({
|
||||
});
|
||||
|
||||
const pageKeys = Object.keys(pages);
|
||||
// > 1 because _error is a lambda but isn't used if a static 404 is available
|
||||
const hasLambdas = !staticPages['_errors/404'] || pageKeys.length > 1;
|
||||
|
||||
if (pageKeys.length === 0) {
|
||||
const nextConfig = await getNextConfig(workPath, entryPath);
|
||||
@@ -628,7 +652,7 @@ export const build = async ({
|
||||
}
|
||||
|
||||
// Assume tracing to be safe, bail if we know we don't need it.
|
||||
let requiresTracing = true;
|
||||
let requiresTracing = hasLambdas;
|
||||
try {
|
||||
if (
|
||||
realNextVersion &&
|
||||
@@ -745,7 +769,10 @@ export const build = async ({
|
||||
const launcherPath = path.join(__dirname, 'templated-launcher.js');
|
||||
const launcherData = await readFile(launcherPath, 'utf8');
|
||||
const allLambdasLabel = `All serverless functions created in`;
|
||||
console.time(allLambdasLabel);
|
||||
|
||||
if (hasLambdas) {
|
||||
console.time(allLambdasLabel);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
pageKeys.map(async page => {
|
||||
@@ -754,6 +781,11 @@ export const build = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't create _error lambda if we have a static 404 page
|
||||
if (staticPages['_errors/404'] && page === '_error.js') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathname = page.replace(/\.js$/, '');
|
||||
|
||||
if (isDynamicRoute(pathname)) {
|
||||
@@ -807,7 +839,10 @@ export const build = async ({
|
||||
}
|
||||
})
|
||||
);
|
||||
console.timeEnd(allLambdasLabel);
|
||||
|
||||
if (hasLambdas) {
|
||||
console.timeEnd(allLambdasLabel);
|
||||
}
|
||||
|
||||
let prerenderGroup = 1;
|
||||
const onPrerenderRoute = (routeKey: string, isLazy: boolean) => {
|
||||
@@ -965,11 +1000,24 @@ export const build = async ({
|
||||
...staticFiles,
|
||||
...staticDirectoryFiles,
|
||||
},
|
||||
/*
|
||||
Desired routes order
|
||||
- Runtime headers
|
||||
- User headers and redirects
|
||||
- Runtime redirects
|
||||
- Runtime routes
|
||||
- Check filesystem, if nothing found continue
|
||||
- User rewrites
|
||||
- Builder rewrites
|
||||
*/
|
||||
routes: [
|
||||
// Add top level rewrite for basePath if provided
|
||||
...nextBasePathRoute,
|
||||
|
||||
// redirects take the highest priority
|
||||
// headers
|
||||
...headers,
|
||||
|
||||
// redirects
|
||||
...redirects,
|
||||
// Before we handle static files we need to set proper caching headers
|
||||
{
|
||||
@@ -989,6 +1037,7 @@ export const build = async ({
|
||||
// Next.js page lambdas, `static/` folder, reserved assets, and `public/`
|
||||
// folder
|
||||
{ handle: 'filesystem' },
|
||||
|
||||
...rewrites,
|
||||
// Dynamic routes
|
||||
...dynamicRoutes,
|
||||
@@ -999,7 +1048,11 @@ export const build = async ({
|
||||
: [
|
||||
{
|
||||
src: path.join('/', entryDirectory, '.*'),
|
||||
dest: path.join('/', entryDirectory, '_error'),
|
||||
dest: path.join(
|
||||
'/',
|
||||
entryDirectory,
|
||||
staticPages['_errors/404'] ? '_errors/404' : '_error'
|
||||
),
|
||||
status: 404,
|
||||
},
|
||||
]),
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Lambda,
|
||||
isSymbolicLink,
|
||||
} from '@now/build-utils';
|
||||
import { Route, Source } from '@now/routing-utils';
|
||||
import { Route, Source, NowHeader, NowRewrite } from '@now/routing-utils';
|
||||
|
||||
type stringMap = { [key: string]: string };
|
||||
|
||||
@@ -259,7 +259,7 @@ async function getRoutes(
|
||||
routes.push(
|
||||
...(await getDynamicRoutes(entryPath, entryDirectory, dynamicPages).then(
|
||||
arr =>
|
||||
arr.map((route: { src: string; dest: string }) => {
|
||||
arr.map((route: Source) => {
|
||||
// convert to make entire RegExp match as one group
|
||||
route.src = route.src
|
||||
.replace('^', `^${prefix}(`)
|
||||
@@ -293,13 +293,11 @@ async function getRoutes(
|
||||
return routes;
|
||||
}
|
||||
|
||||
export type Rewrite = {
|
||||
source: string;
|
||||
destination: string;
|
||||
};
|
||||
|
||||
export type Redirect = Rewrite & {
|
||||
// TODO: update to use type from @now/routing-utils after
|
||||
// adding permanent: true/false handling
|
||||
export type Redirect = NowRewrite & {
|
||||
statusCode?: number;
|
||||
permanent?: boolean;
|
||||
};
|
||||
|
||||
type RoutesManifestRegex = {
|
||||
@@ -310,7 +308,8 @@ type RoutesManifestRegex = {
|
||||
export type RoutesManifest = {
|
||||
basePath: string | undefined;
|
||||
redirects: (Redirect & RoutesManifestRegex)[];
|
||||
rewrites: (Rewrite & RoutesManifestRegex)[];
|
||||
rewrites: (NowRewrite & RoutesManifestRegex)[];
|
||||
headers?: (NowHeader & RoutesManifestRegex)[];
|
||||
dynamicRoutes: {
|
||||
page: string;
|
||||
regex: string;
|
||||
@@ -354,7 +353,7 @@ export async function getDynamicRoutes(
|
||||
dynamicPages: string[],
|
||||
isDev?: boolean,
|
||||
routesManifest?: RoutesManifest
|
||||
): Promise<{ src: string; dest: string }[]> {
|
||||
): Promise<Source[]> {
|
||||
if (!dynamicPages.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -424,7 +423,7 @@ export async function getDynamicRoutes(
|
||||
matcher: getRouteRegex && getRouteRegex(pageName).re,
|
||||
}));
|
||||
|
||||
const routes: { src: string; dest: string }[] = [];
|
||||
const routes: Source[] = [];
|
||||
pageMatchers.forEach(pageMatcher => {
|
||||
// in `now dev` we don't need to prefix the destination
|
||||
const dest = !isDev
|
||||
@@ -466,6 +465,7 @@ export type PseudoFile = {
|
||||
crc32: number;
|
||||
compBuffer: Buffer;
|
||||
uncompressedSize: number;
|
||||
mode: number;
|
||||
};
|
||||
|
||||
export type PseudoSymbolicLink = {
|
||||
@@ -500,15 +500,17 @@ export async function createPseudoLayer(files: {
|
||||
file,
|
||||
isSymlink: true,
|
||||
symlinkTarget: await fs.readlink(file.fsPath),
|
||||
} as PseudoSymbolicLink;
|
||||
};
|
||||
} else {
|
||||
const origBuffer = await streamToBuffer(file.toStream());
|
||||
const compBuffer = await compressBuffer(origBuffer);
|
||||
pseudoLayer[fileName] = {
|
||||
compBuffer,
|
||||
isSymlink: false,
|
||||
crc32: crc32.unsigned(origBuffer),
|
||||
uncompressedSize: origBuffer.byteLength,
|
||||
} as PseudoFile;
|
||||
mode: file.mode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,12 +568,14 @@ export async function createLambdaFromPseudoLayers({
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const { compBuffer, crc32, uncompressedSize } = item;
|
||||
|
||||
const { compBuffer, crc32, uncompressedSize, mode } = item;
|
||||
|
||||
// @ts-ignore: `addDeflatedBuffer` is a valid function, but missing on the type
|
||||
zipFile.addDeflatedBuffer(compBuffer, seedKey, {
|
||||
crc32,
|
||||
uncompressedSize,
|
||||
mode: mode,
|
||||
});
|
||||
|
||||
addedFiles.add(seedKey);
|
||||
|
||||
@@ -5,38 +5,78 @@ module.exports = {
|
||||
{
|
||||
source: '/redir1',
|
||||
destination: '/redir2',
|
||||
statusCode: 301
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/redir2',
|
||||
destination: '/hello',
|
||||
statusCode: 307
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: '/redir/:path',
|
||||
destination: '/:path'
|
||||
}
|
||||
]
|
||||
destination: '/:path',
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/rewrite1',
|
||||
destination: '/rewrite2'
|
||||
destination: '/rewrite2',
|
||||
},
|
||||
{
|
||||
source: '/rewrite2',
|
||||
destination: '/hello'
|
||||
destination: '/hello',
|
||||
},
|
||||
{
|
||||
source: '/rewrite/:first/:second',
|
||||
destination: '/rewrite-2/hello/:second'
|
||||
destination: '/rewrite-2/hello/:second',
|
||||
},
|
||||
{
|
||||
source: '/rewrite-2/:first/:second',
|
||||
destination: '/params'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
destination: '/params',
|
||||
},
|
||||
{
|
||||
source: '/add-header',
|
||||
destination: '/hello',
|
||||
},
|
||||
{
|
||||
source: '/catchall-header/:path*',
|
||||
destination: '/hello',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/add-header',
|
||||
headers: [
|
||||
{
|
||||
key: 'x-hello',
|
||||
value: 'world',
|
||||
},
|
||||
{
|
||||
key: 'x-another',
|
||||
value: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/catchall-header/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'x-hello',
|
||||
value: 'world',
|
||||
},
|
||||
{
|
||||
key: 'x-another',
|
||||
value: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"fetchOptions": {
|
||||
"redirect": "manual"
|
||||
},
|
||||
"status": 301,
|
||||
"status": 308,
|
||||
"responseHeaders": {
|
||||
"location": "/redir2/"
|
||||
}
|
||||
@@ -47,6 +47,20 @@
|
||||
{
|
||||
"path": "/rewrite/THIS_SHOULD_BE_GONE/another",
|
||||
"mustContain": "another"
|
||||
},
|
||||
{
|
||||
"path": "/add-header",
|
||||
"responseHeaders": {
|
||||
"x-hello": "world",
|
||||
"x-another": "value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/catchall-header/first",
|
||||
"responseHeaders": {
|
||||
"x-hello": "world",
|
||||
"x-another": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "^9.1.4-canary.1",
|
||||
"next": "9.2.1-canary.8",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "^9.1.4-canary.1",
|
||||
"next": "9.2.1-canary.8",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ module.exports = {
|
||||
{
|
||||
source: '/second',
|
||||
destination: '/about',
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
8
packages/now-next/test/fixtures/17-static-404/next.config.js
vendored
Normal file
8
packages/now-next/test/fixtures/17-static-404/next.config.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
generateBuildId() {
|
||||
return 'testing-build-id';
|
||||
},
|
||||
experimental: {
|
||||
static404: true,
|
||||
},
|
||||
};
|
||||
28
packages/now-next/test/fixtures/17-static-404/now.json
vendored
Normal file
28
packages/now-next/test/fixtures/17-static-404/now.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "package.json", "use": "@now/next" }],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/",
|
||||
"mustContain": "Hi"
|
||||
},
|
||||
{
|
||||
"path": "/",
|
||||
"responseHeaders": {
|
||||
"x-now-cache": "HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/non-existent",
|
||||
"mustContain": "page could not be found"
|
||||
},
|
||||
{
|
||||
"path": "/non-existent",
|
||||
"mustContain": "__next"
|
||||
},
|
||||
{
|
||||
"path": "/_errors/404",
|
||||
"mustContain": "__next"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/now-next/test/fixtures/17-static-404/package.json
vendored
Normal file
7
packages/now-next/test/fixtures/17-static-404/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "9.2.1-canary.3",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
1
packages/now-next/test/fixtures/17-static-404/pages/index.js
vendored
Normal file
1
packages/now-next/test/fixtures/17-static-404/pages/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export default () => 'Hi';
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/node",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/node-js",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@types/etag": "1.8.0",
|
||||
"@types/test-listen": "1.1.0",
|
||||
"@zeit/ncc": "0.20.4",
|
||||
"@zeit/node-file-trace": "0.4.1",
|
||||
"@zeit/node-file-trace": "0.5.1",
|
||||
"content-type": "1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"etag": "1.8.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/routing-utils",
|
||||
"version": "1.5.2-canary.0",
|
||||
"version": "1.5.2-canary.2",
|
||||
"description": "ZEIT Now routing utilities",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -96,6 +96,12 @@ export function normalizeRoutes(inputRoutes: Route[] | null): NormalizedRoutes {
|
||||
src: route.src,
|
||||
});
|
||||
}
|
||||
if (route.status) {
|
||||
errors.push({
|
||||
message: `You cannot assign "status" after "handle: hit"`,
|
||||
src: route.src,
|
||||
});
|
||||
}
|
||||
if (!route.continue) {
|
||||
errors.push({
|
||||
message: `You must assign "continue: true" after "handle: hit"`,
|
||||
@@ -280,6 +286,33 @@ export function getTransformedRoutes({
|
||||
}
|
||||
|
||||
if (typeof headers !== 'undefined') {
|
||||
const code = 'invalid_headers';
|
||||
const errorsRegex = headers
|
||||
.map(r => checkRegexSyntax(r.source))
|
||||
.filter(notEmpty);
|
||||
if (errorsRegex.length > 0) {
|
||||
return {
|
||||
routes,
|
||||
error: createNowError(
|
||||
code,
|
||||
'Headers `source` contains invalid regex. Read more: https://err.sh/now/invalid-route-source',
|
||||
errorsRegex
|
||||
),
|
||||
};
|
||||
}
|
||||
const errorsPattern = headers
|
||||
.map(r => checkPatternSyntax(r.source))
|
||||
.filter(notEmpty);
|
||||
if (errorsPattern.length > 0) {
|
||||
return {
|
||||
routes,
|
||||
error: createNowError(
|
||||
code,
|
||||
'Headers `source` contains invalid pattern. Read more: https://err.sh/now/invalid-route-source',
|
||||
errorsPattern
|
||||
),
|
||||
};
|
||||
}
|
||||
const normalized = normalizeRoutes(convertHeaders(headers));
|
||||
if (normalized.error) {
|
||||
normalized.error.code = 'invalid_headers';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user