Compare commits

..

65 Commits

Author SHA1 Message Date
Steven
aee33f040d Publish Stable
- @now/build-utils@1.2.0
 - @now/cgi@1.0.1
 - now@16.7.0
 - @now/go@1.0.1
 - @now/next@2.2.0
 - @now/node@1.3.0
 - @now/python@1.0.1
 - @now/ruby@1.0.1
 - @now/static-build@0.14.0
2019-12-14 07:58:04 -05:00
Steven
b100677b3b Publish Canary
- @now/next@2.1.2-canary.3
2019-12-14 07:18:41 -05:00
Joe Haddad
9241b3ae2f [now-next] Compute Rewrites & Redirects Earlier (#3430)
This PR strictly moves code to make the diff for an upcoming PR cleaner.
2019-12-14 03:04:35 +00:00
Steven
1088da6871 Publish Canary
- @now/build-utils@1.1.2-canary.5
 - now@16.6.4-canary.6
 - @now/next@2.1.2-canary.2
 - @now/node@1.2.2-canary.2
 - @now/static-build@0.13.2-canary.3
2019-12-13 18:12:54 -05:00
Andy
f7b4dd4458 Revert "[now-static-build] Add support buildCommand, devCommand and outputDirectory (#3422)" (#3428)
This reverts commit 5a6d1a135f.
2019-12-13 18:12:16 -05:00
Steven
fb85b6b27a Publish Canary
- @now/build-utils@1.1.2-canary.4
 - now@16.6.4-canary.5
 - @now/next@2.1.2-canary.1
 - @now/node@1.2.2-canary.1
 - @now/static-build@0.13.2-canary.2
2019-12-13 17:24:55 -05:00
Steven
2e5e9b9a6f [now-build-utils] Fix error tput: No value for $TERM and no -T specified (#3425) 2019-12-13 17:23:41 -05:00
Steven
d3cc306e5b [now-build-utils] Remove unused execa (#3427)
~~Reverts the execa bump from #3422~~

Removes `execa` since it is no longer used.
2019-12-13 17:23:18 -05:00
Steven
d6c6a2a271 [docs] Fix broken link to runtimes (#3424) 2019-12-13 15:48:06 -05:00
Andy Bitz
6171a58ae3 Publish Canary
- @now/build-utils@1.1.2-canary.3
 - now@16.6.4-canary.4
 - @now/static-build@0.13.2-canary.1
2019-12-13 19:39:50 +01:00
Andy
5a6d1a135f [now-static-build] Add support buildCommand, devCommand and outputDirectory (#3422)
* [now-static-build] Handle `buildCommand`, `devCommand` and `outputDirectory`

* Adjust tests

* Swap order

* Add `node_modules/.bin` to PATH

* Remove @types/execa

* Append PATH only to spawn options

* Remove test check

* Only add when there is a command
2019-12-13 19:30:09 +01:00
Steven
68deab9007 [tests] Fix unit test coverage (#3420)
This PR reduces the time running Circle CI tests.

Since creating the monorepo in #2812, the coverage broke and then was fixed in #2876 with a workaround which would run unit tests twice.

More recently, we enabled Now CLI to always run tests in #3305 so that means coverage data is always generated.

This PR is a final proper fix so that unit tests run once which saves approximately 2 minutes per push (CI workflow).
2019-12-12 22:13:02 +00:00
Steven
d6114e2bef [deps] Fix yarn.lock signal-exit (#3419)
This patch was lost in a previous PR so I added it back
2019-12-12 20:16:21 +00:00
Steven
5fdc55f3fb [now-cli] Remove dead link to max lambda size (#3418)
We used to have a default `maxLambdaSize` and allow the user to increase to 50 MB.

However, this is no longer true. Today, the `maxLambdaSize` for every function is 50 MB and is not configurable, it's a hard limit.

This PR removes the dead link to avoid confusion like in Issue #3416.
2019-12-12 19:41:54 +00:00
Mark Glagola
751b166536 [now-cli] Add renewal price to now domains inspect (#3401)
Adds `Renewal Price` to `now domains inspect` command if the domain was bought with ZEIT.
2019-12-12 18:03:19 +00:00
Andy Bitz
6ffc8d97f4 Publish Canary
- @now/build-utils@1.1.2-canary.2
 - now@16.6.4-canary.3
2019-12-12 18:37:10 +01:00
Andy
67a80d6b83 [now-cli][now-build-utils] Update detectors (#3402)
* [now-build-utils] Consider `yarn build` and `npm run build` as `buildCommand`

* [@now/build-utils] Update new detectors

* Update unit tests

* [@now/build-utils] Update detect-builder and detect-routes

* Update tests

* Run prettier

* Add more tests

* [now-cli] Use default detectors

* Add now-dev test

* Add a generic node project fallback

* Fix build

* Use public as default

* Ensure generic node project is last

* Update tests

* Update tests again

* Update packages/now-build-utils/src/detectors/filesystem.ts

Co-Authored-By: Nathan Rajlich <n@n8.io>

* Remove parentheses

* Revert "Remove parentheses"

This reverts commit 03f9aba07b0a6d4088719ca9afd602ce8fb1e9c1.

* Use getDependencyVersion instead of hasDependency
2019-12-12 18:28:24 +01:00
Steven
934cf772bc Publish Canary
- @now/build-utils@1.1.2-canary.1
 - now@16.6.4-canary.2
2019-12-12 10:58:21 -05:00
Andy
b01a24afdb [now-build-utils][now-cli] Move builds schema and functions schema to build-utils (#3417)
* [@now/build-utils] Add functions schema

* [now-cli] Use functions schema from build-utils

* Move buildsSchema to build-utils

* Add retries to test

* Add await
2019-12-12 16:02:19 +01:00
hi_Haowen
0ad75b52bf [now dev] Fix validate functions config failed in now json (#3414)
Follow up to #3408 .

```
> Error! Checking for updates failed
> Now CLI 16.6.3 dev (beta) — https://zeit.co/feedback/dev
> Error! Invalid `functions` property: ['api/test.js'] should NOT have additional properties
```
2019-12-12 13:59:39 +00:00
Andy Bitz
050772e78a Publish Canary
- now@16.6.4-canary.1
2019-12-12 13:23:12 +01:00
Andy
7c05dc1420 [now-cli] Do not handle cert errors for deployments (#3409)
Domain related things for deployment will now happen async
2019-12-12 13:19:26 +01:00
Steven
bdd25ac727 Publish Canary
- @now/build-utils@1.1.2-canary.0
 - @now/cgi@1.0.1-canary.1
 - now@16.6.4-canary.0
 - @now/go@1.0.1-canary.1
 - @now/next@2.1.2-canary.0
 - @now/node@1.2.2-canary.0
 - @now/python@1.0.1-canary.1
 - @now/ruby@1.0.1-canary.1
 - @now/static-build@0.13.2-canary.0
2019-12-11 17:30:11 -05:00
Steven
3a27328828 [now-build-utils] Discontinue Node 8 (#3406)
This PR adds a `discontinueDate` to Node 8 and prints a warning if the current deployment is using it.

```
    ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
    │                                                                                              │
    │   WARNING                                                                                    │
    │                                                                                              │
    │   Node.js 8.10.x will be discontinued on 2020-01-06.                                         │
    │   Deployments created on or after 2020-01-06 will fail to build.                             │
    │   Please use one of the following supported `engines` in `package.json`: ["12.x","10.x"]     │
    │   This change is a result of a decision made by an upstream infrastructure provider (AWS).   │
    │   Read more: https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html        │
    │                                                                                              │
    └──────────────────────────────────────────────────────────────────────────────────────────────┘
```

Starting January 2020, Node 8 deployments will fail to build and print an error.

```
Found `engines` in `package.json` with an unsupported Node.js version range: 8.10.x
Please use one of the following supported ranges: ["12.x","10.x"]
```

[PRODUCT-796]

[PRODUCT-796]: https://zeit.atlassian.net/browse/PRODUCT-796
2019-12-11 22:27:16 +00:00
Nathan Rajlich
c076a5620f [all] Move "Downloading deployment source files" message to download() (#3413)
Before, the debug log message "Downloading user files..." was copy+pasted to all the builders.

This change centralizes that log message to be inside the `download()` function for consistency and DRY purposes.

Additionally, the wording has changed as per [INFRA-289], and a resulting timestamp message is also printed.

[INFRA-289]: https://zeit.atlassian.net/browse/INFRA-289
2019-12-11 20:59:14 +00:00
Nathan Rajlich
2bd8ef9eed [now-next] Do not pass runtime env vars via argv in now dev (#3340)
Before this change, in `@now/next` when running via `now dev`,
the runtime env vars get passed to the child `dev-server.js`
process via argv.

This is problematic because it causes the env vars to be visible in
the process listing, and also causes the command itself to be very
large.

In some cases, with a lot of env vars, we've seen the command be too
large and it will fail to spawn (https://serverfault.com/a/163390/294389).

This changes the start-up process such that the env vars are passed
in via Node.js `fork()` IPC, rather than via `argv`.
2019-12-11 19:18:08 +00:00
JJ Kasper
500014f2fc [now-next] Handle symlinks in createPseudoLayer (#3404)
This makes sure to handle symlinks created in mono repos while creating the zip for a lambda based page in Next.js

Fixes #3400
2019-12-11 15:43:14 +00:00
JJ Kasper
17687e9bcd Fix unused pre-commit package overriding husky (#3405)
Saw my `pre-commit` hook wasn't being triggered after running `yarn` and noticed it was due to an un-used `pre-commit` dependency in `now-cli` overriding the changes to my `.git/hooks/pre-commit` file done by `husky`

**Note**: you will probably need to run `yarn install --force` after this is applied before the correct `pre-commit` changes are applied by `husky`
2019-12-11 14:35:30 +00:00
Andy
90354e9fe7 [now-cgi][now-next][now-ruby] Unify logging about downloading user files (#3397)
* [now-cgi][now-next][now-ruby] Unify logging about downloading user files

* Update next tests

* Bump Next.js Version
2019-12-10 21:15:32 +01:00
Leo Lamprecht
6236631beb [now-dev] Improved feedback link (#3399)
We've shut down our Typeform for feedback, so we can simply the feedback link. The old one will continue working, but we should start using the new one.

Pending on https://github.com/zeit/front/pull/5874.

Fixes #3377
2019-12-10 18:18:10 +00:00
Steven
75aefdddd6 Publish Stable
- @now/build-utils@1.1.1
 - now@16.6.3
 - now-client@6.0.0
 - @now/next@2.1.1
 - @now/node@1.2.1
 - @now/routing-utils@1.4.0
 - @now/static-build@0.13.1
2019-12-09 08:03:47 -05:00
Steven
566d82e873 Publish Canary
- now@16.6.3-canary.0
 - @now/next@2.1.1-canary.1
 - @now/node@1.2.1-canary.0
 - @now/routing-utils@1.3.4-canary.6
 - @now/static-build@0.13.1-canary.1
2019-12-06 19:49:27 -05:00
Steven
44ae0b654e [now-routing-utils] Use 308 status code (#3392)
We decided that all of the new properties should default to 301 status code for any redirects.
2019-12-07 00:38:26 +00:00
Steven
d8cfaae596 [now-node][now-next][now-static-build] Remove lockfiles from cache (#3391)
The lock files should not be cached because the user may wish to make a new deployment without a `yarn.lock` or `package-lock.json`.

This recently started causing problems because of the order of downloading cache changed from before user files to after user files.

So we need to be extra careful to only cache outputs and not source files.
2019-12-06 23:35:17 +00:00
Steven
a40e0f21ee Publish Canary
- @now/build-utils@1.1.1-canary.2
2019-12-06 15:57:33 -05:00
Steven
ac1f506c98 [now-build-utils] Add --no-audit flag to npm install (#3390)
This PR will reduce deployment time when a `package-lock.json` file is found by avoiding the audit step which usually [sends audit reports](https://docs.npmjs.com/cli/audit#description) to the registry.

The [--no-audit](https://docs.npmjs.com/cli/install) flag was introduced in [npm@6](
https://medium.com/npm-inc/announcing-npm-6-5d0b1799a905) which shipped with Node 10. However, using the flag with npm@5 does not do anything which is great because npm@5 doesn't audit. So this PR is backwards compatible.

### Performance

I tried `npm install` and `npm install --no-audit` with a large project, [StateOfJS](a9fa6d47f9/homepages/stateofjs), which has 2206 packages (audited 21778 packages).

I made sure to `rm -rf node_modules` each time and ran both commands 5 times to make sure it was always faster with `--no-audit`.

- Before: 61 seconds
- After: 49 seconds
2019-12-06 20:48:30 +00:00
Steven
68d5bdcf3d [script] Fix stable publish script (#3389)
Since we switched to a single branch (instead of master/canary), lerna gets confused about which packages to publish because stable and canary releases are in the same branch.

This PR fixes the confusion by looking at the git history and using [--force-publish](https://github.com/lerna/lerna/tree/master/commands/version#--force-publish) on the changed packages.

In order to avoid confusion for the person publishing, I removed the `yarn publish-stable` script in favor of `yarn changelog` which will print the change log and emit a script that can be used to publish stable.

<details><summary>View Example Output</summary>
<p>

```
$ yarn changelog
yarn run v1.19.1
$ node changelog.js
Changes since the last Stable release (21fe0a2):

- [now-cli] Change `--debug` to avoid debugging builders (#3386) [Steven]
- [now-next] Update routes for new check: true behavior (#3383) [JJ Kasper]
- [now-build-utils] Update Detectors API (#3384) [Nathan Rajlich]
- [now-client] Bump version (#3385) [Andy]
- [now-client] (Major) Split `now-client` options (#3382) [Andy]
- [now-cli][now-client] Fix user agent (#3381) [Steven]
- [now-client] Fix `main` in package.json (#3344) [Max]
- [now-build-utils] Change `script` to `scripts` in error message (#3376) [Andy]
- [now-cli] Add support for `check: true` routes in `now dev` (#3364) [Steven]
- [now-cli] Fix preinstall script on windows when `LOCALAPPDATA` is missing (#3365) [Steven]
- [now dev] skip installing already installed versioned runtimes (#3354) [Tommaso De Rossi]
- [now-routing-utils] Update `path-to-regexp` to v6.1.0 (#3361) [Steven]
- [now-routing-utils] Add mergeRoutes function (#3358) [Steven]
- [docs] Remove deprecated LambdaRuntimes (#3346) [Steven]
- [now-routing-utils] Add support for `check: true` (#3343) [Steven]
- [now-static-build] Cache `.cache` folder for gatsby deployments (#3260) (#3342) [Luc]

To publish a stable release, execute the following:

git pull && lerna version --message 'Publish Stable' --exact --force-publish=@now/build-utils,now,now-client,@now/next,@now/routing-utils,@now/static-build
```

</p>
</details>
2019-12-06 19:31:38 +00:00
Andy Bitz
beb51f8c67 Publish Stable
- now@16.6.2
2019-12-06 13:48:44 +01:00
Andy
b881cb7111 [now-cli] Remove github property from payload before sending it (#3388)
* [now-cli] Remove `github` property from payload before sending it

* Add test and remove unused one

* Remove .only

* Remove unused fixture

* Use correct github properties
2019-12-06 13:47:16 +01:00
Andy Bitz
d83bc59257 Publish Stable
- now@16.6.1
2019-12-06 00:18:57 +01:00
Steven
5be9f297de [now-cli] Change --debug to avoid debugging builders (#3386)
* [now-cli] Change `--debug` to avoid debugging builders

* Fix tests

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

* Split options for now-client

* Fix query and teamId

* Adjust tests

* Fix linting

* Ignore scope

* Adjust now-client tests

* Adjust more tests

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

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

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

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

Follow up to #3315.

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

[PRODUCT-740]

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

The algorithm is as follows:

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

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

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

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

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

View File

@@ -340,6 +340,10 @@ jobs:
- run:
name: Running Unit Tests
command: yarn test-unit --clean false
- persist_to_workspace:
root: .
paths:
- packages/now-cli/.nyc_output
coverage:
docker:
@@ -349,12 +353,6 @@ jobs:
- checkout
- attach_workspace:
at: .
- run:
name: Compiling `now dev` HTML error templates
command: node packages/now-cli/scripts/compile-templates.js
- run:
name: Run unit tests
command: yarn workspace now run test-unit
- run:
name: Run coverage report
command: yarn workspace now run coverage

1
.gitignore vendored
View File

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

View File

@@ -6,7 +6,7 @@ A Runtime is an npm module that exposes a `build` function and optionally an `an
Official Runtimes are published to [npmjs.com](https://npmjs.com) as a package and referenced in the `use` property of the `now.json` configuration file.
However, the `use` property will work with any [npm install argument](https://docs.npmjs.com/cli/install) such as a git repo url which is useful for testing your Runtime.
See the [Runtimes Documentation](https://zeit.co/docs/v2/advanced/runtimes) to view example usage.
See the [Runtimes Documentation](https://zeit.co/docs/runtimes) to view example usage.
## Runtime Exports
@@ -285,14 +285,13 @@ This is an abstract enumeration type that is implemented by one of the following
- `nodejs12.x`
- `nodejs10.x`
- `nodejs8.10`
- `go1.x`
- `java-1.8.0-openjdk`
- `java11`
- `python3.8`
- `python3.6`
- `python2.7`
- `dotnetcore2.1`
- `dotnetcore2.0`
- `dotnetcore1.0`
- `ruby2.5`
- `provided`
## JavaScript API

View File

@@ -12,12 +12,31 @@ if (!commit) {
throw new Error('Unable to find last publish commit');
}
const log = execSync(`git log --pretty=format:"- %s [%an]" ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => !line.startsWith('- Publish Canary '))
.join('\n');
const log =
execSync(`git log --pretty=format:"- %s [%an]" ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => !line.startsWith('- Publish Canary '))
.join('\n') || 'NO CHANGES DETECTED';
console.log(`Changes since the last Stable release (${commit.slice(0, 7)}):`);
console.log(`\n${log}\n`);
const pkgs =
Array.from(
new Set(
execSync(`git diff --name-only ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => line.startsWith('packages/'))
.map(line => line.split('/')[1])
.map(pkgName => require(`./packages/${pkgName}/package.json`).name)
)
).join(',') || 'now';
console.log('To publish a stable release, execute the following:');
console.log(
`\ngit pull && lerna version --message 'Publish Stable' --exact --force-publish=${pkgs}\n`
);

View File

@@ -30,7 +30,7 @@
"scripts": {
"lerna": "lerna",
"bootstrap": "lerna bootstrap",
"publish-stable": "git pull && lerna version --message 'Publish Stable' --exact",
"publish-stable": "echo 'Run `yarn changelog` for instructions'",
"publish-canary": "git pull && lerna version prerelease --preid canary --message 'Publish Canary' --exact",
"publish-from-github": "./.circleci/publish.sh",
"changelog": "node changelog.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@now/build-utils",
"version": "1.1.0",
"version": "1.2.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -32,9 +32,9 @@
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",
"async-sema": "2.1.4",
"boxen": "4.2.0",
"cross-spawn": "6.0.5",
"end-of-stream": "1.4.1",
"execa": "^1.0.0",
"fs-extra": "7.0.0",
"glob": "7.1.3",
"into-stream": "5.0.0",

View File

@@ -1,6 +1,12 @@
import minimatch from 'minimatch';
import { valid as validSemver } from 'semver';
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
import {
Builder,
Config,
BuilderFunctions,
DetectorResult,
DetectorOutput,
} from './types';
interface ErrorResponse {
code: string;
@@ -12,26 +18,6 @@ interface Options {
functions?: BuilderFunctions;
}
const src = 'package.json';
const config: Config = { zeroConfig: true };
const MISSING_BUILD_SCRIPT_ERROR: ErrorResponse = {
code: 'missing_build_script',
message:
'Your `package.json` file is missing a `build` property inside the `script` property.' +
'\nMore details: https://zeit.co/docs/v2/platform/frequently-asked-questions#missing-build-script',
};
// Static builders are special cased in `@now/static-build`
function getBuilders({ tag }: Options = {}): Map<string, Builder> {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return new Map<string, Builder>([
['next', { src, use: `@now/next${withTag}`, config }],
]);
}
// Must be a function to ensure that the returned
// object won't be a reference
function getApiBuilders({ tag }: Pick<Options, 'tag'> = {}): Builder[] {
@@ -47,13 +33,8 @@ function getApiBuilders({ tag }: Pick<Options, 'tag'> = {}): Builder[] {
];
}
function hasPublicDirectory(files: string[]) {
return files.some(name => name.startsWith('public/'));
}
function hasBuildScript(pkg: PackageJson | undefined) {
const { scripts = {} } = pkg || {};
return Boolean(scripts && scripts['build']);
function hasDirectory(fileName: string, files: string[]) {
return files.some(name => name.startsWith(`${fileName}/`));
}
function getApiFunctionBuilder(
@@ -91,39 +72,49 @@ function getApiFunctionBuilder(
return use ? { use, src, config } : prevBuilder;
}
async function detectFrontBuilder(
pkg: PackageJson,
function detectFrontBuilder(
detectorResult: Partial<DetectorOutput>,
builders: Builder[],
options: Options
): Promise<Builder> {
): Builder {
const { tag } = options;
const withTag = tag ? `@${tag}` : '';
for (const [dependency, builder] of getBuilders(options)) {
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
// Return the builder when a dependency matches
if (deps[dependency]) {
if (options.functions) {
Object.entries(options.functions).forEach(([key, func]) => {
// When the builder is not used yet we'll use it for the frontend
if (
builders.every(
b => !(b.config && b.config.functions && b.config.functions[key])
)
) {
if (!builder.config) builder.config = {};
if (!builder.config.functions) builder.config.functions = {};
builder.config.functions[key] = { ...func };
}
});
}
const { framework, buildCommand, outputDirectory } = detectorResult;
return builder;
}
const frameworkSlug = framework ? framework.slug : null;
const config: Config = {
zeroConfig: true,
};
if (buildCommand) {
config.buildCommand = buildCommand;
}
// By default we'll choose the `static-build` builder
return { src, use: `@now/static-build${withTag}`, config };
if (outputDirectory) {
config.outputDirectory = outputDirectory;
}
// All unused functions will be used for the frontend
if (options.functions) {
Object.entries(options.functions).forEach(([key, func]) => {
if (
builders.every(
b => !(b.config && b.config.functions && b.config.functions[key])
)
) {
config.functions = config.functions || {};
config.functions[key] = { ...func };
}
});
}
if (frameworkSlug === 'next') {
return { src: 'package.json', use: `@now/next${withTag}`, config };
}
return { src: 'package.json', use: `@now/static-build${withTag}`, config };
}
// Files that match a specific pattern will get ignored
@@ -139,10 +130,6 @@ export function getIgnoreApiFilter(optionsOrBuilders: Options | Builder[]) {
}
return (file: string) => {
if (!file.startsWith('api/')) {
return false;
}
if (file.includes('/.')) {
return false;
}
@@ -169,10 +156,7 @@ export function sortFiles(fileA: string, fileB: string) {
return fileA.localeCompare(fileB);
}
async function detectApiBuilders(
files: string[],
options: Options
): Promise<Builder[]> {
function detectApiBuilders(files: string[], options: Options): Builder[] {
const builds = files
.sort(sortFiles)
.filter(getIgnoreApiFilter(options))
@@ -188,10 +172,10 @@ async function detectApiBuilders(
// When a package has files that conflict with `/api` routes
// e.g. Next.js pages/api we'll check it here and return an error.
async function checkConflictingFiles(
function checkConflictingFiles(
files: string[],
builders: Builder[]
): Promise<ErrorResponse | null> {
): ErrorResponse | null {
// For Next.js
if (builders.some(b => b.use.startsWith('@now/next'))) {
const hasApiPages = files.some(file => file.startsWith('pages/api/'));
@@ -211,10 +195,10 @@ async function checkConflictingFiles(
// When e.g. Next.js receives a `functions` property it has to make sure,
// that it can handle those files, otherwise there are unused functions.
async function checkUnusedFunctionsOnFrontendBuilder(
function checkUnusedFunctionsOnFrontendBuilder(
files: string[],
builder: Builder
): Promise<ErrorResponse | null> {
): ErrorResponse | null {
const { config: { functions = undefined } = {} } = builder;
if (!functions) return null;
@@ -231,7 +215,9 @@ async function checkUnusedFunctionsOnFrontendBuilder(
) {
return {
code: 'unused_function',
message: `The function for ${matchedFile} can't be handled by any builder`,
message:
`The function for "${matchedFile}" can't be handled by any runtime. ` +
`Please provide one with the "runtime" option.`,
};
}
}
@@ -241,8 +227,6 @@ async function checkUnusedFunctionsOnFrontendBuilder(
}
function validateFunctions(files: string[], { functions = {} }: Options) {
const apiBuilders = getApiBuilders();
for (const [path, func] of Object.entries(functions)) {
if (path.length > 256) {
return {
@@ -312,15 +296,6 @@ function validateFunctions(files: string[], { functions = {} }: Options) {
'Function Runtimes must have a valid version, for example `now-php@1.0.0`.',
};
}
if (
apiBuilders.some(b => func.runtime && func.runtime.startsWith(b.use))
) {
return {
code: 'invalid_function_runtime',
message: `The function Runtime ${func.runtime} is not a Community Runtime and must not be specified.`,
};
}
}
if (func.includeFiles !== undefined) {
@@ -349,7 +324,7 @@ function validateFunctions(files: string[], { functions = {} }: Options) {
// to determine what builders to use
export async function detectBuilders(
files: string[],
pkg?: PackageJson | undefined | null,
detectorResult: Partial<DetectorResult> | null = null,
options: Options = {}
): Promise<{
builders: Builder[] | null;
@@ -370,19 +345,23 @@ export async function detectBuilders(
}
// Detect all builders for the `api` directory before anything else
const builders = await detectApiBuilders(files, options);
const builders = detectApiBuilders(files, options);
if (pkg && hasBuildScript(pkg)) {
const frontendBuilder = await detectFrontBuilder(pkg, builders, options);
if (detectorResult && detectorResult.buildCommand) {
const frontendBuilder = detectFrontBuilder(
detectorResult,
builders,
options
);
builders.push(frontendBuilder);
const conflictError = await checkConflictingFiles(files, builders);
const conflictError = checkConflictingFiles(files, builders);
if (conflictError) {
warnings.push(conflictError);
}
const unusedFunctionError = await checkUnusedFunctionsOnFrontendBuilder(
const unusedFunctionError = checkUnusedFunctionsOnFrontendBuilder(
files,
frontendBuilder
);
@@ -394,34 +373,35 @@ export async function detectBuilders(
warnings,
};
}
} else {
if (pkg && builders.length === 0) {
// We only show this error when there are no api builders
// since the dependencies of the pkg could be used for those
errors.push(MISSING_BUILD_SCRIPT_ERROR);
return { errors, warnings, builders: null };
}
// We allow a `public` directory
// when there are no build steps
if (hasPublicDirectory(files)) {
builders.push({
use: '@now/static',
src: 'public/**/*',
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,
});
}
} else if (
detectorResult &&
detectorResult.outputDirectory &&
hasDirectory(detectorResult.outputDirectory, files)
) {
builders.push({
use: '@now/static',
src: [...detectorResult.outputDirectory.split('/'), '**', '*']
.filter(Boolean)
.join('/'),
config: { zeroConfig: true },
});
} else if (hasDirectory('public', files)) {
builders.push({
use: '@now/static',
src: 'public/**/*',
config: { zeroConfig: true },
});
} 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: { zeroConfig: true },
});
}
return {

View File

@@ -217,7 +217,10 @@ async function detectApiRoutes(
for (const file of sortedFiles) {
// We only consider every file in the api directory
// as we will strip extensions as well as resolving "[segments]"
if (!file.startsWith('api/')) {
if (
!file.startsWith('api/') &&
!builders.some(b => b.src === file && b.config!.functions)
) {
continue;
}
@@ -271,14 +274,16 @@ async function detectApiRoutes(
return { defaultRoutes, error: null };
}
function hasPublicBuilder(builders: Builder[]): boolean {
return builders.some(
function getPublicBuilder(builders: Builder[]): Builder | null {
const builder = builders.find(
builder =>
builder.use === '@now/static' &&
builder.src === 'public/**/*' &&
/^.*\/\*\*\/\*$/.test(builder.src) &&
builder.config &&
builder.config.zeroConfig === true
);
return builder || null;
}
export async function detectRoutes(
@@ -286,11 +291,14 @@ export async function detectRoutes(
builders: Builder[]
): Promise<RoutesResult> {
const routesResult = await detectApiRoutes(files, builders);
const publicBuilder = getPublicBuilder(builders);
if (routesResult.defaultRoutes && publicBuilder) {
const directory = publicBuilder.src.replace('/**/*', '');
if (routesResult.defaultRoutes && hasPublicBuilder(builders)) {
routesResult.defaultRoutes.push({
src: '/(.*)',
dest: '/public/$1',
dest: `/${directory}/$1`,
});
}

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectAngular({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasAngular = await hasDependency('@angular/cli');
if (!hasAngular) return false;
const version = await getDependencyVersion('@angular/cli');
if (!version) return false;
return {
buildCommand: ['ng', 'build'],
buildDirectory: 'dist',
devCommand: ['ng', 'serve', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'ng build',
outputDirectory: 'dist',
devCommand: 'ng serve --port $PORT',
framework: {
slug: '@angular/cli',
version,
},
minNodeRange: '10.x',
routes: [
{

View File

@@ -1,17 +1,22 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectBrunch({
fs: { hasDependency, exists },
fs: { exists, getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasBrunch = await hasDependency('brunch');
if (!hasBrunch) return false;
const version = await getDependencyVersion('brunch');
if (!version) return false;
const hasConfig = await exists('brunch-config.js');
if (!hasConfig) return false;
return {
buildCommand: ['brunch', 'build', '--production'],
buildDirectory: 'public',
devCommand: ['brunch', 'watch', '--server', '--port', '$PORT'],
buildCommand:
(await getPackageJsonBuildCommand()) || 'brunch build --production',
outputDirectory: 'public',
devCommand: 'brunch watch --server --port $PORT',
framework: {
slug: 'brunch',
version,
},
};
}

View File

@@ -1,17 +1,21 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectCreateReactAppEjected({
fs: { hasDependency },
fs: { getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasReactDevUtils = await hasDependency('react-dev-utils');
if (!hasReactDevUtils) {
const version = await getDependencyVersion('react-dev-utils');
if (!version) {
return false;
}
return {
buildCommand: ['node', 'scripts/build.js'],
buildDirectory: 'build',
devCommand: ['node', 'scripts/start.js'],
devEnv: { BROWSER: 'none' },
buildCommand: 'node scripts/build.js',
outputDirectory: 'build',
devCommand: 'node scripts/start.js',
framework: {
slug: 'react-dev-utils',
version,
},
devVariables: { BROWSER: 'none' },
routes: [
{
src: '/static/(.*)',

View File

@@ -1,17 +1,21 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectCreateReactApp({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasReactScripts = await hasDependency('react-scripts');
if (!hasReactScripts) {
const version = await getDependencyVersion('react-scripts');
if (!version) {
return false;
}
return {
buildCommand: ['react-scripts', 'build'],
buildDirectory: 'build',
devCommand: ['react-scripts', 'start'],
devEnv: { BROWSER: 'none' },
buildCommand: (await getPackageJsonBuildCommand()) || 'react-scripts build',
outputDirectory: 'build',
devCommand: 'react-scripts start',
devVariables: { BROWSER: 'none' },
framework: {
slug: 'react-scripts',
version,
},
routes: [
{
src: '/static/(.*)',

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectDocusaurus({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasDocusaurus = await hasDependency('docusaurus');
if (!hasDocusaurus) return false;
const version = await getDependencyVersion('docusaurus');
if (!version) return false;
return {
buildCommand: ['docusaurus-build'],
buildDirectory: 'build',
devCommand: ['docusaurus-start', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'docusaurus-build',
outputDirectory: 'build',
devCommand: 'docusaurus-start --port $PORT',
framework: {
slug: 'docusaurus',
version,
},
};
}

View File

@@ -1,20 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectEleventy({
fs: { hasDependency },
fs: { getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasEleventy = await hasDependency('@11ty/eleventy');
if (!hasEleventy) return false;
const version = await getDependencyVersion('@11ty/eleventy');
if (!version) return false;
return {
buildCommand: ['npx', '@11ty/eleventy'],
buildDirectory: '_site',
devCommand: [
'npx',
'@11ty/eleventy',
'--serve',
'--watch',
'--port',
'$PORT',
],
buildCommand: 'npx @11ty/eleventy',
outputDirectory: '_site',
devCommand: 'npx @11ty/eleventy --serve --watch --port $PORT',
framework: {
slug: '@11ty/eleventy',
version,
},
};
}

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectEmber({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasEmber = await hasDependency('ember-cli');
if (!hasEmber) return false;
const version = await getDependencyVersion('ember-cli');
if (!version) return false;
return {
buildCommand: ['ember', 'build'],
buildDirectory: 'dist',
devCommand: ['ember', 'serve', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'ember build',
outputDirectory: 'dist',
devCommand: 'ember serve --port $PORT',
framework: {
slug: 'ember-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -102,6 +102,31 @@ export default abstract class DetectorFilesystem {
const { dependencies = {}, devDependencies = {} } = pkg || {};
return name in dependencies || name in devDependencies;
};
public isNpm = async (): Promise<boolean> => {
return this.exists('package-lock.json');
};
public getPackageJsonCommand = async (
name: string
): Promise<string | null> => {
const pkg = await this.readPackageJson();
const { scripts = {} } = pkg || {};
return scripts[name] || null;
};
public getPackageJsonBuildCommand = async (): Promise<string | null> => {
const buildCommand = (await this.isNpm())
? 'npm run build'
: 'yarn run build';
return (await this.getPackageJsonCommand('build')) ? buildCommand : null;
};
public getDependencyVersion = async (name: string): Promise<string> => {
const pkg = await this.readPackageJson();
const { dependencies = {}, devDependencies = {} } = pkg || {};
return dependencies[name] || devDependencies[name];
};
}
async function nullEnoent<T>(p: Promise<T>): Promise<T | null> {

View File

@@ -1,16 +1,20 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectGatsby({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasGatsby = await hasDependency('gatsby');
if (!hasGatsby) {
const version = await getDependencyVersion('gatsby');
if (!version) {
return false;
}
return {
buildCommand: ['gatsby', 'build'],
buildDirectory: 'public',
devCommand: ['gatsby', 'develop', '-p', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'gatsby build',
outputDirectory: 'public',
devCommand: 'gatsby develop -p $PORT',
framework: {
slug: 'gatsby',
version,
},
cachePattern: '.cache/**',
};
}

View File

@@ -0,0 +1,21 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectGenericNodeProject({
fs: { isNpm, getPackageJsonCommand },
}: DetectorParameters): Promise<DetectorResult> {
const useNpm = await isNpm();
const devCommand = await getPackageJsonCommand('dev');
const buildCommand = await getPackageJsonCommand('build');
if (!buildCommand) {
return false;
}
return {
buildCommand: `${useNpm ? 'npm' : 'yarn'} run build`,
devCommand: `${useNpm ? 'npm' : 'yarn'} run ${
devCommand ? 'dev' : 'build'
}`,
outputDirectory: 'public',
};
}

View File

@@ -1,15 +1,19 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectGridsome({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasGridsome = await hasDependency('gridsome');
if (!hasGridsome) {
const version = await getDependencyVersion('gridsome');
if (!version) {
return false;
}
return {
buildCommand: ['gridsome', 'build'],
buildDirectory: 'dist',
devCommand: ['gridsome', 'develop', '-p', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'gridsome build',
outputDirectory: 'dist',
devCommand: 'gridsome develop -p $PORT',
framework: {
slug: 'gridsom',
version,
},
};
}

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectHexo({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasHexo = await hasDependency('hexo');
if (!hasHexo) return false;
const version = await getDependencyVersion('hexo');
if (!version) return false;
return {
buildCommand: ['hexo', 'generate'],
buildDirectory: 'public',
devCommand: ['hexo', 'server', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'hexo generate',
outputDirectory: 'public',
devCommand: 'hexo server --port $PORT',
framework: {
slug: 'hexo',
version,
},
};
}

View File

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

View File

@@ -9,6 +9,7 @@ import docusaurus from './docusaurus';
import eleventy from './eleventy';
import ember from './ember';
import gatsby from './gatsby';
import genericNodeProject from './generic-node-project';
import gridsome from './gridsome';
import hexo from './hexo';
import hugo from './hugo';
@@ -81,5 +82,8 @@ export async function detectDefaults(
d = params.detectors || detectors;
result = await firstTruthy(d.map(detector => detector(params)));
}
if (!result) {
result = await genericNodeProject(params);
}
return result;
}

View File

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

View File

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

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectNext({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasNext = await hasDependency('next');
if (!hasNext) return false;
const version = await getDependencyVersion('next');
if (!version) return false;
return {
buildCommand: ['next', 'build'],
buildDirectory: 'build',
devCommand: ['next', '-p', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'next build',
outputDirectory: '.next/static',
devCommand: 'next -p $PORT',
framework: {
slug: 'next',
version,
},
};
}

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectPolymer({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasPolymer = await hasDependency('polymer-cli');
if (!hasPolymer) return false;
const version = await getDependencyVersion('polymer-cli');
if (!version) return false;
return {
buildCommand: ['polymer', 'build'],
buildDirectory: 'build',
devCommand: ['polymer', 'serve', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'polymer build',
outputDirectory: 'build',
devCommand: 'polymer serve --port $PORT',
framework: {
slug: 'polymer-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectPreact({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasPreact = await hasDependency('preact-cli');
if (!hasPreact) return false;
const version = await getDependencyVersion('preact-cli');
if (!version) return false;
return {
buildCommand: ['preact', 'build'],
buildDirectory: 'build',
devCommand: ['preact', 'watch', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'preact build',
outputDirectory: 'build',
devCommand: 'preact watch --port $PORT',
framework: {
slug: 'preact-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,14 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectSaber({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasSaber = await hasDependency('saber');
if (!hasSaber) return false;
const version = await getDependencyVersion('saber');
if (!version) return false;
return {
buildCommand: ['saber', 'build'],
buildDirectory: 'public',
devCommand: ['saber', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'saber build',
outputDirectory: 'public',
devCommand: 'saber --port $PORT',
routes: [
{
src: '/_saber/.*',
@@ -23,5 +23,9 @@ export default async function detectSaber({
dest: '404.html',
},
],
framework: {
slug: 'saber',
version,
},
};
}

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectSapper({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasSapper = await hasDependency('sapper');
if (!hasSapper) return false;
const version = await getDependencyVersion('sapper');
if (!version) return false;
return {
buildCommand: ['sapper', 'export'],
buildDirectory: '__sapper__/export',
devCommand: ['sapper', 'dev', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'sapper export',
outputDirectory: '__sapper__/export',
devCommand: 'sapper dev --port $PORT',
framework: {
slug: 'sapper',
version,
},
};
}

View File

@@ -1,22 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectStencil({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasStencil = await hasDependency('@stencil/core');
if (!hasStencil) return false;
const version = await getDependencyVersion('@stencil/core');
if (!version) return false;
return {
buildCommand: ['stencil', 'build'],
buildDirectory: 'www',
devCommand: [
'stencil',
'build',
'--dev',
'--watch',
'--serve',
'--port',
'$PORT',
],
buildCommand: (await getPackageJsonBuildCommand()) || 'stencil build',
outputDirectory: 'www',
devCommand: 'stencil build --dev --watch --serve --port $PORT',
framework: {
slug: '@stencil/core',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectSvelte({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasSvelte = await hasDependency('sirv-cli');
if (!hasSvelte) return false;
const version = await getDependencyVersion('sirv-cli');
if (!version) return false;
return {
buildCommand: ['rollup', '-c'],
buildDirectory: 'public',
devCommand: ['sirv', 'public', '--single', '--dev', ' --port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'rollup -c',
outputDirectory: 'public',
devCommand: 'sirv public --single --dev --port $PORT',
framework: {
slug: 'sirv-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectUmiJS({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasUmi = await hasDependency('umi');
if (!hasUmi) return false;
const version = await getDependencyVersion('umi');
if (!version) return false;
return {
buildCommand: ['umi', 'build'],
buildDirectory: 'dist',
devCommand: ['umi', 'dev', '--port', '$PORT'],
buildCommand: (await getPackageJsonBuildCommand()) || 'umi build',
outputDirectory: 'dist',
devCommand: 'umi dev --port $PORT',
framework: {
slug: 'umi',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,19 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectVue({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasVue = await hasDependency('@vue/cli-service');
if (!hasVue) return false;
const version = await getDependencyVersion('@vue/cli-service');
if (!version) return false;
return {
buildCommand: ['vue-cli-service', 'build'],
buildDirectory: 'dist',
devCommand: ['vue-cli-service', 'serve', '--port', '$PORT'],
buildCommand:
(await getPackageJsonBuildCommand()) || 'vue-cli-service build',
outputDirectory: 'dist',
devCommand: 'vue-cli-service serve --port $PORT',
framework: {
slug: '@vue/cli-service',
version,
},
routes: [
{
src: '^/[^/]*\\.(js|txt|ico|json)',

View File

@@ -1,4 +1,5 @@
import path from 'path';
import debug from '../debug';
import FileFsRef from '../file-fs-ref';
import { File, Files, Meta } from '../types';
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
@@ -39,8 +40,12 @@ export default async function download(
basePath: string,
meta?: Meta
): Promise<DownloadedFiles> {
const { isDev = false, skipDownload = false, filesChanged = null, filesRemoved = null } =
meta || {};
const {
isDev = false,
skipDownload = false,
filesChanged = null,
filesRemoved = null,
} = meta || {};
if (isDev || skipDownload) {
// In `now dev`, the `download()` function is a no-op because
@@ -48,11 +53,14 @@ export default async function download(
// source files are already available.
return files as DownloadedFiles;
}
debug('Downloading deployment source files...');
const start = Date.now();
const files2: DownloadedFiles = {};
const filenames = Object.keys(files);
await Promise.all(
Object.keys(files).map(async name => {
filenames.map(async name => {
// If the file does not exist anymore, remove it.
if (Array.isArray(filesRemoved) && filesRemoved.includes(name)) {
await removeFile(basePath, name);
@@ -71,5 +79,8 @@ export default async function download(
})
);
const duration = Date.now() - start;
debug(`Downloaded ${filenames.length} source files: ${duration}ms`);
return files2;
}

View File

@@ -1,13 +1,21 @@
import { intersects } from 'semver';
import boxen from 'boxen';
import { NodeVersion } from '../types';
import debug from '../debug';
const supportedOptions: NodeVersion[] = [
const allOptions: NodeVersion[] = [
{ major: 12, range: '12.x', runtime: 'nodejs12.x' },
{ major: 10, range: '10.x', runtime: 'nodejs10.x' },
{ major: 8, range: '8.10.x', runtime: 'nodejs8.10' },
{
major: 8,
range: '8.10.x',
runtime: 'nodejs8.10',
discontinueDate: new Date('2020-01-06'),
},
];
const supportedOptions = allOptions.filter(o => !isDiscontinued(o));
// This version should match Fargate's default in the PATH
// Today that is Node 8
export const defaultSelection = supportedOptions.find(
@@ -28,13 +36,14 @@ export async function getSupportedNodeVersion(
);
}
} else {
const found = supportedOptions.some(o => {
const found = allOptions.some(o => {
// the array is already in order so return the first
// match which will be the newest version of node
selection = o;
return intersects(o.range, engineRange);
});
if (found) {
const discontinued = isDiscontinued(selection);
if (found && !discontinued) {
if (!silent) {
debug(
'Found `engines` in `package.json`, selecting range: ' +
@@ -42,15 +51,50 @@ export async function getSupportedNodeVersion(
);
}
} else {
if (!silent) {
throw new Error(
'Found `engines` in `package.json` with an unsupported node range: ' +
engineRange +
'\nPlease use one of the following supported ranges: ' +
JSON.stringify(supportedOptions.map(o => o.range))
);
}
throw new Error(
'Found `engines` in `package.json` with an unsupported Node.js version range: ' +
engineRange +
'\nPlease use one of the following supported ranges: ' +
JSON.stringify(supportedOptions.map(o => o.range)) +
(discontinued
? '\nThis change is the result of a decision made by an upstream infrastructure provider (AWS).' +
'\nRead more: https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html'
: '')
);
}
}
const { range, discontinueDate } = selection;
if (discontinueDate && !isDiscontinued(selection)) {
const d = discontinueDate.toISOString().split('T')[0];
const validRanges = supportedOptions
.filter(o => !o.discontinueDate)
.map(o => o.range);
const prevTerm = process.env.TERM;
if (!prevTerm) {
// workaround for https://github.com/sindresorhus/term-size/issues/13
process.env.TERM = 'xterm';
}
console.warn(
boxen(
'NOTICE' +
'\n' +
`\nNode.js version ${range} has reached end-of-life.` +
`\nAs a result, deployments created on or after ${d} will fail to build.` +
'\nPlease use one of the following supported `engines` in `package.json`: ' +
JSON.stringify(validRanges) +
'\nThis change is the result of a decision made by an upstream infrastructure provider (AWS).' +
'\nRead more: https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html',
{ padding: 1 }
)
);
process.env.TERM = prevTerm;
}
return selection;
}
function isDiscontinued({ discontinueDate }: NodeVersion): boolean {
const today = new Date();
return discontinueDate !== undefined && discontinueDate <= today;
}

View File

@@ -155,7 +155,7 @@ export async function runNpmInstall(
commandArgs = args.filter(a => a !== '--prefer-offline');
await spawnAsync(
'npm',
commandArgs.concat(['install', '--unsafe-perm']),
commandArgs.concat(['install', '--no-audit', '--unsafe-perm']),
opts
);
} else {

View File

@@ -3,7 +3,7 @@ import FileFsRef from './file-fs-ref';
import FileRef from './file-ref';
import { Lambda, createLambda, getLambdaOptionsFromFunction } from './lambda';
import { Prerender } from './prerender';
import download, { DownloadedFiles } from './fs/download';
import download, { DownloadedFiles, isSymbolicLink } from './fs/download';
import getWriteableDirectory from './fs/get-writable-directory';
import glob from './fs/glob';
import rename from './fs/rename';
@@ -52,8 +52,10 @@ export {
detectBuilders,
detectRoutes,
debug,
isSymbolicLink,
getLambdaOptionsFromFunction,
};
export { detectDefaults } from './detectors';
export * from './schemas';
export * from './types';

View File

@@ -0,0 +1,61 @@
export const functionsSchema = {
type: 'object',
minProperties: 1,
maxProperties: 50,
additionalProperties: false,
patternProperties: {
'^.{1,256}$': {
type: 'object',
additionalProperties: false,
properties: {
runtime: {
type: 'string',
maxLength: 256,
},
memory: {
// Number between 128 and 3008 in steps of 64
enum: Object.keys(Array.from({ length: 50 }))
.slice(2, 48)
.map(x => Number(x) * 64),
},
maxDuration: {
type: 'number',
minimum: 1,
maximum: 900,
},
includeFiles: {
type: 'string',
maxLength: 256,
},
excludeFiles: {
type: 'string',
maxLength: 256,
},
},
},
},
};
export const buildsSchema = {
type: 'array',
minItems: 0,
maxItems: 128,
items: {
type: 'object',
additionalProperties: false,
required: ['use'],
properties: {
src: {
type: 'string',
minLength: 1,
maxLength: 4096,
},
use: {
type: 'string',
minLength: 3,
maxLength: 256,
},
config: { type: 'object' },
},
},
};

View File

@@ -304,6 +304,7 @@ export interface NodeVersion {
major: number;
range: string;
runtime: string;
discontinueDate?: Date;
}
export interface Builder {
@@ -352,11 +353,11 @@ export interface DetectorParameters {
}
export interface DetectorOutput {
buildCommand: string[];
buildDirectory: string;
buildEnv?: Env;
devCommand?: string[];
devEnv?: Env;
buildCommand: string;
outputDirectory: string;
buildVariables?: Env;
devCommand?: string;
devVariables?: Env;
minNodeRange?: string;
cachePattern?: string;
routes?: Route[];
@@ -365,6 +366,10 @@ export interface DetectorOutput {
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
framework?: {
slug: string;
version: string;
};
}
export type DetectorResult = DetectorOutput | false;

View File

@@ -4,13 +4,34 @@ const {
packAndDeploy,
testDeployment,
} = require('../../../test/lib/deployment/test-deployment');
const { glob, detectBuilders, detectRoutes } = require('../');
const {
glob,
detectBuilders,
detectRoutes,
DetectorFilesystem,
detectDefaults,
} = require('../');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
class LocalFilesystem extends DetectorFilesystem {
constructor(dir) {
super();
this.dir = dir;
}
_exists(name) {
return fs.pathExists(path.join(this.dir, name));
}
_readFile(name) {
return fs.readFile(path.join(this.dir, name));
}
}
beforeAll(async () => {
const buildUtilsPath = path.resolve(__dirname, '..');
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
@@ -67,7 +88,9 @@ for (const builder of buildersToTestWith) {
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 detectorResult = await detectDefaults({
fs: new LocalFilesystem(fixture),
});
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
@@ -110,7 +133,7 @@ it('Test `detectBuilders` and `detectRoutes`', async () => {
},
];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detectorResult);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
@@ -128,7 +151,9 @@ it('Test `detectBuilders` and `detectRoutes`', async () => {
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 detectorResult = await detectDefaults({
fs: new LocalFilesystem(fixture),
});
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
@@ -192,7 +217,7 @@ it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
},
];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detectorResult);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };

View File

@@ -67,8 +67,8 @@ test('detectDefaults() - angular', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'dist');
assert.deepEqual(result.buildCommand, ['ng', 'build']);
assert.equal(result.outputDirectory, 'dist');
assert.deepEqual(result.buildCommand, 'yarn run build');
});
test('detectDefaults() - brunch', async () => {
@@ -76,8 +76,8 @@ test('detectDefaults() - brunch', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.deepEqual(result.buildCommand, ['brunch', 'build', '--production']);
assert.equal(result.outputDirectory, 'public');
assert.deepEqual(result.buildCommand, 'yarn run build');
});
test('detectDefaults() - hugo', async () => {
@@ -85,8 +85,8 @@ test('detectDefaults() - hugo', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.deepEqual(result.buildCommand, ['hugo']);
assert.equal(result.outputDirectory, 'public');
assert.deepEqual(result.buildCommand, 'hugo');
});
test('detectDefaults() - jekyll', async () => {
@@ -94,8 +94,8 @@ test('detectDefaults() - jekyll', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, '_site');
assert.deepEqual(result.buildCommand, ['jekyll', 'build']);
assert.equal(result.outputDirectory, '_site');
assert.deepEqual(result.buildCommand, 'jekyll build');
});
test('detectDefaults() - middleman', async () => {
@@ -103,11 +103,6 @@ test('detectDefaults() - middleman', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'build');
assert.deepEqual(result.buildCommand, [
'bundle',
'exec',
'middleman',
'build',
]);
assert.equal(result.outputDirectory, 'build');
assert.deepEqual(result.buildCommand, 'bundle exec middleman build');
});

View File

@@ -1,9 +1,14 @@
const path = require('path');
const fs = require('fs-extra');
const execa = require('execa');
const assert = require('assert');
const { createZip } = require('../dist/lambda');
const { glob, download, detectBuilders, detectRoutes } = require('../');
const {
glob,
download,
detectBuilders,
detectRoutes,
spawnAsync,
} = require('../');
const {
getSupportedNodeVersion,
defaultSelection,
@@ -39,7 +44,7 @@ it('should create zip files with symlinks properly', async () => {
await fs.mkdirp(outDir);
await fs.writeFile(outFile, await createZip(files));
await execa('unzip', [outFile], { cwd: outDir });
await spawnAsync('unzip', [outFile], { cwd: outDir });
const [linkStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
@@ -110,42 +115,34 @@ it('should support require by path for legacy builders', () => {
});
describe('Test `detectBuilders`', () => {
it('package.json + no build', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
it('package.json + no build command', async () => {
const detected = { framework: { slug: 'next', version: '9.0.0' } };
const files = ['package.json', 'pages/index.js', 'public/index.html'];
const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null);
expect(errors.length).toBe(1);
const { builders } = await detectBuilders(files, detected);
expect(builders.length).toBe(1);
expect(builders[0].src).toBe('public/**/*');
expect(builders[0].use).toBe('@now/static');
});
it('package.json + no build + next', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
it('package.json + build command + next', async () => {
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'pages/index.js'];
const { builders, errors } = await detectBuilders(files, pkg);
const { builders, errors } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/next');
expect(errors).toBe(null);
});
it('package.json + no build + next', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
};
const files = ['package.json', 'pages/index.js'];
const { builders, errors } = await detectBuilders(files, pkg);
expect(builders[0].use).toBe('@now/next');
expect(errors).toBe(null);
});
it('package.json + no build', async () => {
const pkg = {};
it('no detectors + no build command', async () => {
const files = ['package.json'];
const { builders, errors } = await detectBuilders(files, pkg);
const { builders, errors } = await detectBuilders(files, {});
expect(builders).toBe(null);
expect(errors.length).toBe(1);
expect(errors).toBe(null);
});
it('static file', async () => {
@@ -155,7 +152,7 @@ describe('Test `detectBuilders`', () => {
expect(errors).toBe(null);
});
it('no package.json + public', async () => {
it('no package.json + public + api', async () => {
const files = ['api/users.js', 'public/index.html'];
const { builders, errors } = await detectBuilders(files);
expect(builders[1].use).toBe('@now/static');
@@ -173,7 +170,7 @@ describe('Test `detectBuilders`', () => {
expect(errors).toBe(null);
});
it('package.json + no build + root + api', async () => {
it('no package.json + no build command + root + api', async () => {
const files = ['index.html', 'api/[endpoint].js', 'static/image.png'];
const { builders, errors } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node');
@@ -198,13 +195,17 @@ describe('Test `detectBuilders`', () => {
});
it('api + next + public', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'api/endpoint.js', 'public/index.html'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/next');
@@ -213,13 +214,17 @@ describe('Test `detectBuilders`', () => {
});
it('api + next + raw static', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'api/endpoint.js', 'index.html'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/next');
@@ -263,61 +268,85 @@ describe('Test `detectBuilders`', () => {
});
it('next + public', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'public/index.html', 'README.md'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/next');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
});
it('nuxt', async () => {
const pkg = {
scripts: { build: 'nuxt build' },
dependencies: { nuxt: '2.8.1' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: '@vue/cli-service',
version: '2.8.1',
},
};
const files = ['package.json', 'pages/index.js'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/static-build');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
});
it('nuxt + tag canary', async () => {
const pkg = {
scripts: { build: 'nuxt build' },
dependencies: { nuxt: '2.8.1' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: '@vue/cli-service',
version: '2.8.1',
},
};
const files = ['package.json', 'pages/index.js'];
const { builders } = await detectBuilders(files, pkg, { tag: 'canary' });
const { builders } = await detectBuilders(files, detected, {
tag: 'canary',
});
expect(builders[0].use).toBe('@now/static-build@canary');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
});
it('package.json with no build + api', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
it('no build command + api', async () => {
const detected = {
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint].js');
expect(builders.length).toBe(1);
});
it('package.json with no build + public directory', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
it('no build command + public directory', async () => {
const detected = {
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'public/index.html'];
const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null);
expect(errors.length).toBe(1);
const { builders, errors } = await detectBuilders(files, detected);
expect(builders.length).toBe(1);
expect(errors).toBe(null);
});
it('no package.json + api', async () => {
@@ -336,17 +365,23 @@ describe('Test `detectBuilders`', () => {
});
it('package.json + api + canary', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { tag: 'canary' });
const { builders } = await detectBuilders(files, detected, {
tag: 'canary',
});
expect(builders[0].use).toBe('@now/node@canary');
expect(builders[1].use).toBe('@now/node@canary');
expect(builders[2].use).toBe('@now/next@canary');
@@ -354,17 +389,23 @@ describe('Test `detectBuilders`', () => {
});
it('package.json + api + latest', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { tag: 'latest' });
const { builders } = await detectBuilders(files, detected, {
tag: 'latest',
});
expect(builders[0].use).toBe('@now/node@latest');
expect(builders[1].use).toBe('@now/node@latest');
expect(builders[2].use).toBe('@now/next@latest');
@@ -372,17 +413,21 @@ describe('Test `detectBuilders`', () => {
});
it('package.json + api + random tag', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { tag: 'haha' });
const { builders } = await detectBuilders(files, detected, { tag: 'haha' });
expect(builders[0].use).toBe('@now/node@haha');
expect(builders[1].use).toBe('@now/node@haha');
expect(builders[2].use).toBe('@now/next@haha');
@@ -390,13 +435,20 @@ describe('Test `detectBuilders`', () => {
});
it('next.js pages/api + api', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['api/user.js', 'pages/api/user.js'];
const { warnings, errors, builders } = await detectBuilders(files, pkg);
const { warnings, errors, builders } = await detectBuilders(
files,
detected
);
expect(errors).toBe(null);
expect(warnings[0]).toBeDefined();
@@ -420,9 +472,12 @@ describe('Test `detectBuilders`', () => {
});
it('functions with nextjs', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = {
'pages/api/teams/**': {
@@ -435,7 +490,7 @@ describe('Test `detectBuilders`', () => {
'pages/index.js',
'pages/api/teams/members.ts',
];
const { builders, errors } = await detectBuilders(files, pkg, {
const { builders, errors } = await detectBuilders(files, detected, {
functions,
});
@@ -446,6 +501,7 @@ describe('Test `detectBuilders`', () => {
use: '@now/next',
config: {
zeroConfig: true,
buildCommand: 'yarn build',
functions: {
'pages/api/teams/**': {
memory: 128,
@@ -457,9 +513,12 @@ describe('Test `detectBuilders`', () => {
});
it('extend with functions', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = {
'api/users/*.ts': {
@@ -476,7 +535,7 @@ describe('Test `detectBuilders`', () => {
'api/users/[id].ts',
'api/teams/members.ts',
];
const { builders } = await detectBuilders(files, pkg, { functions });
const { builders } = await detectBuilders(files, detected, { functions });
expect(builders.length).toBe(3);
expect(builders[0]).toEqual({
@@ -509,6 +568,7 @@ describe('Test `detectBuilders`', () => {
use: '@now/next',
config: {
zeroConfig: true,
buildCommand: 'yarn build',
},
});
});
@@ -607,31 +667,23 @@ describe('Test `detectBuilders`', () => {
});
it('Do not allow functions that are not used by @now/next', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = { 'test.js': { memory: 1024 } };
const files = ['pages/index.js', 'test.js'];
const { errors } = await detectBuilders(files, pkg, { functions });
const { errors } = await detectBuilders(files, detected, { functions });
expect(errors).toBeDefined();
expect(errors[0].code).toBe('unused_function');
});
it('Do not allow function non Community Runtimes', async () => {
const functions = {
'api/test.js': { memory: 128, runtime: '@now/node@1.0.0' },
};
const files = ['api/test.js'];
const { errors } = await detectBuilders(files, null, { functions });
expect(errors).toBeDefined();
expect(errors[0].code).toBe('invalid_function_runtime');
});
it('Must include includeFiles config property', async () => {
const functions = {
'api/test.js': { includeFiles: 'text/include.txt' },
@@ -731,6 +783,145 @@ describe('Test `detectBuilders`', () => {
expect(errors).not.toBe(null);
expect(errors[0].code).toBe('invalid_function_source');
});
it('Custom static output directory', async () => {
const detected = {
outputDirectory: 'dist',
};
const files = ['dist/index.html', 'dist/style.css'];
const { builders } = await detectBuilders(files, detected);
expect(builders.length).toBe(1);
expect(builders[0].src).toBe('dist/**/*');
expect(builders[0].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(1);
expect(defaultRoutes[0].src).toBe('/(.*)');
expect(defaultRoutes[0].dest).toBe('/dist/$1');
});
it('Custom static output directory with api', async () => {
const detected = {
outputDirectory: 'output',
};
const files = ['api/user.ts', 'output/index.html', 'output/style.css'];
const { builders } = await detectBuilders(files, detected);
expect(builders.length).toBe(2);
expect(builders[1].src).toBe('output/**/*');
expect(builders[1].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
expect(defaultRoutes[1].status).toBe(404);
expect(defaultRoutes[2].src).toBe('/(.*)');
expect(defaultRoutes[2].dest).toBe('/output/$1');
});
it('Custom directory for Serverless Functions', async () => {
const files = ['server/_lib/db.ts', 'server/user.ts', 'server/team.ts'];
const functions = {
'server/**/*.ts': {
memory: 128,
runtime: '@now/node@1.2.1',
},
};
const { builders } = await detectBuilders(files, null, { functions });
expect(builders.length).toBe(3);
expect(builders[0]).toEqual({
use: '@now/node@1.2.1',
src: 'server/team.ts',
config: {
zeroConfig: true,
functions: {
'server/**/*.ts': {
memory: 128,
runtime: '@now/node@1.2.1',
},
},
},
});
expect(builders[1]).toEqual({
use: '@now/node@1.2.1',
src: 'server/user.ts',
config: {
zeroConfig: true,
functions: {
'server/**/*.ts': {
memory: 128,
runtime: '@now/node@1.2.1',
},
},
},
});
// This is expected, since only "api + full static" is supported
// no other directory, so everything else will be deployed
expect(builders[2].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
expect(defaultRoutes[0].dest).toBe('/server/team.ts');
expect(defaultRoutes[0].src).toBe('^/server/(team\\/|team|team\\.ts)$');
expect(defaultRoutes[1].dest).toBe('/server/user.ts');
expect(defaultRoutes[1].src).toBe('^/server/(user\\/|user|user\\.ts)$');
expect(defaultRoutes[2].status).toBe(404);
});
it('Custom directory for Serverless Functions + Next.js', async () => {
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = {
'server/**/*.ts': {
runtime: '@now/node@1.2.1',
},
};
const files = ['package.json', 'pages/index.ts', 'server/user.ts'];
const { builders } = await detectBuilders(files, detected, { functions });
expect(builders.length).toBe(2);
expect(builders[0]).toEqual({
use: '@now/node@1.2.1',
src: 'server/user.ts',
config: {
zeroConfig: true,
functions,
},
});
expect(builders[1]).toEqual({
use: '@now/next',
src: 'package.json',
config: {
buildCommand: 'yarn build',
zeroConfig: true,
},
});
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(2);
expect(defaultRoutes[0].dest).toBe('/server/user.ts');
expect(defaultRoutes[0].src).toBe('^/server/(user\\/|user|user\\.ts)$');
expect(defaultRoutes[1].status).toBe(404);
});
});
it('Test `detectRoutes`', async () => {
@@ -804,13 +995,17 @@ it('Test `detectRoutes`', async () => {
}
{
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['public/index.html', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes[1].status).toBe(404);
expect(defaultRoutes[1].src).toBe('/api(\\/.*)?$');

View File

@@ -3,6 +3,7 @@ const { mkdirp, copyFile } = require('fs-extra');
const {
glob,
debug,
download,
shouldServe,
createLambda,
@@ -14,7 +15,6 @@ exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest;
exports.version = 3;
exports.build = async ({ workPath, files, entrypoint, meta, config }) => {
console.log('downloading files...');
const outDir = await getWritableDirectory();
await download(files, workPath, meta);

View File

@@ -1,6 +1,6 @@
{
"name": "@now/cgi",
"version": "1.0.1-canary.0",
"version": "1.0.1",
"license": "MIT",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "16.6.0",
"version": "16.7.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
@@ -146,7 +146,6 @@
"ora": "3.4.0",
"pcre-to-regexp": "1.0.0",
"pluralize": "7.0.0",
"pre-commit": "1.2.2",
"printf": "0.2.5",
"progress": "2.0.3",
"promisepipe": "3.0.0",

View File

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

View File

@@ -38,7 +38,6 @@ import {
} from '../../util/errors-ts';
import { SchemaValidationFailed } from '../../util/errors';
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
import handleCertError from '../../util/certs/handle-cert-error';
import isWildcardAlias from '../../util/alias/is-wildcard-alias';
import shouldDeployDir from '../../util/deploy/should-deploy-dir';
@@ -294,15 +293,11 @@ export default async function main(
parseEnv(argv['--env'])
);
// Enable debug mode for builders
const buildDebugEnv = debugEnabled ? { NOW_BUILDER_DEBUG: '1' } : {};
// Merge build env out of `build.env` from now.json, and `--build-env` args
const deploymentBuildEnv = Object.assign(
{},
parseEnv(localConfig.build && localConfig.build.env),
parseEnv(argv['--build-env']),
buildDebugEnv
parseEnv(argv['--build-env'])
);
// If there's any undefined values, then inherit them from this process
@@ -386,15 +381,13 @@ export default async function main(
return 1;
}
const deploymentResponse = handleCertError(
output,
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v10')
const deploymentResponse = await getDeploymentByIdOrHost(
now,
contextName,
deployment.id,
'v10'
);
if (deploymentResponse === 1) {
return deploymentResponse;
}
if (
deploymentResponse instanceof DeploymentNotFound ||
deploymentResponse instanceof DeploymentPermissionDenied ||
@@ -404,10 +397,6 @@ export default async function main(
return 1;
}
if (handleCertError(output, deployment) === 1) {
return 1;
}
if (deployment === null) {
error('Uploading failed. Please try again.');
return 1;

View File

@@ -70,7 +70,6 @@ import {
InvalidRegionOrDCForScale,
} from '../../util/errors';
import { SchemaValidationFailed } from '../../util/errors';
import handleCertError from '../../util/certs/handle-cert-error';
import readPackage from '../../util/read-package';
interface Env {
@@ -801,11 +800,6 @@ async function sync({
createArgs
);
const handledResult = handleCertError(output, deployment);
if (handledResult === 1) {
return handledResult;
}
if (
deployment instanceof DomainNotFound ||
deployment instanceof NotDomainOwner ||

View File

@@ -18,7 +18,7 @@ export default async function dev(
output: Output
) {
output.dim(
`Now CLI ${pkg.version} dev (beta) — https://zeit.co/feedback/dev`
`Now CLI ${pkg.version} dev (beta) — https://zeit.co/feedback`
);
const [dir = '.'] = args;

View File

@@ -10,6 +10,7 @@ import formatDate from '../../util/format-date';
import formatNSTable from '../../util/format-ns-table';
import getDomainByName from '../../util/domains/get-domain-by-name';
import getScope from '../../util/get-scope';
import getDomainPrice from '../../util/domains/get-domain-price';
type Options = {
'--debug': boolean;
@@ -23,7 +24,7 @@ export default async function inspect(
) {
const {
authConfig: { token },
config
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
@@ -61,7 +62,12 @@ export default async function inspect(
}
output.debug(`Fetching domain info`);
const domain = await getDomainByName(client, contextName, domainName);
const [domain, renewalPrice] = await Promise.all([
getDomainByName(client, contextName, domainName),
getDomainPrice(client, domainName, 'renewal')
.then(res => (res instanceof Error ? null : res.price))
.catch(() => null),
]);
if (domain instanceof DomainNotFound) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
@@ -104,24 +110,35 @@ export default async function inspect(
` ${chalk.cyan('Bought At')}\t\t\t${formatDate(domain.boughtAt)}\n`
);
output.print(
` ${chalk.cyan('Transferred At')}\t\t${formatDate(domain.transferredAt)}\n`
` ${chalk.cyan('Transferred At')}\t\t${formatDate(
domain.transferredAt
)}\n`
);
output.print(
` ${chalk.cyan('Expires At')}\t\t\t${formatDate(domain.expiresAt)}\n`
);
output.print(
` ${chalk.cyan('NS Verified At')}\t\t${formatDate(domain.nsVerifiedAt)}\n`
` ${chalk.cyan('NS Verified At')}\t\t${formatDate(
domain.nsVerifiedAt
)}\n`
);
output.print(
` ${chalk.cyan('TXT Verified At')}\t\t${formatDate(domain.txtVerifiedAt)}\n`
` ${chalk.cyan('TXT Verified At')}\t\t${formatDate(
domain.txtVerifiedAt
)}\n`
);
output.print(` ${chalk.cyan('CDN Enabled')}\t\t${true}\n`);
if (renewalPrice && domain.boughtAt) {
output.print(
` ${chalk.cyan('Renewal Price')}\t\t$${renewalPrice} USD\n`
);
}
output.print(` ${chalk.cyan('CDN Enabled')}\t\t\t${true}\n`);
output.print('\n');
output.print(chalk.bold(' Nameservers\n\n'));
output.print(
`${formatNSTable(domain.intendedNameservers, domain.nameservers, {
extraSpace: ' '
extraSpace: ' ',
})}\n`
);
output.print('\n');
@@ -129,7 +146,7 @@ export default async function inspect(
output.print(chalk.bold(' Verification Record\n\n'));
output.print(
`${dnsTable([['_now', 'TXT', domain.verificationRecord]], {
extraSpace: ' '
extraSpace: ' ',
})}\n`
);
output.print('\n');

View File

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

View File

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

View File

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

View File

@@ -83,12 +83,6 @@ async function createBuildProcess(
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,

View File

@@ -88,6 +88,17 @@ export default async function(
continue;
}
if (routeConfig.check && devServer) {
const { pathname = '/' } = url.parse(destPath);
const hasDestFile = await devServer.hasFilesystem(pathname);
if (!hasDestFile) {
// If the file is not found, `check: true` will
// behave the same as `continue: true`
reqPathname = destPath;
continue;
}
}
if (isURL(destPath)) {
found = {
found: true,

View File

@@ -22,6 +22,8 @@ import {
PackageJson,
detectBuilders,
detectRoutes,
detectDefaults,
DetectorFilesystem,
} from '@now/build-utils';
import { once } from '../once';
@@ -100,6 +102,25 @@ function sortBuilders(buildA: Builder, buildB: Builder) {
return 0;
}
class DevDetectorFilesystem extends DetectorFilesystem {
private dir: string;
private files: string[];
constructor(dir: string, files: string[]) {
super();
this.dir = dir;
this.files = files;
}
_exists(name: string): Promise<boolean> {
return Promise.resolve(this.files.includes(name));
}
_readFile(name: string): Promise<Buffer> {
return fs.readFile(join(this.dir, name));
}
}
export default class DevServer {
public cwd: string;
public debug: boolean;
@@ -477,8 +498,6 @@ export default class DevServer {
return this.cachedNowConfig;
}
const pkg = await this.getPackageJson();
// The default empty `now.json` is used to serve all files as static
// when no `now.json` is present
let config: NowConfig = this.cachedNowConfig || { version: 2 };
@@ -526,11 +545,19 @@ export default class DevServer {
// no builds -> zero config
if (!config.builds || config.builds.length === 0) {
const { builders, warnings, errors } = await detectBuilders(files, pkg, {
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
functions: config.functions,
const detectorResult = await detectDefaults({
fs: new DevDetectorFilesystem(this.cwd, files),
});
const { builders, warnings, errors } = await detectBuilders(
files,
detectorResult,
{
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
functions: config.functions,
}
);
if (errors) {
this.output.error(errors[0].message);
await this.exit();
@@ -575,29 +602,6 @@ export default class DevServer {
return config;
}
async getPackageJson(): Promise<PackageJson | null> {
const pkgPath = join(this.cwd, 'package.json');
let pkg: PackageJson | null = null;
this.output.debug('Reading `package.json` file');
try {
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
} catch (err) {
if (err.code === 'ENOENT') {
this.output.debug('No `package.json` file present');
} else if (err.name === 'SyntaxError') {
this.output.warn(
`There is a syntax error in the \`package.json\` file: ${err.message}`
);
} else {
throw err;
}
}
return pkg;
}
async tryValidateOrExit(
config: NowConfig,
validate: (c: NowConfig) => string | null

View File

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

View File

@@ -8,62 +8,10 @@ import {
trailingSlashSchema,
} from '@now/routing-utils';
import { NowConfig } from './types';
import { functionsSchema, buildsSchema } from '@now/build-utils';
const ajv = new Ajv();
const buildsSchema = {
type: 'array',
minItems: 0,
maxItems: 128,
items: {
type: 'object',
additionalProperties: false,
required: ['use'],
properties: {
src: {
type: 'string',
minLength: 1,
maxLength: 4096,
},
use: {
type: 'string',
minLength: 3,
maxLength: 256,
},
config: { type: 'object' },
},
},
};
const functionsSchema = {
type: 'object',
minProperties: 1,
maxProperties: 50,
additionalProperties: false,
patternProperties: {
'^.{1,256}$': {
type: 'object',
additionalProperties: false,
properties: {
runtime: {
type: 'string',
maxLength: 256,
},
memory: {
enum: Object.keys(Array.from({ length: 50 }))
.slice(2, 48)
.map(x => Number(x) * 64),
},
maxDuration: {
type: 'number',
minimum: 1,
maximum: 900,
},
},
},
},
};
const validateBuilds = ajv.compile(buildsSchema);
const validateRoutes = ajv.compile(routesSchema);
const validateCleanUrls = ajv.compile(cleanUrlsSchema);

View File

@@ -1097,7 +1097,7 @@ export class LambdaSizeExceededError extends NowError<
size
).toLowerCase()}) exceeds the maximum size limit (${bytes(
maxLambdaSize
).toLowerCase()}). Learn more: https://zeit.co/docs/v2/deployments/concepts/lambdas/#maximum-bundle-size`,
).toLowerCase()}).`,
meta: { size, maxLambdaSize },
});
}

View File

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

View File

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

View File

@@ -0,0 +1 @@
!public

View File

@@ -0,0 +1,7 @@
{
"functions": {
"server/**/*.js": {
"runtime": "@now/node@1.2.1"
}
}
}

View File

@@ -0,0 +1 @@
This is content.

View File

@@ -0,0 +1,3 @@
export default (req, res) => {
res.end(`current hour: ${Math.floor(Date.now() / 10000)}`);
};

View File

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

View File

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

View File

@@ -154,6 +154,22 @@ function testFixtureStdio(directory, fn) {
};
}
test(
'[now dev] validate routes that use `check: true`',
testFixtureStdio('routes-check-true', async (t, port) => {
const result = await fetchWithRetry(
`http://localhost:${port}/blog/post`,
3
);
const response = await result;
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Blog Home/gm);
})
);
test('[now dev] validate builds', async t => {
const directory = fixture('invalid-builds');
const output = await exec(directory);
@@ -291,10 +307,10 @@ test(
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/another', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(301, '/index.html', '', { Location: '/' });
await testPath(301, '/about.html', '', { Location: '/about' });
await testPath(301, '/sub/index.html', '', { Location: '/sub' });
await testPath(301, '/sub/another.html', '', { Location: '/sub/another' });
await testPath(308, '/index.html', '', { Location: '/' });
await testPath(308, '/about.html', '', { Location: '/about' });
await testPath(308, '/sub/index.html', '', { Location: '/sub' });
await testPath(308, '/sub/another.html', '', { Location: '/sub/another' });
})
);
@@ -308,10 +324,10 @@ test(
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/another/', 'Sub Another Page');
await testPath(200, '/style.css/', 'body { color: green }');
await testPath(301, '/index.html', '', { Location: '/' });
await testPath(301, '/about.html', '', { Location: '/about/' });
await testPath(301, '/sub/index.html', '', { Location: '/sub/' });
await testPath(301, '/sub/another.html', '', {
await testPath(308, '/index.html', '', { Location: '/' });
await testPath(308, '/about.html', '', { Location: '/about/' });
await testPath(308, '/sub/index.html', '', { Location: '/sub/' });
await testPath(308, '/sub/another.html', '', {
Location: '/sub/another/',
});
}
@@ -328,9 +344,9 @@ test(
await testPath(200, '/sub/index.html/', 'Sub Index Page');
await testPath(200, '/sub/another.html/', 'Sub Another Page');
await testPath(200, '/style.css/', 'body { color: green }');
await testPath(307, '/about.html', '', { Location: '/about.html/' });
await testPath(307, '/sub', '', { Location: '/sub/' });
await testPath(307, '/sub/another.html', '', {
await testPath(308, '/about.html', '', { Location: '/about.html/' });
await testPath(308, '/sub', '', { Location: '/sub/' });
await testPath(308, '/sub/another.html', '', {
Location: '/sub/another.html/',
});
})
@@ -346,9 +362,9 @@ test(
await testPath(200, '/sub/index.html', 'Sub Index Page');
await testPath(200, '/sub/another.html', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(307, '/about.html/', '', { Location: '/about.html' });
await testPath(307, '/sub/', '', { Location: '/sub' });
await testPath(307, '/sub/another.html/', '', {
await testPath(308, '/about.html/', '', { Location: '/about.html' });
await testPath(308, '/sub/', '', { Location: '/sub' });
await testPath(308, '/sub/another.html/', '', {
Location: '/sub/another.html',
});
})
@@ -1098,8 +1114,7 @@ if (satisfies(process.version, '>= 8.9.0')) {
// start `now dev` detached in child_process
dev.unref();
const result = await fetchWithRetry(`http://localhost:${port}`, 80);
const response = await result;
const response = await fetchWithRetry(`http://localhost:${port}`, 80);
validateResponseHeaders(t, response);
@@ -1118,8 +1133,10 @@ if (satisfies(process.version, '>= 8.9.0')) {
test(
'[now dev] Use runtime from the functions property',
testFixtureStdio('custom-runtime', async (t, port) => {
const result = await fetchWithRetry(`http://localhost:${port}/api/user`, 3);
const response = await result;
const response = await fetchWithRetry(
`http://localhost:${port}/api/user`,
3
);
validateResponseHeaders(t, response);
@@ -1127,3 +1144,20 @@ test(
t.regex(body, /Hello, from Bash!/gm);
})
);
test(
'[now dev] Use public with a custom Serverless Function in `server/date.js',
testFixtureStdio('public-and-server-as-api', async (t, port) => {
const response = await fetchWithRetry(
`http://localhost:${port}/server/date`
);
validateResponseHeaders(t, response);
t.is(response.status, 200);
t.is(
await response.text(),
`current hour: ${Math.floor(Date.now() / 10000)}`
);
})
);

View File

@@ -122,11 +122,6 @@ module.exports = async session => {
'single-dotfile': {
'.testing': 'i am a dotfile',
},
'config-alias-property': {
'now.json':
'{ "alias": "test.now.sh", "builds": [ { "src": "*.html", "use": "@now/static" } ] }',
'index.html': '<span>test alias</span',
},
'config-scope-property-email': {
'now.json': `{ "scope": "${session}@zeit.pub", "builds": [ { "src": "*.html", "use": "@now/static" } ], "version": 2 }`,
'index.html': '<span>test scope email</span',
@@ -204,7 +199,7 @@ fs.writeFileSync(
'index.js',
fs
.readFileSync('index.js', 'utf8')
.replace('BUILD_ENV_DEBUG', process.env.NOW_BUILDER_DEBUG),
.replace('BUILD_ENV_DEBUG', process.env.NOW_BUILDER_DEBUG ? 'on' : 'off'),
);
`,
'index.js': `
@@ -463,6 +458,18 @@ CMD ["node", "index.js"]`,
},
}),
},
'github-and-scope-config': {
'index.txt': 'I Am a Website!',
'now.json': JSON.stringify({
scope: 'i-do-not-exist',
github: {
autoAlias: true,
autoJobCancelation: true,
enabled: true,
silent: true,
},
}),
},
};
for (const typeName of Object.keys(spec)) {

View File

@@ -1427,35 +1427,6 @@ test('ensure we render a prompt when deploying home directory', async t => {
t.true(stderr.includes('> Aborted'));
});
test('ensure the `alias` property is not sent to the API', async t => {
const directory = fixture('config-alias-property');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[directory, '--public', '--name', session, ...defaultArgs, '--force'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 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('ensure the `scope` property works with email', async t => {
const directory = fixture('config-scope-property-email');
@@ -1995,7 +1966,7 @@ test('use `--debug` CLI flag', async t => {
// get the content
const response = await fetch(href);
const content = await response.text();
t.is(content.trim(), '1');
t.is(content.trim(), 'off');
});
test('try to deploy non-existing path', async t => {

View File

@@ -493,6 +493,35 @@ test('list the payment methods', async t => {
t.true(stdout.startsWith(`> 0 cards found under ${contextName}`));
});
test('domains inspect', async t => {
const domainName = `inspect-${contextName}.org`;
const addRes = await execa(
binaryPath,
[`domains`, `add`, domainName, ...defaultArgs],
{ reject: false }
);
t.is(addRes.exitCode, 0);
const { stderr, exitCode } = await execa(
binaryPath,
['domains', 'inspect', domainName, ...defaultArgs],
{
reject: false,
}
);
const rmRes = await execa(
binaryPath,
[`domains`, `rm`, domainName, ...defaultArgs],
{ reject: false, input: 'y' }
);
t.is(rmRes.exitCode, 0);
t.is(exitCode, 0);
t.true(!stderr.includes(`Renewal Price`));
});
test('try to purchase a domain', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
@@ -767,7 +796,19 @@ test('create wildcard alias for deployment', async t => {
t.true(stdout.startsWith(goal));
// Send a test request to the alias
const response = await fetch(`https://test.${contextName}.now.sh`);
// Retries to make sure we consider the time it takes to update
const response = await retry(
async () => {
const response = await fetch(`https://test.${contextName}.now.sh`);
if (response.ok) {
return response;
}
throw new Error(`Error: Returned code ${response.status}`);
},
{ retries: 3 }
);
const content = await response.text();
t.true(response.ok);
@@ -919,35 +960,6 @@ test('ensure we render a prompt when deploying home directory', async t => {
t.true(stderr.includes('> Aborted'));
});
test('ensure the `alias` property is not sent to the API', async t => {
const directory = fixture('config-alias-property');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[directory, '--public', '--name', session, ...defaultArgs, '--force'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 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('ensure the `scope` property works with email', async t => {
const directory = fixture('config-scope-property-email');
@@ -1456,7 +1468,7 @@ test('use `--debug` CLI flag', async t => {
// get the content
const response = await fetch(href);
const content = await response.text();
t.is(content.trim(), '1');
t.is(content.trim(), 'off');
});
test('try to deploy non-existing path', async t => {
@@ -2056,7 +2068,6 @@ test('deploy a Lambda with a specific runtime', async t => {
t.is(build.use, 'now-php@0.0.7', JSON.stringify(build, null, 2));
});
// We need to skip this test until `now-php` supports Runtime version 3
test('fail to deploy a Lambda with a specific runtime but without a locked version', async t => {
const directory = fixture('lambda-with-invalid-runtime');
const output = await execute([directory]);
@@ -2069,6 +2080,13 @@ test('fail to deploy a Lambda with a specific runtime but without a locked versi
);
});
test('ensure `github` and `scope` are not sent to the API', async t => {
const directory = fixture('github-and-scope-config');
const output = await execute([directory]);
t.is(output.exitCode, 0, formatOutput(output));
});
test.after.always(async () => {
// Make sure the token gets revoked
await execa(binaryPath, ['logout', ...defaultArgs]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,6 @@ Learn more: https://github.com/golang/go/wiki/Modules
`);
}
debug('Downloading user files...');
const entrypointArr = entrypoint.split(sep);
// eslint-disable-next-line prefer-const

View File

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

View File

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

View File

@@ -10,25 +10,15 @@ process.on('unhandledRejection', err => {
process.exit(1);
});
async function main(cwd: string) {
const next = require(resolveFrom(cwd, 'next'));
const app = next({ dev: true, dir: cwd });
process.once('message', async ({ dir, runtimeEnv }) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const next = require(resolveFrom(dir, 'next'));
const app = next({ dev: true, dir });
const handler = app.getRequestHandler();
const openPort = await getPort({
port: [5000, 4000],
});
const [openPort] = await Promise.all([getPort(), app.prepare()]);
const url = `http://localhost:${openPort}`;
// Prepare for incoming requests
await app.prepare();
// The runtime env vars are passed in to `argv[2]`
// as a base64-encoded JSON string
const runtimeEnv = JSON.parse(
Buffer.from(process.argv[2], 'base64').toString()
);
syncEnvVars(process.env, process.env, runtimeEnv);
createServer((req, res) => {
@@ -39,6 +29,4 @@ async function main(cwd: string) {
process.send(url);
}
});
}
main(process.cwd());
});

View File

@@ -1,10 +1,10 @@
import { ChildProcess, fork } from 'child_process';
import url from 'url'
import {
pathExists,
readFile,
unlink as unlinkFile,
writeFile,
lstatSync,
} from 'fs-extra';
import os from 'os';
import path from 'path';
@@ -60,8 +60,8 @@ import {
import {
convertRedirects,
convertRewrites
} from '@now/routing-utils/dist/superstatic'
convertRewrites,
} from '@now/routing-utils/dist/superstatic';
interface BuildParamsMeta {
isDev: boolean | undefined;
@@ -77,7 +77,7 @@ interface BuildParamsType extends BuildOptions {
}
export const version = 2;
const htmlContentType = 'text/html; charset=utf-8';
const nowDevChildProcesses = new Set<ChildProcess>();
['SIGINT', 'SIGTERM'].forEach(signal => {
@@ -163,24 +163,20 @@ const name = '[@now/next]';
const urls: stringMap = {};
function startDevServer(entryPath: string, runtimeEnv: EnvConfig) {
// The runtime env vars are encoded and passed in as `argv[2]`, so that the
// dev-server process can replace them onto `process.env` after the Next.js
// "prepare" step
const encodedEnv = Buffer.from(JSON.stringify(runtimeEnv)).toString('base64');
// `env` is omitted since that
// makes it default to `process.env`
const forked = fork(path.join(__dirname, 'dev-server.js'), [encodedEnv], {
// `env` is omitted since that makes it default to `process.env`
const forked = fork(path.join(__dirname, 'dev-server.js'), [], {
cwd: entryPath,
execArgv: [],
});
const getUrl = () =>
new Promise<string>((resolve, reject) => {
forked.on('message', resolve);
forked.on('error', reject);
forked.once('message', resolve);
forked.once('error', reject);
});
forked.send({ dir: entryPath, runtimeEnv });
return { forked, getUrl };
}
@@ -202,7 +198,6 @@ export const build = async ({
const entryPath = path.join(workPath, entryDirectory);
const dotNextStatic = path.join(entryPath, '.next/static');
debug(`${name} Downloading user files...`);
await download(files, workPath, meta);
const pkg = await readPackageJson(entryPath);
@@ -339,6 +334,27 @@ export const build = async ({
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
await runPackageJsonScript(entryPath, shouldRunScript, { ...spawnOpts, env });
const routesManifest = await getRoutesManifest(entryPath, realNextVersion);
const rewrites: Route[] = [];
const redirects: Route[] = [];
if (routesManifest) {
switch (routesManifest.version) {
case 1: {
redirects.push(...convertRedirects(routesManifest.redirects));
rewrites.push(...convertRewrites(routesManifest.rewrites));
break;
}
default: {
// update MIN_ROUTES_MANIFEST_VERSION in ./utils.ts
throw new Error(
'This version of `@now/next` does not support the version of Next.js you are trying to deploy.\n' +
'Please upgrade your `@now/next` builder and try again. Contact support if this continues to happen.'
);
}
}
}
if (isLegacy) {
debug('Running npm install --production...');
await runNpmInstall(
@@ -353,7 +369,6 @@ export const build = async ({
await unlinkFile(path.join(entryPath, '.npmrc'));
}
const exportedPageRoutes: Route[] = [];
const lambdas: { [key: string]: Lambda } = {};
const prerenders: { [key: string]: Prerender | FileFsRef } = {};
const staticPages: { [key: string]: FileFsRef } = {};
@@ -484,18 +499,14 @@ export const build = async ({
return;
}
const staticRoute = path.join(entryDirectory, page);
const staticRoute = path.join(entryDirectory, pathname);
staticPages[staticRoute] = staticPageFiles[page];
staticPages[staticRoute].contentType = htmlContentType;
if (isDynamicRoute(pathname)) {
dynamicPages.push(routeName);
return;
}
exportedPageRoutes.push({
src: `^${path.join('/', entryDirectory, pathname)}$`,
dest: path.join('/', staticRoute),
});
});
const pageKeys = Object.keys(pages);
@@ -589,9 +600,11 @@ export const build = async ({
// Initial files are manually added to the lambda later
return;
}
const { mode } = lstatSync(path.join(workPath, file));
files[file] = new FileFsRef({
fsPath: path.join(workPath, file),
mode,
});
};
@@ -743,14 +756,9 @@ export const build = async ({
if (htmlFsRef == null || jsonFsRef == null) {
throw new Error('invariant: htmlFsRef != null && jsonFsRef != null');
}
const outputPathPageHtml = outputPathPage.concat('.html');
prerenders[outputPathPageHtml] = htmlFsRef;
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
prerenders[outputPathData] = jsonFsRef;
exportedPageRoutes.push({
src: path.posix.join('/', outputPathPage),
dest: outputPathPageHtml,
});
} else {
const lambda = lambdas[outputSrcPathPage];
if (lambda == null) {
@@ -832,8 +840,6 @@ export const build = async ({
let dynamicPrefix = path.join('/', entryDirectory);
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
const routesManifest = await getRoutesManifest(entryPath, realNextVersion)
const dynamicRoutes = await getDynamicRoutes(
entryPath,
entryDirectory,
@@ -842,54 +848,11 @@ export const build = async ({
routesManifest
).then(arr =>
arr.map(route => {
// make sure .html is added to dest for now until
// outputting static files to clean routes is available
if (staticPages[`${route.dest}.html`.substr(1)]) {
route.dest = `${route.dest}.html`;
}
route.src = route.src.replace('^', `^${dynamicPrefix}`);
return route;
})
);
const rewrites: Route[] = []
const redirects: Route[] = []
if (routesManifest) {
switch(routesManifest.version) {
case 1: {
redirects.push(...convertRedirects(routesManifest.redirects))
rewrites.push(...convertRewrites(routesManifest.rewrites))
break
}
default: {
// update MIN_ROUTES_MANIFEST_VERSION in ./utils.ts
throw new Error(
'This version of `@now/next` does not support the version of Next.js you are trying to deploy.\n' +
'Please upgrade your `@now/next` builder and try again. Contact support if this continues to happen.'
);
}
}
}
const topRoutes = [
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{ src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?') },
]
return {
output: {
...publicDirectoryFiles,
@@ -901,17 +864,27 @@ export const build = async ({
...staticDirectoryFiles,
},
routes: [
...topRoutes,
// redirects take the highest priority
...redirects,
...rewrites,
// we need to re-apply the routes above rewrites in-case the are
// rewriting to one of those routes
...topRoutes,
// Static exported pages (.html rewrites)
...exportedPageRoutes,
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{ src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?') },
// Next.js page lambdas, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
...rewrites,
// Dynamic routes
...dynamicRoutes,
...dynamicDataRoutes,
@@ -934,7 +907,7 @@ export const build = async ({
export const prepareCache = async ({
workPath,
entrypoint,
}: PrepareCacheOptions) => {
}: PrepareCacheOptions): Promise<Files> => {
debug('Preparing cache...');
const entryDirectory = path.dirname(entrypoint);
const entryPath = path.join(workPath, entryDirectory);
@@ -954,8 +927,6 @@ export const prepareCache = async ({
const cache = {
...(await glob(path.join(cacheEntrypoint, 'node_modules/**'), workPath)),
...(await glob(path.join(cacheEntrypoint, '.next/cache/**'), workPath)),
...(await glob(path.join(cacheEntrypoint, 'package-lock.json'), workPath)),
...(await glob(path.join(cacheEntrypoint, 'yarn.lock'), workPath)),
};
debug('Cache file manifest produced');
return cache;

View File

@@ -12,6 +12,7 @@ import {
streamToBuffer,
Lambda,
Route,
isSymbolicLink,
} from '@now/build-utils';
type stringMap = { [key: string]: string };
@@ -293,37 +294,36 @@ async function getRoutes(
}
export type Rewrite = {
source: string,
destination: string,
}
source: string;
destination: string;
};
export type Redirect = Rewrite & {
statusCode?: number
}
statusCode?: number;
};
type RoutesManifestRegex = {
regex: string,
regexKeys: string[]
}
regex: string;
regexKeys: string[];
};
export type RoutesManifest = {
redirects: (Redirect & RoutesManifestRegex)[],
rewrites: (Rewrite & RoutesManifestRegex)[],
redirects: (Redirect & RoutesManifestRegex)[];
rewrites: (Rewrite & RoutesManifestRegex)[];
dynamicRoutes: {
page: string,
regex: string,
}[],
version: number
}
page: string;
regex: string;
}[];
version: number;
};
export async function getRoutesManifest(
entryPath: string,
nextVersion?: string,
): Promise< RoutesManifest | undefined> {
const shouldHaveManifest = (
nextVersion && semver.gte(nextVersion, '9.1.4-canary.0')
)
if (!shouldHaveManifest) return
nextVersion?: string
): Promise<RoutesManifest | undefined> {
const shouldHaveManifest =
nextVersion && semver.gte(nextVersion, '9.1.4-canary.0');
if (!shouldHaveManifest) return;
const pathRoutesManifest = path.join(
entryPath,
@@ -338,12 +338,13 @@ export async function getRoutesManifest(
if (shouldHaveManifest && !hasRoutesManifest) {
throw new Error(
`A routes-manifest.json couldn't be found. This could be due to a failure during the build`
)
);
}
const routesManifest: RoutesManifest = require(pathRoutesManifest)
// eslint-disable-next-line @typescript-eslint/no-var-requires
const routesManifest: RoutesManifest = require(pathRoutesManifest);
return routesManifest
return routesManifest;
}
export async function getDynamicRoutes(
@@ -455,11 +456,20 @@ function syncEnvVars(base: EnvConfig, removeEnv: EnvConfig, addEnv: EnvConfig) {
export const ExperimentalTraceVersion = `9.0.4-canary.1`;
export type PseudoLayer = {
[fileName: string]: {
crc32: number;
compBuffer: Buffer;
uncompressedSize: number;
};
[fileName: string]: PseudoFile | PseudoSymbolicLink;
};
export type PseudoFile = {
isSymlink: false;
crc32: number;
compBuffer: Buffer;
uncompressedSize: number;
};
export type PseudoSymbolicLink = {
isSymlink: true;
file: FileFsRef;
symlinkTarget: string;
};
const compressBuffer = (buf: Buffer): Promise<Buffer> => {
@@ -482,13 +492,22 @@ export async function createPseudoLayer(files: {
for (const fileName of Object.keys(files)) {
const file = files[fileName];
const origBuffer = await streamToBuffer(file.toStream());
const compBuffer = await compressBuffer(origBuffer);
pseudoLayer[fileName] = {
compBuffer,
crc32: crc32.unsigned(origBuffer),
uncompressedSize: origBuffer.byteLength,
};
if (isSymbolicLink(file.mode)) {
pseudoLayer[fileName] = {
file,
isSymlink: true,
symlinkTarget: await fs.readlink(file.fsPath),
} as PseudoSymbolicLink;
} else {
const origBuffer = await streamToBuffer(file.toStream());
const compBuffer = await compressBuffer(origBuffer);
pseudoLayer[fileName] = {
compBuffer,
crc32: crc32.unsigned(origBuffer),
uncompressedSize: origBuffer.byteLength,
} as PseudoFile;
}
}
return pseudoLayer;
@@ -521,10 +540,31 @@ export async function createLambdaFromPseudoLayers({
const zipFile = new ZipFile();
const addedFiles = new Set();
const names = Object.keys(files).sort();
const symlinkTargets = new Map<string, string>();
for (const name of names) {
const file = files[name];
if (file.mode && isSymbolicLink(file.mode) && file.type === 'FileFsRef') {
const symlinkTarget = await fs.readlink((file as FileFsRef).fsPath);
symlinkTargets.set(name, symlinkTarget);
}
}
// apply pseudo layers (already compressed objects)
for (const layer of layers) {
for (const seedKey of Object.keys(layer)) {
const { compBuffer, crc32, uncompressedSize } = layer[seedKey];
const item = layer[seedKey];
if (item.isSymlink) {
const { symlinkTarget, file } = item;
zipFile.addBuffer(Buffer.from(symlinkTarget, 'utf8'), seedKey, {
mode: file.mode,
});
continue;
}
const { compBuffer, crc32, uncompressedSize } = item;
// @ts-ignore: `addDeflatedBuffer` is a valid function, but missing on the type
zipFile.addDeflatedBuffer(compBuffer, seedKey, {
@@ -540,8 +580,16 @@ export async function createLambdaFromPseudoLayers({
// was already added in a pseudo layer
if (addedFiles.has(fileName)) continue;
const file = files[fileName];
const fileBuffer = await streamToBuffer(file.toStream());
zipFile.addBuffer(fileBuffer, fileName);
const symlinkTarget = symlinkTargets.get(fileName);
if (typeof symlinkTarget === 'string') {
zipFile.addBuffer(Buffer.from(symlinkTarget, 'utf8'), fileName, {
mode: file.mode,
});
} else {
const fileBuffer = await streamToBuffer(file.toStream());
zipFile.addBuffer(fileBuffer, fileName);
}
}
zipFile.end();

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"next": "^9.1.2-canary.8",
"next": "^9.1.6-canary.1",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}

View File

@@ -1,16 +1,16 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function unstable_getStaticParams() {
export async function unstable_getStaticPaths () {
return [
'/blog/post-1/comment-1',
{ post: 'post-2', comment: 'comment-2' },
{ params: { post: 'post-2', comment: 'comment-2' } },
'/blog/post-1337/comment-1337',
];
}
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps({ params }) {
export async function unstable_getStaticProps ({ params }) {
return {
props: {
post: params.post,

View File

@@ -1,13 +1,14 @@
import React from 'react'
// eslint-disable-next-line camelcase
export async function unstable_getStaticParams () {
export async function unstable_getStaticPaths () {
return [
'/blog/post-1',
{ post: 'post-2' },
{ params: { post: 'post-2' } },
]
}
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps ({ params }) {
if (params.post === 'post-10') {

View File

@@ -0,0 +1,6 @@
node_modules
.next
.env
tsconfig.tsbuildinfo
.DS_Store
*.log

View File

@@ -0,0 +1,5 @@
{
"packages": ["packages/*"],
"npmClient": "yarn",
"version": "0.0.0"
}

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [{ "src": "packages/web/next.config.js", "use": "@now/next" }],
"routes": [{ "src": "/(.*)", "dest": "/packages/web/$1", "continue": true }],
"probes": [
{
"path": "/",
"mustContain": "hello world <!-- -->6"
}
]
}

View File

@@ -0,0 +1,14 @@
{
"private": true,
"workspaces": {
"packages": [
"packages/*"
]
},
"scripts": {
"postinstall": "lerna run build --scope=@jimmy/common"
},
"devDependencies": {
"lerna": "^3.19.0"
}
}

View File

@@ -0,0 +1 @@
export declare const add: (a: number, b: number) => number;

View File

@@ -0,0 +1,6 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.add = (a, b) => {
return a + b;
};
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAa,QAAA,GAAG,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAC;AACf,CAAC,CAAC"}

View File

@@ -0,0 +1,14 @@
{
"name": "@jimmy/common",
"version": "1.0.64",
"types": "dist/index.d.ts",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"watch": "tsc -w",
"build": "tsc"
},
"devDependencies": {
"typescript": "^3.7.3"
}
}

View File

@@ -0,0 +1,3 @@
export const add = (a: number, b: number) => {
return a + b;
};

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