Compare commits

...

54 Commits

Author SHA1 Message Date
Sophearak Tha
372a674625 Publish
- @now/build-utils@0.11.1
 - now@16.4.1
 - now-client@5.2.2
 - @now/next@1.0.5
 - @now/python@0.3.3
2019-10-25 19:34:00 +07:00
Sophearak Tha
fafeadb7ba update now-cli readme (#3202) 2019-10-25 14:14:24 +02:00
Max
966b1e763f Add apiUrl debug log to now-client (#3201) 2019-10-25 14:14:19 +02:00
Luc
bb60e1a5fe Improve styling of 1.0 message (#3200) 2019-10-25 12:47:19 +02:00
Nathan Rajlich
cac9f807cc [now-cli] Gracefully exit when the cwd does not exist (#3194)
There have been Sentry errors where `process.cwd()` fails and throws an error. This patch handles that scenario gracefully by printing a more clear error message to the user and avoids sending a report to Sentry.

Fixes #3193.
2019-10-25 11:49:45 +02:00
Steven
a0b1254820 Fix circle ci xcode version (#3197) 2019-10-25 11:48:27 +02:00
Nathan Rajlich
0faff4132b [now-build-utils] Add more complete PackageJson typings (#3198)
* [now-build-utils] Add more complete `PackageJson` typings

Planning on using this in `now-cli`.

* Make "name" optional

* Use a namespace
2019-10-25 11:47:36 +02:00
Joe Haddad
1793a1287d [now-next] Cache All Next.js Directories (#3190)
Next.js now uses the `css` and `media` folders for its build-in CSS support. These files should be cached forever.
2019-10-25 11:45:37 +02:00
Max
5b572239c1 [now-client] Yield an error event if path provided is not absolute (#3173)
This adds an `error` event when provided path to `now-client` isn't absolute
2019-10-25 11:43:34 +02:00
Steven
f6a66d937e [now-client] Change npm publish to use files key (#3181)
- remove `.npmignore`
- use [`files`](https://docs.npmjs.com/files/package.json#files) key in `package.json`
- update metadata in `package.json`
- fix test harness to generate a token for each test deployment

This PR will prevent publishing [tests](https://cdn.jsdelivr.net/npm/now-client@5.2.0/tests/) to npm and any other unused files.
2019-10-25 11:43:29 +02:00
Sophearak Tha
2cf9a2f489 [now-client] [now-cli] Update version of deployment API (#3188)
* Update version of deployment API

* Add `alias-assigned` event and handling

* Replace v9 api with v10

* Don't return on immediate ready

* Handle alias-assigned for v1 deployments

* Improve event ordering

* Detect upload deployment readiness by `alias-assigned`

* rebuild

* Fix upload readiness event type

* Check for aliases before running status checks

* Improve event flow and wait for `ready` in v1 deployments

* Remove console.log
2019-10-25 11:42:18 +02:00
Nathan Rajlich
454f4dcc61 [now-cli] Don't throw if builder child process can not be killed (#3192)
The Sentry error reports that the process has already been killed, so no need to throw in this case.

Fixes #3191.
2019-10-25 11:42:09 +02:00
Yevhen Amelin
6e1065fde2 [now-python] Set PIP_USER environment variable (#3111)
Disables `--user` parameter of the `pip` utility, which is forcibly set under the hood on Debian systems and causes an error in the `pipInstall` function:

`distutils.errors.DistutilsOptionError: can't combine user with prefix, exec_prefix/home, or install_(plat)base`

Fixes #3089
2019-10-25 11:41:19 +02:00
Max
80ce06b20c [now-client] Add apiUrl support (#3168)
This PR adds support for `apiUrl` option to `now-client` as well as a test for it
2019-10-25 11:41:01 +02:00
Nathan Rajlich
99f3ab8b64 [now-cli] Loosen "engines" requirement to Node >= 8 (#3195)
Now that `now-client` does not use `fetch-h2`, any version of Node 8 or newer should work with `now-cli`.

Related to #2711.
2019-10-25 11:40:23 +02:00
Luc
ca4f6d2491 [now-cli] Disable --prod and --target for Now 1.0 deployments (#3189)
This PR disables Now 1.0 production deployments with the following error message:

> Option --prod is not supported for Now 1.0 deployments. To manually alias a deployment, use `now alias` instead.

It looks like this:
<img width="835" alt="Capture d’écran 2019-10-23 à 19 42 09" src="https://user-images.githubusercontent.com/6616955/67419574-3e125380-f5cd-11e9-81ff-63bde292539b.png">

Also disables `--target` for Now 1.0 deployments.
2019-10-25 11:39:53 +02:00
Steven
2ceb2a78aa Publish
- @now/routing-utils@1.3.1
2019-10-24 16:59:56 -04:00
Steven
d97da21afc [now-routing-utils] Change cleanUrls to redirect only (#3196)
When `cleanUrls` is true, the redirects will be applied to the routes however there are no longer any rewrites. Instead (through a different PR to fmeta-util) we will rename the file output to remove the `.html` extension.
2019-10-24 16:58:38 -04:00
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
112 changed files with 8558 additions and 1019 deletions

View File

@@ -115,15 +115,12 @@ jobs:
test-integration-macos-node-8:
macos:
xcode: '9.2.0'
xcode: '9.0.1'
working_directory: ~/repo
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Update Node.js
command: curl -sfLS install-node.now.sh/8.11 | sh -s -- --yes
- run:
name: Output version
command: node --version
@@ -208,15 +205,12 @@ jobs:
test-integration-macos-now-dev-node-8:
macos:
xcode: '9.2.0'
xcode: '9.0.1'
working_directory: ~/repo
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Update Node.js
command: curl -sfLS install-node.now.sh/8.11 | sh -s -- --yes
- run:
name: Output version
command: node --version
@@ -332,6 +326,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 +478,9 @@ workflows:
- test-integration-once:
requires:
- build
- test-unit:
requires:
- build
filters:
tags:
only: /.*/
@@ -484,6 +499,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.1",
"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
@@ -190,23 +191,110 @@ export interface ShouldServeOptions {
config: Config;
}
/**
* Credit to Iain Reid, MIT license.
* Source: https://gist.github.com/iainreid820/5c1cc527fe6b5b7dba41fec7fe54bf6e
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace PackageJson {
/**
* An author or contributor
*/
export interface Author {
name: string;
email?: string;
homepage?: string;
}
/**
* A map of exposed bin commands
*/
export interface BinMap {
[commandName: string]: string;
}
/**
* A bugs link
*/
export interface Bugs {
email: string;
url: string;
}
export interface Config {
name?: string;
config?: unknown;
}
/**
* A map of dependencies
*/
export interface DependencyMap {
[dependencyName: string]: string;
}
/**
* CommonJS package structure
*/
export interface Directories {
lib?: string;
bin?: string;
man?: string;
doc?: string;
example?: string;
}
export interface Engines {
node?: string;
npm?: string;
}
export interface PublishConfig {
registry?: string;
}
/**
* A project repository
*/
export interface Repository {
type: string;
url: string;
}
export interface ScriptsMap {
[scriptName: string]: string;
}
}
export interface PackageJson {
name?: string;
version?: string;
engines?: {
[key: string]: string;
node: string;
npm: string;
};
scripts?: {
[key: string]: string;
};
dependencies?: {
[key: string]: string;
};
devDependencies?: {
[key: string]: string;
};
readonly name?: string;
readonly version?: string;
readonly description?: string;
readonly keywords?: string[];
readonly homepage?: string;
readonly bugs?: string | PackageJson.Bugs;
readonly license?: string;
readonly author?: string | PackageJson.Author;
readonly contributors?: string[] | PackageJson.Author[];
readonly files?: string[];
readonly main?: string;
readonly bin?: string | PackageJson.BinMap;
readonly man?: string | string[];
readonly directories?: PackageJson.Directories;
readonly repository?: string | PackageJson.Repository;
readonly scripts?: PackageJson.ScriptsMap;
readonly config?: PackageJson.Config;
readonly dependencies?: PackageJson.DependencyMap;
readonly devDependencies?: PackageJson.DependencyMap;
readonly peerDependencies?: PackageJson.DependencyMap;
readonly optionalDependencies?: PackageJson.DependencyMap;
readonly bundledDependencies?: string[];
readonly engines?: PackageJson.Engines;
readonly os?: string[];
readonly cpu?: string[];
readonly preferGlobal?: boolean;
readonly private?: boolean;
readonly publishConfig?: PackageJson.PublishConfig;
}
export interface NodeVersion {

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,11 +2,11 @@
[![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:
```
```bash
npm i -g now
```

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "16.3.0",
"version": "16.4.1",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
@@ -58,7 +58,7 @@
]
},
"engines": {
"node": ">= 8.11"
"node": ">= 8"
},
"devDependencies": {
"@sentry/node": "5.5.0",
@@ -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

@@ -127,6 +127,7 @@ export const legacyArgsMri = {
'session-affinity',
'regions',
'dotenv',
'target',
],
boolean: [
'help',
@@ -144,6 +145,7 @@ export const legacyArgsMri = {
'no-scale',
'no-verify',
'dotenv',
'prod',
],
default: {
C: false,

View File

@@ -388,7 +388,7 @@ export default async function main(
const deploymentResponse = handleCertError(
output,
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v9')
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v10')
);
if (deploymentResponse === 1) {

View File

@@ -304,6 +304,18 @@ export default async function main(
`You are using an old version of the Now Platform. More: ${link(infoUrl)}`
);
if (argv.prod || argv.target) {
error(
`The option ${cmd(
argv.prod ? '--prod' : '--target'
)} is not supported for Now 1.0 deployments. To manually alias a deployment, use ${cmd(
'now alias'
)} instead.`
);
await exit(1);
return 1;
}
const {
authConfig: { token },
config,

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

@@ -4,11 +4,11 @@ import { Deployment } from '../../types';
import {
DeploymentNotFound,
DeploymentPermissionDenied,
InvalidDeploymentId
InvalidDeploymentId,
} from '../errors-ts';
import mapCertError from '../certs/map-cert-error';
type APIVersion = 'v5' | 'v9';
type APIVersion = 'v5' | 'v10';
export default async function getDeploymentByIdOrHost(
client: Client,

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,35 @@ 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,
apiUrl: now._apiUrl,
};
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 +61,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`
@@ -119,7 +144,7 @@ export default async function processDeployment({
}
// Handle ready event
if (event.type === 'ready') {
if (event.type === 'alias-assigned') {
if (deploySpinner) {
deploySpinner();
}
@@ -128,10 +153,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,35 @@ export async function getBuildMatches(
return matches;
}
export async function shutdownBuilder(
match: BuildMatch,
{ debug }: Output
): Promise<void> {
const ops: Promise<void>[] = [];
if (match.buildProcess) {
const { pid } = match.buildProcess;
debug(`Killing builder sub-process with PID ${pid}`);
const killPromise = treeKill(pid)
.then(() => {
debug(`Killed builder with PID ${pid}`);
})
.catch((err: Error) => {
debug(`Failed to kill builder with PID ${pid}: ${err}`);
});
ops.push(killPromise);
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

@@ -784,6 +784,19 @@ export class CantFindConfig extends NowError<
}
}
export class WorkingDirectoryDoesNotExist extends NowError<
'CWD_DOES_NOT_EXIST',
{}
> {
constructor() {
super({
code: 'CWD_DOES_NOT_EXIST',
meta: {},
message: 'The current working directory does not exist.',
});
}
}
export class FileNotFound extends NowError<'FILE_NOT_FOUND', { file: string }> {
constructor(file: string) {
super({

View File

@@ -1,5 +1,9 @@
import path from 'path';
import { CantParseJSONFile, CantFindConfig } from './errors-ts';
import {
CantParseJSONFile,
CantFindConfig,
WorkingDirectoryDoesNotExist,
} from './errors-ts';
import humanizePath from './humanize-path';
import readJSONFile from './read-json-file';
import readPackage from './read-package';
@@ -8,14 +12,25 @@ import { Output } from './output';
let config: Config;
export default async function getConfig(output: Output, configFile?: string) {
const localPath = process.cwd();
export default async function getConfig(
output: Output,
configFile?: string
): Promise<Config | Error> {
// If config was already read, just return it
if (config) {
return config;
}
let localPath: string;
try {
localPath = process.cwd();
} catch (err) {
if (err.code === 'ENOENT') {
return new WorkingDirectoryDoesNotExist();
}
throw err;
}
// First try with the config supplied by the user via --local-config
if (configFile) {
const localFilePath = path.resolve(localPath, configFile);
@@ -27,8 +42,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 }) => {
@@ -477,7 +479,7 @@ export default class Now extends EventEmitter {
}
const url = `/${
isBuilds ? 'v9' : 'v5'
isBuilds ? 'v10' : 'v5'
}/now/deployments/${encodeURIComponent(id)}`;
return this.retry(

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

@@ -263,18 +263,25 @@ if (satisfies(process.version, '10.x')) {
console.log('Skipping `02-angular-node` test since it requires Node >= 10.9');
}
test(
'[now dev] 03-aurelia',
testFixtureStdio('03-aurelia', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
test(
'[now dev] 03-aurelia',
testFixtureStdio('03-aurelia', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Aurelia Navigation Skeleton/gm);
})
);
const body = await response.text();
t.regex(body, /Aurelia Navigation Skeleton/gm);
})
);
} else {
console.log(
'Skipping `03-aurelia` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
);
}
// test(
// '[now dev] 04-create-react-app-node',
@@ -289,31 +296,45 @@ test(
// })
// );
test(
'[now dev] 05-gatsby',
testFixtureStdio('05-gatsby', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
test(
'[now dev] 05-gatsby',
testFixtureStdio('05-gatsby', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Gatsby Default Starter/gm);
})
);
const body = await response.text();
t.regex(body, /Gatsby Default Starter/gm);
})
);
} else {
console.log(
'Skipping `05-gatsby` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
);
}
test(
'[now dev] 06-gridsome',
testFixtureStdio('06-gridsome', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
// mini-css-extract-plugin has `engines: { node: ">= 6.9.0 <7.0.0 || >= 8.9.0" }` in its `package.json`
if (satisfies(process.version, '>= 6.9.0 <7.0.0 || >= 8.9.0')) {
test(
'[now dev] 06-gridsome',
testFixtureStdio('06-gridsome', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Hello, world!/gm);
})
);
const body = await response.text();
t.regex(body, /Hello, world!/gm);
})
);
} else {
console.log(
'Skipping `06-gridsome` test since it requires Node >= 6.9.0 <7.0.0 || >= 8.9.0'
);
}
test(
'[now dev] 07-hexo-node',
@@ -562,18 +583,25 @@ test('[now dev] double slashes redirect', async t => {
}
});
test(
'[now dev] 18-marko',
testFixtureStdio('18-marko', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
test(
'[now dev] 18-marko',
testFixtureStdio('18-marko', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Marko Starter/gm);
})
);
const body = await response.text();
t.regex(body, /Marko Starter/gm);
})
);
} else {
console.log(
'Skipping `18-marko` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
);
}
test(
'[now dev] 19-mithril',
@@ -601,18 +629,23 @@ test(
})
);
test(
'[now dev] 21-charge',
testFixtureStdio('21-charge', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
// @static/charge has `engines: { node: ">= 8.10.0" }` in its `package.json`
if (satisfies(process.version, '>= 8.10.0')) {
test(
'[now dev] 21-charge',
testFixtureStdio('21-charge', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Welcome to my new Charge site/gm);
})
);
const body = await response.text();
t.regex(body, /Welcome to my new Charge site/gm);
})
);
} else {
console.log('Skipping `21-charge` test since it requires Node >= 8.10.0');
}
test(
'[now dev] 22-brunch',
@@ -627,31 +660,43 @@ test(
})
);
test(
'[now dev] 23-docusaurus',
testFixtureStdio('23-docusaurus', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
// react-dev-utils has `engines: { node: ">= 8.10" }` in its `package.json`
if (satisfies(process.version, '>= 8.10')) {
test(
'[now dev] 23-docusaurus',
testFixtureStdio('23-docusaurus', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Test Site · A website for testing/gm);
})
);
const body = await response.text();
t.regex(body, /Test Site · A website for testing/gm);
})
);
} else {
console.log('Skipping `23-docusaurus` test since it requires Node >= 8.10');
}
test(
'[now dev] 24-ember',
testFixtureStdio('24-ember', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
test(
'[now dev] 24-ember',
testFixtureStdio('24-ember', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /HelloWorld/gm);
})
);
const body = await response.text();
t.regex(body, /HelloWorld/gm);
})
);
} else {
console.log(
'Skipping `24-ember` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
);
}
test('[now dev] temporary directory listing', async t => {
const directory = fixture('temporary-directory-listing');
@@ -869,3 +914,29 @@ test('[now dev] do not rebuild for changes in the output directory', async t =>
dev.kill('SIGTERM');
}
});
if (satisfies(process.version, '>= 8.9.0')) {
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');
}
});
} else {
console.log(
'Skipping `25-nextjs-src-dir` test since it requires Node >= 8.9.0'
);
}

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],
@@ -1946,6 +2037,40 @@ test('try to deploy with non-existing team', async t => {
t.true(stderr.includes(goal));
});
testv1('try to deploy v1 deployment with --prod', async t => {
const target = fixture('node');
const goal = `is not supported for Now 1.0 deployments`;
const { stderr, stdout, code } = await execa(binaryPath, [target, '--prod'], {
reject: false,
});
console.log(stderr);
console.log(stdout);
console.log(code);
t.is(code, 1);
t.true(stderr.includes(goal));
});
testv1('try to deploy v1 deployment with --target production', async t => {
const target = fixture('node');
const goal = `is not supported for Now 1.0 deployments`;
const { stderr, stdout, code } = await execa(
binaryPath,
[target, '--target', 'production'],
{ reject: false }
);
console.log(stderr);
console.log(stdout);
console.log(code);
t.is(code, 1);
t.true(stderr.includes(goal));
});
const verifyExampleAngular = (cwd, dir) =>
fs.existsSync(path.join(cwd, dir, 'package.json')) &&
fs.existsSync(path.join(cwd, dir, 'tsconfig.json')) &&
@@ -2380,6 +2505,43 @@ test('now secret rm', async t => {
t.is(output.code, 0, formatOutput(output));
});
test('deploy with a custom API URL', async t => {
const directory = fixture('static-single-file');
const { stdout, stderr, code } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
'--api',
'https://zeit.co/api',
...defaultArgs,
],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(code);
// Ensure the exit code is right
t.is(code, 0);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
// Send a test request to the deployment
const response = await fetch(href);
const contentType = response.headers.get('content-type');
t.is(contentType, 'text/html; charset=utf-8');
});
test.after.always(async () => {
// Make sure the token gets revoked
await execa(binaryPath, ['logout', ...defaultArgs]);

View File

@@ -1,3 +0,0 @@
src
types
.git

View File

@@ -1,9 +1,18 @@
{
"name": "now-client",
"version": "5.1.4",
"version": "5.2.2",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"homepage": "https://zeit.co",
"license": "MIT",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/zeit/now.git",
"directory": "packages/now-client"
},
"scripts": {
"build": "tsc",
"prepare": "npm run build",

View File

@@ -1,12 +1,16 @@
import { readdir as readRootFolder, lstatSync } from 'fs-extra';
import readdir from 'recursive-readdir';
import { relative, join } from 'path';
import { relative, join, isAbsolute } 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...');
@@ -47,6 +53,22 @@ export default function buildCreateDeployment(
let rootFiles: string[];
if (Array.isArray(path)) {
for (const filePath of path) {
if (!isAbsolute(filePath)) {
throw new DeploymentError({
code: 'invalid_path',
message: `Provided path ${filePath} is not absolute`,
});
}
}
} else if (!isAbsolute(path)) {
throw new DeploymentError({
code: 'invalid_path',
message: `Provided path ${path} is not absolute`,
});
}
if (isDirectory && !Array.isArray(path)) {
debug(`Provided 'path' is a directory. Reading subpaths... `);
rootFiles = await readRootFolder(path);
@@ -60,7 +82,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 +92,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 +111,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 (
@@ -124,15 +187,21 @@ export default function buildCreateDeployment(
force,
defaultName,
debug: debug_,
apiUrl,
...metadata
} = options;
if (apiUrl) {
debug(`Using provided API URL: ${apiUrl}`);
}
debug(`Setting platform version to ${version}`);
metadata.version = version;
const deploymentOpts = {
debug: debug_,
totalFiles: files.size,
nowConfig,
token,
isDirectory,
path,
@@ -140,6 +209,7 @@ export default function buildCreateDeployment(
force,
defaultName,
metadata,
apiUrl,
};
debug(`Creating the deployment and starting upload...`);

View File

@@ -1,6 +1,5 @@
import { DeploymentFile } from './utils/hashes';
import {
parseNowJSON,
fetch,
API_DEPLOYMENTS,
prepareFiles,
@@ -10,6 +9,7 @@ import {
import checkDeploymentStatus from './deployment-status';
import { generateQueryString } from './utils/query-string';
import { Deployment, DeploymentOptions, NowJsonOptions } from './types';
import { isReady, isAliasAssigned } from './utils/ready-state';
export interface Options {
metadata: DeploymentOptions;
@@ -22,6 +22,8 @@ export interface Options {
defaultName?: string;
preflight?: boolean;
debug?: boolean;
nowConfig?: NowJsonOptions;
apiUrl?: string;
}
async function* createDeployment(
@@ -50,6 +52,7 @@ async function* createDeployment(
...metadata,
files: preparedFiles,
}),
apiUrl: options.apiUrl,
}
);
@@ -73,6 +76,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 +115,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) {
@@ -207,9 +194,12 @@ export default async function* deploy(
}
if (deployment) {
if (deployment.readyState === 'READY') {
debug('Deployment is READY. Not performing additional polling');
return yield { type: 'ready', payload: deployment };
if (isReady(deployment) && isAliasAssigned(deployment)) {
debug('Deployment state changed to READY 3');
yield { type: 'ready', payload: deployment };
debug('Deployment alias assigned');
return yield { type: 'alias-assigned', payload: deployment };
}
try {
@@ -219,7 +209,8 @@ export default async function* deploy(
options.token,
metadata.version,
options.teamId,
debug
debug,
options.apiUrl
)) {
yield event;
}

View File

@@ -1,7 +1,13 @@
import sleep from 'sleep-promise';
import ms from 'ms';
import { fetch, API_DEPLOYMENTS, API_DEPLOYMENTS_LEGACY } from './utils';
import { isDone, isReady, isFailed } from './utils/ready-state';
import {
isDone,
isReady,
isFailed,
isAliasAssigned,
isAliasError,
} from './utils/ready-state';
import { Deployment, DeploymentBuild } from './types';
interface DeploymentStatus {
@@ -15,7 +21,8 @@ export default async function* checkDeploymentStatus(
token: string,
version: number | undefined,
teamId: string | undefined,
debug: Function
debug: Function,
apiUrl?: string
): AsyncIterableIterator<DeploymentStatus> {
let deploymentState = deployment;
let allBuildsCompleted = false;
@@ -25,20 +32,24 @@ export default async function* checkDeploymentStatus(
debug(`Using ${version ? `${version}.0` : '2.0'} API for status checks`);
// If the deployment is ready, we don't want any of this to run
if (isDone(deploymentState)) {
debug(`Deployment is already READY. Not running status checks`);
if (isDone(deploymentState) && isAliasAssigned(deploymentState)) {
debug(
`Deployment is already READY and aliases are assigned. Not running status checks`
);
return;
}
// Build polling
debug('Waiting for builds and the deployment to complete...');
let readyEventFired = false;
while (true) {
if (!allBuildsCompleted) {
const buildsData = await fetch(
`${apiDeployments}/${deployment.id}/builds${
teamId ? `?teamId=${teamId}` : ''
}`,
token
token,
{ apiUrl }
);
const data = await buildsData.json();
@@ -84,16 +95,30 @@ export default async function* checkDeploymentStatus(
return yield { type: 'error', payload: deploymentUpdate.error };
}
if (isReady(deploymentUpdate)) {
debug('Deployment state changed to READY');
return yield { type: 'ready', payload: deploymentUpdate };
if (isReady(deploymentUpdate) && !readyEventFired) {
debug('Deployment state changed to READY 2');
readyEventFired = true;
yield { type: 'ready', payload: deploymentUpdate };
}
if (isFailed(deploymentUpdate)) {
debug('Deployment has failed');
if (isAliasAssigned(deploymentUpdate)) {
debug('Deployment alias assigned');
return yield { type: 'alias-assigned', payload: deploymentUpdate };
}
const aliasError = isAliasError(deploymentUpdate);
if (isFailed(deploymentUpdate) || aliasError) {
debug(
aliasError
? 'Alias assignment error has occurred'
: 'Deployment has failed'
);
return yield {
type: 'error',
payload: deploymentUpdate.error || deploymentUpdate,
payload: aliasError
? deploymentUpdate.aliasError
: deploymentUpdate.error || deploymentUpdate,
};
}
}

View File

@@ -54,6 +54,8 @@ export interface Deployment {
};
target: string;
alias: string[];
aliasAssigned: boolean;
aliasError: string | null;
}
export interface DeploymentBuild {
@@ -117,6 +119,7 @@ export interface DeploymentOptions {
sessionAffinity?: 'ip' | 'random';
config?: { [key: string]: any };
debug?: boolean;
apiUrl?: string;
}
export interface NowJsonOptions {
@@ -124,9 +127,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

@@ -28,7 +28,7 @@ export default async function* upload(
files: Map<string, DeploymentFile>,
options: Options
): AsyncIterableIterator<any> {
const { token, teamId, debug: isDebug } = options;
const { token, teamId, debug: isDebug, apiUrl } = options;
const debug = createDebug(isDebug);
if (!files && !token && !teamId) {
@@ -51,7 +51,7 @@ export default async function* upload(
}
} else {
// If the deployment has succeeded here, don't continue
if (event.type === 'ready') {
if (event.type === 'alias-assigned') {
debug('Deployment succeeded on file check');
return yield event;
@@ -103,6 +103,7 @@ export default async function* upload(
},
body: stream,
teamId,
apiUrl,
},
isDebug
);
@@ -184,7 +185,7 @@ export default async function* upload(
try {
debug('Starting deployment creation');
for await (const event of deploy(files, options)) {
if (event.type === 'ready') {
if (event.type === 'alias-assigned') {
debug('Deployment is ready');
return yield event;
}

View File

@@ -11,11 +11,10 @@ import { Sema } from 'async-sema';
import { readFile } from 'fs-extra';
const semaphore = new Sema(10);
export const API_FILES = 'https://api.zeit.co/v2/now/files';
export const API_DEPLOYMENTS = 'https://api.zeit.co/v9/now/deployments';
export const API_DEPLOYMENTS_LEGACY = 'https://api.zeit.co/v3/now/deployments';
export const API_DELETE_DEPLOYMENTS_LEGACY =
'https://api.zeit.co/v2/now/deployments';
export const API_FILES = '/v2/now/files';
export const API_DEPLOYMENTS = '/v10/now/deployments';
export const API_DEPLOYMENTS_LEGACY = '/v3/now/deployments';
export const API_DELETE_DEPLOYMENTS_LEGACY = '/v2/now/deployments';
export const EVENTS = new Set([
// File events
@@ -26,19 +25,20 @@ export const EVENTS = new Set([
// Deployment events
'created',
'ready',
'alias-assigned',
'warning',
'error',
// Build events
'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 (
@@ -109,6 +109,9 @@ export const fetch = async (
const debug = createDebug(debugEnabled);
let time: number;
url = `${opts.apiUrl || 'https://api.zeit.co'}${url}`;
delete opts.apiUrl;
if (opts.teamId) {
const parsedUrl = parseUrl(url, true);
const query = parsedUrl.query;

View File

@@ -14,3 +14,7 @@ export const isFailed = ({
export const isDone = (
buildOrDeployment: Deployment | DeploymentBuild
): boolean => isReady(buildOrDeployment) || isFailed(buildOrDeployment);
export const isAliasAssigned = (deployment: Deployment): boolean =>
Boolean(deployment.aliasAssigned);
export const isAliasError = (deployment: Deployment): boolean =>
Boolean(deployment.aliasError);

View File

@@ -0,0 +1,30 @@
import fetch from 'node-fetch';
const str = 'aHR0cHM6Ly9hcGktdG9rZW4tZmFjdG9yeS56ZWl0LnNo';
async function fetchTokenWithRetry(url: string, retries = 3): Promise<string> {
try {
const res = await fetch(url);
const data = await res.json();
return data.token;
} catch (error) {
console.log(`Failed to fetch token. Retries remaining: ${retries}`);
if (retries === 0) {
throw error;
}
await sleep(500);
return fetchTokenWithRetry(url, retries - 1);
}
}
export async function generateNewToken(): Promise<string> {
const token = await fetchTokenWithRetry(
Buffer.from(str, 'base64').toString()
);
return token;
}
export function sleep(ms: number) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}

View File

@@ -1,2 +0,0 @@
// zeit-support user
export const TOKEN = 'HRp5EAN0TZBnSUBIleD3ZrMW'

View File

@@ -1,19 +1,24 @@
import path from 'path';
import { TOKEN } from './constants';
import { generateNewToken } from './common';
import { fetch, API_DEPLOYMENTS } from '../src/utils';
import { Deployment } from './types';
import { createDeployment } from '../src/index';
describe('create v2 deployment', () => {
let deployment: Deployment;
let token = '';
beforeEach(async () => {
token = await generateNewToken();
});
afterEach(async () => {
if (deployment) {
const response = await fetch(
`${API_DEPLOYMENTS}/${deployment.id}`,
TOKEN,
token,
{
method: 'DELETE'
method: 'DELETE',
}
);
expect(response.status).toEqual(200);
@@ -24,8 +29,8 @@ describe('create v2 deployment', () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token: TOKEN,
name: 'now-client-tests-v2'
token,
name: 'now-client-tests-v2',
}
)) {
if (event.type === 'warning') {
@@ -43,8 +48,8 @@ describe('create v2 deployment', () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token: TOKEN,
name: 'now-client-tests-v2'
token,
name: 'now-client-tests-v2',
}
)) {
if (event.type === 'file_count') {
@@ -62,8 +67,8 @@ describe('create v2 deployment', () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token: TOKEN,
name: 'now-client-tests-v2'
token,
name: 'now-client-tests-v2',
}
)) {
if (event.type === 'ready') {

View File

@@ -1,77 +1,83 @@
import path from 'path'
import { TOKEN } from './constants'
import { fetch, API_DELETE_DEPLOYMENTS_LEGACY } from '../src/utils'
import { Deployment } from './types'
import { createLegacyDeployment } from '../src/index'
import path from 'path';
import { generateNewToken } from './common';
import { fetch, API_DELETE_DEPLOYMENTS_LEGACY } from '../src/utils';
import { Deployment } from './types';
import { createLegacyDeployment } from '../src/index';
describe('create v1 deployment', () => {
let deployment: Deployment | undefined
let deployment: Deployment | undefined;
let token = '';
beforeEach(async () => {
token = await generateNewToken();
});
afterEach(async () => {
if (deployment) {
const response = await fetch(
`${API_DELETE_DEPLOYMENTS_LEGACY}/${deployment.deploymentId || deployment.uid}`,
TOKEN,
`${API_DELETE_DEPLOYMENTS_LEGACY}/${deployment.deploymentId ||
deployment.uid}`,
token,
{
method: 'DELETE'
method: 'DELETE',
}
)
expect(response.status).toEqual(200)
deployment = undefined
);
expect(response.status).toEqual(200);
deployment = undefined;
}
})
});
it('will create a v1 static deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'static'),
{
token: TOKEN,
name: 'now-client-tests-v1-static'
token,
name: 'now-client-tests-v1-static',
}
)) {
if (event.type === 'ready') {
deployment = event.payload
deployment = event.payload;
if (deployment) {
expect(deployment.readyState || deployment.state).toEqual('READY')
break
expect(deployment.readyState || deployment.state).toEqual('READY');
break;
}
}
}
})
});
it('will create a v1 npm deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'npm'),
{
token: TOKEN,
name: 'now-client-tests-v1-npm'
token,
name: 'now-client-tests-v1-npm',
}
)) {
if (event.type === 'ready') {
deployment = event.payload
deployment = event.payload;
if (deployment) {
expect(deployment.readyState || deployment.state).toEqual('READY')
break
expect(deployment.readyState || deployment.state).toEqual('READY');
break;
}
}
}
})
});
it('will create a v1 Docker deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'docker'),
{
token: TOKEN,
name: 'now-client-tests-v1-docker'
token,
name: 'now-client-tests-v1-docker',
}
)) {
if (event.type === 'ready') {
deployment = event.payload
deployment = event.payload;
if (deployment) {
expect(deployment.readyState || deployment.state).toEqual('READY')
break
expect(deployment.readyState || deployment.state).toEqual('READY');
break;
}
}
}
})
})
});
});

View File

@@ -0,0 +1,32 @@
import { generateNewToken } from './common';
import { createDeployment } from '../src/index';
describe('path handling', () => {
let token = '';
beforeEach(async () => {
token = await generateNewToken();
});
it('will fali with a relative path', async () => {
try {
await createDeployment('./fixtures/v2/now.json', {
token,
name: 'now-client-tests-v2',
});
} catch (e) {
expect(e.code).toEqual('invalid_path');
}
});
it('will fali with an array of relative paths', async () => {
try {
await createDeployment(['./fixtures/v2/now.json'], {
token,
name: 'now-client-tests-v2',
});
} catch (e) {
expect(e.code).toEqual('invalid_path');
}
});
});

View File

@@ -1 +1 @@
jest.setTimeout(120000)
jest.setTimeout(120000);

View File

@@ -1,6 +1,6 @@
{
"name": "@now/next",
"version": "1.0.2",
"version": "1.0.5",
"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,
@@ -695,7 +715,7 @@ export const build = async ({
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
src: '/_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+',
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },

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.3",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/python-now-python",

View File

@@ -16,6 +16,14 @@ import {
async function pipInstall(pipPath: string, workDir: string, ...args: string[]) {
const target = '.';
// See: https://github.com/pypa/pip/issues/4222#issuecomment-417646535
//
// Disable installing to the Python user install directory, which is
// the default behavior on Debian systems and causes error:
//
// distutils.errors.DistutilsOptionError: can't combine user with
// prefix, exec_prefix/home, or install_(plat)base
process.env.PIP_USER = '0';
debug(
`Running "pip install --disable-pip-version-check --target ${target} --upgrade ${args.join(
' '

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.1",
"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,79 @@ 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 = [];
if (cleanUrls) {
const clean = convertCleanUrls(filePaths);
routes.push(...clean.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 rewrites !== 'undefined') {
routes.push({ handle: 'filesystem' });
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,96 @@ 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,
},
{ 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;
}

Some files were not shown because too many files have changed in this diff Show More