Compare commits

..

78 Commits

Author SHA1 Message Date
Andy Bitz
bd6e0f9f93 Publish Stable
- @now/next@2.3.0
2019-12-17 15:30:37 +01:00
Andy
aa8d002309 [now-cli][now-build-utils] Adjust tests for new detectors (#3443)
* Adjust README

* Fix now-dev test

* Add hugo to the PATH

* Fix Hugo build

* Add more logging

* Resolve path

* Do not use the build script as dev command

* Update yarn.lock file

* fetch with retry

* Remove sh from README

* Use Set for Hugo config files
2019-12-17 15:27:12 +01:00
Andy Bitz
42ce9aca86 Publish Canary
- @now/build-utils@1.2.1-canary.0
 - now@16.7.1-canary.0
 - @now/next@2.2.1-canary.1
 - @now/routing-utils@1.4.1-canary.0
 - @now/static-build@0.14.1-canary.0
2019-12-17 00:25:35 +01:00
Andy
156f596189 [now-cli][now-static-build] Add support for buildCommand, devCommand and outputDirectory (#3434)
* Revert "Revert "[now-static-build] Add support `buildCommand`, `devCommand` and `outputDirectory` (#3422)" (#3428)"

This reverts commit f7b4dd4458.

* Handle generic node projects with /public

* Remove .only

* Ensure node_modules/.bin is also available during `now dev`

* Remove config log

* Adjust test

* Fix integration tests

* Fix public check

* Remove build + public

* Remove _scan

* Remove any casting

* Use `spawnCommand` for dev

* Remove unused import

* Remove cross-spawn

* Fix null config

* Fix build

* Only do a single Buffer.concat
2019-12-17 00:21:58 +01:00
JJ Kasper
8acfd5bf71 [now-next] Add testing for export handling (#3437)
Follow up to #3431 adding tests for this behavior

Tests to add:
- [x] custom routes: redirects
- [x] custom routes: rewrites
- [x] confirming each test is actually a `next export` deploy somehow
2019-12-16 22:47:36 +00:00
Andy
76c99ebb28 [now-build-utils] Restore previous detectors (#3441) 2019-12-16 23:20:48 +01:00
Alejandro Pacheco
5fb119b99c [now-cli] Changed scale error message (#3394)
The error message does not give a clear reason of why it failed
2019-12-16 21:44:40 +00:00
Steven
99b766d9cb [now-routing-utils] Add validation for handle:hit and handle:miss (#3438)
This PR adds initial support for `handle: hit` and `handle: miss` routes.
2019-12-16 20:56:48 +00:00
Andy
c207cf9b40 [now-cli] Change --team warning style (#3439)
* [now-cli] Change warning style

* Adjust test

* Fix screen error
2019-12-16 19:38:43 +01:00
Andy
dd00ac4621 [now-cli] Change automatic version detection message (#3440)
* [now-cli] Change automatic version detection message

* Fix unit test
2019-12-16 19:16:23 +01:00
Allen Hai
3d18a067a0 remove unused vars from builder-cache file (#3411) 2019-12-16 09:03:58 -05:00
Tim Neutkens
c48571a799 Publish Canary
- @now/next@2.2.1-canary.0
2019-12-16 12:49:02 +01:00
Joe Haddad
6eeb6983d9 [now-next] Support for next export (#3431)
* Add Support for `next export`

* Add some test cases

* tests require canary next.js

* bump

* fix test cases

* Update packages/now-next/src/index.ts

Co-Authored-By: Joe Haddad <joe.haddad@zeit.co>

* Update packages/now-next/src/index.ts

Co-Authored-By: Joe Haddad <joe.haddad@zeit.co>
2019-12-16 12:46:39 +01:00
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
180 changed files with 14902 additions and 1466 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.1-canary.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

@@ -0,0 +1,432 @@
import minimatch from 'minimatch';
import { valid as validSemver } from 'semver';
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
interface ErrorResponse {
code: string;
message: string;
}
interface Options {
tag?: 'canary' | 'latest' | string;
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 `scripts` 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[] {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return [
{ src: 'api/**/*.js', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.ts', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.go', use: `@now/go${withTag}`, config },
{ src: 'api/**/*.py', use: `@now/python${withTag}`, config },
{ src: 'api/**/*.rb', use: `@now/ruby${withTag}`, config },
];
}
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 getApiFunctionBuilder(
file: string,
prevBuilder: Builder | undefined,
{ functions = {} }: Pick<Options, 'functions'>
) {
const key = Object.keys(functions).find(
k => file === k || minimatch(file, k)
);
const fn = key ? functions[key] : undefined;
if (!fn || (!fn.runtime && !prevBuilder)) {
return prevBuilder;
}
const src = (prevBuilder && prevBuilder.src) || file;
const use = fn.runtime || (prevBuilder && prevBuilder.use);
const config: Config = { zeroConfig: true };
if (key) {
Object.assign(config, {
functions: {
[key]: fn,
},
});
}
const { includeFiles, excludeFiles } = fn;
if (includeFiles) Object.assign(config, { includeFiles });
if (excludeFiles) Object.assign(config, { excludeFiles });
return use ? { use, src, config } : prevBuilder;
}
async function detectFrontBuilder(
pkg: PackageJson,
builders: Builder[],
options: Options
): Promise<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 };
}
});
}
return builder;
}
}
// By default we'll choose the `static-build` builder
return { src, use: `@now/static-build${withTag}`, config };
}
// Files that match a specific pattern will get ignored
export function getIgnoreApiFilter(optionsOrBuilders: Options | Builder[]) {
const possiblePatterns: string[] = getApiBuilders().map(b => b.src);
if (Array.isArray(optionsOrBuilders)) {
optionsOrBuilders.forEach(({ src }) => possiblePatterns.push(src));
} else if (optionsOrBuilders.functions) {
Object.keys(optionsOrBuilders.functions).forEach(p =>
possiblePatterns.push(p)
);
}
return (file: string) => {
if (!file.startsWith('api/')) {
return false;
}
if (file.includes('/.')) {
return false;
}
if (file.includes('/_')) {
return false;
}
if (file.endsWith('.d.ts')) {
return false;
}
if (possiblePatterns.every(p => !(file === p || minimatch(file, p)))) {
return false;
}
return true;
};
}
// We need to sort the file paths by alphabet to make
// sure the routes stay in the same order e.g. for deduping
export function sortFiles(fileA: string, fileB: string) {
return fileA.localeCompare(fileB);
}
async function detectApiBuilders(
files: string[],
options: Options
): Promise<Builder[]> {
const builds = files
.sort(sortFiles)
.filter(getIgnoreApiFilter(options))
.map(file => {
const apiBuilders = getApiBuilders(options);
const apiBuilder = apiBuilders.find(b => minimatch(file, b.src));
const fnBuilder = getApiFunctionBuilder(file, apiBuilder, options);
return fnBuilder ? { ...fnBuilder, src: file } : null;
});
return builds.filter(Boolean) as Builder[];
}
// 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(
files: string[],
builders: Builder[]
): Promise<ErrorResponse | null> {
// For Next.js
if (builders.some(b => b.use.startsWith('@now/next'))) {
const hasApiPages = files.some(file => file.startsWith('pages/api/'));
const hasApiBuilders = builders.some(b => b.src.startsWith('api/'));
if (hasApiPages && hasApiBuilders) {
return {
code: 'conflicting_files',
message:
'It is not possible to use `api` and `pages/api` at the same time, please only use one option',
};
}
}
return null;
}
// 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(
files: string[],
builder: Builder
): Promise<ErrorResponse | null> {
const { config: { functions = undefined } = {} } = builder;
if (!functions) return null;
if (builder.use.startsWith('@now/next')) {
const matchingFiles = files.filter(file =>
Object.keys(functions).some(key => file === key || minimatch(file, key))
);
for (const matchedFile of matchingFiles) {
if (
!matchedFile.startsWith('src/pages/') &&
!matchedFile.startsWith('pages/')
) {
return {
code: 'unused_function',
message: `The function for ${matchedFile} can't be handled by any builder`,
};
}
}
}
return null;
}
function validateFunctions(files: string[], { functions = {} }: Options) {
const apiBuilders = getApiBuilders();
for (const [path, func] of Object.entries(functions)) {
if (path.length > 256) {
return {
code: 'invalid_function_glob',
message: 'Function globs must be less than 256 characters long.',
};
}
if (!func || typeof func !== 'object') {
return {
code: 'invalid_function',
message: 'Function must be an object.',
};
}
if (Object.keys(func).length === 0) {
return {
code: 'invalid_function',
message: 'Function must contain at least one property.',
};
}
if (
func.maxDuration !== undefined &&
(func.maxDuration < 1 ||
func.maxDuration > 900 ||
!Number.isInteger(func.maxDuration))
) {
return {
code: 'invalid_function_duration',
message: 'Functions must have a duration between 1 and 900.',
};
}
if (
func.memory !== undefined &&
(func.memory < 128 || func.memory > 3008 || func.memory % 64 !== 0)
) {
return {
code: 'invalid_function_memory',
message:
'Functions must have a memory value between 128 and 3008 in steps of 64.',
};
}
if (path.startsWith('/')) {
return {
code: 'invalid_function_source',
message: `The function path "${path}" is invalid. The path must be relative to your project root and therefore cannot start with a slash.`,
};
}
if (files.some(f => f === path || minimatch(f, path)) === false) {
return {
code: 'invalid_function_source',
message: `No source file matched the function for ${path}.`,
};
}
if (func.runtime !== undefined) {
const tag = `${func.runtime}`.split('@').pop();
if (!tag || !validSemver(tag)) {
return {
code: 'invalid_function_runtime',
message:
'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) {
if (typeof func.includeFiles !== 'string') {
return {
code: 'invalid_function_property',
message: `The property \`includeFiles\` must be a string.`,
};
}
}
if (func.excludeFiles !== undefined) {
if (typeof func.excludeFiles !== 'string') {
return {
code: 'invalid_function_property',
message: `The property \`excludeFiles\` must be a string.`,
};
}
}
}
return null;
}
// When zero config is used we can call this function
// to determine what builders to use
export async function detectBuildersLegacy(
files: string[],
pkg?: PackageJson | undefined | null,
options: Options = {}
): Promise<{
builders: Builder[] | null;
errors: ErrorResponse[] | null;
warnings: ErrorResponse[];
}> {
const errors: ErrorResponse[] = [];
const warnings: ErrorResponse[] = [];
const functionError = validateFunctions(files, options);
if (functionError) {
return {
builders: null,
errors: [functionError],
warnings,
};
}
// Detect all builders for the `api` directory before anything else
const builders = await detectApiBuilders(files, options);
if (pkg && hasBuildScript(pkg)) {
const frontendBuilder = await detectFrontBuilder(pkg, builders, options);
builders.push(frontendBuilder);
const conflictError = await checkConflictingFiles(files, builders);
if (conflictError) {
warnings.push(conflictError);
}
const unusedFunctionError = await checkUnusedFunctionsOnFrontendBuilder(
files,
frontendBuilder
);
if (unusedFunctionError) {
return {
builders: null,
errors: [unusedFunctionError],
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,
});
}
}
return {
builders: builders.length ? builders : null,
errors: errors.length ? errors : null,
warnings,
};
}

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,71 @@ function getApiFunctionBuilder(
return use ? { use, src, config } : prevBuilder;
}
async function detectFrontBuilder(
pkg: PackageJson,
function detectFrontBuilder(
detectorResult: Partial<DetectorOutput>,
builders: Builder[],
files: string[],
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,
devCommand,
} = detectorResult;
const frameworkSlug = framework ? framework.slug : null;
const config: Config = {
zeroConfig: true,
};
if (framework) {
config.framework = framework;
}
if (devCommand) {
config.devCommand = devCommand;
}
if (buildCommand) {
config.buildCommand = buildCommand;
}
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 };
}
});
}
return builder;
if (frameworkSlug === 'next') {
return { src: 'package.json', use: `@now/next${withTag}`, config };
}
if (frameworkSlug === 'hugo') {
const configFiles = new Set(['config.yaml', 'config.toml', 'config.json']);
const source = files.find(file => configFiles.has(file));
if (source) {
return { src: source, use: `@now/static-build${withTag}`, config };
}
}
// By default we'll choose the `static-build` builder
return { src, use: `@now/static-build${withTag}`, config };
return { src: 'package.json', use: `@now/static-build${withTag}`, config };
}
// Files that match a specific pattern will get ignored
@@ -139,10 +152,6 @@ export function getIgnoreApiFilter(optionsOrBuilders: Options | Builder[]) {
}
return (file: string) => {
if (!file.startsWith('api/')) {
return false;
}
if (file.includes('/.')) {
return false;
}
@@ -169,10 +178,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 +194,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 +217,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 +237,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 +249,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 +318,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 +346,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 +367,24 @@ 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,
files,
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 +396,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

@@ -0,0 +1,298 @@
import { parse as parsePath } from 'path';
import { Route, Builder } from './types';
import { getIgnoreApiFilter, sortFiles } from './detect-builders-legacy';
function escapeName(name: string) {
const special = '[]^$.|?*+()'.split('');
for (const char of special) {
name = name.replace(new RegExp(`\\${char}`, 'g'), `\\${char}`);
}
return name;
}
function joinPath(...segments: string[]) {
const joinedPath = segments.join('/');
return joinedPath.replace(/\/{2,}/g, '/');
}
function concatArrayOfText(texts: string[]): string {
if (texts.length <= 2) {
return texts.join(' and ');
}
const last = texts.pop();
return `${texts.join(', ')}, and ${last}`;
}
// Takes a filename or foldername, strips the extension
// gets the part between the "[]" brackets.
// It will return `null` if there are no brackets
// and therefore no segment.
function getSegmentName(segment: string): string | null {
const { name } = parsePath(segment);
if (name.startsWith('[') && name.endsWith(']')) {
return name.slice(1, -1);
}
return null;
}
function createRouteFromPath(filePath: string): Route {
const parts = filePath.split('/');
let counter = 1;
const query: string[] = [];
const srcParts = parts.map((segment, index): string => {
const name = getSegmentName(segment);
const isLast = index === parts.length - 1;
if (name !== null) {
// We can't use `URLSearchParams` because `$` would get escaped
query.push(`${name}=$${counter++}`);
return `([^\\/]+)`;
} else if (isLast) {
const { name: fileName, ext } = parsePath(segment);
const isIndex = fileName === 'index';
const prefix = isIndex ? '\\/' : '';
const names = [
isIndex ? prefix : `${fileName}\\/`,
prefix + escapeName(fileName),
prefix + escapeName(fileName) + escapeName(ext),
].filter(Boolean);
// Either filename with extension, filename without extension
// or nothing when the filename is `index`
return `(${names.join('|')})${isIndex ? '?' : ''}`;
}
return segment;
});
const { name: fileName } = parsePath(filePath);
const isIndex = fileName === 'index';
const src = isIndex
? `^/${srcParts.slice(0, -1).join('/')}${srcParts.slice(-1)[0]}$`
: `^/${srcParts.join('/')}$`;
const dest = `/${filePath}${query.length ? '?' : ''}${query.join('&')}`;
return { src, dest };
}
// Check if the path partially matches and has the same
// name for the path segment at the same position
function partiallyMatches(pathA: string, pathB: string): boolean {
const partsA = pathA.split('/');
const partsB = pathB.split('/');
const long = partsA.length > partsB.length ? partsA : partsB;
const short = long === partsA ? partsB : partsA;
let index = 0;
for (const segmentShort of short) {
const segmentLong = long[index];
const nameLong = getSegmentName(segmentLong);
const nameShort = getSegmentName(segmentShort);
// If there are no segments or the paths differ we
// return as they are not matching
if (segmentShort !== segmentLong && (!nameLong || !nameShort)) {
return false;
}
if (nameLong !== nameShort) {
return true;
}
index += 1;
}
return false;
}
// Counts how often a path occurs when all placeholders
// got resolved, so we can check if they have conflicts
function pathOccurrences(filePath: string, files: string[]): string[] {
const getAbsolutePath = (unresolvedPath: string): string => {
const { dir, name } = parsePath(unresolvedPath);
const parts = joinPath(dir, name).split('/');
return parts.map(part => part.replace(/\[.*\]/, '1')).join('/');
};
const currentAbsolutePath = getAbsolutePath(filePath);
return files.reduce((prev: string[], file: string): string[] => {
const absolutePath = getAbsolutePath(file);
if (absolutePath === currentAbsolutePath) {
prev.push(file);
} else if (partiallyMatches(filePath, file)) {
prev.push(file);
}
return prev;
}, []);
}
// Checks if a placeholder with the same name is used
// multiple times inside the same path
function getConflictingSegment(filePath: string): string | null {
const segments = new Set<string>();
for (const segment of filePath.split('/')) {
const name = getSegmentName(segment);
if (name !== null && segments.has(name)) {
return name;
}
if (name) {
segments.add(name);
}
}
return null;
}
function sortFilesBySegmentCount(fileA: string, fileB: string): number {
const lengthA = fileA.split('/').length;
const lengthB = fileB.split('/').length;
if (lengthA > lengthB) {
return -1;
}
if (lengthA < lengthB) {
return 1;
}
// Paths that have the same segment length but
// less placeholders are preferred
const countSegments = (prev: number, segment: string) =>
getSegmentName(segment) ? prev + 1 : 0;
const segmentLengthA = fileA.split('/').reduce(countSegments, 0);
const segmentLengthB = fileB.split('/').reduce(countSegments, 0);
if (segmentLengthA > segmentLengthB) {
return 1;
}
if (segmentLengthA < segmentLengthB) {
return -1;
}
return 0;
}
interface RoutesResult {
defaultRoutes: Route[] | null;
error: { [key: string]: string } | null;
}
async function detectApiRoutes(
files: string[],
builders: Builder[]
): Promise<RoutesResult> {
if (!files || files.length === 0) {
return { defaultRoutes: null, error: null };
}
// The deepest routes need to be
// the first ones to get handled
const sortedFiles = files
.filter(getIgnoreApiFilter(builders))
.sort(sortFiles)
.sort(sortFilesBySegmentCount);
const defaultRoutes: Route[] = [];
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/')) {
continue;
}
const conflictingSegment = getConflictingSegment(file);
if (conflictingSegment) {
return {
defaultRoutes: null,
error: {
code: 'conflicting_path_segment',
message:
`The segment "${conflictingSegment}" occurs more than ` +
`one time in your path "${file}". Please make sure that ` +
`every segment in a path is unique`,
},
};
}
const occurrences = pathOccurrences(file, sortedFiles).filter(
name => name !== file
);
if (occurrences.length > 0) {
const messagePaths = concatArrayOfText(
occurrences.map(name => `"${name}"`)
);
return {
defaultRoutes: null,
error: {
code: 'conflicting_file_path',
message:
`Two or more files have conflicting paths or names. ` +
`Please make sure path segments and filenames, without their extension, are unique. ` +
`The path "${file}" has conflicts with ${messagePaths}`,
},
};
}
defaultRoutes.push(createRouteFromPath(file));
}
// 404 Route to disable directory listing
if (defaultRoutes.length) {
defaultRoutes.push({
status: 404,
src: '/api(\\/.*)?$',
});
}
return { defaultRoutes, error: null };
}
function hasPublicBuilder(builders: Builder[]): boolean {
return builders.some(
builder =>
builder.use === '@now/static' &&
builder.src === 'public/**/*' &&
builder.config &&
builder.config.zeroConfig === true
);
}
export async function detectRoutesLegacy(
files: string[],
builders: Builder[]
): Promise<RoutesResult> {
const routesResult = await detectApiRoutes(files, builders);
if (routesResult.defaultRoutes && hasPublicBuilder(builders)) {
routesResult.defaultRoutes.push({
src: '/(.*)',
dest: '/public/$1',
});
}
return routesResult;
}

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,19 @@
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 && devCommand ? `yarn run ${devCommand}` : undefined,
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

@@ -39,6 +39,66 @@ export function spawnAsync(
});
}
export function execAsync(
command: string,
args: string[],
opts: SpawnOptions = {}
) {
return new Promise<{ stdout: string; stderr: string; code: number }>(
(resolve, reject) => {
opts.stdio = 'pipe';
const stdoutList: Buffer[] = [];
const stderrList: Buffer[] = [];
const child = spawn(command, args, opts);
child.stderr!.on('data', data => {
stderrList.push(data);
});
child.stdout!.on('data', data => {
stdoutList.push(data);
});
child.on('error', reject);
child.on('close', (code, signal) => {
if (code !== 0) {
return reject(
new Error(
`Program "${command}" exited with non-zero exit code ${code} ${signal}.`
)
);
}
return resolve({
code,
stdout: Buffer.concat(stdoutList).toString(),
stderr: Buffer.concat(stderrList).toString(),
});
});
}
);
}
export function spawnCommand(command: string, options: SpawnOptions = {}) {
if (process.platform === 'win32') {
return spawn('cmd.exe', ['/C', command], options);
}
return spawn('sh', ['-c', command], options);
}
export async function execCommand(command: string, options: SpawnOptions = {}) {
if (process.platform === 'win32') {
await spawnAsync('cmd.exe', ['/C', command], options);
} else {
await spawnAsync('sh', ['-c', command], options);
}
return true;
}
async function chmodPlusX(fsPath: string) {
const s = await fs.stat(fsPath);
const newMode = s.mode | 64 | 8 | 1; // eslint-disable-line no-bitwise
@@ -155,7 +215,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,12 +3,15 @@ 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';
import {
execAsync,
spawnAsync,
execCommand,
spawnCommand,
installDependencies,
runPackageJsonScript,
runNpmInstall,
@@ -38,9 +41,12 @@ export {
getWriteableDirectory,
glob,
rename,
execAsync,
spawnAsync,
installDependencies,
runPackageJsonScript,
execCommand,
spawnCommand,
runNpmInstall,
runBundleInstall,
runPipInstall,
@@ -52,8 +58,13 @@ export {
detectBuilders,
detectRoutes,
debug,
isSymbolicLink,
getLambdaOptionsFromFunction,
};
export { detectBuildersLegacy } from './detect-builders-legacy';
export { detectRoutesLegacy } from './detect-routes-legacy';
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

@@ -52,6 +52,13 @@ export interface Config {
zeroConfig?: boolean;
import?: { [key: string]: string };
functions?: BuilderFunctions;
outputDirectory?: string;
buildCommand?: string;
devCommand?: string;
framework?: {
slug: string;
version: string;
};
}
export interface Meta {
@@ -304,6 +311,7 @@ export interface NodeVersion {
major: number;
range: string;
runtime: string;
discontinueDate?: Date;
}
export interface Builder {
@@ -352,11 +360,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 +373,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,11 @@ describe('Test `detectBuilders`', () => {
use: '@now/next',
config: {
zeroConfig: true,
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
functions: {
'pages/api/teams/**': {
memory: 128,
@@ -457,9 +517,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 +539,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 +572,11 @@ describe('Test `detectBuilders`', () => {
use: '@now/next',
config: {
zeroConfig: true,
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
},
});
});
@@ -607,31 +675,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 +791,149 @@ 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: {
zeroConfig: true,
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
},
});
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 +1007,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.1-canary.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

@@ -239,7 +239,7 @@ export default async function main(ctx) {
return 1;
}
if (deployment.version === 2) {
output.error('Cannot scale a deployment containing builds');
output.error('Cannot scale a Now 2.0 deployment');
now.close();
return 1;
}

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

@@ -1,4 +1,3 @@
import chalk from 'chalk';
import execa from 'execa';
import semver from 'semver';
import pipe from 'promisepipe';
@@ -8,7 +7,6 @@ import { extract } from 'tar-fs';
import { createHash } from 'crypto';
import { createGunzip } from 'zlib';
import { join, resolve } from 'path';
import { funCacheDir } from '@zeit/fun';
import { PackageJson } from '@now/build-utils';
import XDGAppPaths from 'xdg-app-paths';
import {
@@ -17,7 +15,6 @@ import {
readFile,
readJSON,
writeFile,
remove,
} from 'fs-extra';
import pkg from '../../../package.json';
@@ -104,11 +101,7 @@ export async function prepareBuilderDir() {
if (!hasBundledBuilders(dependencies)) {
const extractor = extract(builderDir);
await pipe(
createReadStream(bundledTarballPath),
createGunzip(),
extractor
);
await pipe(createReadStream(bundledTarballPath), createGunzip(), extractor);
}
return builderDir;
@@ -158,6 +151,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 +166,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

@@ -1,4 +1,5 @@
import chalk from 'chalk';
import boxen from 'boxen';
import { format } from 'util';
import { Console } from 'console';
@@ -18,10 +19,32 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
}
function warn(str: string, slug: string | null = null) {
log(chalk`{yellow.bold WARN!} ${str}`);
if (slug !== null) {
log(`More details: https://err.sh/now/${slug}`);
const prevTerm = process.env.TERM;
if (!prevTerm) {
// workaround for https://github.com/sindresorhus/term-size/issues/13
process.env.TERM = 'xterm';
}
print(
boxen(
chalk.bold.yellow('WARN! ') +
str +
(slug ? `\nMore details: https://err.sh/now/${slug}` : ''),
{
padding: {
top: 0,
bottom: 0,
left: 1,
right: 1,
},
borderColor: 'yellow',
}
)
);
print('\n');
process.env.TERM = prevTerm;
}
function note(str: string) {
@@ -59,7 +82,7 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
_times: new Map(),
log(a: string, ...args: string[]) {
debug(format(a, ...args));
}
},
};
async function time(label: string, fn: Promise<any> | (() => Promise<any>)) {
@@ -85,6 +108,6 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
debug,
dim,
time,
note
note,
};
}

View File

@@ -58,9 +58,7 @@ export default async function preferV2Deployment({
)} is missing a ${cmd('start')} script. ${INFO}`;
}
} else if (!pkg && !hasDockerfile) {
return `Deploying to Now 2.0, because no ${highlight(
'Dockerfile'
)} was found. ${INFO}`;
return `Deploying to Now 2.0 automatically. ${INFO}`;
}
if (client && projectName) {

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

@@ -1,5 +0,0 @@
{
"scripts": {
"build": "./hugo"
}
}

View File

@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@@ -1238,7 +1238,7 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
define-properties@^1.1.2:
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -1660,7 +1660,7 @@ fsevents@^1.2.7:
nan "^2.12.1"
node-pre-gyp "^0.12.0"
function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
function-bind@^1.1.0, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
@@ -3305,21 +3305,21 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string.prototype.trimleft@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.0.0.tgz#68b6aa8e162c6a80e76e3a8a0c2e747186e271ff"
integrity sha1-aLaqjhYsaoDnbjqKDC50cYbicf8=
string.prototype.trimleft@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
dependencies:
define-properties "^1.1.2"
function-bind "^1.0.2"
define-properties "^1.1.3"
function-bind "^1.1.1"
string.prototype.trimright@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.0.0.tgz#ab4a56d802a01fbe7293e11e84f24dc8164661dd"
integrity sha1-q0pW2AKgH75yk+EehPJNyBZGYd0=
string.prototype.trimright@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
dependencies:
define-properties "^1.1.2"
function-bind "^1.0.2"
define-properties "^1.1.3"
function-bind "^1.1.1"
string_decoder@~0.10.31, string_decoder@~0.10.x:
version "0.10.31"

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

@@ -15,6 +15,11 @@ let port = 3000;
const binaryPath = path.resolve(__dirname, `../../scripts/start.js`);
const fixture = name => path.join('test', 'dev', 'fixtures', name);
// Adds Hugo to the PATH
process.env.PATH = `${path.resolve(fixture('08-hugo'))}${path.delimiter}${
process.env.PATH
}`;
function fetchWithRetry(url, retries = 3, opts = {}) {
return new Promise(async (resolve, reject) => {
try {
@@ -128,17 +133,15 @@ function testFixtureStdio(directory, fn) {
readyResolve = resolve;
});
console.log(`> testing ${directory}`);
dev = execa(binaryPath, ['dev', dir, '-l', port]);
dev.stderr.on('data', async data => {
output += data.toString();
if (data.toString().includes('Ready! Available at')) {
if (output.includes('Ready! Available at')) {
readyResolve();
}
if (
data.toString().includes('Command failed') ||
data.toString().includes('Error!')
) {
if (output.includes('Command failed') || output.includes('Error!')) {
dev.kill('SIGTERM');
console.log(output);
process.exit(1);
@@ -154,6 +157,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 +310,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 +327,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 +347,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 +365,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',
});
})
@@ -495,13 +514,12 @@ if (satisfies(process.version, '>= 6.9.0 <7.0.0 || >= 8.9.0')) {
test(
'[now dev] 06-gridsome',
testFixtureStdio('06-gridsome', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Hello, world!/gm);
t.regex(body, /<div id="app"><\/div>/gm);
})
);
} else {
@@ -513,8 +531,7 @@ if (satisfies(process.version, '>= 6.9.0 <7.0.0 || >= 8.9.0')) {
test(
'[now dev] 07-hexo-node',
testFixtureStdio('07-hexo-node', async (t, port) => {
const result = await fetchWithRetry(`http://localhost:${port}`, 180);
const response = await result;
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
validateResponseHeaders(t, response);
@@ -526,8 +543,7 @@ test(
test(
'[now dev] 08-hugo',
testFixtureStdio('08-hugo', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(t, response);
@@ -824,8 +840,7 @@ if (satisfies(process.version, '>= 8.10.0')) {
test(
'[now dev] 22-brunch',
testFixtureStdio('22-brunch', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
const response = await fetchWithRetry(`http://localhost:${port}`, 50);
validateResponseHeaders(t, response);
@@ -1098,8 +1113,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 +1132,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 +1143,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);
@@ -874,7 +915,7 @@ test('ensure we render a warning for deployments with no files', async t => {
// Ensure the warning is printed
t.true(
stderr.includes(
'> WARN! There are no files (or only files starting with a dot) inside your deployment.'
'WARN! There are no files (or only files starting with a dot) inside your deployment.'
)
);
@@ -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

@@ -17,7 +17,7 @@ import getURL from './helpers/get-url';
import {
npm as getNpmFiles_,
docker as getDockerFiles_,
staticFiles as getStaticFiles_
staticFiles as getStaticFiles_,
} from '../src/util/get-files';
import didYouMean from '../src/util/init/did-you-mean';
import { isValidName } from '../src/util/is-valid-name';
@@ -32,7 +32,7 @@ const fixture = name => join(prefix, name);
const getNpmFiles = async dir => {
const { pkg, nowConfig, hasNowJson } = await readMetadata(dir, {
quiet: true,
strict: false
strict: false,
});
return getNpmFiles_(dir, pkg, nowConfig, { hasNowJson, output });
@@ -41,7 +41,7 @@ const getNpmFiles = async dir => {
const getDockerFiles = async dir => {
const { nowConfig, hasNowJson } = await readMetadata(dir, {
quiet: true,
strict: false
strict: false,
});
return getDockerFiles_(dir, nowConfig, { hasNowJson, output });
@@ -51,7 +51,7 @@ const getStaticFiles = async (dir, isBuilds = false) => {
const { nowConfig, hasNowJson } = await readMetadata(dir, {
deploymentType: 'static',
quiet: true,
strict: false
strict: false,
});
return getStaticFiles_(dir, nowConfig, { hasNowJson, output, isBuilds });
@@ -169,11 +169,9 @@ test('`now.files` overrides `.gitignore` in Static with custom config path', asy
const path = 'now-json-static-gitignore-override';
// Simulate custom args passed by the user
process.argv = [...process.argv, '--local-config', './now.json']
process.argv = [...process.argv, '--local-config', './now.json'];
let files = await getStaticFiles(
fixture(path)
);
let files = await getStaticFiles(fixture(path));
files = files.sort(alpha);
@@ -185,9 +183,7 @@ test('`now.files` overrides `.gitignore` in Static with custom config path', asy
test('`now.files` overrides `.gitignore` in Static', async t => {
const path = 'now-json-static-gitignore-override';
let files = await getStaticFiles(
fixture(path)
);
let files = await getStaticFiles(fixture(path));
files = files.sort(alpha);
t.is(files.length, 3);
@@ -337,7 +333,7 @@ test('support docker', async t => {
test('gets correct name of docker deployment', async t => {
const { name, deploymentType } = await readMetadata(fixture('dockerfile'), {
quiet: true,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -375,7 +371,7 @@ test('throw for unsupported `now.json` type property', async t => {
try {
await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
} catch (err) {
t.is(err.code, 'unsupported_deployment_type');
@@ -387,7 +383,7 @@ test('support `now.json` files with package.json non quiet', async t => {
const f = fixture('now-json-no-name');
const { deploymentType } = await readMetadata(f, {
quiet: false,
strict: false
strict: false,
});
t.is(deploymentType, 'npm');
@@ -404,7 +400,7 @@ test('support `now.json` files with package.json non quiet', async t => {
test('support `now.json` files with package.json non quiet not specified', async t => {
const f = fixture('now-json-no-name');
const { deploymentType } = await readMetadata(f, {
strict: false
strict: false,
});
t.is(deploymentType, 'npm');
@@ -423,7 +419,7 @@ test('No commands in Dockerfile with automatic strictness', async t => {
try {
await readMetadata(f, {
quiet: true
quiet: true,
});
} catch (err) {
t.is(err.code, 'no_dockerfile_commands');
@@ -437,7 +433,7 @@ test('No commands in Dockerfile', async t => {
try {
await readMetadata(f, {
quiet: true,
strict: true
strict: true,
});
} catch (err) {
t.is(err.code, 'no_dockerfile_commands');
@@ -451,7 +447,7 @@ test('Missing Dockerfile for `docker` type', async t => {
try {
await readMetadata(f, {
quiet: true,
strict: true
strict: true,
});
} catch (err) {
t.is(err.code, 'dockerfile_missing');
@@ -463,7 +459,7 @@ test('support `now.json` files with Dockerfile', async t => {
const f = fixture('now-json-docker');
const { deploymentType, nowConfig, hasNowJson } = await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -479,7 +475,7 @@ test('load name from Dockerfile', async t => {
const f = fixture('now-json-docker-name');
const { deploymentType, name } = await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -490,7 +486,7 @@ test('support `now.json` files with Dockerfile non quiet', async t => {
const f = fixture('now-json-docker');
const { deploymentType, nowConfig, hasNowJson } = await readMetadata(f, {
quiet: false,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -507,7 +503,7 @@ test('throws when both `now.json` and `package.json:now` exist', async t => {
try {
await readMetadata(fixture('now-json-throws'), {
quiet: true,
strict: false
strict: false,
});
} catch (err) {
e = err;
@@ -523,7 +519,7 @@ test('throws when `package.json` and `Dockerfile` exist', async t => {
try {
await readMetadata(fixture('multiple-manifests-throws'), {
quiet: true,
strict: false
strict: false,
});
} catch (err) {
e = err;
@@ -536,7 +532,7 @@ test('support `package.json:now.type` to bypass multiple manifests error', async
const f = fixture('type-in-package-now-with-dockerfile');
const { type, nowConfig, hasNowJson } = await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
t.is(type, 'npm');
t.is(nowConfig.type, 'npm');
@@ -544,16 +540,16 @@ test('support `package.json:now.type` to bypass multiple manifests error', async
});
test('friendly error for malformed JSON', async t => {
const err = await t.throwsAsync(
() => readMetadata(fixture('json-syntax-error'), {
const err = await t.throwsAsync(() =>
readMetadata(fixture('json-syntax-error'), {
quiet: true,
strict: false
strict: false,
})
);
t.is(err.name, 'JSONError');
t.is(
err.message,
'Unexpected token \'o\' at 2:5 in test/fixtures/unit/json-syntax-error/package.json\n oops\n ^'
"Unexpected token 'o' at 2:5 in test/fixtures/unit/json-syntax-error/package.json\n oops\n ^"
);
});
@@ -597,7 +593,7 @@ test('`wait` utility does not invoke spinner before n miliseconds', async t => {
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {}
stop: () => {},
});
const timeOut = 200;
@@ -612,7 +608,7 @@ test('`wait` utility invokes spinner after n miliseconds', async t => {
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {}
stop: () => {},
});
const timeOut = 200;
@@ -635,7 +631,7 @@ test('`wait` utility does not invoke spinner when stopped before delay', async t
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {}
stop: () => {},
});
const timeOut = 200;
@@ -694,8 +690,8 @@ test('4xx response error as correct JSON', async t => {
const fn = async (req, res) => {
send(res, 400, {
error: {
message: 'The request is not correct'
}
message: 'The request is not correct',
},
});
};
@@ -721,7 +717,7 @@ test('5xx response error as HTML', async t => {
test('5xx response error with random JSON', async t => {
const fn = async (req, res) => {
send(res, 500, {
wrong: 'property'
wrong: 'property',
});
};
@@ -733,23 +729,27 @@ test('5xx response error with random JSON', async t => {
});
test('getProjectName with argv - option 1', t => {
const project = getProjectName({argv: {
name: 'abc'
}});
const project = getProjectName({
argv: {
name: 'abc',
},
});
t.is(project, 'abc');
});
test('getProjectName with argv - option 2', t => {
const project = getProjectName({argv: {
'--name': 'abc'
}});
const project = getProjectName({
argv: {
'--name': 'abc',
},
});
t.is(project, 'abc');
});
test('getProjectName with now.json', t => {
const project = getProjectName({
argv: {},
nowConfig: {name: 'abc'}
nowConfig: { name: 'abc' },
});
t.is(project, 'abc');
});
@@ -758,7 +758,7 @@ test('getProjectName with a file', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
isFile: true
isFile: true,
});
t.is(project, 'files');
});
@@ -767,7 +767,7 @@ test('getProjectName with a multiple files', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png']
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png'],
});
t.is(project, 'files');
});
@@ -776,7 +776,7 @@ test('getProjectName with a directory', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa']
paths: ['/tmp/aa'],
});
t.is(project, 'aa');
});
@@ -797,8 +797,8 @@ test('4xx error message with proper message', async t => {
const fn = async (req, res) => {
send(res, 403, {
error: {
message: 'This is a test'
}
message: 'This is a test',
},
});
};
@@ -813,8 +813,8 @@ test('5xx error message with proper message', async t => {
const fn = async (req, res) => {
send(res, 500, {
error: {
message: 'This is a test'
}
message: 'This is a test',
},
});
};
@@ -842,8 +842,8 @@ test('4xx response error as correct JSON with more properties', async t => {
send(res, 403, {
error: {
message: 'The request is not correct',
additionalProperty: 'test'
}
additionalProperty: 'test',
},
});
};
@@ -861,8 +861,8 @@ test('429 response error with retry header', async t => {
send(res, 429, {
error: {
message: 'You were rate limited'
}
message: 'You were rate limited',
},
});
};
@@ -878,8 +878,8 @@ test('429 response error without retry header', async t => {
const fn = async (req, res) => {
send(res, 429, {
error: {
message: 'You were rate limited'
}
message: 'You were rate limited',
},
});
};
@@ -891,8 +891,41 @@ test('429 response error without retry header', async t => {
t.is(formatted.retryAfter, undefined);
});
test('guess user\'s intention with custom didYouMean', async t => {
const examples = ['apollo','create-react-app','docz','gatsby','go','gridsome','html-minifier','mdx-deck','monorepo','nextjs','nextjs-news','nextjs-static','node-server','nodejs','nodejs-canvas-partyparrot','nodejs-coffee','nodejs-express','nodejs-hapi','nodejs-koa','nodejs-koa-ts','nodejs-pdfkit','nuxt-static','optipng','php-7','puppeteer-screenshot','python','redirect','serverless-ssr-reddit','static','vue','vue-ssr','vuepress'];
test("guess user's intention with custom didYouMean", async t => {
const examples = [
'apollo',
'create-react-app',
'docz',
'gatsby',
'go',
'gridsome',
'html-minifier',
'mdx-deck',
'monorepo',
'nextjs',
'nextjs-news',
'nextjs-static',
'node-server',
'nodejs',
'nodejs-canvas-partyparrot',
'nodejs-coffee',
'nodejs-express',
'nodejs-hapi',
'nodejs-koa',
'nodejs-koa-ts',
'nodejs-pdfkit',
'nuxt-static',
'optipng',
'php-7',
'puppeteer-screenshot',
'python',
'redirect',
'serverless-ssr-reddit',
'static',
'vue',
'vue-ssr',
'vuepress',
];
t.is(didYouMean('md', examples, 0.7), 'mdx-deck');
t.is(didYouMean('koa', examples, 0.7), 'nodejs-koa');
@@ -906,16 +939,26 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = null;
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
t.regex(reason, /Dockerfile/gm);
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.regex(reason, /Deploying to Now 2\.0 automatically/gm);
}
{
const localConfig = undefined;
const pkg = { scripts: { 'start': 'echo hi' } };
const pkg = { scripts: { start: 'echo hi' } };
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
@@ -924,16 +967,26 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = { scripts: { 'now-start': 'echo hi' } };
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
{
const localConfig = { 'version': 1 };
const localConfig = { version: 1 };
const pkg = null;
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
@@ -942,16 +995,26 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = null;
const hasDockerfile = true;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
{
const localConfig = undefined;
const pkg = { scripts: { 'build': 'echo hi' } };
const pkg = { scripts: { build: 'echo hi' } };
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.regex(reason, /package\.json/gm);
}
@@ -960,7 +1023,12 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = null;
const hasDockerfile = false;
const hasServerfile = true;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
});

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.3.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,16 +1,3 @@
import { ChildProcess, fork } from 'child_process';
import url from 'url'
import {
pathExists,
readFile,
unlink as unlinkFile,
writeFile,
} from 'fs-extra';
import os from 'os';
import path from 'path';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import {
BuildOptions,
Config,
@@ -20,6 +7,7 @@ import {
FileBlob,
FileFsRef,
Files,
getLambdaOptionsFromFunction,
getNodeVersion,
getSpawnOptions,
glob,
@@ -30,10 +18,24 @@ import {
Route,
runNpmInstall,
runPackageJsonScript,
getLambdaOptionsFromFunction,
} from '@now/build-utils';
import {
convertRedirects,
convertRewrites,
} from '@now/routing-utils/dist/superstatic';
import nodeFileTrace, { NodeFileTraceReasons } from '@zeit/node-file-trace';
import { ChildProcess, fork } from 'child_process';
import {
lstatSync,
pathExists,
readFile,
unlink as unlinkFile,
writeFile,
} from 'fs-extra';
import os from 'os';
import path from 'path';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import createServerlessConfig from './create-serverless-config';
import nextLegacyVersions from './legacy-versions';
import {
@@ -43,10 +45,14 @@ import {
excludeFiles,
ExperimentalTraceVersion,
getDynamicRoutes,
getExportIntent,
getExportStatus,
getNextConfig,
getPathsInside,
getPrerenderManifest,
getRoutes,
getRoutesManifest,
getSourceFilePathFromPage,
isDynamicRoute,
normalizePackageJson,
normalizePage,
@@ -54,15 +60,8 @@ import {
stringMap,
syncEnvVars,
validateEntrypoint,
getSourceFilePathFromPage,
getRoutesManifest,
} from './utils';
import {
convertRedirects,
convertRewrites
} from '@now/routing-utils/dist/superstatic'
interface BuildParamsMeta {
isDev: boolean | undefined;
env?: EnvConfig;
@@ -77,7 +76,7 @@ interface BuildParamsType extends BuildOptions {
}
export const version = 2;
const htmlContentType = 'text/html; charset=utf-8';
const nowDevChildProcesses = new Set<ChildProcess>();
['SIGINT', 'SIGTERM'].forEach(signal => {
@@ -163,24 +162,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 +197,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 +333,122 @@ 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.'
);
}
}
}
const exportIntent = await getExportIntent(entryPath);
const userExport = await getExportStatus(entryPath);
if (exportIntent || userExport) {
const { trailingSlash = false } = exportIntent || {};
if (!userExport) {
await writePackageJson(entryPath, {
...pkg,
scripts: {
...pkg.scripts,
'now-automatic-next-export': `next export --outdir "${path.resolve(
entryPath,
'out'
)}"`,
},
});
await runPackageJsonScript(entryPath, 'now-automatic-next-export', {
...spawnOpts,
env,
});
}
const resultingExport = await getExportStatus(entryPath);
if (!resultingExport) {
throw new Error(
'Exporting Next.js app failed. Please check your build logs and contact us if this continues.'
);
}
if (resultingExport.success !== true) {
throw new Error(
'Export of Next.js app failed. Please check your build logs.'
);
}
const outDirectory = resultingExport.outDirectory;
debug(`next export should use trailing slash: ${trailingSlash}`);
// This handles pages, `public/`, and `static/`.
const filesAfterBuild = await glob('**', outDirectory);
const output: Files = { ...filesAfterBuild };
// Strip `.html` extensions from build output
Object.entries(output)
.filter(([name]) => name.endsWith('.html'))
.forEach(([name, value]) => {
const cleanName = name.slice(0, -5);
delete output[name];
output[cleanName] = value;
if (value.type === 'FileBlob' || value.type === 'FileFsRef') {
value.contentType = value.contentType || 'text/html; charset=utf-8';
}
});
return {
output,
routes: [
// TODO: low priority: handle trailingSlash
// redirects take the highest priority
...redirects,
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// 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 pages, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
...rewrites,
// Dynamic routes
// TODO: do we want to do this?: ...dynamicRoutes,
],
watch: [],
childProcesses: [],
};
}
if (isLegacy) {
debug('Running npm install --production...');
await runNpmInstall(
@@ -353,7 +463,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 +593,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 +694,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 +850,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 +934,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 +942,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 +958,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 +1001,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 +1021,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();
@@ -576,6 +624,73 @@ export type NextPrerenderedRoutes = {
};
};
export async function getExportIntent(
entryPath: string
): Promise<false | { trailingSlash: boolean }> {
const pathExportMarker = path.join(entryPath, '.next', 'export-marker.json');
const hasExportMarker: boolean = await fs
.access(pathExportMarker, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
if (!hasExportMarker) {
return false;
}
const manifest: {
version: 1;
exportTrailingSlash: boolean;
hasExportPathMap: boolean;
} = JSON.parse(await fs.readFile(pathExportMarker, 'utf8'));
switch (manifest.version) {
case 1: {
if (manifest.hasExportPathMap !== true) {
return false;
}
return { trailingSlash: manifest.exportTrailingSlash };
}
default: {
return false;
}
}
}
export async function getExportStatus(
entryPath: string
): Promise<false | { success: boolean; outDirectory: string }> {
const pathExportDetail = path.join(entryPath, '.next', 'export-detail.json');
const hasExportDetail: boolean = await fs
.access(pathExportDetail, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
if (!hasExportDetail) {
return false;
}
const manifest: {
version: 1;
success: boolean;
outDirectory: string;
} = JSON.parse(await fs.readFile(pathExportDetail, 'utf8'));
switch (manifest.version) {
case 1: {
return {
success: !!manifest.success,
outDirectory: manifest.outDirectory,
};
}
default: {
return false;
}
}
}
export async function getPrerenderManifest(
entryPath: string
): Promise<NextPrerenderedRoutes> {

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

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