Compare commits

...

37 Commits

Author SHA1 Message Date
JJ Kasper
e09a418423 Publish Stable
- @now/next@2.3.12
2020-01-24 12:33:56 -06:00
JJ Kasper
52d4464368 Publish Canary
- @now/next@2.3.12-canary.0
2020-01-24 12:31:50 -06:00
JJ Kasper
4dc635e5f2 [now-next] Revert handle: miss/hit (#3658)
This reverts `handle: miss/hit` as it still needs some things sorted out before we're ready to use it in `@now/next`
2020-01-24 18:22:25 +00:00
Steven
510fb7ee7e Publish Canary
- @now/build-utils@1.3.7-canary.1
 - now@17.0.0-canary.19
2020-01-24 12:16:13 -05:00
Steven
243451e94b [now-build-utils] Add function detectApiExtensions() (#3653)
* [now-routing-utils] Add function detectApiExtensions

* Add more tests, fix broken test

* Add missing check for extensions

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-01-24 12:07:29 -05:00
Andy Bitz
11bbda977d Publish Stable
- @now/frameworks@0.0.7
 - @now/next@2.3.11
2020-01-24 17:20:55 +01:00
Andy Bitz
62c050f394 Publish Canary
- now@17.0.0-canary.18
 - now-client@7.0.0-canary.1
2020-01-24 17:19:27 +01:00
Andy
bd5a013312 [now-client] (Major) Remove builds check from now-client and change events (#3648)
* [now-client][now-cli] Remove builds check from now-client

* [now-client] Adjust README and change version

* Change events and adjust build error

* Use message from error

* Update packages/now-cli/src/util/deploy/process-deployment.ts

Co-Authored-By: Luc <luc.leray@gmail.com>

* [now-cli] Rename event

* Make types more consistent

* Fix type in process-legacy-deployment

* Adjust type in test

* Update type

* Make events type simpler

Co-authored-by: Max <8418866+rdev@users.noreply.github.com>
Co-authored-by: Luc <luc.leray@gmail.com>
2020-01-24 17:18:56 +01:00
Andy Bitz
1823cf452e Publish Canary
- @now/next@2.3.11-canary.4
2020-01-24 16:09:18 +01:00
Andy
c426d72ccf [now-next] Include file mode for pseudo layers (#3655) 2020-01-24 16:08:39 +01:00
Alex Grover
ddf59c052d Remove duplicated line from .gitignore (#3651) 2020-01-24 11:03:51 +01:00
luc
1dcf6e7fb1 Publish Canary
- now@17.0.0-canary.17
2020-01-23 20:06:18 +01:00
Luc
d4f4792988 [now-cli] Add --confirm to help (#3625)
```
$ now --help
[...]
  -c, --confirm                  Confirm default options and skip questions
```
2020-01-23 19:00:55 +00:00
Steven
7e1f2bd10e Publish Canary
- @now/build-utils@1.3.7-canary.0
2020-01-23 12:29:22 -05:00
Steven
a80a1d0c1d [now-build-utils] Fix api directory detection (#3647)
There was an issue where `@now/next` was emitting an api directory with serverless functions but the functions should not be renamed.
2020-01-23 16:54:34 +00:00
Steven
8ff747b4d7 Publish Canary
- @now/next@2.3.11-canary.3
 - @now/routing-utils@1.5.2-canary.2
2020-01-23 09:37:30 -05:00
Steven
aa63b5a581 [github] Update codeowners (#3642)
Added a few more code owners
2020-01-23 00:25:25 +00:00
luc
2094ec3c99 Publish Canary
- now@17.0.0-canary.16
2020-01-23 00:57:56 +01:00
Luc
bf30d10211 [now-cli] Output error if NOW_PROJECT_ID/NOW_ORG_ID is defined without the other (#3630)
* output error if one of both env is missing

* add test

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2020-01-23 00:53:15 +01:00
Luc
ccc03c9c6e [now-cli] Add warning if linked project was deleted or access was removed (#3631)
![image](https://user-images.githubusercontent.com/6616955/72800302-4ff4a880-3c47-11ea-8d74-0ae0c18469da.png)
2020-01-22 23:27:30 +00:00
Steven
4b7367e2dc [now-routing-utils] Fix segments in query string (#3640)
This PR a regression when path segments are used in the query string.

Take a look at the following ASCII Table for why I had to delete certain parts of the parsed url before formatting again.

https://nodejs.org/api/url.html#url_url_strings_and_url_objects

Related to #3539
2020-01-22 22:46:47 +00:00
JJ Kasper
00aa56a095 [now-next] Add headers support for custom-routes (#3494)
This adds support for `headers` in custom-routes which was landed in Next.js. 

This also updates `@now/routing-utils` `convertHeaders` to call `sourceToRegex` to match behavior with Next.js and allow using `segments` to match in the header `source` as not being able to use the same syntax for a header `source` as a `redirect` source could get confusing
2020-01-22 20:45:36 +00:00
Steven
56ae93a2a5 [examples] Fix jekyll readme build command (#3639)
Fixes #3634
2020-01-22 18:19:12 +00:00
JJ Kasper
adb32a09d3 Publish Canary
- @now/next@2.3.11-canary.2
 - @now/routing-utils@1.5.2-canary.1
2020-01-22 11:46:57 -06:00
JJ Kasper
3358d8e44c [now-next] Add handle: miss and handle: hit for custom-routes (#3489)
This is required to match custom-routes behavior in Next.js by checking dynamic routes after each rewrite although is currently blocked on `now dev` also supporting the feature

This reverts commit 0da98a7f5d.
2020-01-22 17:39:27 +00:00
Steven
c3bd2698e8 [now-routing-utils] Disallow "status" in hit phase (#3637)
This will prevent any strange behavior since production already ignores status code in the hit phase.
2020-01-22 17:03:10 +00:00
Steven
a7baa4761d Publish Canary
- now@17.0.0-canary.15
2020-01-21 19:56:55 -05:00
Steven
5dd2daa970 [now dev] Add support for handle: miss and handle: hit (#3537)
- [x] Add tests from now-proxy for `handle: miss`
- [x] Add tests from now-proxy for `handle: hit`
- [x] Add file output renaming when `featHandleMiss` is true (also assign true for now dev)
2020-01-22 00:54:24 +00:00
JJ Kasper
dd36a489ed Publish Canary
- @now/frameworks@0.0.7-canary.0
 - @now/next@2.3.11-canary.1
 - @now/static-build@0.14.11-canary.0
2020-01-20 15:36:58 -06:00
JJ Kasper
2e742209e3 [now-next] Add initial support for static 404 (#3628)
This adds initial support for static 404 pages when enabled for Next.js applications > `9.2.1-canary.3` it also disables tracing/logging related to lambdas when there aren't any lambdas besides the `_error` when a static 404 is being used 

Closes: #3368
2020-01-20 20:55:08 +00:00
Andy
8d13464cba [frameworks][now-static-build] Use hugo -D --gc as build command (#3624)
* [frameworks][now-static-build] Use `hugo --gc` as build command

* Add -D option
2020-01-20 19:46:15 +01:00
Max Rovensky
20fdcfa0af Publish Stable
- @now/static-build@0.14.10
2020-01-18 04:00:08 +08:00
Max
fac004f83c [now-static-build] Remove legacy message from static build (#3618)
* Remove legacy message from static build

* Remove unused config

* Remove unused config
2020-01-17 20:59:10 +01:00
Andy Bitz
5fee4bbad1 Publish Stable
- @now/static-build@0.14.9
2020-01-17 20:21:52 +01:00
Andy
18e4b18839 [now-static-build] Ignore commands for non-zero-config (#3617) 2020-01-17 20:19:53 +01:00
Max Rovensky
b8627fd384 Publish Stable
- @now/static-build@0.14.8
2020-01-18 02:42:30 +08:00
Max
4e2db6f8a5 [now-static-build] Improve static build errors copy (#3616)
* Improve static build errors copy

* Update packages/now-static-build/src/index.ts

Co-Authored-By: Leo Lamprecht <mindrun@icloud.com>

* Update packages/now-static-build/src/index.ts

Co-Authored-By: Leo Lamprecht <mindrun@icloud.com>

Co-authored-by: Leo Lamprecht <mindrun@icloud.com>
2020-01-17 19:36:52 +01:00
97 changed files with 1415 additions and 321 deletions

8
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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.

View File

@@ -13,7 +13,6 @@
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -580,7 +580,7 @@
},
"settings": {
"buildCommand": {
"value": "hugo"
"value": "hugo -D --gc"
},
"devCommand": {
"value": "hugo server -D -w -p $PORT"

View File

@@ -1,6 +1,6 @@
{
"name": "@now/frameworks",
"version": "0.0.6",
"version": "0.0.7",
"main": "frameworks.json",
"license": "UNLICENSED"
}

View File

@@ -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",

View File

@@ -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))

View File

@@ -66,6 +66,7 @@ export {
detectRoutes,
detectOutputDirectory,
detectApiDirectory,
detectApiExtensions,
} from './detect-routes';
export { detectBuilders } from './detect-builders';
export { detectFramework } from './detect-framework';

View File

@@ -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);
});
});
/**

View File

@@ -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);

View File

@@ -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",

View File

@@ -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();

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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`
);

View File

@@ -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))

View File

@@ -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,
};
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -1213,7 +1213,7 @@ export class BuildError extends NowError<'BUILD_ERROR', {}> {
meta,
}: {
message: string;
meta: { entrypoint: string };
meta: { entrypoint?: string };
}) {
super({
code: 'BUILD_ERROR',

View File

@@ -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);
}

View File

@@ -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 youre not a member of it anymore.\n',
emoji('warning')
)
);
}
return [null, null];
}

View File

@@ -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,
});
});

View File

@@ -1,6 +1,6 @@
{
"functions": {
"api/**/*.sh": {
"api/user": {
"runtime": "now-bash@1.0.3"
}
}

View File

@@ -3,10 +3,7 @@
"builds": [
{
"use": "@now/static-build",
"src": "package.json",
"config": {
"zeroConfig": true
}
"src": "package.json"
}
]
}

View File

@@ -0,0 +1 @@
Blog Post

View File

@@ -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
}
]
}

View File

@@ -0,0 +1 @@
Blog Page

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"routes": [
{ "handle": "hit" },
{
"src": "^/.*",
"headers": { "test": "1" },
"continue": true
}
]
}

View File

@@ -0,0 +1 @@
Blog Post

View File

@@ -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
}
]
}

View File

@@ -0,0 +1 @@
About Page

View File

@@ -0,0 +1 @@
Index Page

View File

@@ -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
}
]
}

View File

@@ -0,0 +1 @@
Blog Post Page

View File

@@ -0,0 +1 @@
About Page

View File

@@ -0,0 +1 @@
Index Page

View File

@@ -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
}
]
}

View File

@@ -0,0 +1 @@
Blog Post Page

View File

@@ -0,0 +1 @@
About Page

View File

@@ -0,0 +1 @@
Index Page

View File

@@ -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
}
]
}

View File

@@ -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 }
]
}

View File

@@ -0,0 +1 @@
About Page

View File

@@ -0,0 +1 @@
Index Page

View File

@@ -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
}
]
}

View File

@@ -0,0 +1 @@
Index Page

View File

@@ -0,0 +1,8 @@
{
"version": 2,
"routes": [
{ "handle": "miss" },
{ "src": "/pathA(?:/.+)?", "status": 404, "continue": true },
{ "src": "/pathB(?:/.+)?", "status": 404, "continue": true }
]
}

View File

@@ -0,0 +1,3 @@
export default (_req, res) => {
res.end('current date: ' + new Date().toISOString());
};

View File

@@ -0,0 +1,3 @@
export default (_req, res) => {
res.end('random number: ' + Math.random());
};

View File

@@ -0,0 +1,3 @@
{
"name": "test-public-and-api"
}

View File

@@ -0,0 +1 @@
This is the about page

View File

@@ -0,0 +1 @@
This is the home page.

View File

@@ -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" }
]
}

View File

@@ -0,0 +1 @@
This is a post.

View File

@@ -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);

View File

@@ -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 youre 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]);

View File

@@ -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'
];
```

View File

@@ -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",

View File

@@ -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'));

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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...');

View File

@@ -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'>

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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,
},
]),

View File

@@ -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);

View File

@@ -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',
},
],
},
];
},
},

View File

@@ -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"
}
}
]
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -0,0 +1,8 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
experimental: {
static404: true,
},
};

View 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"
}
]
}

View File

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

View File

@@ -0,0 +1 @@
export default () => 'Hi';

View File

@@ -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",

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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' }],

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
{
"name": "@now/static-build",
"version": "0.14.7",
"version": "0.14.11-canary.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/static-builds",

View File

@@ -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 => {

View File

@@ -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. ${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: `Build failed because Output Directory is not a directory: "${distDirName}". ${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: `Build failed because Output Directory is empty: "${distDirName}". ${legacyMsg}`,
message: `The Output Directory "${distDirName}" is empty.`,
link,
});
}
@@ -115,6 +111,10 @@ function getCommand(
config: Config,
framework: Framework | undefined
) {
if (!config.zeroConfig) {
return null;
}
const propName = name === 'build' ? 'buildCommand' : 'devCommand';
if (typeof config[propName] === 'string') {
@@ -458,7 +458,7 @@ export async function build({
}
}
validateDistDir(distPath, config);
validateDistDir(distPath);
if (framework) {
const frameworkRoutes = await getFrameworkRoutes(
@@ -485,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);