mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-26 11:49:15 +00:00
Compare commits
33 Commits
@now/stati
...
@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 |
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",
|
||||
"version": "0.0.7",
|
||||
"main": "frameworks.json",
|
||||
"license": "UNLICENSED"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/build-utils",
|
||||
"version": "1.3.6",
|
||||
"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.14",
|
||||
"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(
|
||||
|
||||
@@ -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,7 +360,7 @@ 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) {
|
||||
|
||||
@@ -130,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`
|
||||
);
|
||||
@@ -190,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();
|
||||
}
|
||||
@@ -203,7 +200,7 @@ export default async function processDeployment({
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'all-builds-completed') {
|
||||
if (event.type === 'ready') {
|
||||
if (queuedSpinner) {
|
||||
queuedSpinner();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ 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]> {
|
||||
@@ -67,6 +68,15 @@ export async function getLinkedProject(
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
70
packages/now-cli/test/integration.js
vendored
70
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';
|
||||
@@ -2488,7 +2488,7 @@ test('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`', async t => {
|
||||
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], {
|
||||
@@ -2502,6 +2502,40 @@ test('deploy with unknown `NOW_ORG_ID` and `NOW_PROJECT_ID`', async t => {
|
||||
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');
|
||||
|
||||
@@ -2522,6 +2556,38 @@ test('deploy with `NOW_ORG_ID` and `NOW_PROJECT_ID`', async t => {
|
||||
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.11-canary.0",
|
||||
"version": "2.3.12",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/next-js",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -37,6 +37,45 @@ module.exports = {
|
||||
source: '/rewrite-2/:first/:second',
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
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/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';
|
||||
|
||||
@@ -71,11 +71,29 @@ export function convertRewrites(rewrites: NowRewrite[]): Route[] {
|
||||
export function convertHeaders(headers: NowHeader[]): Route[] {
|
||||
return headers.map(h => {
|
||||
const obj: { [key: string]: string } = {};
|
||||
h.headers.forEach(kv => {
|
||||
obj[kv.key] = kv.value;
|
||||
const { src, segments } = sourceToRegex(h.source);
|
||||
const hasSegments = segments.length > 0;
|
||||
const indexes: { [k: string]: string } = {};
|
||||
|
||||
segments.forEach((name, index) => {
|
||||
indexes[name] = toSegmentDest(index);
|
||||
});
|
||||
|
||||
h.headers.forEach(({ key, value }) => {
|
||||
if (hasSegments) {
|
||||
if (key.includes(':')) {
|
||||
const keyCompiler = compile(key);
|
||||
key = keyCompiler(indexes);
|
||||
}
|
||||
if (value.includes(':')) {
|
||||
const valueCompiler = compile(value);
|
||||
value = valueCompiler(indexes);
|
||||
}
|
||||
}
|
||||
obj[key] = value;
|
||||
});
|
||||
const route: Route = {
|
||||
src: h.source,
|
||||
src,
|
||||
headers: obj,
|
||||
continue: true,
|
||||
};
|
||||
@@ -112,32 +130,50 @@ export function sourceToRegex(
|
||||
|
||||
function replaceSegments(segments: string[], destination: string): string {
|
||||
const parsedDestination = parseUrl(destination, true);
|
||||
let { pathname, hash } = parsedDestination;
|
||||
delete parsedDestination.href;
|
||||
delete parsedDestination.path;
|
||||
delete parsedDestination.search;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { pathname, hash, query, ...rest } = parsedDestination;
|
||||
pathname = pathname || '';
|
||||
hash = hash || '';
|
||||
|
||||
if ((pathname + hash).includes(':') && segments.length > 0) {
|
||||
const pathnameCompiler = compile(pathname);
|
||||
const hashCompiler = compile(hash);
|
||||
if (segments.length > 0) {
|
||||
const indexes: { [k: string]: string } = {};
|
||||
|
||||
segments.forEach((name, index) => {
|
||||
indexes[name] = toSegmentDest(index);
|
||||
});
|
||||
pathname = pathnameCompiler(indexes);
|
||||
hash = hash ? `${hashCompiler(indexes)}` : null;
|
||||
|
||||
if (destination.includes(':')) {
|
||||
const pathnameCompiler = compile(pathname);
|
||||
const hashCompiler = compile(hash);
|
||||
pathname = pathnameCompiler(indexes);
|
||||
hash = hash ? `${hashCompiler(indexes)}` : null;
|
||||
|
||||
for (const [key, strOrArray] of Object.entries(query)) {
|
||||
let value = Array.isArray(strOrArray) ? strOrArray[0] : strOrArray;
|
||||
if (value) {
|
||||
const queryCompiler = compile(value);
|
||||
value = queryCompiler(indexes);
|
||||
}
|
||||
query[key] = value;
|
||||
}
|
||||
} else {
|
||||
for (const [name, value] of Object.entries(indexes)) {
|
||||
query[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
destination = formatUrl({
|
||||
...parsedDestination,
|
||||
...rest,
|
||||
pathname,
|
||||
query,
|
||||
hash,
|
||||
});
|
||||
} else if (segments.length > 0) {
|
||||
let prefix = '?';
|
||||
segments.forEach((name, index) => {
|
||||
destination += `${prefix}${name}=${toSegmentDest(index)}`;
|
||||
prefix = '&';
|
||||
});
|
||||
|
||||
// url.format() escapes the query string but we must preserve dollar signs
|
||||
destination = destination.replace(/=%24/g, '=$');
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
|
||||
40
packages/now-routing-utils/test/index.spec.js
vendored
40
packages/now-routing-utils/test/index.spec.js
vendored
@@ -494,6 +494,26 @@ describe('normalizeRoutes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if routes after `handle: hit` use `status', () => {
|
||||
const input = [
|
||||
{
|
||||
handle: 'hit',
|
||||
},
|
||||
{
|
||||
src: '^/(.*)$',
|
||||
status: 404,
|
||||
continue: true,
|
||||
},
|
||||
];
|
||||
const { error } = normalizeRoutes(input);
|
||||
|
||||
assert.deepEqual(error.code, 'invalid_routes');
|
||||
assert.deepEqual(
|
||||
error.errors[0].message,
|
||||
'You cannot assign "status" after "handle: hit"'
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if routes after `handle: miss` do not use `check: true`', () => {
|
||||
const input = [
|
||||
{
|
||||
@@ -577,6 +597,26 @@ describe('getTransformedRoutes', () => {
|
||||
assert.equal(actual.error.code, 'invalid_redirects');
|
||||
});
|
||||
|
||||
test('should error when headers is invalid regex', () => {
|
||||
const nowConfig = {
|
||||
headers: [{ source: '^/(*.)\\.html$', destination: '/file.html' }],
|
||||
};
|
||||
const actual = getTransformedRoutes({ nowConfig });
|
||||
assert.notEqual(actual.error, null);
|
||||
assert.equal(actual.error.code, 'invalid_headers');
|
||||
});
|
||||
|
||||
test('should error when headers is invalid pattern', () => {
|
||||
const nowConfig = {
|
||||
headers: [
|
||||
{ source: '/:?', headers: [{ key: 'x-hello', value: 'world' }] },
|
||||
],
|
||||
};
|
||||
const actual = getTransformedRoutes({ nowConfig });
|
||||
assert.notEqual(actual.error, null);
|
||||
assert.equal(actual.error.code, 'invalid_headers');
|
||||
});
|
||||
|
||||
test('should error when rewrites is invalid regex', () => {
|
||||
const nowConfig = {
|
||||
rewrites: [{ source: '^/(*.)\\.html$', destination: '/file.html' }],
|
||||
|
||||
@@ -301,6 +301,23 @@ test('convertRewrites', () => {
|
||||
{ source: '/some/old/path', destination: '/some/new/path' },
|
||||
{ source: '/firebase/(.*)', destination: 'https://www.firebase.com' },
|
||||
{ source: '/projects/:id/edit', destination: '/projects.html' },
|
||||
{
|
||||
source: '/users/:id',
|
||||
destination: '/api/user?identifier=:id&version=v2',
|
||||
},
|
||||
{
|
||||
source: '/:file/:id',
|
||||
destination: '/:file/get?identifier=:id',
|
||||
},
|
||||
{
|
||||
source: '/qs-and-hash/:id/:hash',
|
||||
destination: '/api/get?identifier=:id#:hash',
|
||||
},
|
||||
{
|
||||
source: '/fullurl',
|
||||
destination:
|
||||
'https://user:pass@sub.example.com:8080/path/goes/here?v=1&id=2#hash',
|
||||
},
|
||||
{ source: '/catchall/:hello*/', destination: '/catchall/:hello*' },
|
||||
{
|
||||
source: '/another-catch/:hello+/',
|
||||
@@ -329,6 +346,27 @@ test('convertRewrites', () => {
|
||||
dest: '/projects.html?id=$1',
|
||||
check: true,
|
||||
},
|
||||
{
|
||||
src: '^\\/users(?:\\/([^\\/#\\?]+?))$',
|
||||
dest: '/api/user?identifier=$1&version=v2',
|
||||
check: true,
|
||||
},
|
||||
{
|
||||
src: '^(?:\\/([^\\/#\\?]+?))(?:\\/([^\\/#\\?]+?))$',
|
||||
dest: '/$1/get?identifier=$2',
|
||||
check: true,
|
||||
},
|
||||
{
|
||||
src: '^\\/qs-and-hash(?:\\/([^\\/#\\?]+?))(?:\\/([^\\/#\\?]+?))$',
|
||||
dest: '/api/get?identifier=$1#$2',
|
||||
check: true,
|
||||
},
|
||||
{
|
||||
src: '^\\/fullurl$',
|
||||
dest:
|
||||
'https://user:pass@sub.example.com:8080/path/goes/here?v=1&id=2#hash',
|
||||
check: true,
|
||||
},
|
||||
{
|
||||
src: '^\\/catchall(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?\\/$',
|
||||
dest: '/catchall/$1',
|
||||
@@ -363,6 +401,10 @@ test('convertRewrites', () => {
|
||||
['/some/old/path'],
|
||||
['/firebase/one', '/firebase/two'],
|
||||
['/projects/one/edit', '/projects/two/edit'],
|
||||
['/users/four', '/users/five'],
|
||||
['/file1/yep', '/file2/nope'],
|
||||
['/qs-and-hash/test/first', '/qs-and-hash/test/second'],
|
||||
['/fullurl'],
|
||||
['/catchall/first/', '/catchall/first/second/'],
|
||||
['/another-catch/first/', '/another-catch/first/second/'],
|
||||
['/firebase/admin', '/firebase/anotherAdmin'],
|
||||
@@ -374,6 +416,10 @@ test('convertRewrites', () => {
|
||||
['/nope'],
|
||||
['/fire', '/firebasejumper/two'],
|
||||
['/projects/edit', '/projects/two/delete', '/projects'],
|
||||
['/users/edit/four', '/users/five/delete', '/users'],
|
||||
['/'],
|
||||
['/qs-and-hash', '/qs-and-hash/onlyone'],
|
||||
['/full'],
|
||||
['/random-catch/'],
|
||||
['/another-catch/'],
|
||||
['/firebase/user/1', '/firebase/another/1'],
|
||||
@@ -408,19 +454,40 @@ test('convertHeaders', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/blog/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'on-blog',
|
||||
value: ':path*',
|
||||
},
|
||||
{
|
||||
key: ':path*',
|
||||
value: 'blog',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const expected = [
|
||||
{
|
||||
src: '(.*)+/(.*)\\.(eot|otf|ttf|ttc|woff|font\\.css)',
|
||||
src: '^(.*)+(?:\\/(.*))\\.(eot|otf|ttf|ttc|woff|font\\.css)$',
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
continue: true,
|
||||
},
|
||||
{
|
||||
src: '404.html',
|
||||
src: '^404\\.html$',
|
||||
headers: { 'Cache-Control': 'max-age=300', 'Set-Cookie': 'error=404' },
|
||||
continue: true,
|
||||
},
|
||||
{
|
||||
continue: true,
|
||||
headers: {
|
||||
'on-blog': '$1',
|
||||
$1: 'blog',
|
||||
},
|
||||
src: '^\\/blog(?:\\/((?:[^\\/#\\?]+?)(?:\\/(?:[^\\/#\\?]+?))*))?$',
|
||||
},
|
||||
];
|
||||
|
||||
deepEqual(actual, expected);
|
||||
@@ -428,11 +495,13 @@ test('convertHeaders', () => {
|
||||
const mustMatch = [
|
||||
['hello/world/file.eot', 'another/font.ttf', 'dir/arial.font.css'],
|
||||
['404.html'],
|
||||
['/blog/first-post', '/blog/another/one'],
|
||||
];
|
||||
|
||||
const mustNotMatch = [
|
||||
['hello/file.jpg', 'hello/font-css', 'dir/arial.font-css'],
|
||||
['403.html', '500.html'],
|
||||
['/blogg', '/random'],
|
||||
];
|
||||
|
||||
assertRegexMatches(actual, mustMatch, mustNotMatch);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/static-build",
|
||||
"version": "0.14.9",
|
||||
"version": "0.14.11-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/static-builds",
|
||||
|
||||
@@ -384,7 +384,7 @@ const frameworkList: Framework[] = [
|
||||
{
|
||||
name: 'Hugo',
|
||||
slug: 'hugo',
|
||||
buildCommand: 'hugo -D',
|
||||
buildCommand: 'hugo -D --gc',
|
||||
getOutputDirName: async (dirPrefix: string): Promise<string> => {
|
||||
const config = await readConfigFile(
|
||||
['config.json', 'config.yaml', 'config.toml'].map(fileName => {
|
||||
|
||||
@@ -47,7 +47,7 @@ async function checkForPort(
|
||||
}
|
||||
}
|
||||
|
||||
function validateDistDir(distDir: string, config: Config) {
|
||||
function validateDistDir(distDir: string) {
|
||||
const distDirName = path.basename(distDir);
|
||||
const exists = () => existsSync(distDir);
|
||||
const isDirectory = () => statSync(distDir).isDirectory();
|
||||
@@ -56,14 +56,10 @@ function validateDistDir(distDir: string, config: Config) {
|
||||
const link =
|
||||
'https://zeit.co/docs/v2/platform/frequently-asked-questions#missing-public-directory';
|
||||
|
||||
const legacyMsg = !config.zeroConfig
|
||||
? '\nMake sure you configure the the correct `distDir`.'
|
||||
: '';
|
||||
|
||||
if (!exists()) {
|
||||
throw new NowBuildError({
|
||||
code: 'NOW_STATIC_BUILD_NO_OUT_DIR',
|
||||
message: `No Output Directory named "${distDirName}" found after the Build completed. ${legacyMsg}`,
|
||||
message: `No Output Directory named "${distDirName}" found after the Build completed.`,
|
||||
link,
|
||||
});
|
||||
}
|
||||
@@ -71,7 +67,7 @@ function validateDistDir(distDir: string, config: Config) {
|
||||
if (!isDirectory()) {
|
||||
throw new NowBuildError({
|
||||
code: 'NOW_STATIC_BUILD_NOT_A_DIR',
|
||||
message: `The path specified as Output Directory ("${distDirName}") is not actually a directory. ${legacyMsg}`,
|
||||
message: `The path specified as Output Directory ("${distDirName}") is not actually a directory.`,
|
||||
link,
|
||||
});
|
||||
}
|
||||
@@ -79,7 +75,7 @@ function validateDistDir(distDir: string, config: Config) {
|
||||
if (isEmpty()) {
|
||||
throw new NowBuildError({
|
||||
code: 'NOW_STATIC_BUILD_EMPTY_OUT_DIR',
|
||||
message: `The Output Directory "${distDirName}" is empty. ${legacyMsg}`,
|
||||
message: `The Output Directory "${distDirName}" is empty.`,
|
||||
link,
|
||||
});
|
||||
}
|
||||
@@ -462,7 +458,7 @@ export async function build({
|
||||
}
|
||||
}
|
||||
|
||||
validateDistDir(distPath, config);
|
||||
validateDistDir(distPath);
|
||||
|
||||
if (framework) {
|
||||
const frameworkRoutes = await getFrameworkRoutes(
|
||||
@@ -489,7 +485,7 @@ export async function build({
|
||||
);
|
||||
const spawnOpts = getSpawnOptions(meta, nodeVersion);
|
||||
await runShellScript(path.join(workPath, entrypoint), [], spawnOpts);
|
||||
validateDistDir(distPath, config);
|
||||
validateDistDir(distPath);
|
||||
|
||||
const output = await glob('**', distPath, mountpoint);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user