Compare commits

..

24 Commits

Author SHA1 Message Date
JJ Kasper
51d440431e Publish Canary
- @now/build-utils@1.1.1-canary.1
 - @now/next@2.1.1-canary.0
2019-12-05 15:18:35 -06:00
JJ Kasper
7cf061122c [now-next] Update routes for new check: true behavior (#3383)
As discussed this moves the `handle: filesystem` usage to the right location now that we have `check: true` for the `rewrites`
2019-12-05 20:48:17 +00:00
Nathan Rajlich
1254368505 [now-build-utils] Update Detectors API (#3384)
* Changes the `buildCommand` and `devCommand` from `string[]` to `string`
 * Renames `buildEnv` to `buildVariables` and `devEnv` to `devVariables`
2019-12-05 20:01:28 +00:00
Andy Bitz
9d4b830c5f Publish Canary
- now@16.6.1-canary.1
 - now-client@6.0.0-canary.1
2019-12-05 14:14:50 +01:00
Andy
37401b4363 [now-client] Bump version (#3385) 2019-12-05 14:14:00 +01:00
Andy
10fe08e14f [now-client] (Major) Split now-client options (#3382)
* Change types

* Split options for now-client

* Fix query and teamId

* Adjust tests

* Fix linting

* Ignore scope

* Adjust now-client tests

* Adjust more tests

* Apply prettier
2019-12-05 14:08:39 +01:00
Steven
0ecdb35d50 [now-cli][now-client] Fix user agent (#3381)
Since switching to a single branch, each package in the monorepo can be independently versioned so that some packages are using a canary version and others using a stable version.

This PR fixes an issue where a canary version of `now-cli` is bundling a stable version of `now-client` and thus does does not deploy zero config using canary builders.

The solution is to pass the User Agent from `now-cli` to `now-client` in a new option.

A nice side-effect of this PR is that we will switch the User Agent back to what it used to be pre-now-client days. It will look something like `now 16.6.1-canary.0 node-v10.17.0 darwin (x64)`.
2019-12-04 23:10:31 +00:00
Steven
caee8fe9ef Publish Canary
- now-client@5.2.5-canary.0
2019-12-04 16:46:43 -05:00
Max
7d92c27b2d [now-client] Fix main in package.json (#3344)
This sets `main` in `now-client` to a proper path.

Follow up to #3315.

Fixes #3373.
2019-12-04 13:25:27 -08:00
Steven
701eabbaba Publish Canary
- now@16.6.1-canary.0
2019-12-04 09:28:21 -05:00
Andy Bitz
e74a1b2d1a Publish Canary
- @now/build-utils@1.1.1-canary.0
2019-12-02 23:41:28 +01:00
Andy
e087b02333 [now-build-utils] Change script to scripts in error message (#3376)
Change `script` to `scripts` in error message.

[PRODUCT-740]

[PRODUCT-740]: https://zeit.atlassian.net/browse/PRODUCT-740
2019-12-02 22:33:27 +00:00
Steven
eea7f902b5 [now-cli] Add support for check: true routes in now dev (#3364)
This PR adds `now dev` support for routes that define `check: true`.

The algorithm is as follows:

- If a matching `dest` file is found, then serve it
- If a matching `src` file is found, then serve it
- Otherwise, behave the same as `continue: true` and continue processing routes
2019-11-28 10:58:13 +00:00
Steven
db7583201b [now-cli] Fix preinstall script on windows when LOCALAPPDATA is missing (#3365)
Usually `LOCALAPPDATA` is set to `C:\Users\{username}\AppData\Local` but occasionally, it is unassigned and causes installation failures. Looks like this could be due to the [registry](https://liquidwarelabs.zendesk.com/hc/en-us/articles/210634163-How-To-Make-APPDATA-and-LOCALAPPDATA-Environment-Variables-Follow-The-Registry-Keys).

If `LOCALAPPDATA` is missing, we can assume that now.exe was not installed before and can skip the deletion step that happens in the preinstall script.
2019-11-28 00:35:53 +00:00
Tommaso De Rossi
023001a8b1 [now dev] skip installing already installed versioned runtimes (#3354)
Fixes #3353
The current solution might break if a user interrupts `now dev` while yarn wrote the package in the cache package.json but has not yet added to node_modules.
This happens in like 20 ms but is possible, so we could execute `yarn` every time to be sure.
Tell me if the above is a problem or not
2019-11-27 11:33:18 +00:00
Steven
4ff8ab2435 Publish Canary
- @now/routing-utils@1.3.4-canary.5
2019-11-26 19:00:34 -05:00
Steven
d2cccbfce6 [now-routing-utils] Update path-to-regexp to v6.1.0 (#3361)
This bumps `path-to-regexp` to the latest version 6.1.0 which fixes optional capture groups like the test I added for `/next.js`.
2019-11-26 23:41:35 +00:00
Steven
970e6c400c Publish Canary
- @now/routing-utils@1.3.4-canary.4
2019-11-26 13:10:09 -05:00
Steven
b4cb7345a1 [now-routing-utils] Add mergeRoutes function (#3358)
This moves the merging logic to `@now/routing-utils` and adds support for `check: true`.

- Builder before filesystem, continue: true
- User before filesystem
- Builder before filesystem, check: true
- Builder before filesystem, continue: false
- Handle filesystem
- Builder after filesystem, continue: true
- User after filesystem
- Builder after filesystem, check: true
- Builder after filesystem, continue: false
2019-11-26 17:51:19 +00:00
Steven
7e75d8c1a3 [docs] Remove deprecated LambdaRuntimes (#3346)
- Removes Node 8.10 and old .NET which are EOL
- Adds a couple missing such as Ruby
2019-11-22 21:12:23 +00:00
Steven
a4ea551160 Publish Canary
- @now/routing-utils@1.3.4-canary.3
2019-11-22 14:41:33 -05:00
Steven
f56ad447a0 [now-routing-utils] Add support for check: true (#3343)
This PR adds support for `check: true` for a route object. It is basically a way to add a rewrite and still check the filesystem.
2019-11-22 19:03:45 +00:00
luc
7656422057 Publish Canary
- @now/static-build@0.13.1-canary.0
2019-11-22 17:39:35 +08:00
Luc
afa2231add [now-static-build] Cache .cache folder for gatsby deployments (#3260) (#3342)
Apply 77348ea71e again.

> Adds `.cache` folder to the Now cache for Gatsby deployments.

> Also adds a generic optional `cachePattern` property to the frameworks array so we can optimize cache paths for other frameworks in the future.
2019-11-22 09:16:51 +00:00
77 changed files with 1200 additions and 593 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
node_modules
package-lock.json
dist
.vscode
npm-debug.log
yarn-error.log
.nyc_output

View File

@@ -285,14 +285,13 @@ This is an abstract enumeration type that is implemented by one of the following
- `nodejs12.x`
- `nodejs10.x`
- `nodejs8.10`
- `go1.x`
- `java-1.8.0-openjdk`
- `java11`
- `python3.8`
- `python3.6`
- `python2.7`
- `dotnetcore2.1`
- `dotnetcore2.0`
- `dotnetcore1.0`
- `ruby2.5`
- `provided`
## JavaScript API

View File

@@ -1,6 +1,6 @@
{
"name": "@now/build-utils",
"version": "1.1.0",
"version": "1.1.1-canary.1",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",

View File

@@ -18,7 +18,7 @@ const config: Config = { zeroConfig: true };
const MISSING_BUILD_SCRIPT_ERROR: ErrorResponse = {
code: 'missing_build_script',
message:
'Your `package.json` file is missing a `build` property inside the `script` property.' +
'Your `package.json` file is missing a `build` property inside the `scripts` property.' +
'\nMore details: https://zeit.co/docs/v2/platform/frequently-asked-questions#missing-build-script',
};

View File

@@ -6,9 +6,9 @@ export default async function detectAngular({
const hasAngular = await hasDependency('@angular/cli');
if (!hasAngular) return false;
return {
buildCommand: ['ng', 'build'],
buildCommand: 'ng build',
buildDirectory: 'dist',
devCommand: ['ng', 'serve', '--port', '$PORT'],
devCommand: 'ng serve --port $PORT',
minNodeRange: '10.x',
routes: [
{

View File

@@ -10,8 +10,8 @@ export default async function detectBrunch({
if (!hasConfig) return false;
return {
buildCommand: ['brunch', 'build', '--production'],
buildCommand: 'brunch build --production',
buildDirectory: 'public',
devCommand: ['brunch', 'watch', '--server', '--port', '$PORT'],
devCommand: 'brunch watch --server --port $PORT',
};
}

View File

@@ -8,10 +8,10 @@ export default async function detectCreateReactAppEjected({
return false;
}
return {
buildCommand: ['node', 'scripts/build.js'],
buildCommand: 'node scripts/build.js',
buildDirectory: 'build',
devCommand: ['node', 'scripts/start.js'],
devEnv: { BROWSER: 'none' },
devCommand: 'node scripts/start.js',
devVariables: { BROWSER: 'none' },
routes: [
{
src: '/static/(.*)',

View File

@@ -8,10 +8,10 @@ export default async function detectCreateReactApp({
return false;
}
return {
buildCommand: ['react-scripts', 'build'],
buildCommand: 'react-scripts build',
buildDirectory: 'build',
devCommand: ['react-scripts', 'start'],
devEnv: { BROWSER: 'none' },
devCommand: 'react-scripts start',
devVariables: { BROWSER: 'none' },
routes: [
{
src: '/static/(.*)',

View File

@@ -6,8 +6,8 @@ export default async function detectDocusaurus({
const hasDocusaurus = await hasDependency('docusaurus');
if (!hasDocusaurus) return false;
return {
buildCommand: ['docusaurus-build'],
buildCommand: 'docusaurus-build',
buildDirectory: 'build',
devCommand: ['docusaurus-start', '--port', '$PORT'],
devCommand: 'docusaurus-start --port $PORT',
};
}

View File

@@ -6,15 +6,8 @@ export default async function detectEleventy({
const hasEleventy = await hasDependency('@11ty/eleventy');
if (!hasEleventy) return false;
return {
buildCommand: ['npx', '@11ty/eleventy'],
buildCommand: 'npx @11ty/eleventy',
buildDirectory: '_site',
devCommand: [
'npx',
'@11ty/eleventy',
'--serve',
'--watch',
'--port',
'$PORT',
],
devCommand: 'npx @11ty/eleventy --serve --watch --port $PORT',
};
}

View File

@@ -6,9 +6,9 @@ export default async function detectEmber({
const hasEmber = await hasDependency('ember-cli');
if (!hasEmber) return false;
return {
buildCommand: ['ember', 'build'],
buildCommand: 'ember build',
buildDirectory: 'dist',
devCommand: ['ember', 'serve', '--port', '$PORT'],
devCommand: 'ember serve --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -8,9 +8,9 @@ export default async function detectGatsby({
return false;
}
return {
buildCommand: ['gatsby', 'build'],
buildCommand: 'gatsby build',
buildDirectory: 'public',
devCommand: ['gatsby', 'develop', '-p', '$PORT'],
devCommand: 'gatsby develop -p $PORT',
cachePattern: '.cache/**',
};
}

View File

@@ -8,8 +8,8 @@ export default async function detectGridsome({
return false;
}
return {
buildCommand: ['gridsome', 'build'],
buildCommand: 'gridsome build',
buildDirectory: 'dist',
devCommand: ['gridsome', 'develop', '-p', '$PORT'],
devCommand: 'gridsome develop -p $PORT',
};
}

View File

@@ -6,8 +6,8 @@ export default async function detectHexo({
const hasHexo = await hasDependency('hexo');
if (!hasHexo) return false;
return {
buildCommand: ['hexo', 'generate'],
buildCommand: 'hexo generate',
buildDirectory: 'public',
devCommand: ['hexo', 'server', '--port', '$PORT'],
devCommand: 'hexo server --port $PORT',
};
}

View File

@@ -19,8 +19,8 @@ export default async function detectHugo({
return false;
}
return {
buildCommand: ['hugo'],
buildCommand: 'hugo',
buildDirectory: config.publishDir || 'public',
devCommand: ['hugo', 'server', '-D', '-w', '-p', '$PORT'],
devCommand: 'hugo server -D -w -p $PORT',
};
}

View File

@@ -15,16 +15,8 @@ export default async function detectJekyll({
return false;
}
return {
buildCommand: ['jekyll', 'build'],
buildCommand: 'jekyll build',
buildDirectory: config.destination || '_site',
devCommand: [
'bundle',
'exec',
'jekyll',
'serve',
'--watch',
'--port',
'$PORT',
],
devCommand: 'bundle exec jekyll serve --watch --port $PORT',
};
}

View File

@@ -7,8 +7,8 @@ export default async function detectMiddleman({
if (!hasConfig) return false;
return {
buildCommand: ['bundle', 'exec', 'middleman', 'build'],
buildCommand: 'bundle exec middleman build',
buildDirectory: 'build',
devCommand: ['bundle', 'exec', 'middleman', 'server', '-p', '$PORT'],
devCommand: 'bundle exec middleman server -p $PORT',
};
}

View File

@@ -6,8 +6,8 @@ export default async function detectNext({
const hasNext = await hasDependency('next');
if (!hasNext) return false;
return {
buildCommand: ['next', 'build'],
buildCommand: 'next build',
buildDirectory: 'build',
devCommand: ['next', '-p', '$PORT'],
devCommand: 'next -p $PORT',
};
}

View File

@@ -6,9 +6,9 @@ export default async function detectPolymer({
const hasPolymer = await hasDependency('polymer-cli');
if (!hasPolymer) return false;
return {
buildCommand: ['polymer', 'build'],
buildCommand: 'polymer build',
buildDirectory: 'build',
devCommand: ['polymer', 'serve', '--port', '$PORT'],
devCommand: 'polymer serve --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectPreact({
const hasPreact = await hasDependency('preact-cli');
if (!hasPreact) return false;
return {
buildCommand: ['preact', 'build'],
buildCommand: 'preact build',
buildDirectory: 'build',
devCommand: ['preact', 'watch', '--port', '$PORT'],
devCommand: 'preact watch --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectSaber({
const hasSaber = await hasDependency('saber');
if (!hasSaber) return false;
return {
buildCommand: ['saber', 'build'],
buildCommand: 'saber build',
buildDirectory: 'public',
devCommand: ['saber', '--port', '$PORT'],
devCommand: 'saber --port $PORT',
routes: [
{
src: '/_saber/.*',

View File

@@ -6,8 +6,8 @@ export default async function detectSapper({
const hasSapper = await hasDependency('sapper');
if (!hasSapper) return false;
return {
buildCommand: ['sapper', 'export'],
buildCommand: 'sapper export',
buildDirectory: '__sapper__/export',
devCommand: ['sapper', 'dev', '--port', '$PORT'],
devCommand: 'sapper dev --port $PORT',
};
}

View File

@@ -6,17 +6,9 @@ export default async function detectStencil({
const hasStencil = await hasDependency('@stencil/core');
if (!hasStencil) return false;
return {
buildCommand: ['stencil', 'build'],
buildCommand: 'stencil build',
buildDirectory: 'www',
devCommand: [
'stencil',
'build',
'--dev',
'--watch',
'--serve',
'--port',
'$PORT',
],
devCommand: 'stencil build --dev --watch --serve --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectSvelte({
const hasSvelte = await hasDependency('sirv-cli');
if (!hasSvelte) return false;
return {
buildCommand: ['rollup', '-c'],
buildCommand: 'rollup -c',
buildDirectory: 'public',
devCommand: ['sirv', 'public', '--single', '--dev', ' --port', '$PORT'],
devCommand: 'sirv public --single --dev --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectUmiJS({
const hasUmi = await hasDependency('umi');
if (!hasUmi) return false;
return {
buildCommand: ['umi', 'build'],
buildCommand: 'umi build',
buildDirectory: 'dist',
devCommand: ['umi', 'dev', '--port', '$PORT'],
devCommand: 'umi dev --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectVue({
const hasVue = await hasDependency('@vue/cli-service');
if (!hasVue) return false;
return {
buildCommand: ['vue-cli-service', 'build'],
buildCommand: 'vue-cli-service build',
buildDirectory: 'dist',
devCommand: ['vue-cli-service', 'serve', '--port', '$PORT'],
devCommand: 'vue-cli-service serve --port $PORT',
routes: [
{
src: '^/[^/]*\\.(js|txt|ico|json)',

View File

@@ -352,11 +352,11 @@ export interface DetectorParameters {
}
export interface DetectorOutput {
buildCommand: string[];
buildCommand: string;
buildDirectory: string;
buildEnv?: Env;
devCommand?: string[];
devEnv?: Env;
buildVariables?: Env;
devCommand?: string;
devVariables?: Env;
minNodeRange?: string;
cachePattern?: string;
routes?: Route[];

View File

@@ -68,7 +68,7 @@ test('detectDefaults() - angular', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'dist');
assert.deepEqual(result.buildCommand, ['ng', 'build']);
assert.deepEqual(result.buildCommand, 'ng build');
});
test('detectDefaults() - brunch', async () => {
@@ -77,7 +77,7 @@ test('detectDefaults() - brunch', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.deepEqual(result.buildCommand, ['brunch', 'build', '--production']);
assert.deepEqual(result.buildCommand, 'brunch build --production');
});
test('detectDefaults() - hugo', async () => {
@@ -86,7 +86,7 @@ test('detectDefaults() - hugo', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.deepEqual(result.buildCommand, ['hugo']);
assert.deepEqual(result.buildCommand, 'hugo');
});
test('detectDefaults() - jekyll', async () => {
@@ -95,7 +95,7 @@ test('detectDefaults() - jekyll', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, '_site');
assert.deepEqual(result.buildCommand, ['jekyll', 'build']);
assert.deepEqual(result.buildCommand, 'jekyll build');
});
test('detectDefaults() - middleman', async () => {
@@ -104,10 +104,5 @@ test('detectDefaults() - middleman', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'build');
assert.deepEqual(result.buildCommand, [
'bundle',
'exec',
'middleman',
'build',
]);
assert.deepEqual(result.buildCommand, 'bundle exec middleman build');
});

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "16.6.0",
"version": "16.6.1-canary.1",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",

View File

@@ -2,6 +2,7 @@
const fs = require('fs');
const { promisify } = require('util');
const { join, delimiter } = require('path');
const { homedir } = require('os');
const stat = promisify(fs.stat);
const unlink = promisify(fs.unlink);
@@ -39,7 +40,16 @@ function isGlobal() {
// See: https://git.io/fj4jD
function getNowPath() {
if (process.platform === 'win32') {
const path = join(process.env.LOCALAPPDATA, 'now-cli', 'now.exe');
const { LOCALAPPDATA, USERPROFILE, HOMEPATH } = process.env;
const home = homedir() || USERPROFILE || HOMEPATH;
let path;
if (LOCALAPPDATA) {
path = join(LOCALAPPDATA, 'now-cli', 'now.exe');
} else if (home) {
path = join(home, 'AppData', 'Local', 'now-cli', 'now.exe');
} else {
path = '';
}
return fs.existsSync(path) ? path : null;
}
@@ -48,7 +58,7 @@ function getNowPath() {
const paths = [
join(process.env.HOME || '/', 'bin'),
'/usr/local/bin',
'/usr/bin'
'/usr/bin',
];
for (const basePath of paths) {

View File

@@ -2,13 +2,7 @@ import { NowConfig } from './util/dev/types';
export type ThenArg<T> = T extends Promise<infer U> ? U : T;
export interface Config extends NowConfig {
alias?: string[] | string;
aliases?: string[] | string;
name?: string;
type?: string;
scope?: string;
}
export type Config = NowConfig;
export interface NowContext {
argv: string[];

View File

@@ -6,12 +6,14 @@ import {
createDeployment,
createLegacyDeployment,
DeploymentOptions,
} from 'now-client/dist';
NowClientOptions,
} from 'now-client';
import wait from '../output/wait';
import { Output } from '../output';
// @ts-ignore
import Now from '../../util';
import { NowConfig } from '../dev/types';
import ua from '../ua';
export default async function processDeployment({
now,
@@ -21,9 +23,9 @@ export default async function processDeployment({
requestBody,
uploadStamp,
deployStamp,
legacy,
env,
isLegacy,
quiet,
force,
nowConfig,
}: {
now: Now;
@@ -33,27 +35,36 @@ export default async function processDeployment({
requestBody: DeploymentOptions;
uploadStamp: () => number;
deployStamp: () => number;
legacy: boolean;
env: any;
isLegacy: boolean;
quiet: boolean;
nowConfig?: NowConfig;
force?: boolean;
}) {
const { warn, log, debug, note } = output;
let bar: Progress | null = null;
const path0 = paths[0];
const opts: DeploymentOptions = {
...requestBody,
debug: now._debug,
const { env = {} } = requestBody;
const nowClientOptions: NowClientOptions = {
teamId: now.currentTeam,
apiUrl: now._apiUrl,
token: now._token,
debug: now._debug,
userAgent: ua,
path: paths[0],
force,
};
if (!legacy) {
if (!isLegacy) {
let queuedSpinner = null;
let buildSpinner = null;
let deploySpinner = null;
for await (const event of createDeployment(path0, opts, nowConfig)) {
for await (const event of createDeployment(
nowClientOptions,
requestBody,
nowConfig
)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
@@ -110,7 +121,7 @@ export default async function processDeployment({
now._host = event.payload.url;
if (!quiet) {
const version = legacy ? `${chalk.grey('[v1]')} ` : '';
const version = isLegacy ? `${chalk.grey('[v1]')} ` : '';
log(`https://${event.payload.url} ${version}${deployStamp()}`);
} else {
process.stdout.write(`https://${event.payload.url}`);
@@ -176,7 +187,11 @@ export default async function processDeployment({
}
}
} else {
for await (const event of createLegacyDeployment(path0, opts, nowConfig)) {
for await (const event of createLegacyDeployment(
nowClientOptions,
requestBody,
nowConfig
)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
@@ -224,7 +239,7 @@ export default async function processDeployment({
now._host = event.payload.url;
if (!quiet) {
const version = legacy ? `${chalk.grey('[v1]')} ` : '';
const version = isLegacy ? `${chalk.grey('[v1]')} ` : '';
log(`${event.payload.url} ${version}${deployStamp()}`);
} else {
process.stdout.write(`https://${event.payload.url}`);

View File

@@ -158,6 +158,14 @@ export function getBuildUtils(packages: string[]): string {
return `@now/build-utils@${version}`;
}
function parseVersionSafe(rawSpec: string) {
try {
return semver.parse(rawSpec);
} catch (e) {
return null;
}
}
export function filterPackage(
builderSpec: string,
distTag: string,
@@ -165,6 +173,17 @@ export function filterPackage(
) {
if (builderSpec in localBuilders) return false;
const parsed = npa(builderSpec);
const parsedVersion = parseVersionSafe(parsed.rawSpec);
// skip install of already installed runtime
if (
parsed.name &&
parsed.type === 'version' &&
parsedVersion &&
buildersPkg.dependencies &&
parsedVersion.version == buildersPkg.dependencies[parsed.name]
) {
return false;
}
if (
parsed.name &&
parsed.type === 'tag' &&

View File

@@ -88,6 +88,17 @@ export default async function(
continue;
}
if (routeConfig.check && devServer) {
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 (isURL(destPath)) {
found = {
found: true,

View File

@@ -9,9 +9,12 @@ import {
PackageJson,
BuilderFunctions,
} from '@now/build-utils';
import { NowConfig } from 'now-client';
import { NowRedirect, NowRewrite, NowHeader, Route } from '@now/routing-utils';
import { Output } from '../output';
export { NowConfig };
export interface DevServerOptions {
output: Output;
debug: boolean;
@@ -31,24 +34,6 @@ export interface BuildMatch extends BuildConfig {
export type RouteConfig = Route;
export interface NowConfig {
name?: string;
version?: number;
env?: EnvConfig;
build?: {
env?: EnvConfig;
};
builds?: BuildConfig[];
routes?: RouteConfig[];
files?: string[];
cleanUrls?: boolean;
rewrites?: NowRewrite[];
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
functions?: BuilderFunctions;
}
export interface HttpHandler {
(req: http.IncomingMessage, res: http.ServerResponse): void;
}

View File

@@ -53,7 +53,6 @@ export default class Now extends EventEmitter {
nowConfig = {},
hasNowJson = false,
sessionAffinity = 'random',
atlas = false,
// Latest
name,
@@ -71,39 +70,21 @@ export default class Now extends EventEmitter {
) {
const opts = { output: this._output, hasNowJson };
const { log, warn, debug } = this._output;
const isBuilds = type === null;
const isLegacy = type !== null;
let files = [];
let hashes = {};
const relatives = {};
let engines;
let deployment;
let requestBody = {};
if (isBuilds) {
requestBody = {
token: this._token,
teamId: this.currentTeam,
env,
build,
public: wantsPublic || nowConfig.public,
name,
project,
meta,
regions,
force: forceNew,
};
if (target) {
requestBody.target = target;
}
} else if (type === 'npm') {
if (type === 'npm') {
files = await getNpmFiles(paths[0], pkg, nowConfig, opts);
// A `start` or `now-start` npm script, or a `server.js` file
// in the root directory of the deployment are required
if (
!isBuilds &&
isLegacy &&
!hasNpmStart(pkg) &&
!hasFile(paths[0], files, 'server.js')
) {
@@ -139,30 +120,29 @@ export default class Now extends EventEmitter {
const uploadStamp = stamp();
if (isBuilds) {
deployment = await processDeployment({
now: this,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
nowConfig,
});
} else {
// Read `registry.npmjs.org` authToken from .npmrc
let authToken;
let requestBody = {
...nowConfig,
env,
build,
public: wantsPublic || nowConfig.public,
name,
project,
meta,
regions,
target: target || undefined,
};
if (type === 'npm' && forwardNpm) {
authToken =
(await readAuthToken(paths[0])) || (await readAuthToken(homedir()));
}
// Ignore specific items from Now.json
delete requestBody.scope;
if (isLegacy) {
// Read `registry.npmjs.org` authToken from .npmrc
const registryAuthToken =
type === 'npm' && forwardNpm
? (await readAuthToken(paths[0])) || (await readAuthToken(homedir()))
: undefined;
requestBody = {
token: this._token,
teamId: this.currentTeam,
env,
build,
meta,
@@ -172,31 +152,29 @@ export default class Now extends EventEmitter {
project,
description,
deploymentType: type,
registryAuthToken: authToken,
registryAuthToken,
engines,
scale,
sessionAffinity,
limits: nowConfig.limits,
atlas,
config: nowConfig,
functions: nowConfig.functions,
};
deployment = await processDeployment({
legacy: true,
now: this,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
env,
nowConfig,
});
}
deployment = await processDeployment({
isLegacy,
now: this,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
nowConfig,
force: forceNew,
});
// We report about files whose sizes are too big
let missingVersion = false;
@@ -228,7 +206,7 @@ export default class Now extends EventEmitter {
}
}
if (!isBuilds && !quiet && type === 'npm' && deployment.nodeVersion) {
if (isLegacy && !quiet && type === 'npm' && deployment.nodeVersion) {
if (engines && engines.node && !missingVersion) {
log(
chalk`Using Node.js {bold ${deployment.nodeVersion}} (requested: {dim \`${engines.node}\`})`

View File

@@ -4,8 +4,8 @@ import { filterPackage } from '../src/util/dev/builder-cache';
test('[dev-builder] filter install "latest", cached canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1-canary.0'
}
'@now/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage('@now/build-utils', 'canary', buildersPkg);
t.is(result, true);
@@ -14,8 +14,8 @@ test('[dev-builder] filter install "latest", cached canary', async t => {
test('[dev-builder] filter install "canary", cached stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1'
}
'@now/build-utils': '0.0.1',
},
};
const result = filterPackage(
'@now/build-utils@canary',
@@ -28,8 +28,8 @@ test('[dev-builder] filter install "canary", cached stable', async t => {
test('[dev-builder] filter install "latest", cached stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1'
}
'@now/build-utils': '0.0.1',
},
};
const result = filterPackage('@now/build-utils', 'latest', buildersPkg);
t.is(result, false);
@@ -38,8 +38,8 @@ test('[dev-builder] filter install "latest", cached stable', async t => {
test('[dev-builder] filter install "canary", cached canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1-canary.0'
}
'@now/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'@now/build-utils@canary',
@@ -52,8 +52,8 @@ test('[dev-builder] filter install "canary", cached canary', async t => {
test('[dev-builder] filter install URL, cached stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1'
}
'@now/build-utils': '0.0.1',
},
};
const result = filterPackage('https://tarball.now.sh', 'latest', buildersPkg);
t.is(result, true);
@@ -62,8 +62,8 @@ test('[dev-builder] filter install URL, cached stable', async t => {
test('[dev-builder] filter install URL, cached canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1-canary.0'
}
'@now/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage('https://tarball.now.sh', 'canary', buildersPkg);
t.is(result, true);
@@ -72,8 +72,8 @@ test('[dev-builder] filter install URL, cached canary', async t => {
test('[dev-builder] filter install "latest", cached URL - stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': 'https://tarball.now.sh'
}
'@now/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage('@now/build-utils', 'latest', buildersPkg);
t.is(result, true);
@@ -82,9 +82,49 @@ test('[dev-builder] filter install "latest", cached URL - stable', async t => {
test('[dev-builder] filter install "latest", cached URL - canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': 'https://tarball.now.sh'
}
'@now/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage('@now/build-utils', 'canary', buildersPkg);
t.is(result, true);
});
test('[dev-builder] filter install not bundled version, cached same version', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage('not-bundled-package@0.0.1', '_', buildersPkg);
t.is(result, false);
});
test('[dev-builder] filter install not bundled version, cached different version', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.9',
},
};
const result = filterPackage('not-bundled-package@0.0.1', '_', buildersPkg);
t.is(result, true);
});
test('[dev-builder] filter install not bundled stable, cached version', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage('not-bundled-package', '_', buildersPkg);
t.is(result, true);
});
test('[dev-builder] filter install not bundled tagged, cached tagged', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '16.9.0-alpha.0',
},
};
const result = filterPackage('not-bundled-package@alpha', '_', buildersPkg);
t.is(result, true);
});

View File

@@ -0,0 +1,19 @@
{
"routes": [
{
"src": "/blog/(.*)",
"check": true,
"dest": "/blog?post=$1"
},
{
"src": "/(.*)",
"check": true,
"dest": "/src/$1"
},
{
"src": "/(.*)",
"check": true,
"dest": "/fake/$1"
}
]
}

View File

@@ -0,0 +1 @@
Blog Home

View File

@@ -154,6 +154,22 @@ function testFixtureStdio(directory, fn) {
};
}
test(
'[now dev] validate routes that use `check: true`',
testFixtureStdio('routes-check-true', async (t, port) => {
const result = await fetchWithRetry(
`http://localhost:${port}/blog/post`,
3
);
const response = await result;
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Blog Home/gm);
})
);
test('[now dev] validate builds', async t => {
const directory = fixture('invalid-builds');
const output = await exec(directory);

View File

@@ -1,8 +1,8 @@
{
"name": "now-client",
"version": "5.2.4",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"version": "6.0.0-canary.1",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://zeit.co",
"license": "MIT",
"files": [

View File

@@ -8,7 +8,13 @@ import {
isAliasAssigned,
isAliasError,
} from './utils/ready-state';
import { Deployment, DeploymentBuild } from './types';
import { createDebug } from './utils';
import {
Dictionary,
Deployment,
NowClientOptions,
DeploymentBuild,
} from './types';
interface DeploymentStatus {
type: string;
@@ -16,22 +22,22 @@ interface DeploymentStatus {
}
/* eslint-disable */
export default async function* checkDeploymentStatus(
export async function* checkDeploymentStatus(
deployment: Deployment,
token: string,
version: number | undefined,
teamId: string | undefined,
debug: Function,
apiUrl?: string
clientOptions: NowClientOptions
): AsyncIterableIterator<DeploymentStatus> {
const { version } = deployment;
const { token, teamId, apiUrl, userAgent } = clientOptions;
const debug = createDebug(clientOptions.debug);
let deploymentState = deployment;
let allBuildsCompleted = false;
const buildsState: { [key: string]: DeploymentBuild } = {};
const buildsState: Dictionary<DeploymentBuild> = {};
const apiDeployments = getApiDeploymentsUrl({
version,
builds: deployment.builds,
functions: deployment.functions
functions: deployment.functions,
});
debug(`Using ${version ? `${version}.0` : '2.0'} API for status checks`);
@@ -54,7 +60,7 @@ export default async function* checkDeploymentStatus(
teamId ? `?teamId=${teamId}` : ''
}`,
token,
{ apiUrl }
{ apiUrl, userAgent }
);
const data = await buildsData.json();
@@ -91,7 +97,8 @@ export default async function* checkDeploymentStatus(
`${apiDeployments}/${deployment.id || deployment.deploymentId}${
teamId ? `?teamId=${teamId}` : ''
}`,
token
token,
{ apiUrl, userAgent }
);
const deploymentUpdate = await deploymentData.json();

View File

@@ -3,26 +3,22 @@ import { readdir as readRootFolder, lstatSync } from 'fs-extra';
import readdir from 'recursive-readdir';
import { relative, join, isAbsolute } from 'path';
import hashes, { mapToObject } from './utils/hashes';
import uploadAndDeploy from './upload';
import { upload } from './upload';
import { getNowIgnore, createDebug, parseNowJSON } from './utils';
import { DeploymentError } from './errors';
import {
CreateDeploymentFunction,
DeploymentOptions,
NowJsonOptions,
} from './types';
import { NowConfig, NowClientOptions, DeploymentOptions } from './types';
export { EVENTS } from './utils';
export default function buildCreateDeployment(
version: number
): CreateDeploymentFunction {
export default function buildCreateDeployment(version: number) {
return async function* createDeployment(
path: string | string[],
options: DeploymentOptions = {},
nowConfig?: NowJsonOptions
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions,
nowConfig: NowConfig = {}
): AsyncIterableIterator<any> {
const debug = createDebug(options.debug);
const { path } = clientOptions;
const debug = createDebug(clientOptions.debug);
const cwd = process.cwd();
debug('Creating deployment...');
@@ -38,9 +34,9 @@ export default function buildCreateDeployment(
});
}
if (typeof options.token !== 'string') {
if (typeof clientOptions.token !== 'string') {
debug(
`Error: 'token' is expected to be a string. Received ${typeof options.token}`
`Error: 'token' is expected to be a string. Received ${typeof clientOptions.token}`
);
throw new DeploymentError({
@@ -49,7 +45,8 @@ export default function buildCreateDeployment(
});
}
const isDirectory = !Array.isArray(path) && lstatSync(path).isDirectory();
clientOptions.isDirectory =
!Array.isArray(path) && lstatSync(path).isDirectory();
let rootFiles: string[];
@@ -69,7 +66,7 @@ export default function buildCreateDeployment(
});
}
if (isDirectory && !Array.isArray(path)) {
if (clientOptions.isDirectory && !Array.isArray(path)) {
debug(`Provided 'path' is a directory. Reading subpaths... `);
rootFiles = await readRootFolder(path);
debug(`Read ${rootFiles.length} subpaths`);
@@ -90,7 +87,7 @@ export default function buildCreateDeployment(
debug('Building file tree...');
if (isDirectory && !Array.isArray(path)) {
if (clientOptions.isDirectory && !Array.isArray(path)) {
// Directory path
const dirContents = await readdir(path, ignores);
const relativeFileList = dirContents.map(filePath =>
@@ -156,15 +153,14 @@ export default function buildCreateDeployment(
// from getting confused about a deployment that renders 404.
if (
fileList.length === 0 ||
fileList.every((item): boolean => {
if (!item) {
return true;
}
const segments = item.split('/');
return segments[segments.length - 1].startsWith('.');
})
fileList.every(item =>
item
? item
.split('/')
.pop()!
.startsWith('.')
: true
)
) {
debug(
`Deployment path has no files (or only dotfiles). Yielding a warning event`
@@ -181,39 +177,24 @@ export default function buildCreateDeployment(
debug(`Yielding a 'hashes-calculated' event with ${files.size} hashes`);
yield { type: 'hashes-calculated', payload: mapToObject(files) };
const {
token,
teamId,
force,
defaultName,
debug: debug_,
apiUrl,
...metadata
} = options;
if (clientOptions.apiUrl) {
debug(`Using provided API URL: ${clientOptions.apiUrl}`);
}
if (apiUrl) {
debug(`Using provided API URL: ${apiUrl}`);
if (clientOptions.userAgent) {
debug(`Using provided user agent: ${clientOptions.userAgent}`);
}
debug(`Setting platform version to ${version}`);
metadata.version = version;
const deploymentOpts = {
debug: debug_,
totalFiles: files.size,
nowConfig,
token,
isDirectory,
path,
teamId,
force,
defaultName,
metadata,
apiUrl,
};
deploymentOptions.version = version;
debug(`Creating the deployment and starting upload...`);
for await (const event of uploadAndDeploy(files, deploymentOpts)) {
for await (const event of upload(
files,
nowConfig,
clientOptions,
deploymentOptions
)) {
debug(`Yielding a '${event.type}' event`);
yield event;
}

View File

@@ -5,51 +5,41 @@ import {
createDebug,
getApiDeploymentsUrl,
} from './utils';
import checkDeploymentStatus from './deployment-status';
import { checkDeploymentStatus } from './check-deployment-status';
import { generateQueryString } from './utils/query-string';
import { Deployment, DeploymentOptions, NowJsonOptions } from './types';
import { isReady, isAliasAssigned } from './utils/ready-state';
export interface Options {
metadata: DeploymentOptions;
totalFiles: number;
path: string | string[];
token: string;
teamId?: string;
force?: boolean;
isDirectory?: boolean;
defaultName?: string;
preflight?: boolean;
debug?: boolean;
nowConfig?: NowJsonOptions;
apiUrl?: string;
}
import {
Deployment,
DeploymentOptions,
NowConfig,
NowClientOptions,
} from './types';
async function* createDeployment(
metadata: DeploymentOptions,
files: Map<string, DeploymentFile>,
options: Options,
debug: Function
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<{ type: string; payload: any }> {
const preparedFiles = prepareFiles(files, options);
const apiDeployments = getApiDeploymentsUrl(metadata);
const debug = createDebug(clientOptions.debug);
const preparedFiles = prepareFiles(files, clientOptions);
const apiDeployments = getApiDeploymentsUrl(deploymentOptions);
debug('Sending deployment creation API request');
try {
const dpl = await fetch(
`${apiDeployments}${generateQueryString(options)}`,
options.token,
`${apiDeployments}${generateQueryString(clientOptions)}`,
clientOptions.token,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...metadata,
...deploymentOptions,
files: preparedFiles,
}),
apiUrl: options.apiUrl,
apiUrl: clientOptions.apiUrl,
userAgent: clientOptions.userAgent,
}
);
@@ -85,87 +75,88 @@ async function* createDeployment(
}
}
const getDefaultName = (
path: string | string[] | undefined,
isDirectory: boolean | undefined,
function getDefaultName(
files: Map<string, DeploymentFile>,
debug: Function
): string => {
clientOptions: NowClientOptions
): string {
const debug = createDebug(clientOptions.debug);
const { isDirectory, path } = clientOptions;
if (isDirectory && typeof path === 'string') {
debug('Provided path is a directory. Using last segment as default name');
const segments = path.split('/');
return segments[segments.length - 1];
return path.split('/').pop()!;
} else {
debug(
'Provided path is not a directory. Using last segment of the first file as default name'
);
const filePath = Array.from(files.values())[0].names[0];
const segments = filePath.split('/');
return segments[segments.length - 1];
return filePath.split('/').pop()!;
}
};
}
export default async function* deploy(
export async function* deploy(
files: Map<string, DeploymentFile>,
options: Options
nowConfig: NowConfig,
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<{ type: string; payload: any }> {
const debug = createDebug(options.debug);
const nowJsonMetadata = options.nowConfig || {};
delete nowJsonMetadata.github;
delete nowJsonMetadata.scope;
const meta = options.metadata || {};
const metadata = { ...nowJsonMetadata, ...meta };
const debug = createDebug(clientOptions.debug);
// Check if we should default to a static deployment
if (!metadata.version && !metadata.name) {
metadata.version = 2;
metadata.name =
options.totalFiles === 1
? 'file'
: getDefaultName(options.path, options.isDirectory, files, debug);
if (!deploymentOptions.version && !deploymentOptions.name) {
deploymentOptions.version = 2;
deploymentOptions.name =
files.size === 1 ? 'file' : getDefaultName(files, clientOptions);
if (metadata.name === 'file') {
if (deploymentOptions.name === 'file') {
debug('Setting deployment name to "file" for single-file deployment');
}
}
if (options.totalFiles === 1 && !metadata.builds && !metadata.routes) {
if (
files.size === 1 &&
!deploymentOptions.builds &&
!deploymentOptions.routes
) {
debug(`Assigning '/' route for single file deployment`);
const filePath = Array.from(files.values())[0].names[0];
const segments = filePath.split('/');
metadata.routes = [
deploymentOptions.routes = [
{
src: '/',
dest: `/${segments[segments.length - 1]}`,
dest: `/${filePath.split('/').pop()}`,
},
];
}
if (!metadata.name) {
metadata.name =
options.defaultName ||
getDefaultName(options.path, options.isDirectory, files, debug);
debug('No name provided. Defaulting to', metadata.name);
if (!deploymentOptions.name) {
deploymentOptions.name =
clientOptions.defaultName || getDefaultName(files, clientOptions);
debug('No name provided. Defaulting to', deploymentOptions.name);
}
if (metadata.version === 1 && !metadata.deploymentType) {
debug(`Setting 'type' for 1.0 deployment to '${nowJsonMetadata.type}'`);
metadata.deploymentType = nowJsonMetadata.type;
if (
deploymentOptions.version === 1 &&
!deploymentOptions.deploymentType &&
nowConfig.type
) {
debug(`Setting 'type' for 1.0 deployment to '${nowConfig.type}'`);
deploymentOptions.deploymentType = nowConfig.type.toUpperCase() as DeploymentOptions['deploymentType'];
}
if (metadata.version === 1) {
if (deploymentOptions.version === 1 && !deploymentOptions.config) {
debug(`Writing 'config' values for 1.0 deployment`);
const nowConfig = { ...nowJsonMetadata };
delete nowConfig.version;
deploymentOptions.config = { ...nowConfig };
delete deploymentOptions.config.version;
}
metadata.config = {
...nowConfig,
...metadata.config,
};
if (
deploymentOptions.version === 1 &&
!deploymentOptions.forceNew &&
clientOptions.force
) {
debug(`Setting 'forceNew' for 1.0 deployment`);
deploymentOptions.forceNew = clientOptions.force;
}
let deployment: Deployment | undefined;
@@ -173,10 +164,9 @@ export default async function* deploy(
try {
debug('Creating deployment');
for await (const event of createDeployment(
metadata,
files,
options,
debug
clientOptions,
deploymentOptions
)) {
if (event.type === 'created') {
debug('Deployment created');
@@ -203,11 +193,7 @@ export default async function* deploy(
debug('Waiting for deployment to be ready...');
for await (const event of checkDeploymentStatus(
deployment,
options.token,
metadata.version,
options.teamId,
debug,
options.apiUrl
clientOptions
)) {
yield event;
}

View File

@@ -1,18 +1,25 @@
import { BuilderFunctions } from '@now/build-utils';
import { Builder, BuilderFunctions } from '@now/build-utils';
import { NowHeader, Route, NowRedirect, NowRewrite } from '@now/routing-utils';
export interface Route {
src: string;
dest: string;
headers?: {
[key: string]: string;
};
status?: number;
methods?: string[];
export interface Dictionary<T> {
[key: string]: T;
}
export interface Build {
src: string;
use: string;
/**
* Options for `now-client` or
* properties that should not
* be part of the payload.
*/
export interface NowClientOptions {
token: string;
path: string | string[];
debug?: boolean;
teamId?: string;
apiUrl?: string;
force?: boolean;
userAgent?: string;
defaultName?: string;
isDirectory?: boolean;
}
export interface Deployment {
@@ -20,13 +27,11 @@ export interface Deployment {
deploymentId?: string;
url: string;
name: string;
meta: {
[key: string]: string | number | boolean;
};
meta: Dictionary<string | number | boolean>;
version: number;
regions: string[];
routes: Route[];
builds?: Build[];
builds?: Builder[];
functions?: BuilderFunctions;
plan: string;
public: boolean;
@@ -47,13 +52,9 @@ export interface Deployment {
| 'ERROR';
createdAt: string;
createdIn: string;
env: {
[key: string]: string;
};
env: Dictionary<string>;
build: {
env: {
[key: string]: string;
};
env: Dictionary<string>;
};
target: string;
alias: string[];
@@ -91,51 +92,69 @@ export interface DeploymentGithubData {
autoJobCancelation: boolean;
}
export interface DeploymentOptions {
interface LegacyNowConfig {
type?: string;
aliases?: string | string[];
}
export interface NowConfig extends LegacyNowConfig {
name?: string;
version?: number;
env?: Dictionary<string>;
build?: {
env?: Dictionary<string>;
};
builds?: Builder[];
routes?: Route[];
files?: string[];
cleanUrls?: boolean;
rewrites?: NowRewrite[];
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
functions?: BuilderFunctions;
github?: DeploymentGithubData;
scope?: string;
alias?: string | string[];
}
interface LegacyDeploymentOptions {
project?: string;
forceNew?: boolean;
description?: string;
registryAuthToken?: string;
engines?: Dictionary<string>;
sessionAffinity?: 'ip' | 'key' | 'random';
deploymentType?: 'NPM' | 'STATIC' | 'DOCKER';
scale?: Dictionary<{
min?: number;
max?: number | 'auto';
}>;
limits?: {
duration?: number;
maxConcurrentReqs?: number;
timeout?: number;
};
// Can't be NowConfig, since we don't
// include all legacy types here
config?: Dictionary<any>;
}
/**
* Options that will be sent to the API.
*/
export interface DeploymentOptions extends LegacyDeploymentOptions {
version?: number;
regions?: string[];
routes?: Route[];
builds?: Build[];
builds?: Builder[];
functions?: BuilderFunctions;
env?: {
[key: string]: string;
};
env?: Dictionary<string>;
build?: {
env: {
[key: string]: string;
};
env: Dictionary<string>;
};
target?: string;
token?: string | null;
teamId?: string;
force?: boolean;
name?: string;
defaultName?: string;
isDirectory?: boolean;
path?: string | string[];
github?: DeploymentGithubData;
scope?: string;
public?: boolean;
forceNew?: boolean;
deploymentType?: 'NPM' | 'STATIC' | 'DOCKER';
registryAuthToken?: string;
engines?: { [key: string]: string };
sessionAffinity?: 'ip' | 'random';
config?: { [key: string]: any };
debug?: boolean;
apiUrl?: string;
meta?: Dictionary<string>;
}
export interface NowJsonOptions {
github?: DeploymentGithubData;
scope?: string;
type?: 'NPM' | 'STATIC' | 'DOCKER';
version?: number;
files?: string[];
}
export type CreateDeploymentFunction = (
path: string | string[],
options?: DeploymentOptions,
nowConfig?: NowJsonOptions
) => AsyncIterableIterator<any>;

View File

@@ -4,8 +4,9 @@ import retry from 'async-retry';
import { Sema } from 'async-sema';
import { DeploymentFile } from './utils/hashes';
import { fetch, API_FILES, createDebug } from './utils';
import { DeploymentError } from '.';
import deploy, { Options } from './deploy';
import { DeploymentError } from './errors';
import { deploy } from './deploy';
import { NowConfig, NowClientOptions, DeploymentOptions } from './types';
const isClientNetworkError = (err: Error | DeploymentError) => {
if (err.message) {
@@ -24,12 +25,14 @@ const isClientNetworkError = (err: Error | DeploymentError) => {
return false;
};
export default async function* upload(
export async function* upload(
files: Map<string, DeploymentFile>,
options: Options
nowConfig: NowConfig,
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<any> {
const { token, teamId, debug: isDebug, apiUrl } = options;
const debug = createDebug(isDebug);
const { token, teamId, apiUrl, userAgent } = clientOptions;
const debug = createDebug(clientOptions.debug);
if (!files && !token && !teamId) {
debug(`Neither 'files', 'token' nor 'teamId are present. Exiting`);
@@ -40,7 +43,12 @@ export default async function* upload(
debug('Determining necessary files for upload...');
for await (const event of deploy(files, options)) {
for await (const event of deploy(
files,
nowConfig,
clientOptions,
deploymentOptions
)) {
if (event.type === 'error') {
if (event.payload.code === 'missing_files') {
missingFiles = event.payload.missing;
@@ -105,8 +113,9 @@ export default async function* upload(
body: stream,
teamId,
apiUrl,
userAgent,
},
isDebug
clientOptions.debug
);
if (res.status === 200) {
@@ -185,7 +194,12 @@ export default async function* upload(
try {
debug('Starting deployment creation');
for await (const event of deploy(files, options)) {
for await (const event of deploy(
files,
nowConfig,
clientOptions,
deploymentOptions
)) {
if (event.type === 'alias-assigned') {
debug('Deployment is ready');
return yield event;

View File

@@ -1,12 +1,11 @@
import { DeploymentFile } from './hashes';
import { parse as parseUrl } from 'url';
import fetch_ from 'node-fetch';
import fetch_, { RequestInit } from 'node-fetch';
import { join, sep } from 'path';
import qs from 'querystring';
import ignore from 'ignore';
import { pkgVersion } from '../pkg';
import { Options } from '../deploy';
import { NowJsonOptions, DeploymentOptions } from '../types';
import { NowClientOptions, DeploymentOptions, NowConfig } from '../types';
import { Sema } from 'async-sema';
import { readFile } from 'fs-extra';
const semaphore = new Sema(10);
@@ -44,7 +43,7 @@ export function getApiDeploymentsUrl(
return '/v11/now/deployments';
}
export async function parseNowJSON(filePath?: string): Promise<NowJsonOptions> {
export async function parseNowJSON(filePath?: string): Promise<NowConfig> {
if (!filePath) {
return {};
}
@@ -111,10 +110,18 @@ export async function getNowIgnore(path: string | string[]): Promise<any> {
return { ig, ignores };
}
interface FetchOpts extends RequestInit {
apiUrl?: string;
method?: string;
teamId?: string;
headers?: { [key: string]: any };
userAgent?: string;
}
export const fetch = async (
url: string,
token: string,
opts: any = {},
opts: FetchOpts = {},
debugEnabled?: boolean
): Promise<any> => {
semaphore.acquire();
@@ -133,11 +140,14 @@ export const fetch = async (
delete opts.teamId;
}
const userAgent = opts.userAgent || `now-client-v${pkgVersion}`;
delete opts.userAgent;
opts.headers = {
...opts.headers,
authorization: `Bearer ${token}`,
accept: 'application/json',
'user-agent': `now-client-v${pkgVersion}`,
'user-agent': userAgent,
};
debug(`${opts.method || 'GET'} ${url}`);
@@ -160,7 +170,7 @@ const isWin = process.platform.includes('win');
export const prepareFiles = (
files: Map<string, DeploymentFile>,
options: Options
clientOptions: NowClientOptions
): PreparedFile[] => {
const preparedFiles = [...files.keys()].reduce(
(acc: PreparedFile[], sha: string): PreparedFile[] => {
@@ -171,10 +181,10 @@ export const prepareFiles = (
for (const name of file.names) {
let fileName: string;
if (options.isDirectory) {
if (clientOptions.isDirectory) {
// Directory
fileName = options.path
? name.substring(options.path.length + 1)
fileName = clientOptions.path
? name.substring(clientOptions.path.length + 1)
: name;
} else {
// Array of files or single file

View File

@@ -1,13 +1,16 @@
import { Options } from '../deploy';
import { URLSearchParams } from 'url';
import { NowClientOptions } from '../types';
export const generateQueryString = (options: Options): string => {
if (options.force && options.teamId) {
return `?teamId=${options.teamId}&forceNew=1`;
} else if (options.teamId) {
return `?teamId=${options.teamId}`;
} else if (options.force) {
return `?forceNew=1`;
export function generateQueryString(clientOptions: NowClientOptions): string {
const options = new URLSearchParams();
if (clientOptions.teamId) {
options.set('teamId', clientOptions.teamId);
}
return '';
};
if (clientOptions.force) {
options.set('forceNew', '1');
}
return Array.from(options.entries()).length ? `?${options.toString()}` : '';
}

View File

@@ -28,10 +28,12 @@ describe('create v2 deployment', () => {
it('will display an empty deployment warning', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token,
name: 'now-client-tests-v2',
path: path.resolve(__dirname, 'fixtures', 'v2'),
},
{
name: 'now-clien-tests-v2',
}
)) {
if (event.type === 'warning') {
@@ -47,9 +49,11 @@ describe('create v2 deployment', () => {
it('will report correct file count event', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v2'),
},
{
name: 'now-client-tests-v2',
}
)) {
@@ -66,9 +70,11 @@ describe('create v2 deployment', () => {
it('will create a v2 deployment', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v2'),
},
{
name: 'now-client-tests-v2',
}
)) {
@@ -82,9 +88,11 @@ describe('create v2 deployment', () => {
it('will create a v2 deployment with correct file permissions', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2-file-permissions'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v2-file-permissions'),
},
{
name: 'now-client-tests-v2',
}
)) {
@@ -104,10 +112,12 @@ describe('create v2 deployment', () => {
it('will create a v2 deployment and ignore files specified in .nowignore', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'nowignore'),
{
token,
name: 'now-client-tests-v2-ignore',
path: path.resolve(__dirname, 'fixtures', 'nowignore'),
},
{
name: 'now-client-tests-v2',
}
)) {
if (event.type === 'ready') {

View File

@@ -29,9 +29,11 @@ describe('create v1 deployment', () => {
it('will create a v1 static deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'static'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v1', 'static'),
},
{
name: 'now-client-tests-v1-static',
}
)) {
@@ -47,9 +49,11 @@ describe('create v1 deployment', () => {
it('will create a v1 npm deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'npm'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v1', 'npm'),
},
{
name: 'now-client-tests-v1-npm',
}
)) {
@@ -65,9 +69,11 @@ describe('create v1 deployment', () => {
it('will create a v1 Docker deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'docker'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v1', 'docker'),
},
{
name: 'now-client-tests-v1-docker',
}
)) {

View File

@@ -10,10 +10,15 @@ describe('path handling', () => {
it('will fali with a relative path', async () => {
try {
await createDeployment('./fixtures/v2/now.json', {
token,
name: 'now-client-tests-v2',
});
await createDeployment(
{
token,
path: './fixtures/v2/now.json',
},
{
name: 'now-client-tests-v2',
}
);
} catch (e) {
expect(e.code).toEqual('invalid_path');
}
@@ -21,10 +26,15 @@ describe('path handling', () => {
it('will fali with an array of relative paths', async () => {
try {
await createDeployment(['./fixtures/v2/now.json'], {
token,
name: 'now-client-tests-v2',
});
await createDeployment(
{
token,
path: ['./fixtures/v2/now.json'],
},
{
name: 'now-client-tests-v2',
}
);
} catch (e) {
expect(e.code).toEqual('invalid_path');
}

View File

@@ -1,6 +1,6 @@
{
"name": "@now/next",
"version": "2.1.0",
"version": "2.1.1-canary.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/next-js",

View File

@@ -1,5 +1,4 @@
import { ChildProcess, fork } from 'child_process';
import url from 'url'
import {
pathExists,
readFile,
@@ -60,8 +59,8 @@ import {
import {
convertRedirects,
convertRewrites
} from '@now/routing-utils/dist/superstatic'
convertRewrites,
} from '@now/routing-utils/dist/superstatic';
interface BuildParamsMeta {
isDev: boolean | undefined;
@@ -77,7 +76,7 @@ interface BuildParamsType extends BuildOptions {
}
export const version = 2;
const htmlContentType = 'text/html; charset=utf-8';
const nowDevChildProcesses = new Set<ChildProcess>();
['SIGINT', 'SIGTERM'].forEach(signal => {
@@ -353,7 +352,6 @@ export const build = async ({
await unlinkFile(path.join(entryPath, '.npmrc'));
}
const exportedPageRoutes: Route[] = [];
const lambdas: { [key: string]: Lambda } = {};
const prerenders: { [key: string]: Prerender | FileFsRef } = {};
const staticPages: { [key: string]: FileFsRef } = {};
@@ -484,18 +482,14 @@ export const build = async ({
return;
}
const staticRoute = path.join(entryDirectory, page);
const staticRoute = path.join(entryDirectory, pathname);
staticPages[staticRoute] = staticPageFiles[page];
staticPages[staticRoute].contentType = htmlContentType;
if (isDynamicRoute(pathname)) {
dynamicPages.push(routeName);
return;
}
exportedPageRoutes.push({
src: `^${path.join('/', entryDirectory, pathname)}$`,
dest: path.join('/', staticRoute),
});
});
const pageKeys = Object.keys(pages);
@@ -743,14 +737,9 @@ export const build = async ({
if (htmlFsRef == null || jsonFsRef == null) {
throw new Error('invariant: htmlFsRef != null && jsonFsRef != null');
}
const outputPathPageHtml = outputPathPage.concat('.html');
prerenders[outputPathPageHtml] = htmlFsRef;
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
prerenders[outputPathData] = jsonFsRef;
exportedPageRoutes.push({
src: path.posix.join('/', outputPathPage),
dest: outputPathPageHtml,
});
} else {
const lambda = lambdas[outputSrcPathPage];
if (lambda == null) {
@@ -832,7 +821,7 @@ export const build = async ({
let dynamicPrefix = path.join('/', entryDirectory);
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
const routesManifest = await getRoutesManifest(entryPath, realNextVersion)
const routesManifest = await getRoutesManifest(entryPath, realNextVersion);
const dynamicRoutes = await getDynamicRoutes(
entryPath,
@@ -842,25 +831,20 @@ export const build = async ({
routesManifest
).then(arr =>
arr.map(route => {
// make sure .html is added to dest for now until
// outputting static files to clean routes is available
if (staticPages[`${route.dest}.html`.substr(1)]) {
route.dest = `${route.dest}.html`;
}
route.src = route.src.replace('^', `^${dynamicPrefix}`);
return route;
})
);
const rewrites: Route[] = []
const redirects: Route[] = []
const rewrites: Route[] = [];
const redirects: Route[] = [];
if (routesManifest) {
switch(routesManifest.version) {
switch (routesManifest.version) {
case 1: {
redirects.push(...convertRedirects(routesManifest.redirects))
rewrites.push(...convertRewrites(routesManifest.rewrites))
break
redirects.push(...convertRedirects(routesManifest.redirects));
rewrites.push(...convertRewrites(routesManifest.rewrites));
break;
}
default: {
// update MIN_ROUTES_MANIFEST_VERSION in ./utils.ts
@@ -872,24 +856,6 @@ export const build = async ({
}
}
const topRoutes = [
// 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
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{ src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?') },
]
return {
output: {
...publicDirectoryFiles,
@@ -901,17 +867,27 @@ export const build = async ({
...staticDirectoryFiles,
},
routes: [
...topRoutes,
// redirects take the highest priority
...redirects,
...rewrites,
// we need to re-apply the routes above rewrites in-case the are
// rewriting to one of those routes
...topRoutes,
// Static exported pages (.html rewrites)
...exportedPageRoutes,
// 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
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{ src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?') },
// Next.js page lambdas, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
...rewrites,
// Dynamic routes
...dynamicRoutes,
...dynamicDataRoutes,

View File

@@ -293,37 +293,36 @@ async function getRoutes(
}
export type Rewrite = {
source: string,
destination: string,
}
source: string;
destination: string;
};
export type Redirect = Rewrite & {
statusCode?: number
}
statusCode?: number;
};
type RoutesManifestRegex = {
regex: string,
regexKeys: string[]
}
regex: string;
regexKeys: string[];
};
export type RoutesManifest = {
redirects: (Redirect & RoutesManifestRegex)[],
rewrites: (Rewrite & RoutesManifestRegex)[],
redirects: (Redirect & RoutesManifestRegex)[];
rewrites: (Rewrite & RoutesManifestRegex)[];
dynamicRoutes: {
page: string,
regex: string,
}[],
version: number
}
page: string;
regex: string;
}[];
version: number;
};
export async function getRoutesManifest(
entryPath: string,
nextVersion?: string,
): Promise< RoutesManifest | undefined> {
const shouldHaveManifest = (
nextVersion && semver.gte(nextVersion, '9.1.4-canary.0')
)
if (!shouldHaveManifest) return
nextVersion?: string
): Promise<RoutesManifest | undefined> {
const shouldHaveManifest =
nextVersion && semver.gte(nextVersion, '9.1.4-canary.0');
if (!shouldHaveManifest) return;
const pathRoutesManifest = path.join(
entryPath,
@@ -338,12 +337,12 @@ export async function getRoutesManifest(
if (shouldHaveManifest && !hasRoutesManifest) {
throw new Error(
`A routes-manifest.json couldn't be found. This could be due to a failure during the build`
)
);
}
const routesManifest: RoutesManifest = require(pathRoutesManifest)
const routesManifest: RoutesManifest = require(pathRoutesManifest);
return routesManifest
return routesManifest;
}
export async function getDynamicRoutes(

View File

@@ -14,7 +14,7 @@ it(
const {
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'standard'));
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
@@ -274,7 +274,7 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-config-object'));
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
@@ -308,7 +308,7 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-no-config'));
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
@@ -344,7 +344,7 @@ it(
path.join(__dirname, 'serverless-no-config-build')
);
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)

View File

@@ -1,6 +1,6 @@
{
"name": "@now/routing-utils",
"version": "1.3.4-canary.2",
"version": "1.3.4-canary.5",
"description": "ZEIT Now routing utilities",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"test-unit": "jest --env node --verbose --runInBand"
},
"dependencies": {
"path-to-regexp": "3.1.0"
"path-to-regexp": "6.1.0"
},
"devDependencies": {
"ajv": "^6.0.0",

View File

@@ -17,6 +17,7 @@ import {
} from './superstatic';
export { getCleanUrls } from './superstatic';
export { mergeRoutes } from './merge';
export function isHandler(route: Route): route is Handler {
return typeof (route as Handler).handle !== 'undefined';

View File

@@ -0,0 +1,104 @@
import { Route, MergeRoutesProps } from './types';
import { isHandler } from './index';
interface BuilderToRoute {
[use: string]: Route[];
}
interface BuilderRoutes {
[entrypoint: string]: BuilderToRoute;
}
function getCheckAndContinue(
routes: Route[]
): { checks: Route[]; continues: Route[]; others: Route[] } {
const checks: Route[] = [];
const continues: Route[] = [];
const others: Route[] = [];
for (const route of routes) {
if (isHandler(route)) {
// Should never happen, only here to make TS happy
others.push(route);
} else if (route.check) {
checks.push(route);
} else if (route.continue) {
continues.push(route);
} else {
others.push(route);
}
}
return { checks, continues, others };
}
export function mergeRoutes({ userRoutes, builds }: MergeRoutesProps): Route[] {
const usersRoutesBefore: Route[] = [];
const usersRoutesAfter: Route[] = [];
const builderRoutes: BuilderRoutes = {};
const builderRoutesBefore: Route[] = [];
const builderRoutesAfter: Route[] = [];
let foundUserDefinedFilesystem = false;
(userRoutes || []).forEach(route => {
if (!foundUserDefinedFilesystem) {
if (isHandler(route) && route.handle === 'filesystem') {
foundUserDefinedFilesystem = true;
} else {
usersRoutesBefore.push(route);
}
} else {
usersRoutesAfter.push(route);
}
});
// Convert build results to object mapping
for (const build of builds) {
if (build.routes) {
if (!builderRoutes[build.entrypoint]) {
builderRoutes[build.entrypoint] = {};
}
builderRoutes[build.entrypoint][build.use] = build.routes.map(route => {
//route.built = true; // TODO: is this necessary?
return route;
});
}
}
const sortedPaths = Object.keys(builderRoutes).sort();
sortedPaths.forEach(path => {
const br = builderRoutes[path];
const sortedBuilders = Object.keys(br).sort();
sortedBuilders.forEach(use => {
let isBefore = true;
br[use].forEach(route => {
if (isBefore) {
if (isHandler(route) && route.handle === 'filesystem') {
isBefore = false;
} else {
builderRoutesBefore.push(route);
}
} else {
builderRoutesAfter.push(route);
}
});
});
});
const builderBefore = getCheckAndContinue(builderRoutesBefore);
const builderAfter = getCheckAndContinue(builderRoutesAfter);
const outputRoutes: Route[] = [];
outputRoutes.push(...builderBefore.continues);
outputRoutes.push(...usersRoutesBefore);
outputRoutes.push(...builderBefore.checks);
outputRoutes.push(...builderBefore.others);
if (usersRoutesAfter.length > 0 || builderRoutesAfter.length > 0) {
outputRoutes.push({ handle: 'filesystem' });
}
outputRoutes.push(...builderAfter.continues);
outputRoutes.push(...usersRoutesAfter);
outputRoutes.push(...builderAfter.checks);
outputRoutes.push(...builderAfter.others);
return outputRoutes;
}

View File

@@ -43,6 +43,9 @@ export const routesSchema = {
continue: {
type: 'boolean',
},
check: {
type: 'boolean',
},
status: {
type: 'integer',
minimum: 100,

View File

@@ -3,7 +3,7 @@
* See https://github.com/firebase/superstatic#configuration
*/
import pathToRegexp from 'path-to-regexp';
import { pathToRegexp, Key } from 'path-to-regexp';
import { Route, NowRedirect, NowRewrite, NowHeader } from './types';
export function getCleanUrls(
@@ -45,21 +45,22 @@ export function convertRedirects(redirects: NowRedirect[]): Route[] {
return redirects.map(r => {
const { src, segments } = sourceToRegex(r.source);
const loc = replaceSegments(segments, r.destination);
return {
const route: Route = {
src,
headers: { Location: loc },
status: r.statusCode || 307,
};
return route;
});
}
export function convertRewrites(rewrites: NowRewrite[]): Route[] {
const routes: Route[] = rewrites.map(r => {
return rewrites.map(r => {
const { src, segments } = sourceToRegex(r.source);
const dest = replaceSegments(segments, r.destination);
return { src, dest, continue: true };
const route: Route = { src, dest, check: true };
return route;
});
return routes;
}
export function convertHeaders(headers: NowHeader[]): Route[] {
@@ -68,11 +69,12 @@ export function convertHeaders(headers: NowHeader[]): Route[] {
h.headers.forEach(kv => {
obj[kv.key] = kv.value;
});
return {
const route: Route = {
src: h.source,
headers: obj,
continue: true,
};
return route;
});
}
@@ -95,7 +97,7 @@ export function convertTrailingSlash(enable: boolean): Route[] {
}
function sourceToRegex(source: string): { src: string; segments: string[] } {
const keys: pathToRegexp.Key[] = [];
const keys: Key[] = [];
const r = pathToRegexp(source, keys, { strict: true });
const segments = keys.map(k => k.name).filter(isString);
return { src: r.source, segments };

View File

@@ -17,6 +17,7 @@ export type Source = {
headers?: { [name: string]: string };
methods?: string[];
continue?: boolean;
check?: boolean;
status?: number;
};
@@ -35,6 +36,17 @@ export interface GetRoutesProps {
nowConfig: NowConfig;
}
export interface MergeRoutesProps {
userRoutes?: Route[] | null | undefined;
builds: Build[];
}
export interface Build {
use: string;
entrypoint: string;
routes?: Route[];
}
export interface NowConfig {
name?: string;
version?: number;

View File

@@ -377,6 +377,28 @@ describe('normalizeRoutes', () => {
);
});
test('fails if check is not boolean', () => {
assertError(
[
// @ts-ignore
{
check: 'false',
},
],
[
{
dataPath: '[0].check',
keyword: 'type',
message: 'should be boolean',
params: {
type: 'boolean',
},
schemaPath: '#/items/properties/check/type',
},
]
);
});
test('fails if status is not number', () => {
assertError(
[
@@ -492,7 +514,7 @@ describe('getTransformedRoutes', () => {
status: 302,
},
{ handle: 'filesystem' },
{ src: '^/v1$', dest: '/v2/api.py', continue: true },
{ src: '^/v1$', dest: '/v2/api.py', check: true },
];
assert.deepEqual(actual.error, null);
assert.deepEqual(actual.routes, expected);

View File

@@ -0,0 +1,310 @@
const { deepEqual } = require('assert');
const { mergeRoutes } = require('../dist/merge');
test('mergeRoutes simple', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [{ src: '/node1', dest: '/n1' }, { src: '/node2', dest: '/n2' }],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py2', src: '/python2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes handle filesystem user routes', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ handle: 'filesystem' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [{ src: '/node1', dest: '/n1' }, { src: '/node2', dest: '/n2' }],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py2', src: '/python2' },
{ handle: 'filesystem' },
{ dest: '/u2', src: '/user2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes handle filesystem build routes', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ handle: 'filesystem' },
{ src: '/node2', dest: '/n2' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ handle: 'filesystem' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/py1', src: '/python1' },
{ handle: 'filesystem' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py2', src: '/python2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes handle filesystem both user and builds', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ handle: 'filesystem' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ handle: 'filesystem' },
{ src: '/node2', dest: '/n2' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ handle: 'filesystem' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/n1', src: '/node1' },
{ dest: '/py1', src: '/python1' },
{ handle: 'filesystem' },
{ dest: '/u2', src: '/user2' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py2', src: '/python2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes continue true', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2', continue: true },
{ src: '/user3', dest: '/u3' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ src: '/node3', dest: '/n2', continue: true },
{ src: '/node3', dest: '/n3' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2', continue: true },
{ src: '/python3', dest: '/py3' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ continue: true, dest: '/n2', src: '/node3' },
{ continue: true, dest: '/py2', src: '/python2' },
{ dest: '/u1', src: '/user1' },
{ continue: true, dest: '/u2', src: '/user2' },
{ dest: '/u3', src: '/user3' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n3', src: '/node3' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py3', src: '/python3' },
];
deepEqual(actual, expected);
});
test('mergeRoutes check true', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2' },
{ src: '/user3', dest: '/u3' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ src: '/node3', dest: '/n2', check: true },
{ src: '/node3', dest: '/n3' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2', check: true },
{ src: '/python3', dest: '/py3' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ dest: '/u3', src: '/user3' },
{ check: true, dest: '/n2', src: '/node3' },
{ check: true, dest: '/py2', src: '/python2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n3', src: '/node3' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py3', src: '/python3' },
];
deepEqual(actual, expected);
});
test('mergeRoutes check true, continue true, handle filesystem middle', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1', continue: true },
{ src: '/user2', dest: '/u2' },
{ handle: 'filesystem' },
{ src: '/user3', dest: '/u3' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1', continue: true },
{ src: '/node3', dest: '/n2', check: true },
{ handle: 'filesystem' },
{ src: '/node3', dest: '/n3' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1', check: true },
{ src: '/python2', dest: '/py2', continue: true },
{ handle: 'filesystem' },
{ src: '/python3', dest: '/py3' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ continue: true, dest: '/n1', src: '/node1' },
{ continue: true, dest: '/py2', src: '/python2' },
{ continue: true, dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ check: true, dest: '/n2', src: '/node3' },
{ check: true, dest: '/py1', src: '/python1' },
{ handle: 'filesystem' },
{ dest: '/u3', src: '/user3' },
{ dest: '/n3', src: '/node3' },
{ dest: '/py3', src: '/python3' },
];
deepEqual(actual, expected);
});
test('mergeRoutes check true, continue true, handle filesystem top', () => {
const userRoutes = [{ handle: 'filesystem' }, { src: '/user1', dest: '/u1' }];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ handle: 'filesystem' },
{ src: '/node1', dest: '/n1' },
{ src: '/node2', dest: '/n2', continue: true },
{ src: '/node3', dest: '/n3', check: true },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ handle: 'filesystem' },
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2', check: true },
{ src: '/python3', dest: '/py3', continue: true },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ handle: 'filesystem' },
{ continue: true, dest: '/n2', src: '/node2' },
{ continue: true, dest: '/py3', src: '/python3' },
{ dest: '/u1', src: '/user1' },
{ check: true, dest: '/n3', src: '/node3' },
{ check: true, dest: '/py2', src: '/python2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/py1', src: '/python1' },
];
deepEqual(actual, expected);
});

View File

@@ -148,6 +148,7 @@ test('convertCleanUrls false', () => {
test('convertRedirects', () => {
const actual = convertRedirects([
{ source: '/some/old/path', destination: '/some/new/path' },
{ source: '/next(\\.js)?', destination: 'https://nextjs.org' },
{
source: '/firebase/(.*)',
destination: 'https://www.firebase.com',
@@ -167,17 +168,22 @@ test('convertRedirects', () => {
status: 307,
},
{
src: '^\\/firebase\\/(.*)$',
src: '^\\/next(\\.js)?$',
headers: { Location: 'https://nextjs.org' },
status: 307,
},
{
src: '^\\/firebase(?:\\/(.*))$',
headers: { Location: 'https://www.firebase.com' },
status: 302,
},
{
src: '^\\/projects\\/([^\\/]+?)\\/([^\\/]+?)$',
src: '^\\/projects(?:\\/([^\\/#\\?]+?))(?:\\/([^\\/#\\?]+?))$',
headers: { Location: '/projects.html?id=$1&action=$2' },
status: 307,
},
{
src: '^\\/old\\/([^\\/]+?)\\/path$',
src: '^\\/old(?:\\/([^\\/#\\?]+?))\\/path$',
headers: { Location: '/new/path/$1' },
status: 307,
},
@@ -187,6 +193,7 @@ test('convertRedirects', () => {
const mustMatch = [
['/some/old/path'],
['/next', '/next.js'],
['/firebase/one', '/firebase/2', '/firebase/-', '/firebase/dir/sub'],
['/projects/one/edit', '/projects/two/edit'],
['/old/one/path', '/old/two/path'],
@@ -194,6 +201,7 @@ test('convertRedirects', () => {
const mustNotMatch = [
['/nope'],
['/nextAjs', '/nextjs'],
['/fire', '/firebasejumper/two'],
['/projects/edit', '/projects/two/three/delete', '/projects'],
['/old/path', '/old/two/foo', '/old'],
@@ -210,16 +218,16 @@ test('convertRewrites', () => {
]);
const expected = [
{ src: '^\\/some\\/old\\/path$', dest: '/some/new/path', continue: true },
{ src: '^\\/some\\/old\\/path$', dest: '/some/new/path', check: true },
{
src: '^\\/firebase\\/(.*)$',
src: '^\\/firebase(?:\\/(.*))$',
dest: 'https://www.firebase.com',
continue: true,
check: true,
},
{
src: '^\\/projects\\/([^\\/]+?)\\/edit$',
src: '^\\/projects(?:\\/([^\\/#\\?]+?))\\/edit$',
dest: '/projects.html?id=$1',
continue: true,
check: true,
},
];

View File

@@ -1 +1,5 @@
dist/
# bypass all ignored files for the cache fixtures
# because they contain node_modules and package-lock.json files
!test/cache-fixtures/**

View File

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

View File

@@ -44,6 +44,7 @@ export const frameworks: Framework[] = [
return [];
}
},
cachePattern: '.cache/**',
},
{
name: 'Hexo',
@@ -312,4 +313,5 @@ export interface Framework {
getOutputDirName: (dirPrefix: string) => Promise<string>;
defaultRoutes?: Route[] | ((dirPrefix: string) => Promise<Route[]>);
minNodeRange?: string;
cachePattern?: string;
}

View File

@@ -18,6 +18,7 @@ import {
getNodeVersion,
getSpawnOptions,
Files,
FileFsRef,
Route,
BuildOptions,
Config,
@@ -149,6 +150,24 @@ async function getFrameworkRoutes(
return routes;
}
function getPkg(entrypoint: string, workPath: string) {
if (path.basename(entrypoint) !== 'package.json') {
return null;
}
const pkgPath = path.join(workPath, entrypoint);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
return pkg;
}
function getFramework(pkg: PackageJson) {
const dependencies = Object.assign({}, pkg.dependencies, pkg.devDependencies);
const framework = frameworks.find(
({ dependency }) => dependencies[dependency || '']
);
return framework;
}
export async function build({
files,
entrypoint,
@@ -168,11 +187,9 @@ export async function build({
(config && (config.distDir as string)) || 'dist'
);
const entrypointName = path.basename(entrypoint);
const pkg = getPkg(entrypoint, workPath);
if (entrypointName === 'package.json') {
const pkgPath = path.join(workPath, entrypoint);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
if (pkg) {
const gemfilePath = path.join(workPath, 'Gemfile');
const requirementsPath = path.join(workPath, 'requirements.txt');
@@ -230,15 +247,7 @@ export async function build({
// `public` is the default for zero config
distPath = path.join(workPath, path.dirname(entrypoint), 'public');
const dependencies = Object.assign(
{},
pkg.dependencies,
pkg.devDependencies
);
framework = frameworks.find(
({ dependency }) => dependencies[dependency || '']
);
framework = getFramework(pkg);
}
if (framework) {
@@ -377,7 +386,7 @@ export async function build({
return { routes, watch, output, distPath };
}
if (!config.zeroConfig && entrypointName.endsWith('.sh')) {
if (!config.zeroConfig && entrypoint.endsWith('.sh')) {
debug(`Running build script "${entrypoint}"`);
const nodeVersion = await getNodeVersion(entrypointDir, undefined, config);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
@@ -403,10 +412,27 @@ export async function build({
throw new Error(message);
}
export async function prepareCache({ workPath }: PrepareCacheOptions) {
return {
...(await glob('node_modules/**', workPath)),
...(await glob('package-lock.json', workPath)),
...(await glob('yarn.lock', workPath)),
};
export async function prepareCache({
entrypoint,
workPath,
}: PrepareCacheOptions) {
// default cache paths
const defaultCacheFiles = await glob(
'{node_modules/**,package-lock.json,yarn.lock}',
workPath
);
// framework specific cache paths
let frameworkCacheFiles: { [path: string]: FileFsRef } | null = null;
const pkg = getPkg(entrypoint, workPath);
if (pkg) {
const framework = getFramework(pkg);
if (framework && framework.cachePattern) {
frameworkCacheFiles = await glob(framework.cachePattern, workPath);
}
}
return { ...defaultCacheFiles, ...frameworkCacheFiles };
}

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"gatsby": "^2.13.3"
}
}

View File

@@ -0,0 +1,27 @@
const { prepareCache } = require('../dist');
const path = require('path');
describe('prepareCache', () => {
test('should cache yarn.lock, package-lock.json and node_modules', async () => {
const files = await prepareCache({
workPath: path.resolve(__dirname, './cache-fixtures/default'),
entrypoint: 'index.js',
});
expect(files['yarn.lock']).toBeDefined();
expect(files['package-lock.json']).toBeDefined();
expect(files['node_modules/file']).toBeDefined();
expect(files['index.js']).toBeUndefined();
});
test('should cache `.cache` folder for gatsby deployments', async () => {
const files = await prepareCache({
workPath: path.resolve(__dirname, './cache-fixtures/gatsby'),
entrypoint: 'package.json',
});
expect(files['.cache/file']).toBeDefined();
expect(files['yarn.lock']).toBeDefined();
});
});

View File

@@ -8610,10 +8610,10 @@ path-to-regexp@2.2.1:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==
path-to-regexp@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.1.0.tgz#f45a9cc4dc6331ae8f131e0ce4fde8607f802367"
integrity sha512-PtHLisEvUOepjc+sStXxJ/pDV/s5UBTOKWJY2SOz3e6E/iN/jLknY9WL72kTwRrwXDUbZTEAtSnJbz2fF127DA==
path-to-regexp@6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427"
integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==
path-to-regexp@^1.0.0, path-to-regexp@^1.7.0:
version "1.7.0"