Compare commits

...

36 Commits

Author SHA1 Message Date
Nathan Rajlich
03b5a0c0bf Publish
- @now/build-utils@0.11.0
 - now@16.4.0
 - now-client@5.2.1
 - @now/node@1.0.2
 - @now/python@0.3.2
 - @now/routing-utils@1.3.0
 - @now/static-build@0.11.0
2019-10-21 16:19:11 -07:00
Steven
f76abe3372 [now-build-utils] Add contentType prop to File (#3178)
This PR adds a `contentType` to the File interface.

This is necessary for [PRODUCT-341] to work properly with `cleanUrls` which will strip the file name but we will still need specify `contentType: 'text/html'`.

This is required for @dav-is `api-builds` PR 633 to work properly.

[PRODUCT-341]: https://zeit.atlassian.net/browse/PRODUCT-341
2019-10-21 16:11:16 -07:00
Steven
cdd43b74ae [now-node] Fix helpers when POST json has empty body (#3177)
The `@now/node` helpers json parsing is too strict and doesn't match the behavior of Express when an incoming request has `{ method: 'POST', Content-Type: 'application/json', body: '' }`.

Instead of returning 400, this PR will continue with `body = {}` to match Express.

Fixes https://spectrum.chat/zeit/now/klarna-with-zeit-now~60852003-4db6-4ec4-a611-83b2349ece08
2019-10-21 16:11:09 -07:00
Nathan Rajlich
fa633d0e02 [now-cli] Temporarily force TooTallNate/signal-exit#update/sighub-to-sigint-on-windows for "signal-exit" module (#3171)
Fixes https://github.com/zeit/now/issues/2564.

Upstream fix PR: https://github.com/tapjs/signal-exit/pull/55.
2019-10-21 16:11:00 -07:00
Nathan Rajlich
9b46e60c09 [now-cli] Remove serveProjectAsStatic() (#3170)
Because we treat purely static projects as v2 projects with a `@now/static` builder specified, this is now dead code.
2019-10-21 16:10:54 -07:00
Steven
58eef7f394 [now-build-utils] Fix static file serving from now dev (#3167)
This PR does a few things

- Separate tests into `integration.test.js` and `unit.test.js`
- Use one static build instead of many to avoid `should NOT have more than 128 items` error
- Add a new unit test so we don't regress

Fixes #3159
2019-10-21 16:10:39 -07:00
Nathan Rajlich
e97e0fbb64 [now-cli] Spawn builder child processes with stdio: 'inherit' in now dev (#3113)
Inherit the `now dev` process stdio streams in builder child processes, so that ANSI color codes may be used when stdout is a TTY.

**Examples:**

_Next.js_

<img width="523" alt="Screen Shot 2019-10-01 at 3 42 33 PM" src="https://user-images.githubusercontent.com/71256/66006087-4fe75780-e462-11e9-927f-1e81466c4108.png">

_Gatsby_ (depends on #3112)

<img width="507" alt="Screen Shot 2019-10-01 at 3 40 02 PM" src="https://user-images.githubusercontent.com/71256/66006094-5d9cdd00-e462-11e9-81da-e60bd9516778.png">

Fixes #3135.
2019-10-21 16:10:32 -07:00
Steven
b82876fd82 [now-routing-utils] Add superstatic transformations (#3138)
This implements a new function `getTransformedRoutes()` which transforms some [superstatic](https://github.com/firebase/superstatic#configuration) configuration keys to Now routes so we can eventually use this new keys in `now.json`.

In particular, the following new keys are transformed to `routes`.

- cleanUrls
- rewrites
- redirects
- headers
- trailingSlash

[PRODUCT-341] #close

[PRODUCT-341]: https://zeit.atlassian.net/browse/PRODUCT-341
2019-10-21 16:10:08 -07:00
Steven
02ad32ec22 [tests] Fix tests so they wait for v1 deployments (#3162)
Our tests periodically fail because we're creating too many v1 deployments on a free plan and it times out.

This limits the number of deployments by running v1 exclusively under Node 12.

It also adds a missing waitForDeployment().
2019-10-21 16:09:16 -07:00
Allen Hai
433fe35c93 [now-static-build] Add stencil to list of optimized static frameworks (#3158)
This PR enables a zero-config deployment experience for `stencil` projects. The default build command outputs to a `www` directory.
2019-10-21 16:09:06 -07:00
Jacob Mischka
90c59d6ae2 [now-python] Fix UnicodeDecodeError for binary response from handler (#3148)
Fixes #3147
2019-10-21 16:08:51 -07:00
Joe Haddad
33672c3d78 [now-build-utils] Allow a null fallback in Prerender (#3144)
This allows a `null` `fallback` to be provided to a `Prerender`. The use case is a lazily prerendered route (often meaning dynamically rendered).
2019-10-21 16:08:42 -07:00
Nathan Rajlich
59ae7a989a [now-cli] Set the builder's debug env var based on DevServer.debug (#3139)
This makes it so that a programatically created `DevServer` instance that has `debug` mode enabled also gets set on the builder child processes as expected, rather than only when invoked via CLI.

For example, the `dev-server.unit.js` tests can set `debug: true` and with this change the builder child processes will also have debug logs enabled. See [here](https://git.io/JeW0O).
2019-10-21 16:08:35 -07:00
Nathan Rajlich
5767e9e8c2 [now-static-build] Exit dev server child processes upon SIGINT/SIGTERM (#3136)
Explicitly send the SIGINT / SIGTERM signal to `now dev` server child processes, so that they are not left running when running the now-dev unit tests.

Related to #3113 which has hanging unit tests that never "complete".
2019-10-21 16:08:26 -07:00
Steven
e62b9e8ed9 [now-build-utils] Fix typo occurres => occurs (#3132)
Fixes #2931
2019-10-21 16:08:10 -07:00
Sophearak Tha
59597ccd17 [now-cli] Update readme (#3128) 2019-10-21 16:07:57 -07:00
Andy
7be49c66ef Revert "[now-cli] Remove dev: "now dev" script detection logic in now dev (#3088)" (#3127)
This reverts commit 85170d7231.
2019-10-21 16:07:50 -07:00
Sophearak Tha
1380e25ef3 [now-client] Fix ENOENT regression in now-client (#3125)
This PR should fixes `ENOENT` related errors

```
> Error! ENOENT: no such file or directory, stat '.../node_modules/.bin/...'
```

Related: https://github.com/zeit/now/issues/3104
2019-10-21 16:07:45 -07:00
Nathan Rajlich
e825ce746f [now-cli] Remove dev: "now dev" script detection logic in now dev (#3088)
As of https://github.com/zeit/now-builders/pull/679, this logic is unnecessary because the `@now/static-build` builder will never end up executing the `dev` script when there is a `now.json` file present (and thus, no builds present, aka zero config mode).

Also, statically detecting the `now dev` command from the script command is brittle, as the command could execute a separate shell script that ends up executing `now dev` (and this detection logic would be a false negative).
2019-10-21 16:07:38 -07:00
Joe Haddad
4e58951808 Publish
- @now/next@1.0.4
2019-10-20 14:56:29 -04:00
Joe Haddad
fbd805aad7 [now-next] Update console.time labels for clarity (#3157) 2019-10-20 14:37:35 -04:00
JJ Kasper
2a2705c6e3 Add test for now dev and Next.js src dir (#3149)
Follow up on #3140 we needed to publish the change before we could test it in `now dev`
2019-10-20 14:37:27 -04:00
JJ Kasper
986c957183 [now-next] Add support for src dir in now-dev (#3140)
Fixes: #3133
Fixes: https://github.com/zeit/next.js/issues/9007
2019-10-20 14:37:13 -04:00
Nathan Rajlich
c5d063e876 [now-next] Exit dev server child processes upon SIGINT/SIGTERM (#3137)
Explicitly send the SIGINT / SIGTERM signal to `now dev` server child processes, so that they are not left running when running the now-dev unit tests.

Related to #3113 which has hanging unit tests that never "complete".
2019-10-20 14:37:07 -04:00
Steven
500c36f5d4 Publish
- now@16.3.1
 - now-client@5.2.0
 - @now/next@1.0.3
 - @now/python@0.3.1
 - @now/static-build@0.10.1
2019-10-03 13:54:14 -04:00
Steven
69dbbeac44 [now-cli][now-client] Fix v1 files when defining a directory (#3123)
This is a follow up to #3117 which added a fix for `files` but did not observe directories.

This PR fixes the scenario where a directory is defined such that all files inside the directory should be added uploaded (recursively).

Thanks to @williamli 

[PRODUCT-350] #close

[PRODUCT-350]: https://zeit.atlassian.net/browse/PRODUCT-350
2019-10-03 13:44:21 -04:00
Sophearak Tha
69486c3adb [now-client] [now-cli] Handle notice type (#3122)
This PR handle `notice` type from API respond.
2019-10-03 12:14:02 -04:00
Steven
e6692bb79b [now-cli][now-client] Fix --local-config flag and files key (#3117)
This PR is a followup to #3110 that fixes the first deployment when using the `--local-config` flag and also fixes v1 deployments using the [`files`](https://zeit.co/docs/v1/features/configuration/#files-(array)) key.

The tests have been adjusted so we don't regress in both cases.

Fixes #3099 
Fixes #3105
Fixes #3107
Fixes #3109

[PRODUCT-350] #close

[PRODUCT-350]: https://zeit.atlassian.net/browse/PRODUCT-350
2019-10-03 09:07:01 -04:00
Chris
94fba1d7af [now-python] Encode body as utf-8 before making a request (#3093)
Fixes #3091
2019-10-03 09:06:54 -04:00
Sophearak Tha
223d8f4774 [now-cli] Rename lambda to serverless function (#3100)
This PR fix: [PRODUCT-66] #close

[PRODUCT-66]: https://zeit.atlassian.net/browse/PRODUCT-66
2019-10-03 09:06:49 -04:00
Nathan Rajlich
42e7a7e4e3 [now-static-build] Use stdio: 'inherit' for "dev" script child process (#3112)
Since `@now/static-build` is no longer sniffing the stdio streams for the bound port number in `now dev`, there's no need to have separate stdio streams for the "dev" script. Instead, inherit stdio from the parent process, which will allow for ANSI colors to be used when stdout is a TTY in `now dev`.

Also simplifies the `checkForPort()` function and removes the `promise-timeout` dependency.
2019-10-03 09:06:44 -04:00
Steven
6716fdd49b [now-cli][now-client] Add parameter nowConfig for custom now.json (#3110)
When now-client was implemented, it did not work with `--local-config` flag from now-cli because the only parameters it looks at are the files in a directory.

This fixes the regression in now@16.3.0 so that now-client can accept an optional `nowConfig` object or fallback to the `now.json` file.

Fixes #3099 
Fixes #3105
Fixes #3107
Fixes #3109

[PRODUCT-350] #close


[PRODUCT-350]: https://zeit.atlassian.net/browse/PRODUCT-350
2019-10-03 09:06:38 -04:00
Steven
3b69092fd8 Revert "[tests] Add test all script" since it runs on commit (#3108)
Reverts #3106 from @MAPESO 

The tests seem to be running on each commit which is going to slow down development.
2019-10-03 09:06:32 -04:00
Luis Alvarez D
aa8eaedbc8 [now-next] Upload build-time generated static artifacts (#3096)
For context, when you have a script that generates a new static file at build time (`sitemap.xml` for example), it has to be inside `.next/static`, and then you'll need a Now route for it, with this change you could generate the file inside `public`/`static` and the builder will now take care of it.

The util `includeOnlyEntryDirectory` is no longer being used after this change, should I remove it?
2019-10-03 09:06:25 -04:00
Mark
f519ed373f [tests] Add test all script (#3106)
This PR focuses on adding the `test` script to the `package.json` : )

## Main problem 

Previously there was no `test` script that includes all the tests

<img width="924" alt="Screen Shot 2019-10-01 at 8 13 13 AM" src="https://user-images.githubusercontent.com/16585386/65968681-26f6a080-e429-11e9-9f29-c6fd343fdb12.png">
2019-10-03 09:06:20 -04:00
Ana Trajkovska
851dff4b03 [now-cli] Integrate Projects API v2 (#3063)
This PR integrates v2 of Projects API that fixes an issue for projects named `list` or `remove`, because of the naming of the endpoints in v1. For listing all projects, previously in v1 it was `GET /v1/projects/list` and now it is `GET /v2/projects/`, and for removing a project it was `DELETE /v1/projects/remove`.
2019-10-03 09:06:14 -04:00
97 changed files with 7990 additions and 844 deletions

View File

@@ -332,6 +332,24 @@ jobs:
name: Running Integration Tests Once
command: yarn test-integration-once --clean false
test-unit:
docker:
- image: circleci/node:10
working_directory: ~/repo
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Compiling `now dev` HTML error templates
command: node packages/now-cli/scripts/compile-templates.js
- run:
name: Output version
command: node --version
- run:
name: Running Unit Tests
command: yarn test-unit --clean false
coverage:
docker:
- image: circleci/node:10
@@ -466,6 +484,9 @@ workflows:
- test-integration-once:
requires:
- build
- test-unit:
requires:
- build
filters:
tags:
only: /.*/
@@ -484,6 +505,7 @@ workflows:
- test-integration-linux-now-dev-node-10
- test-integration-linux-now-dev-node-12
- test-integration-once
- test-unit
- test-lint
filters:
tags:

View File

@@ -58,5 +58,8 @@
"hooks": {
"pre-commit": "lint-staged"
}
},
"resolutions": {
"signal-exit": "TooTallNate/signal-exit#update/sighub-to-sigint-on-windows"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@now/build-utils",
"version": "0.10.1",
"version": "0.11.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -12,7 +12,8 @@
},
"scripts": {
"build": "./build.sh",
"test-integration-once": "jest --env node --verbose --runInBand",
"test-unit": "jest --env node --verbose --runInBand test/unit.test.js",
"test-integration-once": "jest --env node --verbose --runInBand test/integration.test.js",
"prepublishOnly": "./build.sh"
},
"devDependencies": {

View File

@@ -173,19 +173,17 @@ export async function detectBuilders(
src: 'public/**/*',
config,
});
} else if (builders.length > 0) {
// We can't use pattern matching, since `!(api)` and `!(api)/**/*`
// won't give the correct results
builders.push(
...files
.filter(name => !name.startsWith('api/'))
.filter(name => !(name === 'package.json'))
.map(name => ({
use: '@now/static',
src: name,
config,
}))
);
} else if (
builders.length > 0 &&
files.some(f => !f.startsWith('api/') && f !== 'package.json')
) {
// Everything besides the api directory
// and package.json can be served as static files
builders.push({
use: '@now/static',
src: '!{api/**,package.json}',
config,
});
}
}

View File

@@ -118,7 +118,7 @@ function partiallyMatches(pathA: string, pathB: string): boolean {
return false;
}
// Counts how often a path occurres when all placeholders
// Counts how often a path occurs when all placeholders
// got resolved, so we can check if they have conflicts
function pathOccurrences(filePath: string, files: string[]): string[] {
const getAbsolutePath = (unresolvedPath: string): string => {
@@ -226,7 +226,7 @@ async function detectApiRoutes(files: string[]): Promise<RoutesResult> {
error: {
code: 'conflicting_path_segment',
message:
`The segment "${conflictingSegment}" occurres more than ` +
`The segment "${conflictingSegment}" occurs more than ` +
`one time in your path "${file}". Please make sure that ` +
`every segment in a path is unique`
}

View File

@@ -4,11 +4,13 @@ import { File } from './types';
interface FileBlobOptions {
mode?: number;
contentType?: string;
data: string | Buffer;
}
interface FromStreamOptions {
mode?: number;
contentType?: string;
stream: NodeJS.ReadableStream;
}
@@ -16,16 +18,22 @@ export default class FileBlob implements File {
public type: 'FileBlob';
public mode: number;
public data: string | Buffer;
public contentType: string | undefined;
constructor({ mode = 0o100644, data }: FileBlobOptions) {
constructor({ mode = 0o100644, contentType, data }: FileBlobOptions) {
assert(typeof mode === 'number');
assert(typeof data === 'string' || Buffer.isBuffer(data));
this.type = 'FileBlob';
this.mode = mode;
this.contentType = contentType;
this.data = data;
}
static async fromStream({ mode = 0o100644, stream }: FromStreamOptions) {
static async fromStream({
mode = 0o100644,
contentType,
stream,
}: FromStreamOptions) {
assert(typeof mode === 'number');
assert(typeof stream.pipe === 'function'); // is-stream
const chunks: Buffer[] = [];
@@ -37,7 +45,7 @@ export default class FileBlob implements File {
});
const data = Buffer.concat(chunks);
return new FileBlob({ mode, data });
return new FileBlob({ mode, contentType, data });
}
toStream(): NodeJS.ReadableStream {

View File

@@ -9,11 +9,13 @@ const semaToPreventEMFILE = new Sema(20);
interface FileFsRefOptions {
mode?: number;
contentType?: string;
fsPath: string;
}
interface FromStreamOptions {
mode: number;
contentType?: string;
stream: NodeJS.ReadableStream;
fsPath: string;
}
@@ -22,17 +24,20 @@ class FileFsRef implements File {
public type: 'FileFsRef';
public mode: number;
public fsPath: string;
public contentType: string | undefined;
constructor({ mode = 0o100644, fsPath }: FileFsRefOptions) {
constructor({ mode = 0o100644, contentType, fsPath }: FileFsRefOptions) {
assert(typeof mode === 'number');
assert(typeof fsPath === 'string');
this.type = 'FileFsRef';
this.mode = mode;
this.contentType = contentType;
this.fsPath = fsPath;
}
static async fromFsPath({
mode,
contentType,
fsPath,
}: FileFsRefOptions): Promise<FileFsRef> {
let m = mode;
@@ -40,11 +45,12 @@ class FileFsRef implements File {
const stat = await fs.lstat(fsPath);
m = stat.mode;
}
return new FileFsRef({ mode: m, fsPath });
return new FileFsRef({ mode: m, contentType, fsPath });
}
static async fromStream({
mode = 0o100644,
contentType,
stream,
fsPath,
}: FromStreamOptions): Promise<FileFsRef> {
@@ -63,7 +69,7 @@ class FileFsRef implements File {
dest.on('error', reject);
});
return new FileFsRef({ mode, fsPath });
return new FileFsRef({ mode, contentType, fsPath });
}
async toStreamAsync(): Promise<NodeJS.ReadableStream> {

View File

@@ -8,6 +8,7 @@ import { File } from './types';
interface FileRefOptions {
mode?: number;
digest: string;
contentType?: string;
mutable?: boolean;
}
@@ -26,14 +27,21 @@ export default class FileRef implements File {
public type: 'FileRef';
public mode: number;
public digest: string;
public contentType: string | undefined;
private mutable: boolean;
constructor({ mode = 0o100644, digest, mutable = false }: FileRefOptions) {
constructor({
mode = 0o100644,
digest,
contentType,
mutable = false,
}: FileRefOptions) {
assert(typeof mode === 'number');
assert(typeof digest === 'string');
this.type = 'FileRef';
this.mode = mode;
this.digest = digest;
this.contentType = contentType;
this.mutable = mutable;
}

View File

@@ -6,7 +6,7 @@ import { Lambda } from './lambda';
interface PrerenderOptions {
expiration: number;
lambda: Lambda;
fallback: FileBlob | FileFsRef | FileRef;
fallback: FileBlob | FileFsRef | FileRef | null;
group?: number;
}
@@ -14,22 +14,29 @@ export class Prerender {
public type: 'Prerender';
public expiration: number;
public lambda: Lambda;
public fallback: FileBlob | FileFsRef | FileRef;
public fallback: FileBlob | FileFsRef | FileRef | null;
public group?: number;
constructor({ expiration, lambda, fallback, group }: PrerenderOptions) {
this.type = 'Prerender';
this.expiration = expiration;
this.lambda = lambda;
this.fallback = fallback;
if (
typeof group !== 'undefined' &&
(group <= 0 || !Number.isInteger(group))
) {
throw new Error('The `group` argument for `Prerender` needs to be a natural number.');
throw new Error(
'The `group` argument for `Prerender` needs to be a natural number.'
);
}
this.group = group;
if (typeof fallback === 'undefined') {
throw new Error(
'The `fallback` argument for `Prerender` needs to be a `FileBlob`, `FileFsRef`, `FileRef`, or null.'
);
}
this.fallback = fallback;
}
}

View File

@@ -8,6 +8,7 @@ export interface Env {
export interface File {
type: string;
mode: number;
contentType?: string;
toStream: () => NodeJS.ReadableStream;
/**
* The absolute path to the file in the filesystem

View File

@@ -0,0 +1,209 @@
const path = require('path');
const fs = require('fs-extra');
const {
packAndDeploy,
testDeployment,
} = require('../../../test/lib/deployment/test-deployment');
const { glob, detectBuilders, detectRoutes } = require('../');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
beforeAll(async () => {
const buildUtilsPath = path.resolve(__dirname, '..');
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
console.log('buildUtilsUrl', buildUtilsUrl);
});
const fixturesPath = path.resolve(__dirname, 'fixtures');
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath)) {
if (fixture.includes('zero-config')) {
// Those have separate tests
continue; // eslint-disable-line no-continue
}
// eslint-disable-next-line no-loop-func
it(`should build ${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath, fixture)
)
).resolves.toBeDefined();
});
}
// few foreign tests
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
// eslint-disable-next-line no-restricted-syntax
for (const builder of buildersToTestWith) {
const fixturesPath2 = path.resolve(
__dirname,
`../../${builder}/test/fixtures`
);
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath2)) {
// don't run all foreign fixtures, just some
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
// eslint-disable-next-line no-loop-func
it(`should build ${builder}/${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath2, fixture)
)
).resolves.toBeDefined();
});
}
}
}
it('Test `detectBuilders` and `detectRoutes`', async () => {
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/my-endpoint',
mustContain: 'my-endpoint',
status: 200,
},
{
path: '/api/other-endpoint',
mustContain: 'other-endpoint',
status: 200,
},
{
path: '/api/team/zeit',
mustContain: 'team/zeit',
status: 200,
},
{
path: '/api/user/myself',
mustContain: 'user/myself',
status: 200,
},
{
path: '/api/not-okay/',
status: 404,
},
{
path: '/api',
status: 404,
},
{
path: '/api/',
status: 404,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});
it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/not-okay',
status: 404,
},
{
path: '/api',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index.js',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/date.js',
mustContain: 'hello from api/date.js',
status: 200,
},
{
// Someone might expect this to be `date.js`,
// but I doubt that there is any case were both
// `date/index.js` and `date.js` exists,
// so it is not special cased
path: '/api/date',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index.js',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});

View File

@@ -8,23 +8,6 @@ const {
getSupportedNodeVersion,
defaultSelection,
} = require('../dist/fs/node-version');
const {
packAndDeploy,
testDeployment,
} = require('../../../test/lib/deployment/test-deployment');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
beforeAll(async () => {
const buildUtilsPath = path.resolve(__dirname, '..');
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
console.log('buildUtilsUrl', buildUtilsUrl);
});
// unit tests
it('should re-create symlinks properly', async () => {
const files = await glob('**', path.join(__dirname, 'symlinks'));
@@ -142,56 +125,6 @@ it('should support require by path for legacy builders', () => {
expect(Lambda2).toBe(index.Lambda);
});
// own fixtures
const fixturesPath = path.resolve(__dirname, 'fixtures');
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath)) {
if (fixture.includes('zero-config')) {
// Those have separate tests
continue; // eslint-disable-line no-continue
}
// eslint-disable-next-line no-loop-func
it(`should build ${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath, fixture)
)
).resolves.toBeDefined();
});
}
// few foreign tests
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
// eslint-disable-next-line no-restricted-syntax
for (const builder of buildersToTestWith) {
const fixturesPath2 = path.resolve(
__dirname,
`../../${builder}/test/fixtures`
);
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath2)) {
// don't run all foreign fixtures, just some
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
// eslint-disable-next-line no-loop-func
it(`should build ${builder}/${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath2, fixture)
)
).resolves.toBeDefined();
});
}
}
}
it('Test `detectBuilders`', async () => {
{
// package.json + no build
@@ -258,7 +191,7 @@ it('Test `detectBuilders`', async () => {
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/users.js');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('index.html');
expect(builders[1].src).toBe('!{api/**,package.json}');
expect(builders.length).toBe(2);
expect(errors).toBe(null);
}
@@ -270,10 +203,8 @@ it('Test `detectBuilders`', async () => {
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint].js');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('index.html');
expect(builders[2].use).toBe('@now/static');
expect(builders[2].src).toBe('static/image.png');
expect(builders.length).toBe(3);
expect(builders[1].src).toBe('!{api/**,package.json}');
expect(builders.length).toBe(2);
expect(errors).toBe(null);
}
@@ -331,10 +262,8 @@ it('Test `detectBuilders`', async () => {
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('favicon.ico');
expect(builders[2].use).toBe('@now/static');
expect(builders[2].src).toBe('index.html');
expect(builders.length).toBe(3);
expect(builders[1].src).toBe('!{api/**,package.json}');
expect(builders.length).toBe(2);
}
{
@@ -502,6 +431,19 @@ it('Test `detectBuilders`', async () => {
expect(builders[0].use).toBe('@now/node');
expect(builders[1].use).toBe('@now/next');
}
{
// many static files + one api file
const files = Array.from({ length: 5000 }).map((_, i) => `file${i}.html`);
files.push('api/index.ts');
const { builders } = await detectBuilders(files);
expect(builders.length).toBe(2);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/index.ts');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('!{api/**,package.json}');
}
});
it('Test `detectRoutes`', async () => {
@@ -647,146 +589,3 @@ it('Test `detectRoutes`', async () => {
expect(defaultRoutes.length).toBe(5);
}
});
it('Test `detectBuilders` and `detectRoutes`', async () => {
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/my-endpoint',
mustContain: 'my-endpoint',
status: 200,
},
{
path: '/api/other-endpoint',
mustContain: 'other-endpoint',
status: 200,
},
{
path: '/api/team/zeit',
mustContain: 'team/zeit',
status: 200,
},
{
path: '/api/user/myself',
mustContain: 'user/myself',
status: 200,
},
{
path: '/api/not-okay/',
status: 404,
},
{
path: '/api',
status: 404,
},
{
path: '/api/',
status: 404,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});
it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/not-okay',
status: 404,
},
{
path: '/api',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index.js',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/date.js',
mustContain: 'hello from api/date.js',
status: 200,
},
{
// Someone might expect this to be `date.js`,
// but I doubt that there is any case were both
// `date/index.js` and `date.js` exists,
// so it is not special cased
path: '/api/date',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index.js',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2)
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture
);
expect(deployment).toBeDefined();
});

View File

@@ -2,7 +2,7 @@
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit)
## Usage
## Usages
To install the latest version of Now CLI, visit [zeit.co/download](https://zeit.co/download) or run this command:

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "16.3.0",
"version": "16.4.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
@@ -168,6 +168,7 @@
"through2": "2.0.3",
"title": "3.4.1",
"tmp-promise": "1.0.3",
"tree-kill": "1.2.1",
"ts-node": "8.3.0",
"typescript": "3.2.4",
"universal-analytics": "0.4.20",

View File

@@ -60,12 +60,6 @@ export default async function main(ctx: NowContext) {
args = getSubcommand(argv._.slice(1), COMMAND_CONFIG).args;
output = createOutput({ debug });
// Builders won't show debug logs by default
// the `NOW_BUILDER_DEBUG` env variable will enable them
if (debug) {
process.env.NOW_BUILDER_DEBUG = '1';
}
if ('--port' in argv) {
output.warn('`--port` is deprecated, please use `--listen` instead');
argv['--listen'] = String(argv['--port']);

View File

@@ -10,7 +10,7 @@ import Client from '../util/client.ts';
import logo from '../util/output/logo';
import getScope from '../util/get-scope';
const e = encodeURIComponent
const e = encodeURIComponent;
const help = () => {
console.log(`
@@ -48,8 +48,8 @@ const main = async ctx => {
argv = mri(ctx.argv.slice(2), {
boolean: ['help'],
alias: {
help: 'h'
}
help: 'h',
},
});
argv._ = argv._.slice(1);
@@ -63,7 +63,10 @@ const main = async ctx => {
await exit(0);
}
const { authConfig: { token }, config: { currentTeam }} = ctx;
const {
authConfig: { token },
config: { currentTeam },
} = ctx;
const client = new Client({ apiUrl, token, currentTeam, debug });
const { contextName } = await getScope(client);
@@ -93,17 +96,21 @@ async function run({ client, contextName }) {
if (args.length !== 0) {
console.error(
error(
`Invalid number of arguments. Usage: ${chalk.cyan('`now projects ls`')}`
`Invalid number of arguments. Usage: ${chalk.cyan(
'`now projects ls`'
)}`
)
);
return exit(1);
}
const list = await client.fetch('/projects/list', {method: 'GET'});
const list = await client.fetch('/v2/projects/', { method: 'GET' });
const elapsed = ms(new Date() - start);
console.log(
`> ${plural('project', list.length, true)} found under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`
`> ${plural('project', list.length, true)} found under ${chalk.bold(
contextName
)} ${chalk.gray(`[${elapsed}]`)}`
);
if (list.length > 0) {
@@ -114,19 +121,19 @@ async function run({ client, contextName }) {
header.concat(
list.map(secret => [
'',
chalk.bold(secret.name),
chalk.gray(`${ms(cur - new Date(secret.updatedAt)) } ago`)
])
chalk.bold(secret.name),
chalk.gray(`${ms(cur - new Date(secret.updatedAt))} ago`),
])
),
{
align: ['l', 'l', 'l'],
hsep: ' '.repeat(2),
stringLength: strlen
stringLength: strlen,
}
);
if (out) {
console.log(`\n${ out }\n`);
console.log(`\n${out}\n`);
}
}
return;
@@ -148,11 +155,11 @@ async function run({ client, contextName }) {
// Check the existence of the project
try {
await client.fetch(`/projects/info/${e(name)}`)
} catch(err) {
await client.fetch(`/projects/info/${e(name)}`);
} catch (err) {
if (err.status === 404) {
console.error(error('No such project exists'))
return exit(1)
console.error(error('No such project exists'));
return exit(1);
}
}
@@ -162,7 +169,9 @@ async function run({ client, contextName }) {
return exit(0);
}
await client.fetch('/projects/remove', {method: 'DELETE', body: {name}});
await client.fetch(`/v2/projects/${name}`, {
method: 'DELETE',
});
const elapsed = ms(new Date() - start);
console.log(
`${chalk.cyan('> Success!')} Project ${chalk.bold(
@@ -193,7 +202,10 @@ async function run({ client, contextName }) {
}
const [name] = args;
await client.fetch('/projects/ensure-project', {method: 'POST', body: {name}});
await client.fetch('/projects/ensure-project', {
method: 'POST',
body: { name },
});
const elapsed = ms(new Date() - start);
console.log(
@@ -204,9 +216,7 @@ async function run({ client, contextName }) {
return;
}
console.error(
error('Please specify a valid subcommand: ls | add | rm')
);
console.error(error('Please specify a valid subcommand: ls | add | rm'));
help();
exit(1);
}
@@ -220,7 +230,7 @@ function readConfirmation(projectName) {
return new Promise(resolve => {
process.stdout.write(
`The project: ${chalk.bold(projectName)} will be removed permanently.\n` +
`It will also delete everything under the project including deployments.\n`
`It will also delete everything under the project including deployments.\n`
);
process.stdout.write(

View File

@@ -195,28 +195,31 @@ export type DNSRecord = {
};
type SRVRecordData = {
name: string,
type: 'SRV',
name: string;
type: 'SRV';
srv: {
port: number,
priority: number,
target: string,
weight: number,
}
}
type MXRecordData = {
name: string,
type: 'MX',
value: string,
mxPriority: number,
port: number;
priority: number;
target: string;
weight: number;
};
};
export type DNSRecordData = {
name: string,
type: string,
value: string,
} | SRVRecordData | MXRecordData;
type MXRecordData = {
name: string;
type: 'MX';
value: string;
mxPriority: number;
};
export type DNSRecordData =
| {
name: string;
type: string;
value: string;
}
| SRVRecordData
| MXRecordData;
export interface Project {
id: string;

View File

@@ -5,13 +5,17 @@ import pluralize from 'pluralize';
import {
createDeployment,
createLegacyDeployment,
DeploymentOptions,
} from '../../../../now-client';
import wait from '../output/wait';
import createOutput from '../output';
import { Output } from '../output';
// @ts-ignore
import Now from '../../util';
import { NowConfig } from '../dev/types';
export default async function processDeployment({
now,
debug,
output,
hashes,
paths,
requestBody,
@@ -20,18 +24,34 @@ export default async function processDeployment({
legacy,
env,
quiet,
}: any) {
const { warn, log } = createOutput({ debug });
nowConfig,
}: {
now: Now;
output: Output;
hashes: { [key: string]: any };
paths: string[];
requestBody: DeploymentOptions;
uploadStamp: () => number;
deployStamp: () => number;
legacy: boolean;
env: any;
quiet: boolean;
nowConfig?: NowConfig;
}) {
const { warn, log, debug, note } = output;
let bar: Progress | null = null;
const path0 = paths[0];
const opts: DeploymentOptions = {
...requestBody,
debug: now._debug,
};
if (!legacy) {
let buildSpinner = null;
let deploySpinner = null;
for await (const event of createDeployment(paths[0], {
...requestBody,
debug: now._debug,
})) {
for await (const event of createDeployment(path0, opts, nowConfig)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
@@ -40,6 +60,10 @@ export default async function processDeployment({
warn(event.payload);
}
if (event.type === 'notice') {
note(event.payload);
}
if (event.type === 'file_count') {
debug(
`Total files ${event.payload.total.size}, ${event.payload.missing.length} changed`
@@ -128,10 +152,7 @@ export default async function processDeployment({
}
}
} else {
for await (const event of createLegacyDeployment(paths[0], {
...requestBody,
debug: now._debug,
})) {
for await (const event of createLegacyDeployment(path0, opts, nowConfig)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}

View File

@@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import ms from 'ms';
import bytes from 'bytes';
import { promisify } from 'util';
import { delimiter, dirname, join } from 'path';
import { fork, ChildProcess } from 'child_process';
import { createFunction } from '@zeit/fun';
@@ -10,8 +11,8 @@ import stripAnsi from 'strip-ansi';
import chalk from 'chalk';
import which from 'which';
import plural from 'pluralize';
import ora, { Ora } from 'ora';
import minimatch from 'minimatch';
import _treeKill from 'tree-kill';
import { Output } from '../output';
import highlight from '../output/highlight';
@@ -40,7 +41,7 @@ interface BuildMessageResult extends BuildMessage {
error?: object;
}
const isLogging = new WeakSet<ChildProcess>();
const treeKill = promisify(_treeKill);
let nodeBinPromise: Promise<string>;
@@ -48,20 +49,13 @@ async function getNodeBin(): Promise<string> {
return which.sync('node', { nothrow: true }) || process.execPath;
}
function pipeChildLogging(child: ChildProcess): void {
if (!isLogging.has(child)) {
child.stdout!.pipe(process.stdout);
child.stderr!.pipe(process.stderr);
isLogging.add(child);
}
}
async function createBuildProcess(
match: BuildMatch,
buildEnv: EnvConfig,
workPath: string,
output: Output,
yarnPath?: string
yarnPath?: string,
debugEnabled: boolean = false
): Promise<ChildProcess> {
if (!nodeBinPromise) {
nodeBinPromise = getNodeBin();
@@ -70,21 +64,33 @@ async function createBuildProcess(
nodeBinPromise,
builderModulePathPromise,
]);
// Ensure that `node` is in the builder's `PATH`
let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`;
// Ensure that `yarn` is in the builder's `PATH`
if (yarnPath) {
PATH = `${yarnPath}${delimiter}${PATH}`;
}
const env: EnvConfig = {
...process.env,
PATH,
...buildEnv,
NOW_REGION: 'dev1',
};
// Builders won't show debug logs by default.
// The `NOW_BUILDER_DEBUG` env variable enables them.
if (debugEnabled) {
env.NOW_BUILDER_DEBUG = '1';
}
const buildProcess = fork(modulePath, [], {
cwd: workPath,
env: {
...process.env,
PATH,
...buildEnv,
NOW_REGION: 'dev1',
},
env,
execPath,
execArgv: [],
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
});
match.buildProcess = buildProcess;
@@ -95,9 +101,6 @@ async function createBuildProcess(
match.buildProcess = undefined;
});
buildProcess.stdout!.setEncoding('utf8');
buildProcess.stderr!.setEncoding('utf8');
return new Promise((resolve, reject) => {
// The first message that the builder process sends is the `ready` event
buildProcess.once('message', ({ type }) => {
@@ -149,7 +152,8 @@ export async function executeBuild(
buildEnv,
workPath,
devServer.output,
yarnPath
yarnPath,
debug
);
}
@@ -170,85 +174,39 @@ export async function executeBuild(
let buildResultOrOutputs: BuilderOutputs | BuildResult;
if (buildProcess) {
let spinLogger;
let spinner: Ora | undefined;
const fullLogs: string[] = [];
buildProcess.send({
type: 'build',
builderName: pkg.name,
buildParams,
});
if (isInitialBuild && !debug && process.stdout.isTTY) {
const logTitle = `${chalk.bold(
`Preparing ${chalk.underline(entrypoint)} for build`
)}:`;
spinner = ora(logTitle).start();
spinLogger = (data: Buffer) => {
const rawLog = stripAnsi(data.toString());
fullLogs.push(rawLog);
const lines = rawLog.replace(/\s+$/, '').split('\n');
const spinText = `${logTitle} ${lines[lines.length - 1]}`;
const maxCols = process.stdout.columns || 80;
const overflow = stripAnsi(spinText).length + 2 - maxCols;
spinner!.text =
overflow > 0 ? `${spinText.slice(0, -overflow - 3)}...` : spinText;
};
buildProcess!.stdout!.on('data', spinLogger);
buildProcess!.stderr!.on('data', spinLogger);
} else {
pipeChildLogging(buildProcess!);
}
try {
buildProcess.send({
type: 'build',
builderName: pkg.name,
buildParams,
});
buildResultOrOutputs = await new Promise((resolve, reject) => {
function onMessage({ type, result, error }: BuildMessageResult) {
cleanup();
if (type === 'buildResult') {
if (result) {
resolve(result);
} else if (error) {
reject(Object.assign(new Error(), error));
}
} else {
reject(new Error(`Got unexpected message type: ${type}`));
buildResultOrOutputs = await new Promise((resolve, reject) => {
function onMessage({ type, result, error }: BuildMessageResult) {
cleanup();
if (type === 'buildResult') {
if (result) {
resolve(result);
} else if (error) {
reject(Object.assign(new Error(), error));
}
} else {
reject(new Error(`Got unexpected message type: ${type}`));
}
function onExit(code: number | null, signal: string | null) {
cleanup();
const err = new Error(
`Builder exited with ${signal || code} before sending build result`
);
reject(err);
}
function cleanup() {
buildProcess!.removeListener('exit', onExit);
buildProcess!.removeListener('message', onMessage);
}
buildProcess!.on('exit', onExit);
buildProcess!.on('message', onMessage);
});
} catch (err) {
if (spinner) {
spinner.stop();
spinner = undefined;
console.log(fullLogs.join(''));
}
throw err;
} finally {
if (spinLogger) {
buildProcess.stdout!.removeListener('data', spinLogger);
buildProcess.stderr!.removeListener('data', spinLogger);
function onExit(code: number | null, signal: string | null) {
cleanup();
const err = new Error(
`Builder exited with ${signal || code} before sending build result`
);
reject(err);
}
if (spinner) {
spinner.stop();
function cleanup() {
buildProcess!.removeListener('exit', onExit);
buildProcess!.removeListener('message', onMessage);
}
pipeChildLogging(buildProcess!);
}
buildProcess!.on('exit', onExit);
buildProcess!.on('message', onMessage);
});
} else {
buildResultOrOutputs = await builder.build(buildParams);
}
@@ -445,3 +403,27 @@ export async function getBuildMatches(
return matches;
}
export async function shutdownBuilder(
match: BuildMatch,
{ debug }: Output
): Promise<void> {
const ops: Promise<void>[] = [];
if (match.buildProcess) {
debug(`Killing builder sub-process with PID ${match.buildProcess.pid}`);
ops.push(treeKill(match.buildProcess.pid));
delete match.buildProcess;
}
if (match.buildOutput) {
for (const asset of Object.values(match.buildOutput)) {
if (asset.type === 'Lambda' && asset.fn) {
debug(`Shutting down Lambda function`);
ops.push(asset.fn.destroy());
}
}
}
await Promise.all(ops);
}

View File

@@ -42,7 +42,7 @@ import isURL from './is-url';
import devRouter from './router';
import getMimeType from './mime-type';
import { getYarnPath } from './yarn-installer';
import { executeBuild, getBuildMatches } from './builder';
import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';
import { generateErrorMessage, generateHttpStatusDescription } from './errors';
import {
builderDirPromise,
@@ -347,13 +347,18 @@ export default class DevServer {
}
// Delete build matches that no longer exists
const ops: Promise<void>[] = [];
for (const src of this.buildMatches.keys()) {
if (!sources.includes(src)) {
this.output.debug(`Removing build match for "${src}"`);
// TODO: shutdown lambda functions
const match = this.buildMatches.get(src);
if (match) {
ops.push(shutdownBuilder(match, this.output));
}
this.buildMatches.delete(src);
}
}
await Promise.all(ops);
// Add the new matches to the `buildMatches` map
const blockingBuilds: Promise<void>[] = [];
@@ -429,6 +434,7 @@ export default class DevServer {
} = buildMatch;
if (pkg.name === '@now/static') continue;
if (pkg.name && updatedBuilders.includes(pkg.name)) {
shutdownBuilder(buildMatch, this.output);
this.buildMatches.delete(src);
this.output.debug(`Invalidated build match for "${src}"`);
}
@@ -729,10 +735,12 @@ export default class DevServer {
this.yarnPath,
this.output
)
.then(updatedBuilders =>
this.invalidateBuildMatches(nowConfig, updatedBuilders)
)
.then(updatedBuilders => {
this.updateBuildersPromise = null;
this.invalidateBuildMatches(nowConfig, updatedBuilders);
})
.catch(err => {
this.updateBuildersPromise = null;
this.output.error(`Failed to update builders: ${err.message}`);
this.output.debug(err.stack);
});
@@ -831,22 +839,18 @@ export default class DevServer {
const ops: Promise<void>[] = [];
for (const match of this.buildMatches.values()) {
if (!match.buildOutput) continue;
for (const asset of Object.values(match.buildOutput)) {
if (asset.type === 'Lambda' && asset.fn) {
ops.push(asset.fn.destroy());
}
}
ops.push(shutdownBuilder(match, this.output));
}
ops.push(close(this.server));
if (this.watcher) {
this.output.debug(`Closing file watcher`);
this.watcher.close();
}
if (this.updateBuildersPromise) {
this.output.debug(`Waiting for builders update to complete`);
ops.push(this.updateBuildersPromise);
}
@@ -1102,7 +1106,7 @@ export default class DevServer {
}
const method = req.method || 'GET';
this.output.log(`${chalk.bold(method)} ${req.url}`);
this.output.debug(`${chalk.bold(method)} ${req.url}`);
try {
const nowConfig = await this.getNowConfig();
@@ -1452,25 +1456,6 @@ export default class DevServer {
return true;
}
/**
* Serve project directory as a static deployment.
*/
serveProjectAsStatic = async (
req: http.IncomingMessage,
res: http.ServerResponse,
nowRequestId: string
) => {
const filePath = req.url ? req.url.replace(/^\//, '') : '';
if (filePath && typeof this.files[filePath] === 'undefined') {
await this.send404(req, res, nowRequestId);
return;
}
this.setResponseHeaders(res, nowRequestId);
return serveStaticFile(req, res, this.cwd, { cleanUrls: true });
};
async hasFilesystem(dest: string): Promise<boolean> {
const requestPath = dest.replace(/^\//, '');
if (

View File

@@ -1,5 +1,5 @@
import Ajv from 'ajv';
import { schema as routesSchema } from '@now/routing-utils';
import { routesSchema } from '@now/routing-utils';
import { NowConfig } from './types';
const ajv = new Ajv();

View File

@@ -15,7 +15,6 @@ export default async function getConfig(output: Output, configFile?: string) {
if (config) {
return config;
}
// First try with the config supplied by the user via --local-config
if (configFile) {
const localFilePath = path.resolve(localPath, configFile);
@@ -27,8 +26,7 @@ export default async function getConfig(output: Output, configFile?: string) {
return localConfig;
}
if (localConfig !== null) {
const castedConfig = localConfig;
config = castedConfig;
config = localConfig;
return config;
}
}

View File

@@ -142,13 +142,14 @@ export default class Now extends EventEmitter {
if (isBuilds) {
deployment = await processDeployment({
now: this,
debug,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
nowConfig,
});
} else {
// Read `registry.npmjs.org` authToken from .npmrc
@@ -183,7 +184,7 @@ export default class Now extends EventEmitter {
deployment = await processDeployment({
legacy: true,
now: this,
debug,
output: this._output,
hashes,
paths,
requestBody,
@@ -191,6 +192,7 @@ export default class Now extends EventEmitter {
deployStamp,
quiet,
env,
nowConfig,
});
}
@@ -377,7 +379,7 @@ export default class Now extends EventEmitter {
if (!app && !Object.keys(meta).length) {
// Get the 35 latest projects and their latest deployment
const query = new URLSearchParams({ limit: 35 });
const projects = await fetchRetry(`/projects/list?${query}`);
const projects = await fetchRetry(`/v2/projects/?${query}`);
const deployments = await Promise.all(
projects.map(async ({ id: projectId }) => {

View File

@@ -31,7 +31,7 @@ function testFixture(name, fn) {
readyResolve = resolve;
});
const debug = false;
const debug = true;
const output = createOutput({ debug });
const origReady = output.ready;
@@ -329,8 +329,8 @@ test(
// HTML response
const res = await fetch(`${server.address}/does-not-exist`, {
headers: {
Accept: 'text/html'
}
Accept: 'text/html',
},
});
t.is(res.status, 404);
t.is(res.headers.get('content-type'), 'text/html; charset=utf-8');
@@ -342,8 +342,8 @@ test(
// JSON response
const res = await fetch(`${server.address}/does-not-exist`, {
headers: {
Accept: 'application/json'
}
Accept: 'application/json',
},
});
t.is(res.status, 404);
t.is(res.headers.get('content-type'), 'application/json');
@@ -401,10 +401,10 @@ test('[DevServer] parseListen()', t => {
t.deepEqual(parseListen('127.0.0.1:3005'), [3005, '127.0.0.1']);
t.deepEqual(parseListen('tcp://127.0.0.1:5000'), [5000, '127.0.0.1']);
t.deepEqual(parseListen('unix:/home/user/server.sock'), [
'/home/user/server.sock'
'/home/user/server.sock',
]);
t.deepEqual(parseListen('pipe:\\\\.\\pipe\\PipeName'), [
'\\\\.\\pipe\\PipeName'
'\\\\.\\pipe\\PipeName',
]);
let err;

View File

@@ -0,0 +1,2 @@
node_modules
.next

View File

@@ -0,0 +1,2 @@
README.md
yarn.lock

View File

@@ -0,0 +1,13 @@
{
"name": "nextjs",
"license": "MIT",
"scripts": {
"dev": "next",
"build": "next build"
},
"dependencies": {
"next": "^9.1.1",
"react": "^16.7.0",
"react-dom": "^16.7.0"
}
}

View File

@@ -0,0 +1,102 @@
import { useEffect, useState } from 'react';
import Head from 'next/head';
function Index() {
const [date, setDate] = useState(null);
useEffect(() => {
async function getDate() {
const res = await fetch('/api/date');
const newDate = await res.text();
setDate(newDate);
}
getDate();
}, []);
return (
<main>
<Head>
<title>Next.js + Node API</title>
</Head>
<h1>Next.js + Node.js API</h1>
<h2>
Deployed with{' '}
<a
href="https://zeit.co/docs"
target="_blank"
rel="noreferrer noopener"
>
ZEIT Now
</a>
!
</h2>
<p>
<a
href="https://github.com/zeit/now-examples/blob/master/nextjs-node"
target="_blank"
rel="noreferrer noopener"
>
This project
</a>{' '}
is a <a href="https://nextjs.org/">Next.js</a> app with two directories,{' '}
<code>/pages</code> for static content and <code>/api</code> which
contains a serverless <a href="https://nodejs.org/en/">Node.js</a>{' '}
function. See{' '}
<a href="/api/date">
<code>api/date</code> for the Date API with Node.js
</a>
.
</p>
<br />
<h2>The date according to Node.js is:</h2>
<p>{date ? date : 'Loading date...'}</p>
<style jsx>{`
main {
align-content: center;
box-sizing: border-box;
display: grid;
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue',
'Helvetica', 'Arial', sans-serif;
hyphens: auto;
line-height: 1.65;
margin: 0 auto;
max-width: 680px;
min-height: 100vh;
padding: 72px 0;
text-align: center;
}
h1 {
font-size: 45px;
}
h2 {
margin-top: 1.5em;
}
p {
font-size: 16px;
}
a {
border-bottom: 1px solid white;
color: #0076ff;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
}
a:hover {
border-bottom: 1px solid #0076ff;
}
code,
pre {
color: #d400ff;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono,
DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace,
serif;
font-size: 0.92em;
}
code:before,
code:after {
content: '\`';
}
`}</style>
</main>
);
}
export default Index;

File diff suppressed because it is too large Load Diff

View File

@@ -869,3 +869,23 @@ test('[now dev] do not rebuild for changes in the output directory', async t =>
dev.kill('SIGTERM');
}
});
test('[now dev] 25-nextjs-src-dir', async t => {
const directory = fixture('25-nextjs-src-dir');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
const result = await fetchWithRetry(`http://localhost:${port}`, 80);
const response = await result;
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Next.js \+ Node.js API/gm);
} finally {
dev.kill('SIGTERM');
}
});

View File

@@ -1,9 +1,7 @@
{
"version": 2,
"name": "now-dev-next",
"builds": [
{ "src": "package.json", "use": "@now/next" }
],
"builds": [{ "src": "package.json", "use": "@now/next@canary" }],
"routes": [
{
"src": "/(.*)",

View File

@@ -3,7 +3,7 @@
"builds": [
{
"src": "package.json",
"use": "@now/static-build",
"use": "@now/static-build@canary",
"config": {
"distDir": "public"
}
@@ -13,7 +13,5 @@
"use": "@now/node"
}
],
"routes": [
{ "src": "^/api/date$", "dest": "api/date.js" }
]
"routes": [{ "src": "^/api/date$", "dest": "api/date.js" }]
}

View File

@@ -267,6 +267,69 @@ module.exports = (req, res) => {
Object.assign(JSON.parse(getConfigFile(true)), { alias: 'zeit.co' })
),
},
'local-config-cloud-v1': {
'.gitignore': '*.html',
'index.js': `
const { createServer } = require('http');
const { readFileSync } = require('fs');
const svr = createServer((req, res) => {
const { url = '/' } = req;
const file = '.' + url;
console.log('reading file ' + file);
try {
let contents = readFileSync(file, 'utf8');
res.end(contents || '');
} catch (e) {
res.statusCode = 404;
res.end('Not found');
}
});
svr.listen(3000);`,
'main.html': '<h1>hello main</h1>',
'test.html': '<h1>hello test</h1>',
'folder/file1.txt': 'file1',
'folder/sub/file2.txt': 'file2',
Dockerfile: `FROM mhart/alpine-node:latest
LABEL name "now-cli-dockerfile-${session}"
RUN mkdir /app
WORKDIR /app
COPY . /app
RUN yarn
EXPOSE 3000
CMD ["node", "index.js"]`,
'now.json': JSON.stringify({
version: 1,
type: 'docker',
features: {
cloud: 'v1',
},
files: ['.gitignore', 'folder', 'index.js', 'main.html'],
}),
'now-test.json': JSON.stringify({
version: 1,
type: 'docker',
features: {
cloud: 'v1',
},
files: ['.gitignore', 'folder', 'index.js', 'test.html'],
}),
},
'local-config-v2': {
[`main-${session}.html`]: '<h1>hello main</h1>',
[`test-${session}.html`]: '<h1>hello test</h1>',
'now.json': JSON.stringify({
version: 2,
builds: [{ src: `main-${session}.html`, use: '@now/static' }],
routes: [{ src: '/another-main', dest: `/main-${session}.html` }],
}),
'now-test.json': JSON.stringify({
version: 2,
builds: [{ src: `test-${session}.html`, use: '@now/static' }],
routes: [{ src: '/another-test', dest: `/test-${session}.html` }],
}),
},
'alias-rules': {
'rules.json': JSON.stringify({
rules: [

View File

@@ -32,16 +32,25 @@ const pickUrl = stdout => {
const createFile = dest => fs.closeSync(fs.openSync(dest, 'w'));
const createDirectory = dest => fs.mkdirSync(dest);
const testv1 = async (...args) => {
if (!process.version.startsWith('v12.')) {
// Only run v1 tests on Node 12
return;
}
await test(...args);
};
const waitForDeployment = async href => {
console.log(`waiting for ${href} to become ready...`);
const start = Date.now();
const max = ms('4m');
const inspectorText = '<title>Deployment Overview';
// eslint-disable-next-line
while (true) {
const response = await fetch(href, { redirect: 'manual' });
if (response.status === 200) {
const text = await response.text();
if (response.status === 200 && !text.includes(inspectorText)) {
break;
}
@@ -50,7 +59,7 @@ const waitForDeployment = async href => {
if (current - start > max || response.status >= 500) {
throw new Error(
`Waiting for "${href}" failed since it took longer than 4 minutes.\n` +
`Received status ${response.status}:\n"${await response.text()}"`
`Received status ${response.status}:\n"${text}"`
);
}
@@ -201,6 +210,80 @@ test('login', async t => {
t.is(typeof token, 'string');
});
test('deploy using --local-config flag v2', async t => {
const target = fixture('local-config-v2');
const { stdout, stderr, code } = await execa(
binaryPath,
['deploy', '--local-config', 'now-test.json', ...defaultArgs],
{
cwd: target,
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(code);
t.is(code, 0);
const { host } = new URL(stdout);
const testRes = await fetch(`https://${host}/test-${contextName}.html`);
const testText = await testRes.text();
t.is(testText, '<h1>hello test</h1>');
const anotherTestRes = await fetch(`https://${host}/another-test`);
const anotherTestText = await anotherTestRes.text();
t.is(anotherTestText, testText);
const mainRes = await fetch(`https://${host}/main-${contextName}.html`);
t.is(mainRes.status, 404, 'Should not deploy/build main now.json');
const anotherMainRes = await fetch(`https://${host}/another-main`);
t.is(anotherMainRes.status, 404, 'Should not deploy/build main now.json');
});
testv1('deploy using --local-config flag type cloud v1', async t => {
const target = fixture('local-config-cloud-v1');
const { stdout, stderr, code } = await execa(
binaryPath,
['deploy', '--public', '--local-config', 'now-test.json', ...defaultArgs],
{
cwd: target,
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(code);
t.is(code, 0);
const { host } = new URL(stdout);
await waitForDeployment(`https://${host}/test.html`);
await waitForDeployment(`https://${host}/folder/file1.txt`);
await waitForDeployment(`https://${host}/folder/sub/file2.txt`);
const testRes = await fetch(`https://${host}/test.html`);
const testText = await testRes.text();
t.is(testText, '<h1>hello test</h1>');
const file1Res = await fetch(`https://${host}/folder/file1.txt`);
const file1Text = await file1Res.text();
t.is(file1Text, 'file1');
const file2Res = await fetch(`https://${host}/folder/sub/file2.txt`);
const file2Text = await file2Res.text();
t.is(file2Text, 'file2');
const mainRes = await fetch(`https://${host}/main.html`);
t.is(mainRes.status, 404, 'Should not deploy/build main now.json');
});
test('print the deploy help message', async t => {
const { stderr, stdout, code } = await execa(
binaryPath,
@@ -291,7 +374,7 @@ test('detect update command', async t => {
}
});
test('login with unregisterd user', async t => {
test('login with unregistered user', async t => {
const { stdout, stderr, code } = await execa(
binaryPath,
['login', `${session}@${session}.com`, ...defaultArgs],
@@ -312,13 +395,14 @@ test('login with unregisterd user', async t => {
t.is(last, goal);
});
test('deploy a node microservice', async t => {
testv1('deploy a v1 node microservice', async t => {
const target = fixture('node');
let { stdout, stderr, code } = await execa(
binaryPath,
[target, '--public', '--name', session, ...defaultArgs],
['--public', '--name', session, ...defaultArgs],
{
cwd: target,
reject: false,
}
);
@@ -334,6 +418,8 @@ test('deploy a node microservice', async t => {
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session, formatOutput({ stdout, stderr }));
await waitForDeployment(href);
// Send a test request to the deployment
let response = await fetch(href);
t.is(response.status, 200);
@@ -365,30 +451,33 @@ test('deploy a node microservice', async t => {
t.is(response.status, 404);
});
test('deploy a node microservice and infer name from `package.json`', async t => {
const target = fixture('node');
testv1(
'deploy a v1 node microservice and infer name from `package.json`',
async t => {
const target = fixture('node');
const { stdout, stderr, code } = await execa(
binaryPath,
[target, '--public', ...defaultArgs],
{
reject: false,
}
);
const { stdout, stderr, code } = await execa(
binaryPath,
[target, '--public', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(code);
console.log(stderr);
console.log(stdout);
console.log(code);
// Ensure the exit code is right
t.is(code, 0);
// Ensure the exit code is right
t.is(code, 0);
// Test if the output is really a URL
const { host } = new URL(stdout);
t.true(host.startsWith(`node-test-${contextName}`));
});
// Test if the output is really a URL
const { host } = new URL(stdout);
t.true(host.startsWith(`node-test-${contextName}`));
}
);
test('deploy a dockerfile project', async t => {
testv1('deploy a v1 dockerfile project', async t => {
const target = fixture('dockerfile');
// Add the "name" field to the `now.json` file
@@ -420,11 +509,7 @@ test('deploy a dockerfile project', async t => {
await waitForDeployment(href);
// Send a test request to the deployment
const response = await fetch(href, {
headers: {
Accept: 'application/json',
},
});
const response = await fetch(href);
t.is(response.status, 200);
const contentType = response.headers.get('content-type');
const textContent = await response.text();
@@ -474,7 +559,7 @@ test('test invalid type for alias rules', async t => {
t.regex(output.stderr, /Path Alias validation error/, formatOutput(output));
});
test('apply alias rules', async t => {
testv1('apply alias rules', async t => {
const fixturePath = fixture('alias-rules');
// Create the rules file
@@ -497,7 +582,7 @@ test('apply alias rules', async t => {
t.is(output.code, 0, formatOutput(output));
});
test('find deployment in list', async t => {
testv1('find deployment in list', async t => {
const output = await execa(binaryPath, ['--debug', 'ls', ...defaultArgs], {
reject: false,
});
@@ -519,7 +604,7 @@ test('find deployment in list', async t => {
t.is(target, context.deployment, formatOutput(output));
});
test('find deployment in list with mixed args', async t => {
testv1('find deployment in list with mixed args', async t => {
const { stdout, stderr, code } = await execa(
binaryPath,
['--debug', 'ls', ...defaultArgs],
@@ -545,7 +630,7 @@ test('find deployment in list with mixed args', async t => {
t.is(target, context.deployment, formatOutput({ stdout, stderr }));
});
test('create an explicit alias for deployment', async t => {
testv1('create an explicit alias for deployment', async t => {
const hosts = {
deployment: context.deployment,
alias: `${session}.now.sh`,
@@ -579,7 +664,7 @@ test('create an explicit alias for deployment', async t => {
context.alias = hosts.alias;
});
test('list the aliases', async t => {
testv1('list the aliases', async t => {
const { stdout, stderr, code } = await execa(
binaryPath,
['alias', 'ls', ...defaultArgs],
@@ -598,7 +683,7 @@ test('list the aliases', async t => {
t.true(results.includes(context.deployment));
});
test('scale the alias', async t => {
testv1('scale the v1 alias', async t => {
const { stdout, stderr, code } = await execa(
binaryPath,
['scale', context.alias, 'bru', '1', ...defaultArgs],
@@ -615,7 +700,7 @@ test('scale the alias', async t => {
t.true(stdout.includes(`(min: 1, max: 1)`));
});
test('remove the explicit alias', async t => {
testv1('remove the explicit alias', async t => {
const goal = `> Success! Alias ${context.alias} removed`;
const { stdout, stderr, code } = await execa(
@@ -634,7 +719,7 @@ test('remove the explicit alias', async t => {
t.true(stdout.startsWith(goal));
});
test('create an alias from "now.json" `alias` for deployment', async t => {
testv1('create an v1 alias from "now.json" `alias` for deployment', async t => {
const target = fixture('dockerfile');
// Add the `alias` field to the "now.json" file
@@ -672,7 +757,7 @@ test('create an alias from "now.json" `alias` for deployment', async t => {
context.alias = json.alias;
});
test('remove the alias from "now.json" `alias`', async t => {
testv1('remove the alias from "now.json" `alias`', async t => {
const goal = `> Success! Alias ${context.alias} removed`;
const { stdout, stderr, code } = await execa(
@@ -695,7 +780,10 @@ test('ignore files specified in .nowignore', async t => {
const directory = fixture('nowignore');
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
const targetCall = await execa(binaryPath, args, { cwd: directory, reject: false });
const targetCall = await execa(binaryPath, args, {
cwd: directory,
reject: false,
});
console.log(targetCall.stderr);
console.log(targetCall.stdout);
@@ -713,7 +801,10 @@ test('ignore files specified in .nowignore via allowlist', async t => {
const directory = fixture('nowignore-allowlist');
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
const targetCall = await execa(binaryPath, args, { cwd: directory, reject: false });
const targetCall = await execa(binaryPath, args, {
cwd: directory,
reject: false,
});
console.log(targetCall.stderr);
console.log(targetCall.stdout);
@@ -727,7 +818,7 @@ test('ignore files specified in .nowignore via allowlist', async t => {
t.is(presentFile.status, 200);
});
test('scale down the deployment directly', async t => {
testv1('scale down the deployment directly', async t => {
const { stdout, stderr, code } = await execa(
binaryPath,
['scale', context.deployment, 'bru', '0', ...defaultArgs],

View File

@@ -1,6 +1,6 @@
{
"name": "now-client",
"version": "5.1.4",
"version": "5.2.1",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"license": "MIT",

View File

@@ -4,9 +4,13 @@ import readdir from 'recursive-readdir';
import { relative, join } from 'path';
import hashes, { mapToObject } from './utils/hashes';
import uploadAndDeploy from './upload';
import { getNowIgnore, createDebug } from './utils';
import { getNowIgnore, createDebug, parseNowJSON } from './utils';
import { DeploymentError } from './errors';
import { CreateDeploymentFunction, DeploymentOptions } from './types';
import {
CreateDeploymentFunction,
DeploymentOptions,
NowJsonOptions,
} from './types';
export { EVENTS } from './utils';
@@ -15,9 +19,11 @@ export default function buildCreateDeployment(
): CreateDeploymentFunction {
return async function* createDeployment(
path: string | string[],
options: DeploymentOptions = {}
options: DeploymentOptions = {},
nowConfig?: NowJsonOptions
): AsyncIterableIterator<any> {
const debug = createDebug(options.debug);
const cwd = process.cwd();
debug('Creating deployment...');
@@ -60,7 +66,7 @@ export default function buildCreateDeployment(
}
// Get .nowignore
let ig = await getNowIgnore(path);
let { ig, ignores } = await getNowIgnore(path);
debug(`Found ${ig.ignores.length} rules in .nowignore`);
@@ -70,7 +76,7 @@ export default function buildCreateDeployment(
if (isDirectory && !Array.isArray(path)) {
// Directory path
const dirContents = await readdir(path);
const dirContents = await readdir(path, ignores);
const relativeFileList = dirContents.map(filePath =>
relative(process.cwd(), filePath)
);
@@ -89,6 +95,47 @@ export default function buildCreateDeployment(
debug(`Deploying the provided path as single file`);
}
if (!nowConfig) {
// If the user did not provide a nowConfig,
// then use the now.json file in the root.
const fileName = 'now.json';
const absolutePath = fileList.find(f => relative(cwd, f) === fileName);
debug(absolutePath ? `Found ${fileName}` : `Missing ${fileName}`);
nowConfig = await parseNowJSON(absolutePath);
}
if (
version === 1 &&
nowConfig &&
Array.isArray(nowConfig.files) &&
nowConfig.files.length > 0
) {
// See the docs: https://zeit.co/docs/v1/features/configuration/#files-(array)
debug('Filtering file list based on `files` key in now.json');
const allowedFiles = new Set<string>(['Dockerfile']);
const allowedDirs = new Set<string>();
nowConfig.files.forEach(relPath => {
if (lstatSync(relPath).isDirectory()) {
allowedDirs.add(relPath);
} else {
allowedFiles.add(relPath);
}
});
fileList = fileList.filter(absPath => {
const relPath = relative(cwd, absPath);
if (allowedFiles.has(relPath)) {
return true;
}
for (let dir of allowedDirs) {
if (relPath.startsWith(dir + '/')) {
return true;
}
}
return false;
});
debug(`Found ${fileList.length} files: ${JSON.stringify(fileList)}`);
}
// This is a useful warning because it prevents people
// from getting confused about a deployment that renders 404.
if (
@@ -133,6 +180,7 @@ export default function buildCreateDeployment(
const deploymentOpts = {
debug: debug_,
totalFiles: files.size,
nowConfig,
token,
isDirectory,
path,

View File

@@ -1,6 +1,5 @@
import { DeploymentFile } from './utils/hashes';
import {
parseNowJSON,
fetch,
API_DEPLOYMENTS,
prepareFiles,
@@ -22,6 +21,7 @@ export interface Options {
defaultName?: string;
preflight?: boolean;
debug?: boolean;
nowConfig?: NowJsonOptions;
}
async function* createDeployment(
@@ -73,6 +73,10 @@ async function* createDeployment(
debug('Deployment created with a warning:', value);
yield { type: 'warning', payload: value };
}
if (name.startsWith('x-now-notice-')) {
debug('Deployment created with a notice:', value);
yield { type: 'notice', payload: value };
}
}
yield { type: 'created', payload: json };
@@ -108,32 +112,12 @@ export default async function* deploy(
options: Options
): AsyncIterableIterator<{ type: string; payload: any }> {
const debug = createDebug(options.debug);
delete options.debug;
debug(`Trying to read 'now.json'`);
const nowJson: DeploymentFile | undefined = Array.from(files.values()).find(
(file: DeploymentFile): boolean => {
return Boolean(
file.names.find((name: string): boolean => name.includes('now.json'))
);
}
);
debug(`'now.json' ${nowJson ? 'found' : "doesn't exist"}`);
const nowJsonMetadata: NowJsonOptions = parseNowJSON(nowJson);
const nowJsonMetadata = options.nowConfig || {};
delete nowJsonMetadata.github;
delete nowJsonMetadata.scope;
const meta = options.metadata || {};
const metadata = { ...nowJsonMetadata, ...meta };
if (nowJson) {
debug(
`Merged 'now.json' metadata and locally provided metadata:`,
JSON.stringify(metadata)
);
}
// Check if we should default to a static deployment
if (!metadata.version && !metadata.name) {

View File

@@ -124,9 +124,11 @@ export interface NowJsonOptions {
scope?: string;
type?: 'NPM' | 'STATIC' | 'DOCKER';
version?: number;
files?: string[];
}
export type CreateDeploymentFunction = (
path: string | string[],
options?: DeploymentOptions
options?: DeploymentOptions,
nowConfig?: NowJsonOptions
) => AsyncIterableIterator<any>;

View File

@@ -26,7 +26,7 @@ const isClientNetworkError = (err: Error | DeploymentError) => {
export default async function* upload(
files: Map<string, DeploymentFile>,
options: Options
options: Options,
): AsyncIterableIterator<any> {
const { token, teamId, debug: isDebug } = options;
const debug = createDebug(isDebug);

View File

@@ -32,13 +32,13 @@ export const EVENTS = new Set([
'build-state-changed',
]);
export function parseNowJSON(file?: DeploymentFile): NowJsonOptions {
if (!file) {
export async function parseNowJSON(filePath?: string): Promise<NowJsonOptions> {
if (!filePath) {
return {};
}
try {
const jsonString = file.data.toString();
const jsonString = await readFile(filePath, 'utf8');
return JSON.parse(jsonString);
} catch (e) {
@@ -96,7 +96,7 @@ export async function getNowIgnore(path: string | string[]): Promise<any> {
const ig = ignore().add(`${ignores.join('\n')}\n${nowIgnore}`);
return ig;
return { ig, ignores };
}
export const fetch = async (

View File

@@ -1,6 +1,6 @@
{
"name": "@now/next",
"version": "1.0.2",
"version": "1.0.4",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/next-js-now-next",

View File

@@ -37,12 +37,10 @@ import {
EnvConfig,
excludeFiles,
ExperimentalTraceVersion,
filesFromDirectory,
getDynamicRoutes,
getNextConfig,
getPathsInside,
getRoutes,
includeOnlyEntryDirectory,
isDynamicRoute,
normalizePackageJson,
normalizePage,
@@ -69,6 +67,20 @@ interface BuildParamsType extends BuildOptions {
export const version = 2;
const nowDevChildProcesses = new Set<ChildProcess>();
['SIGINT', 'SIGTERM'].forEach(signal => {
process.once(signal as NodeJS.Signals, () => {
for (const child of nowDevChildProcesses) {
debug(
`Got ${signal}, killing dev server child process (pid=${child.pid})`
);
process.kill(child.pid, signal);
}
process.exit(0);
});
});
/**
* Read package.json from files
*/
@@ -214,6 +226,7 @@ export const build = async ({
const { forked, getUrl } = startDevServer(entryPath, runtimeEnv);
urls[entrypoint] = await getUrl();
childProcess = forked;
nowDevChildProcesses.add(forked);
debug(
`${name} Development server for ${entrypoint} running at ${urls[entrypoint]}`
);
@@ -337,7 +350,7 @@ export const build = async ({
if (isLegacy) {
const filesAfterBuild = await glob('**', entryPath);
debug('Preparing lambda files...');
debug('Preparing serverless function files...');
let buildId: string;
try {
buildId = await readFile(
@@ -405,7 +418,7 @@ export const build = async ({
],
};
debug(`Creating lambda for page: "${page}"...`);
debug(`Creating serverless function for page: "${page}"...`);
lambdas[path.join(entryDirectory, pathname)] = await createLambda({
files: {
...nextFiles,
@@ -415,11 +428,11 @@ export const build = async ({
handler: 'now__launcher.launcher',
runtime: nodeVersion.runtime,
});
debug(`Created lambda for page: "${page}"`);
debug(`Created serverless function for page: "${page}"`);
})
);
} else {
debug('Preparing lambda files...');
debug('Preparing serverless function files...');
const pagesDir = path.join(entryPath, '.next', 'serverless', 'pages');
const pages = await glob('**/*.js', pagesDir);
@@ -496,7 +509,8 @@ export const build = async ({
} = {};
if (requiresTracing) {
const tracingLabel = 'Tracing Next.js lambdas for external files ...';
const tracingLabel =
'Tracing Next.js serverless functions for external files ...';
console.time(tracingLabel);
const apiPages: string[] = [];
@@ -542,7 +556,7 @@ export const build = async ({
apiFileList.forEach(collectTracedFiles(apiReasons, apiTracedFiles));
console.timeEnd(tracingLabel);
const zippingLabel = 'Compressing shared lambda files';
const zippingLabel = 'Compressing shared serverless function files';
console.time(zippingLabel);
pseudoLayers.push(await createPseudoLayer(tracedFiles));
@@ -560,7 +574,9 @@ export const build = async ({
const assetKeys = Object.keys(assets);
if (assetKeys.length > 0) {
debug('detected (legacy) assets to be bundled with lambda:');
debug(
'detected (legacy) assets to be bundled with serverless function:'
);
assetKeys.forEach(assetFile => debug(`\t${assetFile}`));
debug(
'\nPlease upgrade to Next.js 9.1 to leverage modern asset handling.'
@@ -570,7 +586,7 @@ export const build = async ({
const launcherPath = path.join(__dirname, 'templated-launcher.js');
const launcherData = await readFile(launcherPath, 'utf8');
const allLambdasLabel = `All lambdas created`;
const allLambdasLabel = `All serverless functions created (in parallel)`;
console.time(allLambdasLabel);
await Promise.all(
@@ -586,7 +602,7 @@ export const build = async ({
dynamicPages.push(normalizePage(pathname));
}
const label = `Creating lambda for page: "${page}"...`;
const label = `Created serverless function for "${page}" in`;
console.time(label);
const pageFileName = path.normalize(
@@ -637,6 +653,9 @@ export const build = async ({
'**',
path.join(entryPath, '.next', 'static')
);
const staticFolderFiles = await glob('**', path.join(entryPath, 'static'));
const publicFolderFiles = await glob('**', path.join(entryPath, 'public'));
const staticFiles = Object.keys(nextStaticFiles).reduce(
(mappedFiles, file) => ({
...mappedFiles,
@@ -646,23 +665,24 @@ export const build = async ({
}),
{}
);
const entryDirectoryFiles = includeOnlyEntryDirectory(files, entryDirectory);
const staticDirectoryFiles = filesFromDirectory(
entryDirectoryFiles,
path.join(entryDirectory, 'static')
);
const publicDirectoryFiles = filesFromDirectory(
entryDirectoryFiles,
path.join(entryDirectory, 'public')
);
const publicFiles = Object.keys(publicDirectoryFiles).reduce(
const staticDirectoryFiles = Object.keys(staticFolderFiles).reduce(
(mappedFiles, file) => ({
...mappedFiles,
[file.replace(/public[/\\]+/, '')]: publicDirectoryFiles[file],
[path.join(entryDirectory, 'static', file)]: staticFolderFiles[file],
}),
{}
);
const publicDirectoryFiles = Object.keys(publicFolderFiles).reduce(
(mappedFiles, file) => ({
...mappedFiles,
[path.join(
entryDirectory,
file.replace(/public[/\\]+/, '')
)]: publicFolderFiles[file],
}),
{}
);
let dynamicPrefix = path.join('/', entryDirectory);
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
@@ -682,7 +702,7 @@ export const build = async ({
return {
output: {
...publicFiles,
...publicDirectoryFiles,
...lambdas,
...staticPages,
...staticFiles,

View File

@@ -60,24 +60,6 @@ function excludeFiles(
}, {});
}
/**
* Creates a new Files object holding only the entrypoint files
*/
function includeOnlyEntryDirectory(
files: Files,
entryDirectory: string
): Files {
if (entryDirectory === '.') {
return files;
}
function matcher(filePath: string) {
return !filePath.startsWith(entryDirectory);
}
return excludeFiles(files, matcher);
}
/**
* Exclude package manager lockfiles from files
*/
@@ -92,17 +74,6 @@ function excludeLockFiles(files: Files): Files {
return files;
}
/**
* Include only the files from a selected directory
*/
function filesFromDirectory(files: Files, dir: string): Files {
function matcher(filePath: string) {
return !filePath.startsWith(dir.replace(/\\/g, '/'));
}
return excludeFiles(files, matcher);
}
/**
* Enforce specific package.json configuration for smallest possible lambda
*/
@@ -205,17 +176,35 @@ function getRoutes(
files: Files,
url: string
): Route[] {
let pagesDir = '';
const filesInside: Files = {};
const prefix = entryDirectory === `.` ? `/` : `/${entryDirectory}/`;
const fileKeys = Object.keys(files);
for (const file of Object.keys(files)) {
for (const file of fileKeys) {
if (!pathsInside.includes(file)) {
continue;
}
if (!pagesDir) {
if (file.startsWith(path.join(entryDirectory, 'pages'))) {
pagesDir = 'pages';
}
}
filesInside[file] = files[file];
}
// If default pages dir isn't found check for `src/pages`
if (
!pagesDir &&
fileKeys.some(file =>
file.startsWith(path.join(entryDirectory, 'src/pages'))
)
) {
pagesDir = 'src/pages';
}
const routes: Route[] = [
{
src: `${prefix}_next/(.*)`,
@@ -231,13 +220,13 @@ function getRoutes(
for (const file of filePaths) {
const relativePath = path.relative(entryDirectory, file);
const isPage = pathIsInside('pages', relativePath);
const isPage = pathIsInside(pagesDir, relativePath);
if (!isPage) {
continue;
}
const relativeToPages = path.relative('pages', relativePath);
const relativeToPages = path.relative(pagesDir, relativePath);
const extension = path.extname(relativeToPages);
const pageName = relativeToPages.replace(extension, '').replace(/\\/g, '/');
@@ -484,10 +473,8 @@ export async function createLambdaFromPseudoLayers({
export {
excludeFiles,
validateEntrypoint,
includeOnlyEntryDirectory,
excludeLockFiles,
normalizePackageJson,
filesFromDirectory,
getNextConfig,
getPathsInside,
getRoutes,

View File

@@ -122,6 +122,7 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'public-files'));
expect(output['robots.txt']).toBeDefined();
expect(output['generated.txt']).toBeDefined();
},
FOUR_MINUTES
);

View File

@@ -0,0 +1,4 @@
const fs = require('fs');
// Adds a new file to the public folder at build time
fs.writeFileSync('public/generated.txt', 'Generated');

View File

@@ -1,9 +1,9 @@
{
"scripts": {
"now-build": "next build"
"now-build": "node create-public-file.js && next build"
},
"dependencies": {
"next": "8",
"next": "9",
"react": "16",
"react-dom": "16"
}

View File

@@ -2,9 +2,8 @@ const path = require('path');
const {
excludeFiles,
validateEntrypoint,
includeOnlyEntryDirectory,
normalizePackageJson,
getNextConfig
getNextConfig,
} = require('@now/next/dist/utils');
const { FileRef } = require('@now/build-utils');
@@ -33,7 +32,7 @@ describe('excludeFiles', () => {
const files = {
'pages/index.js': new FileRef({ digest: 'index' }),
'package.json': new FileRef({ digest: 'package' }),
'package-lock.json': new FileRef({ digest: 'package-lock' })
'package-lock.json': new FileRef({ digest: 'package-lock' }),
};
const result = excludeFiles(
files,
@@ -63,21 +62,6 @@ describe('validateEntrypoint', () => {
});
});
describe('includeOnlyEntryDirectory', () => {
it('should include files outside entry directory', () => {
const entryDirectory = 'frontend';
const files = {
'frontend/pages/index.js': new FileRef({ digest: 'index' }),
'package.json': new FileRef({ digest: 'package' }),
'package-lock.json': new FileRef({ digest: 'package-lock' })
};
const result = includeOnlyEntryDirectory(files, entryDirectory);
expect(result['frontend/pages/index.js']).toBeDefined();
expect(result['package.json']).toBeUndefined();
expect(result['package-lock.json']).toBeUndefined();
});
});
describe('normalizePackageJson', () => {
it('should work without a package.json being supplied', () => {
const result = normalizePackageJson();
@@ -85,15 +69,15 @@ describe('normalizePackageJson', () => {
dependencies: {
'next-server': 'v7.0.2-canary.49',
react: 'latest',
'react-dom': 'latest'
'react-dom': 'latest',
},
devDependencies: {
next: 'v7.0.2-canary.49'
next: 'v7.0.2-canary.49',
},
scripts: {
'now-build':
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
}
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
},
});
});
@@ -102,29 +86,29 @@ describe('normalizePackageJson', () => {
dependencies: {
'next-server': 'v7.0.2-canary.49',
react: 'latest',
'react-dom': 'latest'
'react-dom': 'latest',
},
devDependencies: {
next: 'v7.0.2-canary.49'
next: 'v7.0.2-canary.49',
},
scripts: {
'now-build': 'next build'
}
'now-build': 'next build',
},
};
const result = normalizePackageJson(defaultPackage);
expect(result).toEqual({
dependencies: {
'next-server': 'v7.0.2-canary.49',
react: 'latest',
'react-dom': 'latest'
'react-dom': 'latest',
},
devDependencies: {
next: 'v7.0.2-canary.49'
next: 'v7.0.2-canary.49',
},
scripts: {
'now-build':
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
}
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
},
});
});
@@ -133,23 +117,23 @@ describe('normalizePackageJson', () => {
dependencies: {
react: 'latest',
'react-dom': 'latest',
next: 'latest'
}
next: 'latest',
},
};
const result = normalizePackageJson(defaultPackage);
expect(result).toEqual({
dependencies: {
'next-server': 'v7.0.2-canary.49',
react: 'latest',
'react-dom': 'latest'
'react-dom': 'latest',
},
devDependencies: {
next: 'v7.0.2-canary.49'
next: 'v7.0.2-canary.49',
},
scripts: {
'now-build':
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
}
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
},
});
});
@@ -158,23 +142,23 @@ describe('normalizePackageJson', () => {
dependencies: {
react: 'latest',
'react-dom': 'latest',
next: 'latest'
}
next: 'latest',
},
};
const result = normalizePackageJson(defaultPackage);
expect(result).toEqual({
dependencies: {
'next-server': 'v7.0.2-canary.49',
react: 'latest',
'react-dom': 'latest'
'react-dom': 'latest',
},
devDependencies: {
next: 'v7.0.2-canary.49'
next: 'v7.0.2-canary.49',
},
scripts: {
'now-build':
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
}
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
},
});
});
@@ -183,23 +167,23 @@ describe('normalizePackageJson', () => {
dependencies: {
react: 'latest',
'react-dom': 'latest',
next: 'latest'
}
next: 'latest',
},
};
const result = normalizePackageJson(defaultPackage);
expect(result).toEqual({
dependencies: {
'next-server': 'v7.0.2-canary.49',
react: 'latest',
'react-dom': 'latest'
'react-dom': 'latest',
},
devDependencies: {
next: 'v7.0.2-canary.49'
next: 'v7.0.2-canary.49',
},
scripts: {
'now-build':
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
}
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
},
});
});
@@ -211,7 +195,7 @@ describe('normalizePackageJson', () => {
dev: 'next',
build: 'next build',
start: 'next start',
test: "xo && stylelint './pages/**/*.js' && jest"
test: "xo && stylelint './pages/**/*.js' && jest",
},
main: 'pages/index.js',
license: 'MIT',
@@ -226,7 +210,7 @@ describe('normalizePackageJson', () => {
'stylelint-config-recommended': '^2.1.0',
'stylelint-config-styled-components': '^0.1.1',
'stylelint-processor-styled-components': '^1.5.1',
xo: '^0.23.0'
xo: '^0.23.0',
},
dependencies: {
consola: '^2.2.6',
@@ -234,7 +218,7 @@ describe('normalizePackageJson', () => {
next: '^7.0.2',
react: '^16.6.3',
'react-dom': '^16.6.3',
'styled-components': '^4.1.1'
'styled-components': '^4.1.1',
},
xo: {
extends: 'xo-react',
@@ -244,15 +228,15 @@ describe('normalizePackageJson', () => {
'test',
'pages/_document.js',
'pages/index.js',
'pages/home.js'
'pages/home.js',
],
rules: {
'react/no-unescaped-entities': null
}
'react/no-unescaped-entities': null,
},
},
jest: {
testEnvironment: 'node'
}
testEnvironment: 'node',
},
};
const result = normalizePackageJson(defaultPackage);
expect(result).toEqual({
@@ -263,7 +247,7 @@ describe('normalizePackageJson', () => {
'now-build':
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
start: 'next start',
test: "xo && stylelint './pages/**/*.js' && jest"
test: "xo && stylelint './pages/**/*.js' && jest",
},
main: 'pages/index.js',
license: 'MIT',
@@ -283,12 +267,12 @@ describe('normalizePackageJson', () => {
xo: '^0.23.0',
consola: '^2.2.6',
fontfaceobserver: '^2.0.13',
'styled-components': '^4.1.1'
'styled-components': '^4.1.1',
},
dependencies: {
'next-server': 'v7.0.2-canary.49',
react: '^16.6.3',
'react-dom': '^16.6.3'
'react-dom': '^16.6.3',
},
xo: {
extends: 'xo-react',
@@ -298,15 +282,15 @@ describe('normalizePackageJson', () => {
'test',
'pages/_document.js',
'pages/index.js',
'pages/home.js'
'pages/home.js',
],
rules: {
'react/no-unescaped-entities': null
}
'react/no-unescaped-entities': null,
},
},
jest: {
testEnvironment: 'node'
}
testEnvironment: 'node',
},
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node",
"version": "1.0.1",
"version": "1.0.2",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/node-js-now-node",
@@ -11,7 +11,8 @@
},
"scripts": {
"build": "./build.sh",
"test-integration-once": "jest --env node --verbose --runInBand",
"test-unit": "jest --env node --verbose --runInBand test/helpers.test.js",
"test-integration-once": "jest --env node --verbose --runInBand test/integration.test.js",
"prepublishOnly": "npm run build"
},
"files": [

View File

@@ -13,13 +13,14 @@ function getBodyParser(req: NowRequest, body: Buffer) {
if (!req.headers['content-type']) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse: parseContentType } = require('content-type');
const { type } = parseContentType(req.headers['content-type']);
if (type === 'application/json') {
try {
return JSON.parse(body.toString());
const str = body.toString();
return str ? JSON.parse(str) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON');
}
@@ -30,6 +31,7 @@ function getBodyParser(req: NowRequest, body: Buffer) {
}
if (type === 'application/x-www-form-urlencoded') {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse: parseQS } = require('querystring');
// note: querystring.parse does not produce an iterable object
// https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
@@ -46,6 +48,7 @@ function getBodyParser(req: NowRequest, body: Buffer) {
function getQueryParser({ url = '/' }: NowRequest) {
return function parseQuery(): NowRequestQuery {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { URL } = require('url');
// we provide a placeholder base url because we only want searchParams
const params = new URL(url, 'https://n').searchParams;
@@ -67,6 +70,7 @@ function getCookieParser(req: NowRequest) {
return {};
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse } = require('cookie');
return parse(Array.isArray(header) ? header.join(';') : header);
};
@@ -78,6 +82,7 @@ function status(res: NowResponse, statusCode: number): NowResponse {
}
function setCharset(type: string, charset: string) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { parse, format } = require('content-type');
const parsed = parse(type);
parsed.parameters.charset = charset;
@@ -85,6 +90,7 @@ function setCharset(type: string, charset: string) {
}
function createETag(body: any, encoding: 'utf8' | undefined) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const etag = require('etag');
const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
return etag(buf, { weak: true });

View File

@@ -25,6 +25,12 @@
"body": { "who": "john" },
"mustContain": "hello john:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/",
"method": "POST",
"headers": { "Content-Type": "application/json" },
"status": 200
},
{
"path": "/",
"headers": { "cookie": "who=chris" },

View File

@@ -21,7 +21,7 @@ async function fetchWithProxyReq(_url, opts = {}) {
return fetch(_url, {
...opts,
headers: { ...opts.headers, 'x-now-bridge-request-id': '2' }
headers: { ...opts.headers, 'x-now-bridge-request-id': '2' },
});
}
@@ -66,7 +66,7 @@ describe('all helpers', () => {
['body', 0],
['status', 1],
['send', 1],
['json', 1]
['json', 1],
];
test('should not recalculate req properties twice', async () => {
@@ -83,7 +83,7 @@ describe('all helpers', () => {
await fetchWithProxyReq(`${url}/?who=bill`, {
method: 'POST',
body: JSON.stringify({ who: 'mike' }),
headers: { 'content-type': 'application/json', cookie: 'who=jim' }
headers: { 'content-type': 'application/json', cookie: 'who=jim' },
});
// here we test that bodySpy is called twice with exactly the same arguments
@@ -137,7 +137,7 @@ describe('req.query', () => {
expect(mockListener.mock.calls[0][0].query).toMatchObject({
who: 'bill',
where: 'us'
where: 'us',
});
});
@@ -152,13 +152,13 @@ describe('req.cookies', () => {
test('req.cookies should reflect req.cookie header', async () => {
await fetchWithProxyReq(url, {
headers: {
cookie: 'who=bill; where=us'
}
cookie: 'who=bill; where=us',
},
});
expect(mockListener.mock.calls[0][0].cookies).toMatchObject({
who: 'bill',
where: 'us'
where: 'us',
});
});
});
@@ -172,7 +172,7 @@ describe('req.body', () => {
test('req.body should be undefined if content-type is not defined', async () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello'
body: 'hello',
});
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
});
@@ -181,7 +181,7 @@ describe('req.body', () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
headers: { 'content-type': 'text/plain' }
headers: { 'content-type': 'text/plain' },
});
expect(mockListener.mock.calls[0][0].body).toBe('hello');
@@ -191,7 +191,7 @@ describe('req.body', () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
headers: { 'content-type': 'application/octet-stream' }
headers: { 'content-type': 'application/octet-stream' },
});
const [{ body }] = mockListener.mock.calls[0];
@@ -208,7 +208,7 @@ describe('req.body', () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: qs.encode(obj),
headers: { 'content-type': 'application/x-www-form-urlencoded' }
headers: { 'content-type': 'application/x-www-form-urlencoded' },
});
expect(mockListener.mock.calls[0][0].body).toMatchObject(obj);
@@ -217,19 +217,19 @@ describe('req.body', () => {
test('req.body should be an object when content-type is `application/json`', async () => {
const json = {
who: 'bill',
where: 'us'
where: 'us',
};
await fetchWithProxyReq(url, {
method: 'POST',
body: JSON.stringify(json),
headers: { 'content-type': 'application/json' }
headers: { 'content-type': 'application/json' },
});
expect(mockListener.mock.calls[0][0].body).toMatchObject(json);
});
test('should throw error when body is empty and content-type is `application/json`', async () => {
test('should work when body is empty and content-type is `application/json`', async () => {
mockListener.mockImplementation((req, res) => {
console.log(req.body);
res.end();
@@ -238,10 +238,11 @@ describe('req.body', () => {
const res = await fetchWithProxyReq(url, {
method: 'POST',
body: '',
headers: { 'content-type': 'application/json' }
headers: { 'content-type': 'application/json' },
});
expect(res.status).toBe(400);
expect(res.status).toBe(200);
expect(res.body).toMatchObject({});
});
test('should be able to try/catch parse errors', async () => {
@@ -260,7 +261,7 @@ describe('req.body', () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: '{"wrong":"json"',
headers: { 'content-type': 'application/json' }
headers: { 'content-type': 'application/json' },
});
expect(bodySpy).toHaveBeenCalled();

View File

@@ -3,7 +3,7 @@ const path = require('path');
const {
packAndDeploy,
testDeployment
testDeployment,
} = require('../../../test/lib/deployment/test-deployment.js');
jest.setTimeout(4 * 60 * 1000);

View File

@@ -52,16 +52,26 @@ if 'handler' in __now_variables or 'Handler' in __now_variables:
):
body = base64.b64decode(body)
request_body = body.encode('utf-8') if isinstance(body, str) else body
conn = http.client.HTTPConnection('0.0.0.0', port)
conn.request(method, path, headers=headers, body=body)
conn.request(method, path, headers=headers, body=request_body)
res = conn.getresponse()
data = res.read().decode('utf-8')
return {
return_dict = {
'statusCode': res.status,
'headers': format_headers(res.headers),
'body': data,
}
data = res.read()
try:
return_dict['body'] = data.decode('utf-8')
except UnicodeDecodeError:
return_dict['body'] = base64.b64encode(data).decode('utf-8')
return_dict['encoding'] = 'base64'
return return_dict
elif 'app' in __now_variables:
if (
not inspect.iscoroutinefunction(__NOW_HANDLER_FILENAME.app) and

View File

@@ -1,6 +1,6 @@
{
"name": "@now/python",
"version": "0.3.0",
"version": "0.3.2",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/python-now-python",

View File

@@ -0,0 +1,20 @@
from http.server import BaseHTTPRequestHandler
import json
class handler(BaseHTTPRequestHandler):
def do_POST(self):
post_body = json.loads(self.rfile.read(int(self.headers['content-length'])).decode('utf-8'))
name = post_body.get('name', 'someone')
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
response_data = {'greeting': f'hello, {name}'}
self.wfile.write(json.dumps(response_data).encode('utf-8'))
return
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write('ok'.encode('utf-8'))
return

View File

@@ -0,0 +1,19 @@
{
"version": 2,
"builds": [
{
"src": "*.py",
"use": "@now/python"
}
],
"probes": [
{
"path": "/",
"method": "POST",
"body": {
"name": "Χριστοφορε"
},
"status": 200
}
]
}

View File

@@ -0,0 +1,19 @@
module.exports = async function({ deploymentUrl, fetch, randomness }) {
const nowjson = require('./now.json');
const probe = nowjson.probes[0];
const probeUrl = `https://${deploymentUrl}${probe.path}`;
const resp = await fetch(probeUrl, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(probe.body),
});
const text = await resp.text();
const respBody = JSON.parse(text);
if (respBody.greeting !== 'hello, Χριστοφορε') {
throw new Error(`unexpected response: ${respBody}`);
}
};

View File

@@ -0,0 +1,11 @@
from http.server import BaseHTTPRequestHandler
class handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.end_headers()
with open("zeit-white-triangle.png", "rb") as image:
self.wfile.write(image.read())
return

View File

@@ -0,0 +1,16 @@
{
"version": 2,
"builds": [
{
"src": "*.py",
"use": "@now/python"
}
],
"probes": [
{
"path": "/",
"method": "GET",
"status": 200
}
]
}

View File

@@ -0,0 +1,19 @@
const fs = require('fs');
const path = require('path');
module.exports = async function({ deploymentUrl, fetch, randomness }) {
const nowjson = require('./now.json');
const probe = nowjson.probes[0];
const probeUrl = `https://${deploymentUrl}${probe.path}`;
const resp = await fetch(probeUrl);
const bytes = await resp.arrayBuffer();
const image = fs.readFileSync(
path.join(__dirname, 'zeit-white-triangle.png')
);
if (!image.equals(new Uint8Array(bytes))) {
throw new Error(`unexpected response: ${bytes}`);
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -6,14 +6,21 @@ Route validation utilities
`yarn add @now/routing-utils`
exports.normalizeRoutes:
`(routes: Array<Route> | null) => { routes: Array<Route> | null; error: NowError | null }`
```ts
import { normalizeRoutes } from '@now/routing-utils';
exports.schema:
const { routes, error } = normalizeRoutes(inputRoutes);
if (error) {
console.log(error.code, error.message);
}
```
```ts
import { routesSchema } from '@now/routing-utils';
```js
const ajv = new Ajv();
const validate = ajv.compile(schema);
const validate = ajv.compile(routesSchema);
const valid = validate([{ src: '/about', dest: '/about.html' }]);
if (!valid) console.log(validate.errors);

View File

@@ -1,7 +1,7 @@
{
"name": "@now/routing-utils",
"version": "1.2.4",
"description": "ZEIT Now route validation utilities",
"version": "1.3.0",
"description": "ZEIT Now routing utilities",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
@@ -19,6 +19,9 @@
"watch": "tsc --watch",
"test-unit": "jest --env node --verbose --runInBand"
},
"dependencies": {
"path-to-regexp": "3.1.0"
},
"devDependencies": {
"ajv": "^6.0.0",
"typescript": "3.5.2"

View File

@@ -1,36 +1,25 @@
export type NowError = {
code: string;
message: string;
errors: {
message: string;
src?: string;
handle?: string;
}[];
sha?: string; // File errors
};
export type Source = {
src: string;
dest?: string;
headers?: {};
methods?: string[];
continue?: boolean;
status?: number;
};
export type Handler = {
handle: string;
};
export type Route = Source | Handler;
export * from './schemas';
export * from './types';
import {
Route,
Handler,
NormalizedRoutes,
GetRoutesProps,
NowError,
} from './types';
import {
convertCleanUrls,
convertRewrites,
convertRedirects,
convertHeaders,
convertTrailingSlash,
} from './superstatic';
export function isHandler(route: Route): route is Handler {
return typeof (route as Handler).handle !== 'undefined';
}
export function normalizeRoutes(
inputRoutes: Array<Route> | null
): { routes: Array<Route> | null; error: NowError | null } {
export function normalizeRoutes(inputRoutes: Route[] | null): NormalizedRoutes {
if (!inputRoutes || inputRoutes.length === 0) {
return { routes: inputRoutes, error: null };
}
@@ -48,19 +37,19 @@ export function normalizeRoutes(
if (Object.keys(route).length !== 1) {
errors.push({
message: `Cannot have any other keys when handle is used (handle: ${route.handle})`,
handle: route.handle
handle: route.handle,
});
}
if (!['filesystem'].includes(route.handle)) {
errors.push({
message: `This is not a valid handler (handle: ${route.handle})`,
handle: route.handle
handle: route.handle,
});
}
if (handling.includes(route.handle)) {
errors.push({
message: `You can only handle something once (handle: ${route.handle})`,
handle: route.handle
handle: route.handle,
});
} else {
handling.push(route.handle);
@@ -76,23 +65,26 @@ export function normalizeRoutes(
route.src = `${route.src}$`;
}
// Route src should strip escaped forward slash, its not special
route.src = route.src.replace(/\\\//g, '/');
try {
// This feels a bit dangerous if there would be a vulnerability in RegExp.
new RegExp(route.src);
} catch (err) {
errors.push({
message: `Invalid regular expression: "${route.src}"`,
src: route.src
src: route.src,
});
}
} else {
errors.push({
message: 'A route must set either handle or src'
message: 'A route must set either handle or src',
});
}
}
const error =
const error: NowError | null =
errors.length > 0
? {
code: 'invalid_routes',
@@ -101,63 +93,89 @@ export function normalizeRoutes(
null,
2
)}`,
errors
errors,
}
: null;
return { routes, error };
}
/**
* An ajv schema for the routes array
*/
export const schema = {
type: 'array',
maxItems: 1024,
items: {
type: 'object',
additionalProperties: false,
properties: {
src: {
type: 'string',
maxLength: 4096
},
dest: {
type: 'string',
maxLength: 4096
},
methods: {
type: 'array',
maxItems: 10,
items: {
type: 'string',
maxLength: 32
}
},
headers: {
type: 'object',
additionalProperties: false,
minProperties: 1,
maxProperties: 100,
patternProperties: {
'^.{1,256}$': {
type: 'string',
maxLength: 4096
}
}
},
handle: {
type: 'string',
maxLength: 32
},
continue: {
type: 'boolean'
},
status: {
type: 'integer',
minimum: 100,
maximum: 999
}
export function getTransformedRoutes({
nowConfig,
filePaths,
}: GetRoutesProps): NormalizedRoutes {
const { cleanUrls, rewrites, redirects, headers, trailingSlash } = nowConfig;
let { routes } = nowConfig;
const errors: { message: string }[] = [];
if (typeof routes !== 'undefined') {
if (typeof cleanUrls !== 'undefined') {
errors.push({
message: 'Cannot define both `routes` and `cleanUrls`',
});
}
if (typeof trailingSlash !== 'undefined') {
errors.push({
message: 'Cannot define both `routes` and `trailingSlash`',
});
}
if (typeof redirects !== 'undefined') {
errors.push({
message: 'Cannot define both `routes` and `redirects`',
});
}
if (typeof headers !== 'undefined') {
errors.push({
message: 'Cannot define both `routes` and `headers`',
});
}
if (typeof rewrites !== 'undefined') {
errors.push({
message: 'Cannot define both `routes` and `rewrites`',
});
}
} else {
routes = [];
let cleanUrlsRewrites: Route[] | undefined;
if (typeof cleanUrls !== 'undefined') {
const cleanUrls = convertCleanUrls(filePaths);
cleanUrlsRewrites = cleanUrls.rewrites;
routes.push(...cleanUrls.redirects);
}
if (typeof trailingSlash !== 'undefined') {
routes.push(...convertTrailingSlash(trailingSlash));
}
if (typeof redirects !== 'undefined') {
routes.push(...convertRedirects(redirects));
}
if (typeof headers !== 'undefined') {
routes.push(...convertHeaders(headers));
}
if (typeof cleanUrlsRewrites !== 'undefined') {
routes.push(...cleanUrlsRewrites);
}
if (
typeof cleanUrlsRewrites !== 'undefined' ||
typeof rewrites !== 'undefined'
) {
routes.push({ handle: 'filesystem' });
}
if (typeof rewrites !== 'undefined') {
routes.push(...convertRewrites(rewrites));
}
}
};
if (errors.length > 0) {
const error = {
code: 'invalid_routes',
message: `One or more invalid routes were found: \n${JSON.stringify(
errors,
null,
2
)}`,
errors,
};
return { routes: [], error };
}
return normalizeRoutes(routes);
}

View File

@@ -0,0 +1,142 @@
/**
* An ajv schema for the routes array
*/
export const routesSchema = {
type: 'array',
maxItems: 1024,
items: {
type: 'object',
additionalProperties: false,
properties: {
src: {
type: 'string',
maxLength: 4096,
},
dest: {
type: 'string',
maxLength: 4096,
},
methods: {
type: 'array',
maxItems: 10,
items: {
type: 'string',
maxLength: 32,
},
},
headers: {
type: 'object',
additionalProperties: false,
minProperties: 1,
maxProperties: 100,
patternProperties: {
'^.{1,256}$': {
type: 'string',
maxLength: 4096,
},
},
},
handle: {
type: 'string',
maxLength: 32,
},
continue: {
type: 'boolean',
},
status: {
type: 'integer',
minimum: 100,
maximum: 999,
},
},
},
};
export const rewritesSchema = {
type: 'array',
maxItems: 1024,
items: {
type: 'object',
additionalProperties: false,
required: ['source', 'destination'],
properties: {
source: {
type: 'string',
maxLength: 4096,
},
destination: {
type: 'string',
maxLength: 4096,
},
},
},
};
export const redirectsSchema = {
title: 'Redirects',
type: 'array',
maxItems: 1024,
items: {
type: 'object',
additionalProperties: false,
required: ['source', 'destination'],
properties: {
source: {
type: 'string',
maxLength: 4096,
},
destination: {
type: 'string',
maxLength: 4096,
},
statusCode: {
type: 'integer',
minimum: 100,
maximum: 999,
},
},
},
};
export const headersSchema = {
type: 'array',
maxItems: 1024,
items: {
type: 'object',
additionalProperties: false,
required: ['source', 'headers'],
properties: {
source: {
type: 'string',
maxLength: 4096,
},
headers: {
type: 'array',
maxItems: 1024,
items: {
type: 'object',
additionalProperties: false,
required: ['key', 'value'],
properties: {
key: {
type: 'string',
maxLength: 4096,
},
value: {
type: 'string',
maxLength: 4096,
},
},
},
},
},
},
};
export const cleanUrlsSchema = {
type: 'boolean',
};
export const trailingSlashSchema = {
type: 'boolean',
};

View File

@@ -0,0 +1,122 @@
/**
* This converts Superstatic configuration to Now.json Routes
* See https://github.com/firebase/superstatic#configuration
*/
import pathToRegexp from 'path-to-regexp';
import { Route, NowRedirect, NowRewrite, NowHeader } from './types';
export function convertCleanUrls(
filePaths: string[]
): { redirects: Route[]; rewrites: Route[] } {
const htmlFiles = filePaths
.map(toRoute)
.filter(f => f.endsWith('.html'))
.map(f => ({
html: f,
clean: f.slice(0, -5),
}));
const redirects: Route[] = htmlFiles.map(o => ({
src: o.html,
headers: { Location: o.clean },
status: 301,
}));
const rewrites: Route[] = htmlFiles.map(o => ({
src: o.clean,
dest: o.html,
continue: true,
}));
return { redirects, rewrites };
}
export function convertRedirects(redirects: NowRedirect[]): Route[] {
return redirects.map(r => {
const { src, segments } = sourceToRegex(r.source);
const loc = replaceSegments(segments, r.destination);
return {
src,
headers: { Location: loc },
status: r.statusCode || 307,
};
});
}
export function convertRewrites(rewrites: NowRewrite[]): Route[] {
const routes: Route[] = rewrites.map(r => {
const { src, segments } = sourceToRegex(r.source);
const dest = replaceSegments(segments, r.destination);
return { src, dest, continue: true };
});
return routes;
}
export function convertHeaders(headers: NowHeader[]): Route[] {
return headers.map(h => {
const obj: { [key: string]: string } = {};
h.headers.forEach(kv => {
obj[kv.key] = kv.value;
});
return {
src: h.source,
headers: obj,
continue: true,
};
});
}
export function convertTrailingSlash(enable: boolean): Route[] {
const routes: Route[] = [];
if (enable) {
routes.push({
src: '^(.*[^\\/])$',
headers: { Location: '$1/' },
status: 307,
});
} else {
routes.push({
src: '^(.*)\\/$',
headers: { Location: '$1' },
status: 307,
});
}
return routes;
}
function sourceToRegex(source: string): { src: string; segments: string[] } {
const keys: pathToRegexp.Key[] = [];
const r = pathToRegexp(source, keys, { strict: true });
const segments = keys.map(k => k.name).filter(isString);
return { src: r.source, segments };
}
function isString(key: any): key is string {
return typeof key === 'string';
}
function replaceSegments(segments: string[], destination: string): string {
if (destination.includes(':')) {
segments.forEach((name, index) => {
const r = new RegExp(':' + name, 'g');
destination = destination.replace(r, toSegmentDest(index));
});
} else if (segments.length > 0) {
let prefix = '?';
segments.forEach((name, index) => {
destination += `${prefix}${name}=${toSegmentDest(index)}`;
prefix = '&';
});
}
return destination;
}
function toSegmentDest(index: number): string {
const i = index + 1; // js is base 0, regex is base 1
return '$' + i.toString();
}
function toRoute(filePath: string): string {
return filePath.startsWith('/') ? filePath : '/' + filePath;
}

View File

@@ -0,0 +1,67 @@
export type NowError = {
code: string;
message: string;
errors: {
message: string;
src?: string;
handle?: string;
}[];
sha?: string; // File errors
};
export type Source = {
src: string;
dest?: string;
headers?: { [name: string]: string };
methods?: string[];
continue?: boolean;
status?: number;
};
export type Handler = {
handle: string;
};
export type Route = Source | Handler;
export type NormalizedRoutes = {
routes: Route[] | null;
error: NowError | null;
};
export interface GetRoutesProps {
nowConfig: NowConfig;
filePaths: string[];
}
export interface NowConfig {
name?: string;
version?: number;
routes?: Route[];
cleanUrls?: boolean;
rewrites?: NowRewrite[];
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
}
export interface NowRewrite {
source: string;
destination: string;
}
export interface NowRedirect {
source: string;
destination: string;
statusCode?: number;
}
export interface NowHeader {
source: string;
headers: NowHeaderKeyValue[];
}
export interface NowHeaderKeyValue {
key: string;
value: string;
}

View File

@@ -1,18 +1,28 @@
const assert = require('assert');
const Ajv = require('ajv');
const { normalizeRoutes, isHandler, schema } = require('../dist');
const {
normalizeRoutes,
isHandler,
routesSchema,
rewritesSchema,
redirectsSchema,
headersSchema,
cleanUrlsSchema,
trailingSlashSchema,
getTransformedRoutes,
} = require('../');
const ajv = new Ajv();
const assertValid = (routes) => {
const assertValid = (data, schema = routesSchema) => {
const validate = ajv.compile(schema);
const valid = validate(routes);
const valid = validate(data);
if (!valid) console.log(validate.errors);
assert.equal(valid, true);
};
const assertError = (routes, errors) => {
const assertError = (data, errors, schema = routesSchema) => {
const validate = ajv.compile(schema);
const valid = validate(routes);
const valid = validate(data);
assert.equal(valid, false);
assert.deepEqual(validate.errors, errors);
@@ -50,7 +60,6 @@ describe('normalizeRoutes', () => {
test('normalizes src', () => {
const expected = '^/about$';
const expected2 = '^\\/about$';
const sources = [
{ src: '/about' },
{ src: '/about$' },
@@ -70,15 +79,13 @@ describe('normalizeRoutes', () => {
assert.notEqual(normalized.routes, null);
if (normalized.routes) {
normalized.routes.forEach((route) => {
normalized.routes.forEach(route => {
if (isHandler(route)) {
assert.fail(
`Normalizer returned: { handle: ${
route.handle
} } instead of { src: ${expected} }`,
`Normalizer returned: { handle: ${route.handle} } instead of { src: ${expected} }`
);
} else {
assert.ok(route.src === expected || route.src === expected2);
assert.strictEqual(route.src, expected);
}
});
}
@@ -147,7 +154,7 @@ describe('normalizeRoutes', () => {
message: `One or more invalid routes were found: \n${JSON.stringify(
errors,
null,
2,
2
)}`,
errors,
});
@@ -202,7 +209,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/src/type',
},
],
]
);
});
@@ -224,7 +231,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/dest/type',
},
],
]
);
});
@@ -246,7 +253,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/methods/type',
},
],
]
);
});
@@ -268,7 +275,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/methods/items/type',
},
],
]
);
});
@@ -290,7 +297,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/headers/type',
},
],
]
);
});
@@ -315,7 +322,7 @@ describe('normalizeRoutes', () => {
schemaPath:
'#/items/properties/headers/patternProperties/%5E.%7B1%2C256%7D%24/type',
},
],
]
);
});
@@ -337,7 +344,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/handle/type',
},
],
]
);
});
@@ -359,7 +366,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/continue/type',
},
],
]
);
});
@@ -381,7 +388,7 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/properties/status/type',
},
],
]
);
});
@@ -403,7 +410,98 @@ describe('normalizeRoutes', () => {
},
schemaPath: '#/items/additionalProperties',
},
],
]
);
});
});
describe('getTransformedRoutes', () => {
test('should normalize nowConfig.routes', () => {
const nowConfig = { routes: [{ src: '/page', dest: '/page.html' }] };
const filePaths = [];
const actual = getTransformedRoutes({ nowConfig, filePaths });
const expected = normalizeRoutes(nowConfig.routes);
assert.deepEqual(actual, expected);
assertValid(actual.routes);
});
test('should normalize all redirects before rewrites', () => {
const nowConfig = {
cleanUrls: true,
rewrites: [{ source: '/v1', destination: '/v2/api.py' }],
redirects: [
{ source: '/help', destination: '/support', statusCode: 302 },
],
};
const filePaths = ['/index.html', '/support.html', '/v2/api.py'];
const actual = getTransformedRoutes({ nowConfig, filePaths });
const expected = [
{
src: '^/index.html$',
headers: { Location: '/index' },
status: 301,
},
{
src: '^/support.html$',
headers: { Location: '/support' },
status: 301,
},
{
src: '^/help$',
headers: { Location: '/support' },
status: 302,
},
{ src: '^/index$', dest: '/index.html', continue: true },
{ src: '^/support$', dest: '/support.html', continue: true },
{ handle: 'filesystem' },
{ src: '^/v1$', dest: '/v2/api.py', continue: true },
];
assert.deepEqual(actual.error, null);
assert.deepEqual(actual.routes, expected);
assertValid(actual.routes, routesSchema);
});
test('should validate schemas', () => {
const nowConfig = {
cleanUrls: true,
rewrites: [
{ source: '/page', destination: '/page.html' },
{ source: '/home', destination: '/index.html' },
],
redirects: [
{ source: '/version1', destination: '/api1.py' },
{ source: '/version2', destination: '/api2.py', statusCode: 302 },
],
headers: [
{
source: '/(.*)',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
],
},
{
source: '/404',
headers: [
{
key: 'Cache-Control',
value: 'max-age=300',
},
{
key: 'Set-Cookie',
value: 'error=404',
},
],
},
],
trailingSlashSchema: false,
};
assertValid(nowConfig.cleanUrls, cleanUrlsSchema);
assertValid(nowConfig.rewrites, rewritesSchema);
assertValid(nowConfig.redirects, redirectsSchema);
assertValid(nowConfig.headers, headersSchema);
assertValid(nowConfig.trailingSlashSchema, trailingSlashSchema);
});
});

View File

@@ -0,0 +1,246 @@
const { deepEqual } = require('assert');
const { normalizeRoutes } = require('../');
const {
convertCleanUrls,
convertRedirects,
convertRewrites,
convertHeaders,
convertTrailingSlash,
} = require('../dist/superstatic');
function routesToRegExps(routeArray) {
const { routes, error } = normalizeRoutes(routeArray);
if (error) {
throw error;
}
return routes.map(r => new RegExp(r.src));
}
function assertMatches(actual, matches, isExpectingMatch) {
routesToRegExps(actual).forEach((r, i) => {
matches[i].forEach(text => {
deepEqual(r.test(text), isExpectingMatch, `${text} ${r.source}`);
});
});
}
function assertRegexMatches(actual, mustMatch, mustNotMatch) {
assertMatches(actual, mustMatch, true);
assertMatches(actual, mustNotMatch, false);
}
test('convertCleanUrls', () => {
const { redirects, rewrites } = convertCleanUrls([
'file.txt',
'path/to/file.txt',
'file.js',
'path/to/file.js',
'file.html',
'path/to/file.html',
]);
const expectedRedirects = [
{
src: '/file.html',
headers: { Location: '/file' },
status: 301,
},
{
src: '/path/to/file.html',
headers: { Location: '/path/to/file' },
status: 301,
},
];
const expectedRewrites = [
{ src: '/file', dest: '/file.html', continue: true },
{ src: '/path/to/file', dest: '/path/to/file.html', continue: true },
];
deepEqual(redirects, expectedRedirects);
deepEqual(rewrites, expectedRewrites);
});
test('convertRedirects', () => {
const actual = convertRedirects([
{ source: '/some/old/path', destination: '/some/new/path' },
{
source: '/firebase/(.*)',
destination: 'https://www.firebase.com',
statusCode: 302,
},
{
source: '/projects/:id/:action',
destination: '/projects.html',
},
{ source: '/old/:segment/path', destination: '/new/path/:segment' },
]);
const expected = [
{
src: '^\\/some\\/old\\/path$',
headers: { Location: '/some/new/path' },
status: 307,
},
{
src: '^\\/firebase\\/(.*)$',
headers: { Location: 'https://www.firebase.com' },
status: 302,
},
{
src: '^\\/projects\\/([^\\/]+?)\\/([^\\/]+?)$',
headers: { Location: '/projects.html?id=$1&action=$2' },
status: 307,
},
{
src: '^\\/old\\/([^\\/]+?)\\/path$',
headers: { Location: '/new/path/$1' },
status: 307,
},
];
deepEqual(actual, expected);
const mustMatch = [
['/some/old/path'],
['/firebase/one', '/firebase/2', '/firebase/-', '/firebase/dir/sub'],
['/projects/one/edit', '/projects/two/edit'],
['/old/one/path', '/old/two/path'],
];
const mustNotMatch = [
['/nope'],
['/fire', '/firebasejumper/two'],
['/projects/edit', '/projects/two/three/delete', '/projects'],
['/old/path', '/old/two/foo', '/old'],
];
assertRegexMatches(actual, mustMatch, mustNotMatch);
});
test('convertRewrites', () => {
const actual = convertRewrites([
{ source: '/some/old/path', destination: '/some/new/path' },
{ source: '/firebase/(.*)', destination: 'https://www.firebase.com' },
{ source: '/projects/:id/edit', destination: '/projects.html' },
]);
const expected = [
{ src: '^\\/some\\/old\\/path$', dest: '/some/new/path', continue: true },
{
src: '^\\/firebase\\/(.*)$',
dest: 'https://www.firebase.com',
continue: true,
},
{
src: '^\\/projects\\/([^\\/]+?)\\/edit$',
dest: '/projects.html?id=$1',
continue: true,
},
];
deepEqual(actual, expected);
const mustMatch = [
['/some/old/path'],
['/firebase/one', '/firebase/two'],
['/projects/one/edit', '/projects/two/edit'],
['/old/one/path', '/old/two/path'],
];
const mustNotMatch = [
['/nope'],
['/fire', '/firebasejumper/two'],
['/projects/edit', '/projects/two/delete', '/projects'],
['/old/path', '/old/two/foo', '/old'],
];
assertRegexMatches(actual, mustMatch, mustNotMatch);
});
test('convertHeaders', () => {
const actual = convertHeaders([
{
source: '(.*)+/(.*)\\.(eot|otf|ttf|ttc|woff|font\\.css)',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
],
},
{
source: '404.html',
headers: [
{
key: 'Cache-Control',
value: 'max-age=300',
},
{
key: 'Set-Cookie',
value: 'error=404',
},
],
},
]);
const expected = [
{
src: '(.*)+/(.*)\\.(eot|otf|ttf|ttc|woff|font\\.css)',
headers: { 'Access-Control-Allow-Origin': '*' },
continue: true,
},
{
src: '404.html',
headers: { 'Cache-Control': 'max-age=300', 'Set-Cookie': 'error=404' },
continue: true,
},
];
deepEqual(actual, expected);
const mustMatch = [
['hello/world/file.eot', 'another/font.ttf', 'dir/arial.font.css'],
['404.html'],
];
const mustNotMatch = [
['hello/file.jpg', 'hello/font-css', 'dir/arial.font-css'],
['403.html', '500.html'],
];
assertRegexMatches(actual, mustMatch, mustNotMatch);
});
test('convertTrailingSlash enabled', () => {
const actual = convertTrailingSlash(true);
const expected = [
{
src: '^(.*[^\\/])$',
headers: { Location: '$1/' },
status: 307,
},
];
deepEqual(actual, expected);
const mustMatch = [['/index.html', '/dir', '/dir/index.html', '/foo/bar']];
const mustNotMatch = [['/', '/dir/', '/dir/foo/', '/next.php?page=/']];
assertRegexMatches(actual, mustMatch, mustNotMatch);
});
test('convertTrailingSlash disabled', () => {
const actual = convertTrailingSlash(false);
const expected = [
{
src: '^(.*)\\/$',
headers: { Location: '$1' },
status: 307,
},
];
deepEqual(actual, expected);
const mustMatch = [['/dir/', '/index.html/', '/next.php?page=/']];
const mustNotMatch = [['/dirp', '/mkdir', '/dir/foo']];
assertRegexMatches(actual, mustMatch, mustNotMatch);
});

View File

@@ -1,6 +1,6 @@
{
"name": "@now/static-build",
"version": "0.10.0",
"version": "0.11.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build",
@@ -19,11 +19,12 @@
},
"devDependencies": {
"@types/cross-spawn": "6.0.0",
"@types/ms": "0.7.31",
"@types/promise-timeout": "1.3.0",
"cross-spawn": "6.0.5",
"get-port": "5.0.0",
"is-port-reachable": "2.0.1",
"promise-timeout": "1.3.0",
"ms": "2.1.2",
"typescript": "3.5.2"
}
}

View File

@@ -290,6 +290,20 @@ export const frameworks: Framework[] = [
},
],
},
{
name: 'Stencil',
dependency: '@stencil/core',
getOutputDirName: async () => 'www',
defaultRoutes: [
{
handle: 'filesystem',
},
{
src: '/(.*)',
dest: '/index.html',
},
],
},
];
export interface Framework {

View File

@@ -1,8 +1,9 @@
import ms from 'ms';
import path from 'path';
import spawn from 'cross-spawn';
import getPort from 'get-port';
import { timeout } from 'promise-timeout';
import isPortReachable from 'is-port-reachable';
import { ChildProcess, SpawnOptions } from 'child_process';
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
import { frameworks, Framework } from './frameworks';
import {
@@ -25,11 +26,20 @@ import {
PrepareCacheOptions,
} from '@now/build-utils';
async function checkForPort(port: number | undefined): Promise<void> {
const sleep = (n: number) => new Promise(resolve => setTimeout(resolve, n));
const DEV_SERVER_PORT_BIND_TIMEOUT = ms('5m');
async function checkForPort(
port: number | undefined,
timeout: number
): Promise<void> {
const start = Date.now();
while (!(await isPortReachable(port))) {
await new Promise(resolve => {
setTimeout(resolve, 100);
});
if (Date.now() - start > timeout) {
throw new Error(`Detecting port ${port} timed out after ${ms(timeout)}`);
}
await sleep(100);
}
}
@@ -92,7 +102,20 @@ function getCommand(pkg: PackageJson, cmd: string, { zeroConfig }: Config) {
export const version = 2;
const nowDevScriptPorts = new Map();
const nowDevScriptPorts = new Map<string, number>();
const nowDevChildProcesses = new Set<ChildProcess>();
['SIGINT', 'SIGTERM'].forEach(signal => {
process.once(signal as NodeJS.Signals, () => {
for (const child of nowDevChildProcesses) {
debug(
`Got ${signal}, killing dev server child process (pid=${child.pid})`
);
process.kill(child.pid, signal);
}
process.exit(0);
});
});
const getDevRoute = (srcBase: string, devPort: number, route: Route) => {
const basic: Route = {
@@ -256,32 +279,21 @@ export async function build({
devPort = await getPort();
nowDevScriptPorts.set(entrypoint, devPort);
const opts = {
const opts: SpawnOptions = {
cwd: entrypointDir,
stdio: 'inherit',
env: { ...process.env, PORT: String(devPort) },
};
const child = spawn('yarn', ['run', devScript], opts);
const child: ChildProcess = spawn('yarn', ['run', devScript], opts);
child.on('exit', () => nowDevScriptPorts.delete(entrypoint));
if (child.stdout) {
child.stdout.setEncoding('utf8');
child.stdout.pipe(process.stdout);
}
if (child.stderr) {
child.stderr.setEncoding('utf8');
child.stderr.pipe(process.stderr);
}
nowDevChildProcesses.add(child);
// Now wait for the server to have listened on `$PORT`, after which we
// will ProxyPass any requests to that development server that come in
// for this builder.
try {
await timeout(
new Promise(resolve => {
checkForPort(devPort).then(resolve);
}),
5 * 60 * 1000
);
await checkForPort(devPort, DEV_SERVER_PORT_BIND_TIMEOUT);
} catch (err) {
throw new Error(
`Failed to detect a server running on port ${devPort}.\nDetails: https://err.sh/zeit/now/now-static-build-failed-to-detect-a-server`

View File

@@ -1,7 +1,10 @@
declare module 'is-port-reachable' {
export interface IsPortReachableOptions {
timeout?: number | undefined;
host?: string;
}
export default function(port: number | undefined, options?: IsPortReachableOptions): Promise<boolean>;
}
export interface IsPortReachableOptions {
timeout?: number | undefined;
host?: string;
}
export default function(
port: number | undefined,
options?: IsPortReachableOptions
): Promise<boolean>;
}

View File

@@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@@ -0,0 +1,26 @@
dist/
!www/favicon.ico
www/
*~
*.sw[mnpcod]
*.log
*.lock
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
.stencil/
.idea/
.vscode/
.sass-cache/
.versions/
node_modules/
$RECYCLE.BIN/
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
.env

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,17 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@now/static-build",
"config": { "zeroConfig": true }
}
],
"probes": [
{ "path": "/", "mustContain": "Welcome to the Stencil App Starter" },
{
"path": "/profile/stencil",
"mustContain": "Welcome to the Stencil App Starter"
}
]
}

View File

@@ -0,0 +1,18 @@
{
"name": "42-stencil",
"private": true,
"version": "0.0.1",
"description": "Stencil App Starter",
"scripts": {
"build": "stencil build",
"start": "stencil build --dev --watch --serve",
"test": "stencil test --spec --e2e",
"test.watch": "stencil test --spec --e2e --watchAll",
"generate": "stencil generate"
},
"dependencies": {
"@stencil/core": "^1.3.3",
"@stencil/router": "^1.0.1"
},
"license": "MIT"
}

View File

@@ -0,0 +1,41 @@
# Stencil App Starter
Stencil is a compiler for building fast web apps using Web Components.
Stencil combines the best concepts of the most popular frontend frameworks into a compile-time rather than run-time tool. Stencil takes TypeScript, JSX, a tiny virtual DOM layer, efficient one-way data binding, an asynchronous rendering pipeline (similar to React Fiber), and lazy-loading out of the box, and generates 100% standards-based Web Components that run in any browser supporting the Custom Elements v1 spec.
Stencil components are just Web Components, so they work in any major framework or with no framework at all. In many cases, Stencil can be used as a drop in replacement for traditional frontend frameworks given the capabilities now available in the browser, though using it as such is certainly not required.
Stencil also enables a number of key capabilities on top of Web Components, in particular Server Side Rendering (SSR) without the need to run a headless browser, pre-rendering, and objects-as-properties (instead of just strings).
## Getting Started
To start a new project using Stencil, clone this repo to a new directory:
```bash
npm init stencil app
```
and run:
```bash
npm start
```
To build the app for production, run:
```bash
npm run build
```
To run the unit tests once, run:
```
npm test
```
To run the unit tests and watch for file changes during development, run:
```
npm run test.watch
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,65 @@
/* tslint:disable */
/**
* This is an autogenerated file created by the Stencil compiler.
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from '@stencil/core/internal';
import { MatchResults } from '@stencil/router';
export namespace Components {
interface AppHome {}
interface AppProfile {
match: MatchResults;
}
interface AppRoot {}
}
declare global {
interface HTMLAppHomeElement extends Components.AppHome, HTMLStencilElement {}
var HTMLAppHomeElement: {
prototype: HTMLAppHomeElement;
new (): HTMLAppHomeElement;
};
interface HTMLAppProfileElement
extends Components.AppProfile,
HTMLStencilElement {}
var HTMLAppProfileElement: {
prototype: HTMLAppProfileElement;
new (): HTMLAppProfileElement;
};
interface HTMLAppRootElement extends Components.AppRoot, HTMLStencilElement {}
var HTMLAppRootElement: {
prototype: HTMLAppRootElement;
new (): HTMLAppRootElement;
};
interface HTMLElementTagNameMap {
'app-home': HTMLAppHomeElement;
'app-profile': HTMLAppProfileElement;
'app-root': HTMLAppRootElement;
}
}
declare namespace LocalJSX {
interface AppHome extends JSXBase.HTMLAttributes<HTMLAppHomeElement> {}
interface AppProfile extends JSXBase.HTMLAttributes<HTMLAppProfileElement> {
match?: MatchResults;
}
interface AppRoot extends JSXBase.HTMLAttributes<HTMLAppRootElement> {}
interface IntrinsicElements {
'app-home': AppHome;
'app-profile': AppProfile;
'app-root': AppRoot;
}
}
export { LocalJSX as JSX };
declare module '@stencil/core' {
export namespace JSX {
interface IntrinsicElements extends LocalJSX.IntrinsicElements {}
}
}

View File

@@ -0,0 +1,26 @@
.app-home {
padding: 10px;
}
button {
background: #5851ff;
color: white;
margin: 8px;
border: none;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
padding: 16px 20px;
border-radius: 2px;
box-shadow: 0 8px 16px rgba(0,0,0,.1), 0 3px 6px rgba(0,0,0,.08);
outline: 0;
letter-spacing: .04em;
transition: all .15s ease;
cursor: pointer;
}
button:hover {
box-shadow: 0 3px 6px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.1);
transform: translateY(1px);
}

View File

@@ -0,0 +1,28 @@
import { Component, h } from '@stencil/core';
@Component({
tag: 'app-home',
styleUrl: 'app-home.css',
shadow: true
})
export class AppHome {
render() {
return (
<div class='app-home'>
<p>
Welcome to the Stencil App Starter.
You can use this starter to build entire apps all with
web components using Stencil!
Check out our docs on <a href='https://stenciljs.com'>stenciljs.com</a> to get started.
</p>
<stencil-route-link url='/profile/stencil'>
<button>
Profile page
</button>
</stencil-route-link>
</div>
);
}
}

View File

@@ -0,0 +1,3 @@
.app-profile {
padding: 10px;
}

View File

@@ -0,0 +1,31 @@
import { Component, Prop, h } from '@stencil/core';
import { MatchResults } from '@stencil/router';
@Component({
tag: 'app-profile',
styleUrl: 'app-profile.css',
shadow: true
})
export class AppProfile {
@Prop() match: MatchResults;
normalize(name: string): string {
if (name) {
return name.substr(0, 1).toUpperCase() + name.substr(1).toLowerCase();
}
return '';
}
render() {
if (this.match && this.match.params.name) {
return (
<div class="app-profile">
<p>
Hello! My name is {this.normalize(this.match.params.name)}. My name was passed in
through a route param!
</p>
</div>
);
}
}
}

View File

@@ -0,0 +1,15 @@
header {
background: #5851ff;
color: white;
height: 56px;
display: flex;
align-items: center;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
}
h1 {
font-size: 1.4rem;
font-weight: 500;
color: #fff;
padding: 0 12px;
}

View File

@@ -0,0 +1,29 @@
import { Component, h } from '@stencil/core';
@Component({
tag: 'app-root',
styleUrl: 'app-root.css',
shadow: true
})
export class AppRoot {
render() {
return (
<div>
<header>
<h1>Stencil App Starter</h1>
</header>
<main>
<stencil-router>
<stencil-route-switch scrollTopOffset={0}>
<stencil-route url='/' component='app-home' exact={true} />
<stencil-route url='/profile/:name' component='app-profile' />
</stencil-route-switch>
</stencil-router>
</main>
</div>
);
}
}

View File

@@ -0,0 +1,16 @@
/*
Global App CSS
----------------------
Use this file for styles that should be applied to all components.
For example, "font-family" within the "body" selector is a CSS property
most apps will want applied to all components.
Any global CSS variables should also be applied here.
*/
body {
margin: 0px;
padding: 0px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
}

View File

@@ -0,0 +1,7 @@
export default async () => {
/**
* The code to be executed should be placed within a default function that is
* exported by the global script. Ensure all of the code in the global script
* is wrapped in the function() that is exported.
*/
};

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8">
<title>Stencil Starter App</title>
<meta name="Description" content="Welcome to the Stencil App Starter. You can use this starter to build entire apps all with web components using Stencil!">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
<meta name="theme-color" content="#16161d">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="x-ua-compatible" content="IE=Edge">
<script type="module" src="/build/app.esm.js"></script>
<script nomodule src="/build/app.js"></script>
<link href="/build/app.css" rel="stylesheet">
<link rel="apple-touch-icon" href="/assets/icon/icon.png">
<link rel="icon" type="image/x-icon" href="/assets/icon/favicon.ico">
<link rel="manifest" href="/manifest.json">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,2 @@
export * from './components';
import '@stencil/router';

View File

@@ -0,0 +1,15 @@
{
"name": "Stencil Starter",
"short_name": "Stencil",
"start_url": "/",
"display": "standalone",
"icons": [
{
"src": "assets/icon/icon.png",
"sizes": "512x512",
"type": "image/png"
}
],
"background_color": "#16161d",
"theme_color": "#16161d"
}

View File

@@ -0,0 +1,16 @@
import { Config } from '@stencil/core';
// https://stenciljs.com/docs/config
export const config: Config = {
globalStyle: 'src/global/app.css',
globalScript: 'src/global/app.ts',
outputTargets: [
{
type: 'www',
// comment the following line to disable service workers in production
serviceWorker: null,
baseUrl: 'https://myapp.local/',
},
],
};

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"declaration": false,
"experimentalDecorators": true,
"lib": ["dom", "es2015"],
"moduleResolution": "node",
"module": "esnext",
"target": "es2017",
"noUnusedLocals": true,
"noUnusedParameters": true,
"jsx": "react",
"jsxFactory": "h"
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -3,7 +3,7 @@ const path = require('path');
const {
packAndDeploy,
testDeployment
testDeployment,
} = require('../../../test/lib/deployment/test-deployment.js');
jest.setTimeout(12 * 60 * 1000);
@@ -26,7 +26,7 @@ const testsThatFailToBuild = new Set([
'04-wrong-dist-dir',
'05-empty-dist-dir',
'06-missing-script',
'07-nonzero-sh'
'07-nonzero-sh',
]);
// eslint-disable-next-line no-restricted-syntax
@@ -45,8 +45,6 @@ for (const fixture of fs.readdirSync(fixturesPath)) {
});
continue; //eslint-disable-line
}
// eslint-disable-next-line no-loop-func
it(`should build ${fixture}`, async () => {
await expect(
testDeployment(

View File

@@ -1671,6 +1671,11 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.30.tgz#f6c38b7ecbbf698b0bbd138315a0f0f18954f85f"
integrity sha512-OftRLCgAzJP7vmKn9by/GVjnf4hloz/pXNOwPo0vKGAfXI7GqWXJi9N2kRar4cP5s1dGwuwcagWqO6iHBTq1Mg==
"@types/ms@0.7.31":
version "0.7.31"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/multistream@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@types/multistream/-/multistream-2.1.1.tgz#4badd2440ee3570594ea552420fe2e29ebe512bd"
@@ -7937,7 +7942,7 @@ normalize-url@^4.1.0:
integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ==
now-client@./packages/now-client:
version "5.1.1-canary.4"
version "5.1.1-canary.9"
dependencies:
"@zeit/fetch" "5.1.0"
async-retry "1.2.3"
@@ -8567,6 +8572,11 @@ 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@^1.0.0, path-to-regexp@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
@@ -8793,11 +8803,6 @@ promise-retry@^1.1.1:
err-code "^1.0.0"
retry "^0.10.0"
promise-timeout@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/promise-timeout/-/promise-timeout-1.3.0.tgz#d1c78dd50a607d5f0a5207410252a3a0914e1014"
integrity sha512-5yANTE0tmi5++POym6OgtFmwfDvOXABD9oj/jLQr5GPEyuNEb7jH4wbbANJceJid49jwhi1RddxnhnEAb/doqg==
promisepipe@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/promisepipe/-/promisepipe-3.0.0.tgz#c9b6e5aa861ef5fcce6134f6f75e14f8f30bd3b2"
@@ -9640,10 +9645,9 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
signal-exit@3.0.2, signal-exit@^3.0.0, signal-exit@^3.0.2:
signal-exit@3.0.2, signal-exit@TooTallNate/signal-exit#update/sighub-to-sigint-on-windows, signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
resolved "https://codeload.github.com/TooTallNate/signal-exit/tar.gz/58088fa7f715149f8411e089a4a6e3fe6ed265ec"
sinon@4.4.2:
version "4.4.2"
@@ -10511,6 +10515,11 @@ tr46@^1.0.1:
dependencies:
punycode "^2.1.0"
tree-kill@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a"
integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"