Create monorepo (#2812)

* Move now-cli to /packages/now-cli

* Fix .gitignore paths

* Add now-client

* Add lerna to top level

* Add scripts

* Update codeowners

* Fix `/now-cli/build.ts` script

* Fix circleci path to artifacts

* Use relative paths

* Fix path to scripts

* Add test-lint script

* Add missing return type

* Fix typo in test-lint

* Fix string match in shell scripts

* Fix path to hugo

* Add package node_modules

* Delete lock files in packages, use root yarn.lock

* Add missing b.js file

* Add test-integration-now-dev script

* Add missing test files

* Add missing integration test script

* Add missing test files

* Delete travis.yml

* Fix ts-jest in now-client

* Add support for Node 8 (ES2015 target)

* Add support for Node 8

* Add polyfill for Node 8

* Fix polyfill for Node 8

* Only run coverage for now-cli

* Add packages from now-builders

* Run integration tests for builders

* Add node_modules to cache

* Add root readme.md

* Move readme to top level

* Add yarn bootstrap

* Add bootstrap step

* Add dist to `persist_to_workspace`

* Fix 08-yarn-npm integration test

* Remove duplicate path

* Change stdio to inherit

* Add back store_artifacts

* testing - remove bootstrap step

* Add back now-build-utils

* Remove bootstrap step

* Fix test again

* Add console.log()

* Fix lint

* Use local ncc version

* Install go

* Revert changes to stdio and console.log()

* Add missing now-go test

* Add missing integration tests

* Add --runInBand flag

* Fix now-node-bridge persistence

* Add missing symlinks

* Add codeowners

* Consolidate into single run.sh function

* Run uniq

* Fix typo

* Change now-routing-utils to test-unit

* Special case test for node 8

* Add docs from builders

* Only run script for modified packages

* Add test-integration-once which only runs once

* Fix set intersection
This commit is contained in:
Steven
2019-08-23 19:57:00 -04:00
committed by kodiakhq[bot]
parent 774e0a0ebb
commit b0ad5238f7
2161 changed files with 77698 additions and 17851 deletions

View File

@@ -5,6 +5,8 @@ jobs:
docker: docker:
- image: circleci/node:10 - image: circleci/node:10
working_directory: ~/repo working_directory: ~/repo
environment:
GOPATH: $HOME/go
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -12,17 +14,47 @@ jobs:
- v1-dependencies-{{ checksum "yarn.lock" }} - v1-dependencies-{{ checksum "yarn.lock" }}
# fallback to using the latest cache if no exact match is found # fallback to using the latest cache if no exact match is found
- v1-dependencies- - v1-dependencies-
- run:
name: Updating apt packages
command: sudo apt-get update
- run:
name: Installing the latest version of Go
command: sudo apt-get install golang-go
- run: - run:
name: Installing Dependencies name: Installing Dependencies
command: yarn command: yarn install
- save_cache: - save_cache:
paths: paths:
- node_modules - node_modules
- packages/now-build-utils/node_modules
- packages/now-cgi/node_modules
- packages/now-cli/node_modules
- packages/now-client/node_modules
- packages/now-go/node_modules
- packages/now-next/node_modules
- packages/now-node/node_modules
- packages/now-node-bridge/node_modules
- packages/now-python/node_modules
- packages/now-routing-utils/node_modules
- packages/now-ruby/node_modules
- packages/now-static-build/node_modules
key: v1-dependencies-{{ checksum "yarn.lock" }} key: v1-dependencies-{{ checksum "yarn.lock" }}
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: paths:
- node_modules - node_modules
- packages/now-build-utils/node_modules
- packages/now-cgi/node_modules
- packages/now-cli/node_modules
- packages/now-client/node_modules
- packages/now-go/node_modules
- packages/now-next/node_modules
- packages/now-node/node_modules
- packages/now-node-bridge/node_modules
- packages/now-python/node_modules
- packages/now-routing-utils/node_modules
- packages/now-ruby/node_modules
- packages/now-static-build/node_modules
build: build:
docker: docker:
@@ -39,12 +71,26 @@ jobs:
name: Building name: Building
command: yarn build command: yarn build
- store_artifacts: - store_artifacts:
path: dist path: packages/now-cli/dist
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: paths:
- assets - packages/now-build-utils/dist
- dist - packages/now-cgi/dist
- packages/now-cli/dist
- packages/now-cli/assets
- packages/now-client/dist
- packages/now-go/dist
- packages/now-next/dist
- packages/now-node/dist
- packages/now-node/test/fixtures/15-helpers/ts/types.d.ts
- packages/now-node/test/fixtures/11-symlinks/symlink
- packages/now-node-bridge/index.js
- packages/now-node-bridge/bridge.js
- packages/now-python/dist
- packages/now-routing-utils/dist
- packages/now-ruby/dist
- packages/now-static-build/dist
test-lint: test-lint:
docker: docker:
@@ -56,7 +102,7 @@ jobs:
at: . at: .
- run: - run:
name: Compiling `now dev` HTML error templates name: Compiling `now dev` HTML error templates
command: node ./scripts/compile-templates.js command: node packages/now-cli/scripts/compile-templates.js
- run: - run:
name: Linting Code name: Linting Code
command: yarn test-lint command: yarn test-lint
@@ -71,14 +117,14 @@ jobs:
at: . at: .
- run: - run:
name: Compiling `now dev` HTML error templates name: Compiling `now dev` HTML error templates
command: node ./scripts/compile-templates.js command: node packages/now-cli/scripts/compile-templates.js
- run: - run:
name: Running Unit Tests name: Running Unit Tests
command: yarn test-unit --clean false command: yarn test-unit --clean false
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: paths:
- .nyc_output - packages/now-cli/.nyc_output
test-integration-macos-node-8: test-integration-macos-node-8:
macos: macos:
@@ -189,7 +235,7 @@ jobs:
command: node --version command: node --version
- run: - run:
name: Downloading Hugo name: Downloading Hugo
command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo test/dev/fixtures/08-hugo command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: - run:
name: Running Integration Tests for `now dev` name: Running Integration Tests for `now dev`
command: yarn test-integration-now-dev --clean false command: yarn test-integration-now-dev --clean false
@@ -207,7 +253,7 @@ jobs:
command: node --version command: node --version
- run: - run:
name: Downloading Hugo name: Downloading Hugo
command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo test/dev/fixtures/08-hugo command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: - run:
name: Running Integration Tests for `now dev` name: Running Integration Tests for `now dev`
command: yarn test-integration-now-dev --clean false command: yarn test-integration-now-dev --clean false
@@ -225,7 +271,7 @@ jobs:
command: node --version command: node --version
- run: - run:
name: Downloading Hugo name: Downloading Hugo
command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo test/dev/fixtures/08-hugo command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: - run:
name: Running Integration Tests for `now dev` name: Running Integration Tests for `now dev`
command: yarn test-integration-now-dev --clean false command: yarn test-integration-now-dev --clean false
@@ -243,7 +289,7 @@ jobs:
command: node --version command: node --version
- run: - run:
name: Downloading Hugo name: Downloading Hugo
command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.55.6/hugo_0.55.6_Linux-64bit.tar.gz && tar -xzf hugo_0.55.6_Linux-64bit.tar.gz && mv ./hugo test/dev/fixtures/08-hugo command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.55.6/hugo_0.55.6_Linux-64bit.tar.gz && tar -xzf hugo_0.55.6_Linux-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: - run:
name: Running Integration Tests for `now dev` name: Running Integration Tests for `now dev`
command: yarn test-integration-now-dev --clean false command: yarn test-integration-now-dev --clean false
@@ -261,7 +307,7 @@ jobs:
command: node --version command: node --version
- run: - run:
name: Downloading Hugo name: Downloading Hugo
command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.55.6/hugo_0.55.6_Linux-64bit.tar.gz && tar -xzf hugo_0.55.6_Linux-64bit.tar.gz && mv ./hugo test/dev/fixtures/08-hugo command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.55.6/hugo_0.55.6_Linux-64bit.tar.gz && tar -xzf hugo_0.55.6_Linux-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: - run:
name: Running Integration Tests for `now dev` name: Running Integration Tests for `now dev`
command: yarn test-integration-now-dev --clean false command: yarn test-integration-now-dev --clean false
@@ -279,11 +325,26 @@ jobs:
command: node --version command: node --version
- run: - run:
name: Downloading Hugo name: Downloading Hugo
command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.55.6/hugo_0.55.6_Linux-64bit.tar.gz && tar -xzf hugo_0.55.6_Linux-64bit.tar.gz && mv ./hugo test/dev/fixtures/08-hugo command: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.55.6/hugo_0.55.6_Linux-64bit.tar.gz && tar -xzf hugo_0.55.6_Linux-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: - run:
name: Running Integration Tests for `now dev` name: Running Integration Tests for `now dev`
command: yarn test-integration-now-dev --clean false command: yarn test-integration-now-dev --clean false
test-integration-once:
docker:
- image: circleci/node:10
working_directory: ~/repo
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Output version
command: node --version
- run:
name: Running Integration Tests Once
command: yarn test-integration-once --clean false
coverage: coverage:
docker: docker:
- image: circleci/node:10 - image: circleci/node:10
@@ -294,7 +355,7 @@ jobs:
at: . at: .
- run: - run:
name: Run coverage report name: Run coverage report
command: yarn coverage command: yarn workspace now run coverage
source-maps: source-maps:
docker: docker:
@@ -445,6 +506,12 @@ workflows:
filters: filters:
tags: tags:
only: /.*/ only: /.*/
- test-integration-once:
requires:
- build
filters:
tags:
only: /.*/
- coverage: - coverage:
requires: requires:
- test-unit - test-unit
@@ -460,6 +527,7 @@ workflows:
- test-integration-linux-now-dev-node-8 - test-integration-linux-now-dev-node-8
- test-integration-linux-now-dev-node-10 - test-integration-linux-now-dev-node-10
- test-integration-linux-now-dev-node-12 - test-integration-linux-now-dev-node-12
- test-integration-once
- test-lint - test-lint
filters: filters:
tags: tags:

34
.circleci/publish.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
set -euo pipefail
if [ -z "$NPM_TOKEN" ]; then
echo "NPM_TOKEN not found. Did you forget to assign the GitHub Action secret?"
exit 1
fi
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
if [ ! -e ~/.npmrc ]; then
echo "~/.npmrc file does not exist, skipping publish"
exit 0
fi
npm_tag=""
tag="$(git describe --tags --exact-match 2> /dev/null || :)"
if [ -z "$tag" ]; then
echo "Not a tagged commit, skipping publish"
exit 0
fi
if [[ "$tag" =~ -canary ]]; then
echo "Publishing canary release"
npm_tag="--npm-tag canary"
else
echo "Publishing stable release"
fi
# Sometimes this is a false alarm and blocks publish
git checkout yarn.lock
yarn run lerna publish from-git $npm_tag --yes

23
.circleci/run.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail
if [ "$1" == "" ]; then
echo "Please provide at least one argument"
exit 1
fi
circleci_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
found="$(grep -rn \"""$1"\"": packages/*/package.json | cut -d '/' -f2 | uniq)"
modified="$(git diff origin/canary...HEAD --name-only | grep '^packages/' | cut -d '/' -f2 | uniq)"
echo "$found" > /tmp/found.txt
echo "$modified" > /tmp/modified.txt
pkgs="$(comm -12 <(sort /tmp/found.txt) <(sort /tmp/modified.txt))"
echo "The following packages were modified: "
echo "$pkgs" | wc -l
echo "$pkgs"
for dir in $pkgs; do
echo "cd packages/$dir && yarn $1"
cd "$circleci_dir/../packages/$dir"
yarn "$1"
done

20
.github/CODEOWNERS vendored
View File

@@ -1,8 +1,18 @@
# Documentation # Documentation
# https://help.github.com/en/articles/about-code-owners # https://help.github.com/en/articles/about-code-owners
* @tootallnate @leo * @tootallnate @leo
/src/commands/dev/ @tootallnate @leo @styfle @AndyBitz /packages/now-cli/src/commands/dev/ @tootallnate @leo @styfle @AndyBitz
/src/util/dev/ @tootallnate @leo @styfle @AndyBitz /packages/now-cli/src/util/dev/ @tootallnate @leo @styfle @AndyBitz
/src/commands/domains/ @javivelasco @mglagola @anatrajkovska /packages/now-cli/src/commands/domains/ @javivelasco @mglagola @anatrajkovska
/src/commands/certs/ @javivelasco @mglagola @anatrajkovska /packages/now-cli/src/commands/certs/ @javivelasco @mglagola @anatrajkovska
/packages/now-client @leo @rdev
/packages/now-build-utils @styfle @AndyBitz
/packages/now-node @styfle @tootallnate @lucleray
/packages/now-node-bridge @styfle @tootallnate @lucleray
/packages/now-next @Timer
/packages/now-go @styfle @sophearak
/packages/now-python @styfle @sophearak
/packages/now-ruby @styfle @coetry @nathancahill
/packages/now-static-build @styfle @AndyBitz
/packages/now-routing-utils @dav-is

17
.gitignore vendored
View File

@@ -1,7 +1,6 @@
node_modules node_modules
package-lock.json package-lock.json
dist dist
test/fixtures/integration
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
.nyc_output .nyc_output
@@ -9,11 +8,11 @@ coverage
*.swp *.swp
*.bak *.bak
*.tgz *.tgz
.builders packages/now-cli/.builders
/assets packages/now-cli/assets
/src/util/dev/templates/*.ts packages/now-cli/src/util/dev/templates/*.ts
/test/**/yarn.lock packages/now-cli/test/**/yarn.lock
/test/**/node_modules packages/now-cli/test/**/node_modules
/test/dev/fixtures/08-hugo/hugo packages/now-cli/test/dev/fixtures/08-hugo/hugo
/test/dev/fixtures/**/dist packages/now-cli/test/dev/fixtures/**/dist
/test/dev/fixtures/**/public packages/now-cli/test/dev/fixtures/**/public

96
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,96 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via [GitHub Issue](https://github.com/zeit/now-builders/issues/new) or [Spectrum](https://spectrum.chat/zeit) with the owners of this repository before submitting a Pull Request.
Please read our [code of conduct](CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
## Local development
This project is configured in a monorepo pattern where one repo contains multiple npm packages. Dependencies are installed and managed with `yarn`, not `npm` CLI.
To get started, execute the following:
```
git clone https://github.com/zeit/now-builders
yarn install
yarn bootstrap
yarn build
yarn lint
yarn test
```
Make sure all the tests pass before making changes.
## Verifying your change
Once you are done with your changes (we even suggest doing it along the way ), make sure all the test still run by running
```
yarn build && yarn test
```
from the root of the project.
If any test fails, make sure to fix it along with your changes. See [Interpreting test errors](#Interpreting-test-errors) for more information about how the tests are executed, especially the integration tests.
## Pull Request Process
Once you are confident that your changes work properly, open a pull request on the main repository.
The pull request will be reviewed by the maintainers and the tests will be checked by our continuous integration platform.
## Interpreting test errors
There are 2 kinds of tests in this repository Unit tests and Integration tests.
Unit tests are run locally with `jest` and execute quickly because they are testing the smallest units of code.
### Integration tests
Integration tests create deployments to your ZEIT account using the `test` project name. After each test is deployed, the `probes` key is used to check if the response is the expected value. If the value doesn't match, you'll see a message explaining the difference. If the deployment failed to build, you'll see a more generic message like the following:
```
[Error: Fetched page https://test-8ashcdlew.now.sh/root.js does not contain hello Root!. Instead it contains An error occurred with this application.
NO_STATUS_CODE_FRO Response headers:
cache-control=s-maxage=0
connection=close
content-type=text/plain; charset=utf-8
date=Wed, 19 Jun 2019 18:01:37 GMT
server=now
strict-transport-security=max-age=63072000
transfer-encoding=chunked
x-now-id=iad1:hgtzj-1560967297876-44ae12559f95
x-now-trace=iad1]
```
In such cases you can visit the URL of the failed deployment and append `/_logs` so see the build error. In the case above, that would be https://test-8ashcdlew.now.sh/_logs
The logs of this deployment will contain the actual error which may help you to understand what went wrong.
### @zeit/node-file-trace
Some of the Builders use `@zeit/node-file-trace` to tree-shake files before deployment. If you suspect an error with this tree-shaking mechanism, you can create the following script in your project:
```js
const trace = require('@zeit/node-file-trace');
trace(['path/to/entrypoint.js'], {
ts: true,
mixedModules: true,
})
.then(o => console.log(o.fileList))
.then(e => console.error(e));
```
When you run this script, you'll see all imported files. If anything file is missing, the bug is in [@zeit/node-file-trace](https://github.com/zeit/node-file-trace) and not the Builder.
## Deploy a Builder with existing project
Sometimes you want to test changes to a Builder against an existing project, maybe with `now dev` or an actual deployment. You can avoid publishing every Builder change to npm by uploading the Builder as a tarball.
1. Change directory to the desired Builder `cd ./packages/now-node`
2. Run `yarn build` to compile typescript and other build steps
3. Run `npm pack` to create a tarball file
4. Run `now *.tgz` to upload the tarball file and get a URL
5. Edit any existing `now.json` project and replace `use` with the URL
6. Run `now` or `now dev` to deploy with the experimental Builder

447
DEVELOPING_A_BUILDER.md Normal file
View File

@@ -0,0 +1,447 @@
# Builders Developer Reference
The following page is a reference for how to create a Builder using the available Builder's API.
A Builder is an npm module that exposes a `build` function and optionally an `analyze` function and `prepareCache` function.
Official Builders 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 Builder.
See the [Builders Documentation](https://zeit.co/docs/v2/advanced/builders) to view example usage.
## Builder Exports
### `version`
A **required** exported constant that decides which version of the Builder API to use.
The latest and suggested version is `2`.
### `analyze`
An **optional** exported function that returns a unique fingerprint used for the purpose of [build de-duplication](https://zeit.co/docs/v2/advanced/concepts/immutability#deduplication-algorithm). If the `analyze` function is not supplied, a random fingerprint is assigned to each build.
```js
export analyze({
files: Files,
entrypoint: String,
workPath: String,
config: Object
}) : String fingerprint
```
If you are using TypeScript, you should use the following types:
```ts
import { AnalyzeOptions } from '@now/build-utils'
export analyze(options: AnalyzeOptions) {
return 'fingerprint goes here'
}
```
### `build`
A **required** exported function that returns a [Files](#files) data structure that contains the Build outputs, which can be a [Static File](#file) or a [Serverless Function](#serverless-function).
What's a Serverless Function? Read about [Serverless Function concepts](https://zeit.co/docs/v2/deployments/concepts/lambdas) to learn more.
```js
build({
files: Files,
entrypoint: String,
workPath: String,
config: Object,
meta?: {
isDev?: Boolean,
requestPath?: String,
filesChanged?: Array<String>,
filesRemoved?: Array<String>
}
}) : {
watch: Array<String>,
output: Files output,
routes: Object
}
```
If you are using TypeScript, you should use the following types:
```ts
import { BuildOptions } from '@now/build-utils'
export build(options: BuildOptions) {
// Build the code here
return {
output: {
'path-to-file': File,
'path-to-lambda': Lambda
},
watch: [],
routes: {}
}
}
```
### `prepareCache`
An **optional** exported function that is equivalent to [`build`](#build), but it executes the instructions necessary to prepare a cache for the next run.
```js
prepareCache({
files: Files,
entrypoint: String,
workPath: String,
cachePath: String,
config: Object
}) : Files cacheOutput
```
If you are using TypeScript, you can import the types for each of these functions by using the following:
```ts
import { PrepareCacheOptions } from '@now/build-utils'
export prepareCache(options: PrepareCacheOptions) {
return { 'path-to-file': File }
}
```
### `shouldServe`
An **optional** exported function that is only used by `now dev` in [Now CLI](https:///download) and indicates whether a [Builder](https://zeit.co/docs/v2/advanced/builders) wants to be responsible for building a certain request path.
```js
shouldServe({
entrypoint: String,
files: Files,
config: Object,
requestPath: String,
workPath: String
}) : Boolean
```
If you are using TypeScript, you can import the types for each of these functions by using the following:
```ts
import { ShouldServeOptions } from '@now/build-utils'
export shouldServe(options: ShouldServeOptions) {
return Boolean
}
```
If this method is not defined, Now CLI will default to [this function](https://github.com/zeit/now-builders/blob/52994bfe26c5f4f179bdb49783ee57ce19334631/packages/now-build-utils/src/should-serve.ts).
### Builder Options
The exported functions [`analyze`](#analyze), [`build`](#build), and [`prepareCache`](#preparecache) receive one argument with the following properties.
**Properties:**
- `files`: All source files of the project as a [Files](#files) data structure.
- `entrypoint`: Name of entrypoint file for this particular build job. Value `files[entrypoint]` is guaranteed to exist and be a valid [File](#files) reference. `entrypoint` is always a discrete file and never a glob, since globs are expanded into separate builds at deployment time.
- `workPath`: A writable temporary directory where you are encouraged to perform your build process. This directory will be populated with the restored cache from the previous run (if any) for [`analyze`](#analyze) and [`build`](#build).
- `cachePath`: A writable temporary directory where you can build a cache for the next run. This is only passed to `prepareCache`.
- `config`: An arbitrary object passed from by the user in the [Build definition](#defining-the-build-step) in `now.json`.
## Example: html-minifier
Let's walk through what it takes to create a simple builder that takes in a HTML source file and yields a minified HTML static file as its build output.
While this is a very simple builder, the approach demonstrated here can be used to return anything: one or more static files and/or one or more lambdas.
## Setting up the module
### Defining the analyze step
The `analyze` hook is optional. Its goal is to give the developer a tool to avoid wasting time _re-computing a build_ that has already occurred.
The return value of `analyze` is a _fingerprint_: a simple string that uniquely identifies the build process.
If `analyze` is not specified, its behavior is to use as the fingerprint the combined checksums of **all the files in the same directory level as the entrypoint**. This is a default that errs on making sure that we re-execute builds when files _other than the entrypoint_ (like dependencies, manifest files, etc) have changed.
For our `html-minify` example, we know that HTML files don't have dependencies. Therefore, our analyze step can just return the `digest` of the entrypoint.
Our `index.js` file looks as follows:
```js
exports.analyze = function({ files, entrypoint }) {
return files[entrypoint].digest
}
```
This means that we will only re-minify and re-create the build output _only if the file contents (and therefore its digest) change._
### Defining the build step
Your module will need some utilities to manipulate the data structures we pass you, create new ones and alter the filesystem.
To that end, we expose our API as part of a `@now/build-utils` package. This package is always loaded on your behalf, so make sure it's only included as `peerDependencies` in your `package.json`.
Builders can include dependencies of their liking:
```js
const htmlMinifier = require('html-minifier')
exports.version = 2
exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest
exports.build = async ({ files, entrypoint, config }) => {
const stream = files[entrypoint].toStream()
const options = Object.assign({}, config || {})
const { data } = await FileBlob.fromStream({ stream })
const content = data.toString()
const minified = htmlMinifier(content, options)
const result = new FileBlob({ data: minified })
return {
output: {
[entrypoint]: result
},
watch: [],
routes: {}
}
}
```
### Defining a `prepareCache` step
If our builder had performed work that could be re-used in the next build invocation, we could define a `prepareCache` step.
In this case, there are not intermediate artifacts that we can cache, and our `analyze` step already takes care of caching the full output based on the fingerprint of the input.
## Technical Details
### Execution Context
A [Serverless Function](https://zeit.co/docs/v2/advanced/concepts/lambdas) is created where the builder logic is executed. The lambda is run using the Node.js 8 runtime. A brand new sandbox is created for each deployment, for security reasons. The sandbox is cleaned up between executions to ensure no lingering temporary files are shared from build to build.
All the APIs you export ([`analyze`](#analyze), [`build`](#build) and [`prepareCache`](#preparecache)) are not guaranteed to be run in the same process, but the filesystem we expose (e.g.: `workPath` and the results of calling [`getWriteableDirectory`](#getWriteableDirectory) ) is retained.
If you need to share state between those steps, use the filesystem.
### Directory and Cache Lifecycle
When a new build is created, we pre-populate the `workPath` supplied to `analyze` with the results of the `prepareCache` step of the previous build.
The `analyze` step can modify that directory, and it will not be re-created when it's supplied to `build` and `prepareCache`.
To learn how the cache key is computed and invalidated, refer to the [overview](https://zeit.co/docs/v2/advanced/builders#technical-details).
### Accessing Environment and Secrets
The env and secrets specified by the user as `build.env` are passed to the builder process. This means you can access user env via `process.env` in Node.js.
### Utilities as peerDependencies
When you publish your builder to npm, make sure to not specify `@now/build-utils` (as seen below in the API definitions) as a dependency, but rather as part of `peerDependencies`.
## Types
### `Files`
```ts
import { File } from '@now/build-utils'
type Files = { [filePath: string]: File }
```
This is an abstract type that is implemented as a plain [JavaScript Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object). It's helpful to think of it as a virtual filesystem representation.
When used as an input, the `Files` object will only contain `FileRefs`. When `Files` is an output, it may consist of `Lambda` (Serverless Functions) types as well as `FileRefs`.
An example of a valid output `Files` object is:
```json
{
"index.html": FileRef,
"api/index.js": Lambda
}
```
### `File`
This is an abstract type that can be imported if you are using TypeScript.
```ts
import { File } from '@now/build-utils'
```
Valid `File` types include:
- [`FileRef`](#fileref)
- [`FileFsRef`](#filefsref)
- [`FileBlob`](#fileblob)
### `FileRef`
```ts
import { FileRef } from '@now/build-utils'
```
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract file instance stored in our platform, based on the file identifier string (its checksum). When a `Files` object is passed as an input to `analyze` or `build`, all its values will be instances of `FileRef`.
**Properties:**
- `mode : Number` file mode
- `digest : String` a checksum that represents the file
**Methods:**
- `toStream() : Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
### `FileFsRef`
```ts
import { FileFsRef } from '@now/build-utils'
```
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract instance of a file present in the filesystem that the build process is executing in.
**Properties:**
- `mode : Number` file mode
- `fsPath : String` the absolute path of the file in file system
**Methods:**
- `static async fromStream({ mode : Number, stream : Stream, fsPath : String }) : FileFsRef` creates an instance of a [FileFsRef](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) from `Stream`, placing file at `fsPath` with `mode`
- `toStream() : Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
### `FileBlob`
```ts
import { FileBlob } from '@now/build-utils'
```
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) that represents an abstract instance of a file present in memory.
**Properties:**
- `mode : Number` file mode
- `data : String | Buffer` the body of the file
**Methods:**
- `static async fromStream({ mode : Number, stream : Stream }) :FileBlob` creates an instance of a [FileBlob](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) from [`Stream`](https://nodejs.org/api/stream.html) with `mode`
- `toStream() : Stream` creates a [Stream](https://nodejs.org/api/stream.html) of the file body
### `Lambda`
```ts
import { Lambda } from '@now/build-utils'
```
This is a [JavaScript class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), called a Serverless Function, that can be created by supplying `files`, `handler`, `runtime`, and `environment` as an object to the [`createLambda`](#createlambda) helper. The instances of this class should not be created directly. Instead use a call to [`createLambda`](#createlambda).
**Properties:**
- `files : Files` the internal filesystem of the lambda
- `handler : String` path to handler file and (optionally) a function name it exports
- `runtime : LambdaRuntime` the name of the lambda runtime
- `environment : Object` key-value map of handler-related (aside of those passed by user) environment variables
### `LambdaRuntime`
This is an abstract enumeration type that is implemented by one of the following possible `String` values:
- `nodejs10.x`
- `nodejs8.10`
- `go1.x`
- `java-1.8.0-openjdk`
- `python3.6`
- `python2.7`
- `dotnetcore2.1`
- `dotnetcore2.0`
- `dotnetcore1.0`
## JavaScript API
The following is exposed by `@now/build-utils` to simplify the process of writing Builders, manipulating the file system, using the above types, etc.
### `createLambda`
Signature: `createLambda(Object spec) : Lambda`
```ts
import { createLambda } from '@now/build-utils'
```
Constructor for the [`Lambda`](#lambda) type.
```js
const { createLambda, FileBlob } = require('@now/build-utils')
await createLambda({
runtime: 'nodejs8.10',
handler: 'index.main',
files: {
'index.js': new FileBlob({ data: 'exports.main = () => {}' })
}
})
```
### `download`
Signature: `download() : Files`
```ts
import { download } from '@now/build-utils'
```
This utility allows you to download the contents of a [`Files`](#files) data structure, therefore creating the filesystem represented in it.
Since `Files` is an abstract way of representing files, you can think of `download` as a way of making that virtual filesystem _real_.
If the **optional** `meta` property is passed (the argument for [build](#build)), only the files that have changed are downloaded. This is decided using `filesRemoved` and `filesChanged` inside that object.
```js
await download(files, workPath, meta)
```
### `glob`
Signature: `glob() : Files`
```ts
import { glob } from '@now/build-utils'
```
This utility allows you to _scan_ the filesystem and return a [`Files`](#files) representation of the matched glob search string. It can be thought of as the reverse of [`download`](#download).
The following trivial example downloads everything to the filesystem, only to return it back (therefore just re-creating the passed-in [`Files`](#files)):
```js
const { glob, download } = require('@now/build-utils')
exports.build = ({ files, workPath }) => {
await download(files, workPath)
return glob('**', workPath)
}
```
### `getWriteableDirectory`
Signature: `getWriteableDirectory() : String`
```ts
import { getWriteableDirectory } from '@now/build-utils'
```
In some occasions, you might want to write to a temporary directory.
### `rename`
Signature: `rename(Files) : Files`
```ts
import { rename } from '@now/build-utils'
```
Renames the keys of the [`Files`](#files) object, which represent the paths. For example, to remove the `*.go` suffix you can use:
```js
const rename = require('@now/build-utils')
const originalFiles = { 'one.go': fileFsRef1, 'two.go': fileFsRef2 }
const renamedFiles = rename(originalFiles, path => path.replace(/\.go$/, '')
```

40
PUBLISHING.md Normal file
View File

@@ -0,0 +1,40 @@
# Publishing to npm
Always publish to the Canary Channel as soon as a PR is merged into the `canary` branch.
```
yarn publish-canary
```
Publish the Stable Channel weekly.
- Cherry pick each commit from `canary` to `master` branch
- Verify that you are _in-sync_ with canary (with the exception of the `version` line in `package.json`)
- Deploy the modified Builders
```
# View differences excluding "Publish" commits
git checkout canary && git pull
git log --pretty=format:"$ad- %s [%an]" | grep -v Publish > ~/Desktop/canary.txt
git checkout master && git pull
git log --pretty=format:"$ad- %s [%an]" | grep -v Publish > ~/Desktop/master.txt
diff ~/Desktop/canary.txt ~/Desktop/master.txt
# Cherry pick all PRs from canary into master ...
git cherry-pick <PR501_COMMIT_SHA>
git cherry-pick <PR502_COMMIT_SHA>
git cherry-pick <PR503_COMMIT_SHA>
git cherry-pick <PR504_COMMIT_SHA>
# Verify the only difference is "version" in package.json
git diff origin/canary
# Ship it
yarn publish-stable
```
After running this publish step, GitHub Actions will take care of publishing the modified Builder packages to npm.
If for some reason GitHub Actions fails to publish the npm package, you may do so
manually by running `npm publish` from the package directory. Make sure to
use `npm publish --tag canary` if you are publishing a canary release!

25
VERSIONING.md Normal file
View File

@@ -0,0 +1,25 @@
# Versioning
Builders are released to two different channels.
## Channels
| Channel | Git Branch | npm dist-tag | use example |
| ------- | ------------------------------------------------------------- | ------------ | ------------------ |
| Canary | [canary](https://github.com/zeit/now-builders/commits/canary) | `@canary` | `@now/node@canary` |
| Stable | [master](https://github.com/zeit/now-builders/commits/master) | `@latest` | `@now/node@latest` |
All PRs are submitted to the `canary` branch. Once a PR is merged into the `canary` branch, it should be published to npm immediately using the Canary Channel.
## Version Selection
Since Builders are published to [npmjs.com](https://npmjs.com), this makes versioning works the same for Builders as it does for any npm package. The `use` statement in [now.json](https://zeit.co/docs/v2/advanced/configuration#builds) has a similar syntax to `npm install`.
The following are valid examples [@now/node](https://www.npmjs.com/package/@now/node?activeTab=versions):
- `@now/node`
- `@now/node@0.7.3`
- `@now/node@canary`
- `@now/node@0.7.2-canary.2`
We always recommend using the latest version by leaving off the dist-tag suffix, `@now/node` for example.

14
lerna.json Normal file
View File

@@ -0,0 +1,14 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"command": {
"publish": {
"npmClient": "npm",
"allowBranch": ["master", "canary"],
"registry": "https://registry.npmjs.org/"
}
},
"version": "independent"
}

View File

@@ -1,200 +1,32 @@
{ {
"name": "now", "name": "now-builders",
"version": "16.1.2", "version": "0.0.0",
"preferGlobal": true, "private": true,
"license": "Apache-2.0", "license": "MIT",
"description": "The command-line interface for Now", "workspaces": [
"repository": "zeit/now-cli", "packages/*"
"scripts": { ],
"test": "yarn test-lint", "dependencies": {
"preinstall": "node ./scripts/preinstall.js", "lerna": "3.16.4"
"test-unit": "nyc ava test/*unit.js --serial --fail-fast --verbose", },
"test-integration": "ava test/integration.js --serial --fail-fast", "devDependencies": {
"test-integration-now-dev": "ava test/dev/integration.js --serial --fail-fast --verbose", "@zeit/ncc": "0.20.4",
"test-lint": "eslint . --ext .js,.ts", "async-retry": "1.2.3",
"prepublishOnly": "yarn build", "buffer-replace": "1.0.0",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", "node-fetch": "2.6.0"
"build": "ts-node ./scripts/build.ts", },
"build-dev": "ts-node ./scripts/build.ts --dev", "scripts": {
"format-modified": "prettier --parser typescript --write --single-quote `git diff --name-only HEAD * | grep -e \".*\\.ts$\" -e \".*\\.js$\" | xargs echo`" "lerna": "lerna",
}, "bootstrap": "lerna bootstrap",
"nyc": { "publish-stable": "git checkout master && git pull && lerna version --exact",
"include": [ "publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --exact",
"src/**" "publish-from-github": "./.circleci/publish.sh",
], "build": "./.circleci/run.sh build",
"extension": [ "test-lint": "./.circleci/run.sh test-lint",
".js", "test-unit": "./.circleci/run.sh test-unit",
".ts" "test-integration": "./.circleci/run.sh test-integration",
], "test-integration-once": "./.circleci/run.sh test-integration-once",
"require": [ "test-integration-now-dev": "./.circleci/run.sh test-integration-now-dev"
"ts-node/register" }
],
"reporter": [
"text",
"html"
],
"sourceMap": true,
"instrument": true,
"all": true
},
"git": {
"pre-commit": [
"test-lint",
"format-modified"
]
},
"bin": {
"now": "./dist/index.js"
},
"files": [
"dist",
"scripts/preinstall.js"
],
"ava": {
"compileEnhancements": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register/transpile-only",
"esm"
]
},
"engines": {
"node": ">= 8.11"
},
"devDependencies": {
"@now/build-utils": "0.9.13",
"@now/go": "latest",
"@now/next": "latest",
"@now/node": "latest",
"@now/php": "latest",
"@now/static-build": "latest",
"@sentry/node": "5.5.0",
"@types/ansi-escapes": "3.0.0",
"@types/ansi-regex": "4.0.0",
"@types/async-retry": "1.2.1",
"@types/bytes": "3.0.0",
"@types/debug": "0.0.31",
"@types/dotenv": "6.1.1",
"@types/escape-html": "0.0.20",
"@types/execa": "0.9.0",
"@types/fs-extra": "5.0.5",
"@types/glob": "7.1.1",
"@types/http-proxy": "1.16.2",
"@types/load-json-file": "2.0.7",
"@types/micro": "7.3.3",
"@types/mime-types": "2.1.0",
"@types/minimatch": "3.0.3",
"@types/mri": "1.1.0",
"@types/ms": "0.7.30",
"@types/node": "11.11.0",
"@types/node-fetch": "2.1.4",
"@types/npm-package-arg": "6.1.0",
"@types/pluralize": "0.0.29",
"@types/progress": "2.0.3",
"@types/psl": "1.1.0",
"@types/semver": "6.0.1",
"@types/tar-fs": "1.16.1",
"@types/text-table": "0.2.0",
"@types/universal-analytics": "0.4.2",
"@types/which": "1.3.1",
"@types/write-json-file": "2.2.1",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.1.0",
"@zeit/dockerignore": "0.0.5",
"@zeit/fun": "0.9.1",
"@zeit/git-hooks": "0.1.4",
"@zeit/ncc": "0.18.5",
"@zeit/source-map-support": "0.6.2",
"alpha-sort": "2.0.1",
"ansi-escapes": "3.0.0",
"ansi-regex": "3.0.0",
"arg": "2.0.0",
"async-listen": "1.2.0",
"async-retry": "1.1.3",
"async-sema": "2.1.4",
"ava": "2.2.0",
"bytes": "3.0.0",
"cache-or-tmp-directory": "1.0.0",
"chalk": "2.4.2",
"chokidar": "2.1.6",
"clipboardy": "2.1.0",
"codecov": "3.1.0",
"cpy": "7.2.0",
"credit-card": "3.0.1",
"date-fns": "1.29.0",
"death": "1.1.0",
"debug": "3.1.0",
"deployment-type": "1.0.1",
"docker-file-parser": "1.0.2",
"dot": "1.1.2",
"dotenv": "4.0.0",
"download": "6.2.5",
"email-prompt": "0.3.2",
"email-validator": "1.1.1",
"epipebomb": "1.0.0",
"escape-html": "1.0.3",
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-config-prettier": "4.1.0",
"eslint-import-resolver-typescript": "1.1.1",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4",
"esm": "3.1.4",
"execa": "1.0.0",
"fetch-h2": "2.0.3",
"fs-extra": "7.0.1",
"glob": "7.1.2",
"http-proxy": "1.17.0",
"ini": "1.3.4",
"inquirer": "3.3.0",
"is-url": "1.2.2",
"jaro-winkler": "0.2.8",
"jsonlines": "0.1.1",
"load-json-file": "3.0.0",
"micro": "9.1.2",
"mime-types": "2.1.24",
"minimatch": "3.0.4",
"mri": "1.1.0",
"ms": "2.1.1",
"node-fetch": "1.7.3",
"npm-package-arg": "6.1.0",
"nyc": "13.2.0",
"ora": "3.4.0",
"pcre-to-regexp": "0.0.5",
"pluralize": "7.0.0",
"pre-commit": "1.2.2",
"prettier": "1.16.2",
"printf": "0.2.5",
"progress": "2.0.3",
"promisepipe": "3.0.0",
"psl": "1.1.31",
"qr-image": "3.2.0",
"raw-body": "2.4.1",
"read-pkg": "2.0.0",
"rx-lite-aggregates": "4.0.8",
"semver": "5.5.0",
"serve-handler": "6.1.1",
"sinon": "4.4.2",
"strip-ansi": "5.2.0",
"stripe": "5.1.0",
"tar-fs": "1.16.3",
"test-listen": "1.1.0",
"text-table": "0.2.0",
"then-sleep": "1.0.1",
"through2": "2.0.3",
"title": "3.4.1",
"tmp-promise": "1.0.3",
"ts-node": "8.3.0",
"typescript": "3.2.4",
"universal-analytics": "0.4.20",
"update-check": "1.5.3",
"utility-types": "2.1.0",
"which": "1.3.1",
"which-promise": "1.0.0",
"write-json-file": "2.2.0",
"yarn": "1.17.3"
} }
}

3
packages/now-build-utils/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
dist
test/symlinks-out
test/symlinks.zip

View File

@@ -0,0 +1,3 @@
/src
/test
tmp

View File

@@ -0,0 +1,6 @@
tsc
rm dist/index.js
ncc build src/index.ts -o dist/main
mv dist/main/index.js dist/index.js
rm -rf dist/main

View File

@@ -0,0 +1 @@
module.exports = require('./dist/index').FileBlob;

View File

@@ -0,0 +1 @@
module.exports = require('./dist/index').FileFsRef;

View File

@@ -0,0 +1 @@
module.exports = require('./dist/index').FileRef;

View File

@@ -0,0 +1 @@
module.exports = require('../dist/index').download;

View File

@@ -0,0 +1 @@
module.exports = require('../dist/index').getWriteableDirectory;

View File

@@ -0,0 +1 @@
module.exports = require('../dist/index').glob;

View File

@@ -0,0 +1 @@
module.exports = require('../dist/index').rename;

View File

@@ -0,0 +1 @@
module.exports = require('../dist/index');

View File

@@ -0,0 +1 @@
module.exports = require('../dist/index').streamToBuffer;

View File

@@ -0,0 +1 @@
module.exports = require('./dist/index');

View File

@@ -0,0 +1,43 @@
{
"name": "@now/build-utils",
"version": "0.9.13",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
"homepage": "https://zeit.co/docs/v2/deployments/builders/developer-guide",
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-build-utils"
},
"scripts": {
"build": "./build.sh",
"test-integration-once": "jest --env node --verbose --runInBand",
"prepublishOnly": "./build.sh"
},
"devDependencies": {
"@types/async-retry": "^1.2.1",
"@types/cross-spawn": "6.0.0",
"@types/end-of-stream": "^1.4.0",
"@types/fs-extra": "^5.0.5",
"@types/glob": "^7.1.1",
"@types/multistream": "2.1.1",
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"async-retry": "1.2.3",
"async-sema": "2.1.4",
"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",
"minimatch": "3.0.4",
"multistream": "2.1.1",
"node-fetch": "2.2.0",
"semver": "6.1.1",
"typescript": "3.5.2",
"yazl": "2.4.3"
}
}

View File

@@ -0,0 +1,7 @@
export default function debug(message: string, ...additional: any[]) {
if (process.env.NOW_BUILDER_DEBUG) {
console.log(message, ...additional);
} else if (process.env.NOW_BUILDER_ANNOTATE) {
console.log(`[now-builder-debug] ${message}`, ...additional);
}
}

View File

@@ -0,0 +1,182 @@
import { PackageJson, Builder, Config } from './types';
import minimatch from 'minimatch';
interface ErrorResponse {
code: string;
message: string;
}
interface Options {
tag?: 'canary' | 'latest' | string;
}
const src: string = '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/advanced/platform/frequently-asked-questions#missing-build-script',
};
// Static builders are special cased in `@now/static-build`
function getBuilders(): Map<string, Builder> {
return new Map<string, Builder>([
['next', { src, use: '@now/next', config }],
]);
}
// Must be a function to ensure that the returned
// object won't be a reference
function getApiBuilders(): Builder[] {
return [
{ src: 'api/**/*.js', use: '@now/node', config },
{ src: 'api/**/*.ts', use: '@now/node', config },
{ src: 'api/**/*.go', use: '@now/go', config },
{ src: 'api/**/*.py', use: '@now/python', config },
{ src: 'api/**/*.rb', use: '@now/ruby', 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']);
}
async function detectBuilder(pkg: PackageJson): Promise<Builder> {
for (const [dependency, builder] of getBuilders()) {
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
// Return the builder when a dependency matches
if (deps[dependency]) {
return builder;
}
}
// By default we'll choose the `static-build` builder
return { src, use: '@now/static-build', config };
}
// Files that match a specific pattern will get ignored
export function ignoreApiFilter(file: string) {
if (file.includes('/.')) {
return false;
}
if (file.includes('/_')) {
return false;
}
if (file.endsWith('.d.ts')) {
return false;
}
// If the file does not match any builder we also
// don't want to create a route e.g. `package.json`
if (getApiBuilders().every(({ src }) => !minimatch(file, src))) {
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[]): Promise<Builder[]> {
const builds = files
.sort(sortFiles)
.filter(ignoreApiFilter)
.map(file => {
const result = getApiBuilders().find(
({ src }): boolean => minimatch(file, src)
);
return result ? { ...result, src: file } : null;
});
const finishedBuilds = builds.filter(Boolean);
return finishedBuilds as Builder[];
}
// When zero config is used we can call this function
// to determine what builders to use
export async function detectBuilders(
files: string[],
pkg?: PackageJson | undefined | null,
options?: Options
): Promise<{
builders: Builder[] | null;
errors: ErrorResponse[] | null;
}> {
const errors: ErrorResponse[] = [];
// Detect all builders for the `api` directory before anything else
let builders = await detectApiBuilders(files);
if (pkg && hasBuildScript(pkg)) {
builders.push(await detectBuilder(pkg));
} 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, 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) {
// We can't use pattern matching, since `!(api)` and `!(api)/**/*`
// won't give the correct results
builders.push(
...files
.filter(name => !name.startsWith('api/'))
.filter(name => !(name === 'package.json'))
.map(name => ({
use: '@now/static',
src: name,
config,
}))
);
}
}
// Change the tag for the builders
if (builders && builders.length) {
const tag = options && options.tag;
if (tag) {
builders = builders.map((originBuilder: Builder) => {
// Copy builder to make sure it is not a reference
const builder = { ...originBuilder };
// @now/static has no canary builder
if (builder.use !== '@now/static') {
builder.use = `${builder.use}@${tag}`;
}
return builder;
});
}
}
return {
builders: builders.length ? builders : null,
errors: errors.length ? errors : null,
};
}

View File

@@ -0,0 +1,297 @@
import { Route, Builder } from './types';
import { parse as parsePath } from 'path';
import { ignoreApiFilter, sortFiles } from './detect-builders';
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: number = 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 = [
prefix,
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 occurres 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[]): 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(ignoreApiFilter)
.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}" occurres 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 detectRoutes(
files: string[],
builders: Builder[]
): Promise<RoutesResult> {
const routesResult = await detectApiRoutes(files);
if (routesResult.defaultRoutes && hasPublicBuilder(builders)) {
routesResult.defaultRoutes.push({
src: '/(.*)',
dest: '/public/$1',
});
}
return routesResult;
}

View File

@@ -0,0 +1,46 @@
import assert from 'assert';
import intoStream from 'into-stream';
import { File } from './types';
interface FileBlobOptions {
mode?: number;
data: string | Buffer;
}
interface FromStreamOptions {
mode?: number;
stream: NodeJS.ReadableStream;
}
export default class FileBlob implements File {
public type: 'FileBlob';
public mode: number;
public data: string | Buffer;
constructor({ mode = 0o100644, data }: FileBlobOptions) {
assert(typeof mode === 'number');
assert(typeof data === 'string' || Buffer.isBuffer(data));
this.type = 'FileBlob';
this.mode = mode;
this.data = data;
}
static async fromStream({ mode = 0o100644, stream }: FromStreamOptions) {
assert(typeof mode === 'number');
assert(typeof stream.pipe === 'function'); // is-stream
const chunks: Buffer[] = [];
await new Promise<void>((resolve, reject) => {
stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
stream.on('error', error => reject(error));
stream.on('end', () => resolve());
});
const data = Buffer.concat(chunks);
return new FileBlob({ mode, data });
}
toStream(): NodeJS.ReadableStream {
return intoStream(this.data);
}
}

View File

@@ -0,0 +1,97 @@
import assert from 'assert';
import fs from 'fs-extra';
import multiStream from 'multistream';
import path from 'path';
import Sema from 'async-sema';
import { File } from './types';
const semaToPreventEMFILE = new Sema(20);
interface FileFsRefOptions {
mode?: number;
fsPath: string;
}
interface FromStreamOptions {
mode: number;
stream: NodeJS.ReadableStream;
fsPath: string;
}
class FileFsRef implements File {
public type: 'FileFsRef';
public mode: number;
public fsPath: string;
constructor({ mode = 0o100644, fsPath }: FileFsRefOptions) {
assert(typeof mode === 'number');
assert(typeof fsPath === 'string');
this.type = 'FileFsRef';
this.mode = mode;
this.fsPath = fsPath;
}
static async fromFsPath({
mode,
fsPath,
}: FileFsRefOptions): Promise<FileFsRef> {
let m = mode;
if (!m) {
const stat = await fs.lstat(fsPath);
m = stat.mode;
}
return new FileFsRef({ mode: m, fsPath });
}
static async fromStream({
mode = 0o100644,
stream,
fsPath,
}: FromStreamOptions): Promise<FileFsRef> {
assert(typeof mode === 'number');
assert(typeof stream.pipe === 'function'); // is-stream
assert(typeof fsPath === 'string');
await fs.mkdirp(path.dirname(fsPath));
await new Promise<void>((resolve, reject) => {
const dest = fs.createWriteStream(fsPath, {
mode: mode & 0o777,
});
stream.pipe(dest);
stream.on('error', reject);
dest.on('finish', resolve);
dest.on('error', reject);
});
return new FileFsRef({ mode, fsPath });
}
async toStreamAsync(): Promise<NodeJS.ReadableStream> {
await semaToPreventEMFILE.acquire();
const release = () => semaToPreventEMFILE.release();
const stream = fs.createReadStream(this.fsPath);
stream.on('close', release);
stream.on('error', release);
return stream;
}
toStream(): NodeJS.ReadableStream {
let flag = false;
// eslint-disable-next-line consistent-return
return multiStream(cb => {
if (flag) return cb(null, null);
flag = true;
this.toStreamAsync()
.then(stream => {
cb(null, stream);
})
.catch(error => {
cb(error, null);
});
});
}
}
export = FileFsRef;

View File

@@ -0,0 +1,95 @@
import assert from 'assert';
import fetch from 'node-fetch';
import multiStream from 'multistream';
import retry from 'async-retry';
import Sema from 'async-sema';
import { File } from './types';
interface FileRefOptions {
mode?: number;
digest: string;
}
const semaToDownloadFromS3 = new Sema(5);
class BailableError extends Error {
public bail: boolean;
constructor(...args: string[]) {
super(...args);
this.bail = false;
}
}
export default class FileRef implements File {
public type: 'FileRef';
public mode: number;
public digest: string;
constructor({ mode = 0o100644, digest }: FileRefOptions) {
assert(typeof mode === 'number');
assert(typeof digest === 'string');
this.type = 'FileRef';
this.mode = mode;
this.digest = digest;
}
async toStreamAsync(): Promise<NodeJS.ReadableStream> {
let url = '';
// sha:24be087eef9fac01d61b30a725c1a10d7b45a256
const [digestType, digestHash] = this.digest.split(':');
if (digestType === 'sha') {
// This CloudFront URL edge caches the `now-files` S3 bucket to prevent
// overloading it
// `https://now-files.s3.amazonaws.com/${digestHash}`
url = `https://dmmcy0pwk6bqi.cloudfront.net/${digestHash}`;
} else if (digestType === 'sha+ephemeral') {
// This URL is currently only used for cache files that constantly
// change. We shouldn't cache it on CloudFront because it'd always be a
// MISS.
url = `https://now-ephemeral-files.s3.amazonaws.com/${digestHash}`;
} else {
throw new Error('Expected digest to be sha');
}
await semaToDownloadFromS3.acquire();
// console.time(`downloading ${url}`);
try {
return await retry(
async () => {
const resp = await fetch(url);
if (!resp.ok) {
const error = new BailableError(
`download: ${resp.status} ${resp.statusText} for ${url}`
);
if (resp.status === 403) error.bail = true;
throw error;
}
return resp.body;
},
{ factor: 1, retries: 3 }
);
} finally {
// console.timeEnd(`downloading ${url}`);
semaToDownloadFromS3.release();
}
}
toStream(): NodeJS.ReadableStream {
let flag = false;
// eslint-disable-next-line consistent-return
return multiStream(cb => {
if (flag) return cb(null, null);
flag = true;
this.toStreamAsync()
.then(stream => {
cb(null, stream);
})
.catch(error => {
cb(error, null);
});
});
}
}

View File

@@ -0,0 +1,75 @@
import path from 'path';
import FileFsRef from '../file-fs-ref';
import { File, Files, Meta } from '../types';
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
export interface DownloadedFiles {
[filePath: string]: FileFsRef;
}
const S_IFMT = 61440; /* 0170000 type of file */
const S_IFLNK = 40960; /* 0120000 symbolic link */
export function isSymbolicLink(mode: number): boolean {
return (mode & S_IFMT) === S_IFLNK;
}
async function downloadFile(file: File, fsPath: string): Promise<FileFsRef> {
const { mode } = file;
if (mode && isSymbolicLink(mode) && file.type === 'FileFsRef') {
const [target] = await Promise.all([
readlink((file as FileFsRef).fsPath),
mkdirp(path.dirname(fsPath)),
]);
await symlink(target, fsPath);
return FileFsRef.fromFsPath({ mode, fsPath });
} else {
const stream = file.toStream();
return FileFsRef.fromStream({ mode, stream, fsPath });
}
}
async function removeFile(basePath: string, fileMatched: string) {
const file = path.join(basePath, fileMatched);
await remove(file);
}
export default async function download(
files: Files,
basePath: string,
meta?: Meta
): Promise<DownloadedFiles> {
const { isDev = false, skipDownload = false, filesChanged = null, filesRemoved = null } =
meta || {};
if (isDev || skipDownload) {
// In `now dev`, the `download()` function is a no-op because
// the `basePath` matches the `cwd` of the dev server, so the
// source files are already available.
return files as DownloadedFiles;
}
const files2: DownloadedFiles = {};
await Promise.all(
Object.keys(files).map(async name => {
// If the file does not exist anymore, remove it.
if (Array.isArray(filesRemoved) && filesRemoved.includes(name)) {
await removeFile(basePath, name);
return;
}
// If a file didn't change, do not re-download it.
if (Array.isArray(filesChanged) && !filesChanged.includes(name)) {
return;
}
const file = files[name];
const fsPath = path.join(basePath, name);
files2[name] = await downloadFile(file, fsPath);
})
);
return files2;
}

View File

@@ -0,0 +1,10 @@
import { join } from 'path';
import { tmpdir } from 'os';
import { mkdirp } from 'fs-extra';
export default async function getWritableDirectory() {
const name = Math.floor(Math.random() * 0x7fffffff).toString(16);
const directory = join(tmpdir(), name);
await mkdirp(directory);
return directory;
}

View File

@@ -0,0 +1,70 @@
import path from 'path';
import assert from 'assert';
import vanillaGlob_ from 'glob';
import { promisify } from 'util';
import { lstat, Stats } from 'fs-extra';
import FileFsRef from '../file-fs-ref';
type GlobOptions = vanillaGlob_.IOptions;
interface FsFiles {
[filePath: string]: FileFsRef;
}
const vanillaGlob = promisify(vanillaGlob_);
export default async function glob(
pattern: string,
opts: GlobOptions | string,
mountpoint?: string
): Promise<FsFiles> {
let options: GlobOptions;
if (typeof opts === 'string') {
options = { cwd: opts };
} else {
options = opts;
}
if (!options.cwd) {
throw new Error(
'Second argument (basePath) must be specified for names of resulting files'
);
}
if (!path.isAbsolute(options.cwd)) {
throw new Error(`basePath/cwd must be an absolute path (${options.cwd})`);
}
const results: FsFiles = {};
options.symlinks = {};
options.statCache = {};
options.stat = true;
options.dot = true;
const files = await vanillaGlob(pattern, options);
for (const relativePath of files) {
const fsPath = path.join(options.cwd!, relativePath).replace(/\\/g, '/');
let stat: Stats = options.statCache![fsPath] as Stats;
assert(
stat,
`statCache does not contain value for ${relativePath} (resolved to ${fsPath})`
);
if (stat.isFile()) {
const isSymlink = options.symlinks![fsPath];
if (isSymlink) {
stat = await lstat(fsPath);
}
let finalPath = relativePath;
if (mountpoint) {
finalPath = path.join(mountpoint, finalPath);
}
results[finalPath] = new FileFsRef({ mode: stat.mode, fsPath });
}
}
return results;
}

View File

@@ -0,0 +1,53 @@
import { intersects } from 'semver';
import { NodeVersion } from '../types';
const supportedOptions: NodeVersion[] = [
{ major: 10, range: '10.x', runtime: 'nodejs10.x' },
{ major: 8, range: '8.10.x', runtime: 'nodejs8.10' },
];
// This version should match Fargate's default in the PATH
// Today that is Node 8
export const defaultSelection = supportedOptions.find(
o => o.major === 8
) as NodeVersion;
export async function getSupportedNodeVersion(
engineRange?: string,
silent?: boolean
): Promise<NodeVersion> {
let selection = defaultSelection;
if (!engineRange) {
if (!silent) {
console.log(
'missing `engines` in `package.json`, using default range: ' +
selection.range
);
}
} else {
const found = supportedOptions.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) {
if (!silent) {
console.log(
'Found `engines` in `package.json`, selecting range: ' +
selection.range
);
}
} else {
if (!silent) {
throw new Error(
'found `engines` in `package.json` with an unsupported node range: ' +
engineRange +
'\nplease use `10.x` or `8.10.x` instead'
);
}
}
}
return selection;
}

View File

@@ -0,0 +1,12 @@
import { Files } from '../types';
type Delegate = (name: string) => string;
export default function rename(files: Files, delegate: Delegate): Files {
return Object.keys(files).reduce(
(newFiles, name) => ({
...newFiles,
[delegate(name)]: files[name],
}),
{}
);
}

View File

@@ -0,0 +1,200 @@
import assert from 'assert';
import fs from 'fs-extra';
import path from 'path';
import spawn from 'cross-spawn';
import { SpawnOptions } from 'child_process';
import { deprecate } from 'util';
import { Meta, PackageJson, NodeVersion, Config } from '../types';
import { getSupportedNodeVersion } from './node-version';
function spawnAsync(
command: string,
args: string[],
cwd: string,
opts: SpawnOptions = {}
) {
return new Promise<void>((resolve, reject) => {
const stderrLogs: Buffer[] = [];
opts = { stdio: 'inherit', cwd, ...opts };
const child = spawn(command, args, opts);
if (opts.stdio === 'pipe' && child.stderr) {
child.stderr.on('data', data => stderrLogs.push(data));
}
child.on('error', reject);
child.on('close', (code, signal) => {
if (code === 0) {
return resolve();
}
const errorLogs = stderrLogs.map(line => line.toString()).join('');
if (opts.stdio !== 'inherit') {
reject(new Error(`Exited with ${code || signal}\n${errorLogs}`));
} else {
reject(new Error(`Exited with ${code || signal}`));
}
});
});
}
async function chmodPlusX(fsPath: string) {
const s = await fs.stat(fsPath);
const newMode = s.mode | 64 | 8 | 1; // eslint-disable-line no-bitwise
if (s.mode === newMode) return;
const base8 = newMode.toString(8).slice(-3);
await fs.chmod(fsPath, base8);
}
export async function runShellScript(
fsPath: string,
args: string[] = [],
spawnOpts?: SpawnOptions
) {
assert(path.isAbsolute(fsPath));
const destPath = path.dirname(fsPath);
await chmodPlusX(fsPath);
await spawnAsync(`./${path.basename(fsPath)}`, args, destPath, spawnOpts);
return true;
}
export function getSpawnOptions(
meta: Meta,
nodeVersion: NodeVersion
): SpawnOptions {
const opts = {
env: { ...process.env },
};
if (!meta.isDev) {
opts.env.PATH = `/node${nodeVersion.major}/bin:${opts.env.PATH}`;
}
return opts;
}
export async function getNodeVersion(
destPath: string,
minNodeVersion?: string,
config?: Config
): Promise<NodeVersion> {
const { packageJson } = await scanParentDirs(destPath, true);
let range: string | undefined;
let silent = false;
if (packageJson && packageJson.engines && packageJson.engines.node) {
range = packageJson.engines.node;
} else if (minNodeVersion) {
range = minNodeVersion;
silent = true;
} else if (config && config.zeroConfig) {
// Use latest node version zero config detected
range = '10.x';
silent = true;
}
return getSupportedNodeVersion(range, silent);
}
async function scanParentDirs(destPath: string, readPackageJson = false) {
assert(path.isAbsolute(destPath));
let hasPackageLockJson = false;
let packageJson: PackageJson | undefined;
let currentDestPath = destPath;
// eslint-disable-next-line no-constant-condition
while (true) {
const packageJsonPath = path.join(currentDestPath, 'package.json');
// eslint-disable-next-line no-await-in-loop
if (await fs.pathExists(packageJsonPath)) {
// eslint-disable-next-line no-await-in-loop
if (readPackageJson) {
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
}
// eslint-disable-next-line no-await-in-loop
hasPackageLockJson = await fs.pathExists(
path.join(currentDestPath, 'package-lock.json')
);
break;
}
const newDestPath = path.dirname(currentDestPath);
if (currentDestPath === newDestPath) break;
currentDestPath = newDestPath;
}
return { hasPackageLockJson, packageJson };
}
export async function runNpmInstall(
destPath: string,
args: string[] = [],
spawnOpts?: SpawnOptions
) {
assert(path.isAbsolute(destPath));
let commandArgs = args;
console.log(`installing to ${destPath}`);
const { hasPackageLockJson } = await scanParentDirs(destPath);
const opts = spawnOpts || { env: process.env };
if (hasPackageLockJson) {
commandArgs = args.filter(a => a !== '--prefer-offline');
await spawnAsync(
'npm',
commandArgs.concat(['install', '--unsafe-perm']),
destPath,
opts
);
} else {
await spawnAsync(
'yarn',
commandArgs.concat(['--ignore-engines', '--cwd', destPath]),
destPath,
opts
);
}
}
export async function runPackageJsonScript(
destPath: string,
scriptName: string,
opts?: SpawnOptions
) {
assert(path.isAbsolute(destPath));
const { packageJson, hasPackageLockJson } = await scanParentDirs(
destPath,
true
);
const hasScript = Boolean(
packageJson &&
packageJson.scripts &&
scriptName &&
packageJson.scripts[scriptName]
);
if (!hasScript) return false;
if (hasPackageLockJson) {
console.log(`running "npm run ${scriptName}"`);
await spawnAsync('npm', ['run', scriptName], destPath, opts);
} else {
console.log(`running "yarn run ${scriptName}"`);
await spawnAsync(
'yarn',
['--cwd', destPath, 'run', scriptName],
destPath,
opts
);
}
return true;
}
/**
* @deprecate installDependencies() is deprecated.
* Please use runNpmInstall() instead.
*/
export const installDependencies = deprecate(
runNpmInstall,
'installDependencies() is deprecated. Please use runNpmInstall() instead.'
);

View File

@@ -0,0 +1,28 @@
import eos from 'end-of-stream';
export default function streamToBuffer(
stream: NodeJS.ReadableStream
): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const buffers: Buffer[] = [];
stream.on('data', buffers.push.bind(buffers));
eos(stream, err => {
if (err) {
reject(err);
return;
}
switch (buffers.length) {
case 0:
resolve(Buffer.allocUnsafe(0));
break;
case 1:
resolve(buffers[0]);
break;
default:
resolve(Buffer.concat(buffers));
}
});
});
}

View File

@@ -0,0 +1,47 @@
import FileBlob from './file-blob';
import FileFsRef from './file-fs-ref';
import FileRef from './file-ref';
import { Lambda, createLambda } from './lambda';
import download, { DownloadedFiles } from './fs/download';
import getWriteableDirectory from './fs/get-writable-directory';
import glob from './fs/glob';
import rename from './fs/rename';
import {
installDependencies,
runPackageJsonScript,
runNpmInstall,
runShellScript,
getNodeVersion,
getSpawnOptions,
} from './fs/run-user-scripts';
import streamToBuffer from './fs/stream-to-buffer';
import shouldServe from './should-serve';
import { detectBuilders } from './detect-builders';
import { detectRoutes } from './detect-routes';
import debug from './debug';
export {
FileBlob,
FileFsRef,
FileRef,
Lambda,
createLambda,
download,
DownloadedFiles,
getWriteableDirectory,
glob,
rename,
installDependencies,
runPackageJsonScript,
runNpmInstall,
runShellScript,
getNodeVersion,
getSpawnOptions,
streamToBuffer,
shouldServe,
detectBuilders,
detectRoutes,
debug,
};
export * from './types';

View File

@@ -0,0 +1,107 @@
import assert from 'assert';
import Sema from 'async-sema';
import { ZipFile } from 'yazl';
import { readlink } from 'fs-extra';
import { Files } from './types';
import FileFsRef from './file-fs-ref';
import { isSymbolicLink } from './fs/download';
import streamToBuffer from './fs/stream-to-buffer';
interface Environment {
[key: string]: string;
}
interface LambdaOptions {
zipBuffer: Buffer;
handler: string;
runtime: string;
environment: Environment;
}
interface CreateLambdaOptions {
files: Files;
handler: string;
runtime: string;
environment?: Environment;
}
export class Lambda {
public type: 'Lambda';
public zipBuffer: Buffer;
public handler: string;
public runtime: string;
public environment: Environment;
constructor({ zipBuffer, handler, runtime, environment }: LambdaOptions) {
this.type = 'Lambda';
this.zipBuffer = zipBuffer;
this.handler = handler;
this.runtime = runtime;
this.environment = environment;
}
}
const sema = new Sema(10);
const mtime = new Date(1540000000000);
export async function createLambda({
files,
handler,
runtime,
environment = {},
}: CreateLambdaOptions): Promise<Lambda> {
assert(typeof files === 'object', '"files" must be an object');
assert(typeof handler === 'string', '"handler" is not a string');
assert(typeof runtime === 'string', '"runtime" is not a string');
assert(typeof environment === 'object', '"environment" is not an object');
await sema.acquire();
try {
const zipBuffer = await createZip(files);
return new Lambda({
zipBuffer,
handler,
runtime,
environment,
});
} finally {
sema.release();
}
}
export async function createZip(files: Files): Promise<Buffer> {
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 readlink((file as FileFsRef).fsPath);
symlinkTargets.set(name, symlinkTarget);
}
}
const zipFile = new ZipFile();
const zipBuffer = await new Promise<Buffer>((resolve, reject) => {
for (const name of names) {
const file = files[name];
const opts = { mode: file.mode, mtime };
const symlinkTarget = symlinkTargets.get(name);
if (typeof symlinkTarget === 'string') {
zipFile.addBuffer(Buffer.from(symlinkTarget, 'utf8'), name, opts);
} else {
const stream = file.toStream() as import('stream').Readable;
stream.on('error', reject);
zipFile.addReadStream(stream, name, opts);
}
}
zipFile.end();
streamToBuffer(zipFile.outputStream)
.then(resolve)
.catch(reject);
});
return zipBuffer;
}

View File

@@ -0,0 +1,27 @@
import { parse } from 'path';
import { ShouldServeOptions } from './types';
import FileFsRef from './file-fs-ref';
export default function shouldServe({
entrypoint,
files,
requestPath,
}: ShouldServeOptions): boolean {
requestPath = requestPath.replace(/\/$/, ''); // sanitize trailing '/'
entrypoint = entrypoint.replace(/\\/, '/'); // windows compatibility
if (entrypoint === requestPath && hasProp(files, entrypoint)) {
return true;
}
const { dir, name } = parse(entrypoint);
if (name === 'index' && dir === requestPath && hasProp(files, entrypoint)) {
return true;
}
return false;
}
function hasProp(obj: { [path: string]: FileFsRef }, key: string): boolean {
return Object.hasOwnProperty.call(obj, key);
}

View File

@@ -0,0 +1,216 @@
import FileRef from './file-ref';
import FileFsRef from './file-fs-ref';
export interface File {
type: string;
mode: number;
toStream: () => NodeJS.ReadableStream;
/**
* The absolute path to the file in the filesystem
*/
fsPath?: string;
}
export interface Files {
[filePath: string]: File;
}
export interface Route {
src?: string;
dest?: string;
handle?: string;
type?: string;
headers?: {
[key: string]: string;
};
continue?: boolean;
status?: number;
}
export interface Config {
[key: string]:
| string
| string[]
| boolean
| number
| { [key: string]: string }
| undefined;
maxLambdaSize?: string;
includeFiles?: string | string[];
bundle?: boolean;
ldsflags?: string;
helpers?: boolean;
rust?: string;
debug?: boolean;
zeroConfig?: boolean;
import?: { [key: string]: string };
}
export interface Meta {
isDev?: boolean;
skipDownload?: boolean;
requestPath?: string;
filesChanged?: string[];
filesRemoved?: string[];
}
export interface AnalyzeOptions {
/**
* All source files of the project
*/
files: {
[filePath: string]: FileRef;
};
/**
* Name of entrypoint file for this particular build job. Value
* `files[entrypoint]` is guaranteed to exist and be a valid File reference.
* `entrypoint` is always a discrete file and never a glob, since globs are
* expanded into separate builds at deployment time.
*/
entrypoint: string;
/**
* A writable temporary directory where you are encouraged to perform your
* build process. This directory will be populated with the restored cache.
*/
workPath: string;
/**
* An arbitrary object passed by the user in the build definition defined
* in `now.json`.
*/
config: Config;
}
export interface BuildOptions {
/**
* All source files of the project
*/
files: Files;
/**
* Name of entrypoint file for this particular build job. Value
* `files[entrypoint]` is guaranteed to exist and be a valid File reference.
* `entrypoint` is always a discrete file and never a glob, since globs are
* expanded into separate builds at deployment time.
*/
entrypoint: string;
/**
* A writable temporary directory where you are encouraged to perform your
* build process. This directory will be populated with the restored cache.
*/
workPath: string;
/**
* An arbitrary object passed by the user in the build definition defined
* in `now.json`.
*/
config: Config;
/**
* Metadata related to the invoker of the builder, used by `now dev`.
* Builders may use the properties on this object to change behavior based
* on the build environment.
*/
meta?: Meta;
}
export interface PrepareCacheOptions {
/**
* All source files of the project
*/
files: Files;
/**
* Name of entrypoint file for this particular build job. Value
* `files[entrypoint]` is guaranteed to exist and be a valid File reference.
* `entrypoint` is always a discrete file and never a glob, since globs are
* expanded into separate builds at deployment time.
*/
entrypoint: string;
/**
* A writable temporary directory where you are encouraged to perform your
* build process.
*/
workPath: string;
/**
* A writable temporary directory where you can build a cache to use for
* the next run.
*/
cachePath: string;
/**
* An arbitrary object passed by the user in the build definition defined
* in `now.json`.
*/
config: Config;
}
export interface ShouldServeOptions {
/**
* A path string from a request.
*/
requestPath: string;
/**
* Name of entrypoint file for this particular build job. Value
* `files[entrypoint]` is guaranteed to exist and be a valid File reference.
* `entrypoint` is always a discrete file and never a glob, since globs are
* expanded into separate builds at deployment time.
*/
entrypoint: string;
/**
* All source files of the project
*/
files: {
[path: string]: FileFsRef;
};
/**
* A writable temporary directory where you are encouraged to perform your
* build process. This directory will be populated with the restored cache.
*/
workPath: string;
/**
* An arbitrary object passed by the user in the build definition defined
* in `now.json`.
*/
config: Config;
}
export interface PackageJson {
name: string;
version: string;
engines?: {
[key: string]: string;
node: string;
npm: string;
};
scripts?: {
[key: string]: string;
};
dependencies?: {
[key: string]: string;
};
devDependencies?: {
[key: string]: string;
};
}
export interface NodeVersion {
major: number;
range: string;
runtime: string;
}
export interface Builder {
use: string;
src: string;
config?: Config;
}

View File

@@ -0,0 +1 @@
now.json

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end(req.query.endpoint);
};

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"build": "mkdir -p public && echo 'hello from index.txt' > public/index.txt"
}
}

View File

@@ -0,0 +1 @@
now.json

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end('hello from api/date.js');
};

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end('hello from api/date/index.js');
};

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end('hello from api/index.js');
};

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"build": "mkdir -p public && echo 'hello from index.txt' > public/index.txt"
}
}

View File

@@ -0,0 +1,5 @@
const cowsay = require('cowsay').say;
module.exports = (req, resp) => {
resp.end(cowsay({ text: 'cross-cow:RANDOMNESS_PLACEHOLDER' }));
};

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"lib": "../lib"
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "lib",
"version": "0.0.1",
"dependencies": {
"cowsay": "*"
}
}

View File

@@ -0,0 +1,10 @@
{
"version": 2,
"builds": [{ "src": "api/index.js", "use": "@now/node" }],
"probes": [
{
"path": "/api/index.js",
"mustContain": "cross-cow:RANDOMNESS_PLACEHOLDER"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [
{ "src": "with-npm/index.js", "use": "@now/node" },
{ "src": "with-yarn/index.js", "use": "@now/node" }
],
"probes": [
{ "path": "/with-npm", "mustContain": "npm:RANDOMNESS_PLACEHOLDER" },
{ "path": "/with-yarn", "mustContain": "yarn:RANDOMNESS_PLACEHOLDER" }
]
}

View File

@@ -0,0 +1,14 @@
const fs = require('fs');
const path = require('path');
const execpath = path.basename(process.env.npm_execpath);
console.log('execpath', execpath);
if (execpath === 'npm-cli.js') {
fs.writeFileSync(
'index.js',
'module.exports = (_, resp) => resp.end("npm:RANDOMNESS_PLACEHOLDER");',
);
} else {
throw new Error('npm is expected');
}

View File

@@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"now-build": "node must-be-npm.js"
}
}

View File

@@ -0,0 +1,14 @@
const fs = require('fs');
const path = require('path');
const execpath = path.basename(process.env.npm_execpath);
console.log('execpath', execpath);
if (execpath === 'yarn.js' || execpath === 'yarn') {
fs.writeFileSync(
'index.js',
'module.exports = (_, resp) => resp.end("yarn:RANDOMNESS_PLACEHOLDER");',
);
} else {
throw new Error('yarn is expected');
}

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"now-build": "node must-be-yarn.js"
}
}

View File

@@ -0,0 +1,9 @@
const scheduler = require('@google-cloud/scheduler');
module.exports = (_, res) => {
if (scheduler) {
res.end('found:RANDOMNESS_PLACEHOLDER');
} else {
res.end('nope:RANDOMNESS_PLACEHOLDER');
}
};

View File

@@ -0,0 +1,10 @@
{
"version": 2,
"builds": [
{
"src": "index.js",
"use": "@now/node"
}
],
"probes": [{ "path": "/", "mustContain": "found:RANDOMNESS_PLACEHOLDER" }]
}

View File

@@ -0,0 +1,8 @@
{
"name": "15-yarn-ignore-engines",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@google-cloud/scheduler": "0.3.0"
}
}

View File

@@ -0,0 +1 @@
contents

View File

@@ -0,0 +1 @@
./a.txt

View File

@@ -0,0 +1,779 @@
/* global beforeAll, expect, it, jest */
const path = require('path');
const fs = require('fs-extra');
// eslint-disable-next-line import/no-extraneous-dependencies
const execa = require('execa');
const assert = require('assert');
const { createZip } = require('../dist/lambda');
const {
glob, download, detectBuilders, detectRoutes,
} = require('../');
const {
getSupportedNodeVersion,
defaultSelection,
} = require('../dist/fs/node-version');
const {
packAndDeploy,
testDeployment,
} = require('../../../test/lib/deployment/test-deployment');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
beforeAll(async () => {
const buildUtilsPath = path.resolve(__dirname, '..');
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
console.log('buildUtilsUrl', buildUtilsUrl);
});
// unit tests
it('should re-create symlinks properly', async () => {
const files = await glob('**', path.join(__dirname, 'symlinks'));
assert.equal(Object.keys(files).length, 2);
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
const files2 = await download(files, outDir);
assert.equal(Object.keys(files2).length, 2);
const [linkStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'a.txt')),
]);
assert(linkStat.isSymbolicLink());
assert(aStat.isFile());
});
it('should create zip files with symlinks properly', async () => {
const files = await glob('**', path.join(__dirname, 'symlinks'));
assert.equal(Object.keys(files).length, 2);
const outFile = path.join(__dirname, 'symlinks.zip');
await fs.remove(outFile);
const outDir = path.join(__dirname, 'symlinks-out');
await fs.remove(outDir);
await fs.mkdirp(outDir);
await fs.writeFile(outFile, await createZip(files));
await execa('unzip', [outFile], { cwd: outDir });
const [linkStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
fs.lstat(path.join(outDir, 'a.txt')),
]);
assert(linkStat.isSymbolicLink());
assert(aStat.isFile());
});
it('should only match supported node versions', () => {
expect(getSupportedNodeVersion('10.x')).resolves.toHaveProperty('major', 10);
expect(getSupportedNodeVersion('8.10.x')).resolves.toHaveProperty('major', 8);
expect(getSupportedNodeVersion('8.11.x')).rejects.toThrow();
expect(getSupportedNodeVersion('6.x')).rejects.toThrow();
expect(getSupportedNodeVersion('999.x')).rejects.toThrow();
expect(getSupportedNodeVersion('foo')).rejects.toThrow();
expect(getSupportedNodeVersion('')).resolves.toBe(defaultSelection);
expect(getSupportedNodeVersion(null)).resolves.toBe(defaultSelection);
expect(getSupportedNodeVersion(undefined)).resolves.toBe(defaultSelection);
});
it('should match all semver ranges', () => {
// See https://docs.npmjs.com/files/package.json#engines
expect(getSupportedNodeVersion('10.0.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('10.x')).resolves.toHaveProperty('major', 10);
expect(getSupportedNodeVersion('>=10')).resolves.toHaveProperty('major', 10);
expect(getSupportedNodeVersion('>=10.3.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('8.5.0 - 10.5.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('>=9.0.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('>=9.5.0 <=10.5.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('~10.5.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('^10.5.0')).resolves.toHaveProperty(
'major',
10,
);
});
it('should support require by path for legacy builders', () => {
const index = require('@now/build-utils');
const download2 = require('@now/build-utils/fs/download.js');
const getWriteableDirectory2 = require('@now/build-utils/fs/get-writable-directory.js');
const glob2 = require('@now/build-utils/fs/glob.js');
const rename2 = require('@now/build-utils/fs/rename.js');
const {
runNpmInstall: runNpmInstall2,
} = require('@now/build-utils/fs/run-user-scripts.js');
const streamToBuffer2 = require('@now/build-utils/fs/stream-to-buffer.js');
const FileBlob2 = require('@now/build-utils/file-blob.js');
const FileFsRef2 = require('@now/build-utils/file-fs-ref.js');
const FileRef2 = require('@now/build-utils/file-ref.js');
const { Lambda: Lambda2 } = require('@now/build-utils/lambda.js');
expect(download2).toBe(index.download);
expect(getWriteableDirectory2).toBe(index.getWriteableDirectory);
expect(glob2).toBe(index.glob);
expect(rename2).toBe(index.rename);
expect(runNpmInstall2).toBe(index.runNpmInstall);
expect(streamToBuffer2).toBe(index.streamToBuffer);
expect(FileBlob2).toBe(index.FileBlob);
expect(FileFsRef2).toBe(index.FileFsRef);
expect(FileRef2).toBe(index.FileRef);
expect(Lambda2).toBe(index.Lambda);
});
// own fixtures
const fixturesPath = path.resolve(__dirname, 'fixtures');
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath)) {
if (fixture.includes('zero-config')) {
// Those have separate tests
continue; // eslint-disable-line no-continue
}
// eslint-disable-next-line no-loop-func
it(`should build ${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath, fixture),
),
).resolves.toBeDefined();
});
}
// few foreign tests
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
// eslint-disable-next-line no-restricted-syntax
for (const builder of buildersToTestWith) {
const fixturesPath2 = path.resolve(
__dirname,
`../../${builder}/test/fixtures`,
);
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath2)) {
// don't run all foreign fixtures, just some
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
// eslint-disable-next-line no-loop-func
it(`should build ${builder}/${fixture}`, async () => {
await expect(
testDeployment(
{ builderUrl, buildUtilsUrl },
path.join(fixturesPath2, fixture),
),
).resolves.toBeDefined();
});
}
}
}
it('Test `detectBuilders`', async () => {
{
// package.json + no build
const pkg = { dependencies: { next: '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);
}
{
// package.json + no build + next
const pkg = {
scripts: { build: 'next build' },
dependencies: { 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);
}
{
// package.json + no build + next
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);
}
{
// package.json + no build
const pkg = {};
const files = ['package.json'];
const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null);
expect(errors.length).toBe(1);
}
{
// static file
const files = ['index.html'];
const { builders, errors } = await detectBuilders(files);
expect(builders).toBe(null);
expect(errors).toBe(null);
}
{
// no package.json + public
const files = ['api/users.js', 'public/index.html'];
const { builders, errors } = await detectBuilders(files);
expect(builders[1].use).toBe('@now/static');
expect(errors).toBe(null);
}
{
// no package.json + no build + raw static + api
const files = ['api/users.js', 'index.html'];
const { builders, errors } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/users.js');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('index.html');
expect(builders.length).toBe(2);
expect(errors).toBe(null);
}
{
// package.json + no build + root + api
const files = ['index.html', 'api/[endpoint].js', 'static/image.png'];
const { builders, errors } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint].js');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('index.html');
expect(builders[2].use).toBe('@now/static');
expect(builders[2].src).toBe('static/image.png');
expect(builders.length).toBe(3);
expect(errors).toBe(null);
}
{
// api + ignore files
const files = [
'api/_utils/handler.js',
'api/[endpoint]/.helper.js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint]/[id].js');
expect(builders.length).toBe(1);
}
{
// api + next + public
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
};
const files = ['package.json', 'api/endpoint.js', 'public/index.html'];
const { builders } = await detectBuilders(files, pkg);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/next');
expect(builders[1].src).toBe('package.json');
expect(builders.length).toBe(2);
}
{
// api + next + raw static
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
};
const files = ['package.json', 'api/endpoint.js', 'index.html'];
const { builders } = await detectBuilders(files, pkg);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/next');
expect(builders[1].src).toBe('package.json');
expect(builders.length).toBe(2);
}
{
// api + raw static
const files = ['api/endpoint.js', 'index.html', 'favicon.ico'];
const { builders } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('favicon.ico');
expect(builders[2].use).toBe('@now/static');
expect(builders[2].src).toBe('index.html');
expect(builders.length).toBe(3);
}
{
// api + public
const files = [
'api/endpoint.js',
'public/index.html',
'public/favicon.ico',
'README.md',
];
const { builders } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/static');
expect(builders[1].src).toBe('public/**/*');
expect(builders.length).toBe(2);
}
{
// just public
const files = ['public/index.html', 'public/favicon.ico', 'README.md'];
const { builders } = await detectBuilders(files);
expect(builders[0].src).toBe('public/**/*');
expect(builders.length).toBe(1);
}
{
// next + public
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
};
const files = ['package.json', 'public/index.html', 'README.md'];
const { builders } = await detectBuilders(files, pkg);
expect(builders[0].use).toBe('@now/next');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
}
{
// nuxt
const pkg = {
scripts: { build: 'nuxt build' },
dependencies: { nuxt: '2.8.1' },
};
const files = ['package.json', 'pages/index.js'];
const { builders } = await detectBuilders(files, pkg);
expect(builders[0].use).toBe('@now/static-build');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
}
{
// package.json with no build + api
const pkg = { dependencies: { next: '9.0.0' } };
const files = ['package.json', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint].js');
expect(builders.length).toBe(1);
}
{
// package.json with no build + public directory
const pkg = { dependencies: { next: '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);
}
{
// no package.json + api
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { builders } = await detectBuilders(files);
expect(builders.length).toBe(2);
}
{
// no package.json + no api
const files = ['index.html'];
const { builders, errors } = await detectBuilders(files);
expect(builders).toBe(null);
expect(errors).toBe(null);
}
{
// package.json + api + canary
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { 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');
expect(builders.length).toBe(3);
}
{
// package.json + api + latest
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { 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');
expect(builders.length).toBe(3);
}
{
// package.json + api + random tag
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { 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');
expect(builders.length).toBe(3);
}
});
it('Test `detectRoutes`', async () => {
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
expect(defaultRoutes[0].dest).toBe('/api/team.js');
expect(defaultRoutes[1].dest).toBe('/api/user.go');
expect(defaultRoutes[2].dest).not.toBeDefined();
expect(defaultRoutes[2].status).toBe(404);
}
{
const files = ['api/user.go', 'api/user.js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders);
expect(error.code).toBe('conflicting_file_path');
}
{
const files = ['api/[user].go', 'api/[team]/[id].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders);
expect(error.code).toBe('conflicting_file_path');
}
{
const files = ['api/[team]/[team].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders);
expect(error.code).toBe('conflicting_path_segment');
}
{
const files = ['api/date/index.js', 'api/date/index.go'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, error } = await detectRoutes(files, builders);
expect(defaultRoutes).toBe(null);
expect(error.code).toBe('conflicting_file_path');
}
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
}
{
const files = [
'public/index.html',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes[2].status).toBe(404);
expect(defaultRoutes[2].src).toBe('/api(\\/.*)?$');
expect(defaultRoutes[3].src).toBe('/(.*)');
expect(defaultRoutes[3].dest).toBe('/public/$1');
expect(defaultRoutes.length).toBe(4);
}
{
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
};
const files = ['public/index.html', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes[1].status).toBe(404);
expect(defaultRoutes[1].src).toBe('/api(\\/.*)?$');
expect(defaultRoutes.length).toBe(2);
}
{
const files = ['public/index.html'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(1);
}
{
const files = ['api/date/index.js', 'api/date.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
expect(defaultRoutes[0].src).toBe(
'^/api/date(\\/|\\/index|\\/index\\.js)?$',
);
expect(defaultRoutes[0].dest).toBe('/api/date/index.js');
expect(defaultRoutes[1].src).toBe('^/api/(date|date\\.js)$');
expect(defaultRoutes[1].dest).toBe('/api/date.js');
}
{
const files = ['api/date.js', 'api/[date]/index.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
expect(defaultRoutes[0].src).toBe(
'^/api/([^\\/]+)(\\/|\\/index|\\/index\\.js)?$',
);
expect(defaultRoutes[0].dest).toBe('/api/[date]/index.js?date=$1');
expect(defaultRoutes[1].src).toBe('^/api/(date|date\\.js)$');
expect(defaultRoutes[1].dest).toBe('/api/date.js');
}
{
const files = [
'api/index.ts',
'api/index.d.ts',
'api/users/index.ts',
'api/users/index.d.ts',
'api/food.ts',
'api/ts/gold.ts',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(builders.length).toBe(4);
expect(builders[0].use).toBe('@now/node');
expect(builders[1].use).toBe('@now/node');
expect(builders[2].use).toBe('@now/node');
expect(builders[3].use).toBe('@now/node');
expect(defaultRoutes.length).toBe(5);
}
});
it('Test `detectBuilders` and `detectRoutes`', async () => {
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/my-endpoint',
mustContain: 'my-endpoint',
status: 200,
},
{
path: '/api/other-endpoint',
mustContain: 'other-endpoint',
status: 200,
},
{
path: '/api/team/zeit',
mustContain: 'team/zeit',
status: 200,
},
{
path: '/api/user/myself',
mustContain: 'user/myself',
status: 200,
},
{
path: '/api/not-okay/',
status: 404,
},
{
path: '/api',
status: 404,
},
{
path: '/api/',
status: 404,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2),
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture,
);
expect(deployment).toBeDefined();
});
it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
const probes = [
{
path: '/api/not-okay',
status: 404,
},
{
path: '/api',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/index.js',
mustContain: 'hello from api/index.js',
status: 200,
},
{
path: '/api/date.js',
mustContain: 'hello from api/date.js',
status: 200,
},
{
// Someone might expect this to be `date.js`,
// but I doubt that there is any case were both
// `date/index.js` and `date.js` exists,
// so it is not special cased
path: '/api/date',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/api/date/index.js',
mustContain: 'hello from api/date/index.js',
status: 200,
},
{
path: '/',
mustContain: 'hello from index.txt',
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
path.join(fixture, 'now.json'),
JSON.stringify(nowConfig, null, 2),
);
const deployment = await testDeployment(
{ builderUrl, buildUtilsUrl },
fixture,
);
expect(deployment).toBeDefined();
});

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": true,
"lib": ["esnext"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "./dist",
"types": ["node"],
"strict": true,
"target": "esnext"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,32 @@
root = true
[*]
indent_style = tab
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{*.json,*.json.example,*.gyp,*.yml}]
indent_style = space
indent_size = 2
[*.py]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
# Ideal settings - some plugins might support these.
[*.js]
quote_type = single
[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
curly_bracket_next_line = false
spaces_around_operators = true
spaces_around_brackets = outside
# close enough to 1TB
indent_brace_style = K&R

2
packages/now-cgi/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
handler

7
packages/now-cgi/build.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
export GOOS=linux
export GOARCH=amd64
export GOPATH=$HOME/go
go get github.com/aws/aws-lambda-go/events
go get github.com/aws/aws-lambda-go/lambda
go build -o handler main.go

46
packages/now-cgi/index.js Normal file
View File

@@ -0,0 +1,46 @@
const path = require('path');
const { mkdirp, copyFile } = require('fs-extra');
const glob = require('@now/build-utils/fs/glob'); // eslint-disable-line import/no-extraneous-dependencies
const download = require('@now/build-utils/fs/download'); // eslint-disable-line import/no-extraneous-dependencies
const { createLambda } = require('@now/build-utils/lambda'); // eslint-disable-line import/no-extraneous-dependencies
const getWritableDirectory = require('@now/build-utils/fs/get-writable-directory'); // eslint-disable-line import/no-extraneous-dependencies
const { shouldServe } = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest;
exports.build = async ({
workPath, files, entrypoint, meta,
}) => {
console.log('downloading files...');
const outDir = await getWritableDirectory();
await download(files, workPath, meta);
const handlerPath = path.join(__dirname, 'handler');
await copyFile(handlerPath, path.join(outDir, 'handler'));
const entrypointOutDir = path.join(outDir, path.dirname(entrypoint));
await mkdirp(entrypointOutDir);
// For now only the entrypoint file is copied into the lambda
await copyFile(
path.join(workPath, entrypoint),
path.join(outDir, entrypoint),
);
const lambda = await createLambda({
files: await glob('**', outDir),
handler: 'handler',
runtime: 'go1.x',
environment: {
SCRIPT_FILENAME: entrypoint,
},
});
return {
[entrypoint]: lambda,
};
};
exports.shouldServe = shouldServe;

36
packages/now-cgi/main.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
now "../../utils/go/bridge"
"net/http"
"net/http/cgi"
"os"
"path/filepath"
)
type CgiHandler struct {
http.Handler
Dir string
Script string
}
func (h *CgiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := cgi.Handler{
Path: h.Script,
Root: "/" + h.Script,
Dir: h.Dir,
Env: []string{
"HTTPS=on",
"SERVER_PORT=443",
"SERVER_SOFTWARE=@now/cgi",
},
}
handler.ServeHTTP(w, r)
}
func main() {
workdir, _ := filepath.Abs(".")
script := os.Getenv("SCRIPT_FILENAME")
handler := &CgiHandler{nil, workdir, script}
now.Start(handler)
}

View File

@@ -0,0 +1,25 @@
{
"name": "@now/cgi",
"version": "0.1.4",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-cgi"
},
"scripts": {
"test-integration": "best -I test/*.js",
"prepublish": "./build.sh"
},
"files": [
"index.js",
"handler"
],
"dependencies": {
"fs-extra": "7.0.0"
},
"devDependencies": {
"@zeit/best": "0.4.3",
"rmfr": "2.0.0"
}
}

862
packages/now-cgi/yarn.lock Normal file
View File

@@ -0,0 +1,862 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
dependencies:
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@nodelib/fs.stat@^1.0.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
"@zeit/best@0.4.3":
version "0.4.3"
resolved "http://registry.npmjs.org/@zeit/best/-/best-0.4.3.tgz#eaebdfa8b24121a97b1753501ea8c9330d549b30"
dependencies:
arg "1.0.0"
chalk "2.3.1"
diff "3.5.0"
globby "8.0.0"
signal-exit "3.0.2"
ansi-styles@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
dependencies:
color-convert "^1.9.0"
append-type@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/append-type/-/append-type-1.0.1.tgz#e29a6eb22cec0c0b9b93063b3cf6b10d9e0c60f4"
arg@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/arg/-/arg-1.0.0.tgz#444d885a4e25b121640b55155ef7cd03975d6050"
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
arr-flatten@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
arr-union@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
array-to-sentence@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/array-to-sentence/-/array-to-sentence-1.1.0.tgz#c804956dafa53232495b205a9452753a258d39fc"
array-union@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
dependencies:
array-uniq "^1.0.1"
array-uniq@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
array-unique@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
assert-valid-glob-opts@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-valid-glob-opts/-/assert-valid-glob-opts-1.0.0.tgz#ab9b5438ec5e929f5bb08201819affb1227f730a"
dependencies:
glob-option-error "^1.0.0"
validate-glob-opts "^1.0.0"
assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
atob@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
base@^0.11.1:
version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
dependencies:
cache-base "^1.0.1"
class-utils "^0.3.5"
component-emitter "^1.2.1"
define-property "^1.0.0"
isobject "^3.0.1"
mixin-deep "^1.2.0"
pascalcase "^0.1.1"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
dependencies:
arr-flatten "^1.1.0"
array-unique "^0.3.2"
extend-shallow "^2.0.1"
fill-range "^4.0.0"
isobject "^3.0.1"
repeat-element "^1.1.2"
snapdragon "^0.8.1"
snapdragon-node "^2.0.1"
split-string "^3.0.2"
to-regex "^3.0.1"
cache-base@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
dependencies:
collection-visit "^1.0.0"
component-emitter "^1.2.1"
get-value "^2.0.6"
has-value "^1.0.0"
isobject "^3.0.1"
set-value "^2.0.0"
to-object-path "^0.3.0"
union-value "^1.0.0"
unset-value "^1.0.0"
call-me-maybe@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
chalk@2.3.1:
version "2.3.1"
resolved "http://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
dependencies:
ansi-styles "^3.2.0"
escape-string-regexp "^1.0.5"
supports-color "^5.2.0"
class-utils@^0.3.5:
version "0.3.6"
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
dependencies:
arr-union "^3.1.0"
define-property "^0.2.5"
isobject "^3.0.0"
static-extend "^0.1.1"
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
dependencies:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
dependencies:
color-name "1.1.3"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
component-emitter@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
ms "2.0.0"
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
define-property@^0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
dependencies:
is-descriptor "^0.1.0"
define-property@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
dependencies:
is-descriptor "^1.0.0"
define-property@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
dependencies:
is-descriptor "^1.0.2"
isobject "^3.0.1"
diff@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
dir-glob@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
dependencies:
arrify "^1.0.1"
path-type "^3.0.0"
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
dependencies:
debug "^2.3.3"
define-property "^0.2.5"
extend-shallow "^2.0.1"
posix-character-classes "^0.1.0"
regex-not "^1.0.0"
snapdragon "^0.8.1"
to-regex "^3.0.1"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
dependencies:
is-extendable "^0.1.0"
extend-shallow@^3.0.0, extend-shallow@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
dependencies:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
extglob@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
dependencies:
array-unique "^0.3.2"
define-property "^1.0.0"
expand-brackets "^2.1.4"
extend-shallow "^2.0.1"
fragment-cache "^0.2.1"
regex-not "^1.0.0"
snapdragon "^0.8.1"
to-regex "^3.0.1"
fast-glob@^2.0.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.3.tgz#d09d378e9ef6b0076a0fa1ba7519d9d4d9699c28"
dependencies:
"@mrmlnc/readdir-enhanced" "^2.2.1"
"@nodelib/fs.stat" "^1.0.1"
glob-parent "^3.1.0"
is-glob "^4.0.0"
merge2 "^1.2.1"
micromatch "^3.1.10"
fill-range@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
dependencies:
extend-shallow "^2.0.1"
is-number "^3.0.0"
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
fragment-cache@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
dependencies:
map-cache "^0.2.2"
fs-extra@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6"
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
glob-option-error@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/glob-option-error/-/glob-option-error-1.0.0.tgz#57cc65def9c7d5c1461baf13129bb5403cff6176"
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
dependencies:
is-glob "^3.1.0"
path-dirname "^1.0.0"
glob-to-regexp@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
glob@^7.0.5, glob@^7.1.2:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
globby@8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.0.tgz#e6f8340ead9a52fa417ec0e75ae664ae0026f5c6"
dependencies:
array-union "^1.0.1"
dir-glob "^2.0.0"
fast-glob "^2.0.2"
glob "^7.1.2"
ignore "^3.3.5"
pify "^3.0.0"
slash "^1.0.0"
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.1.14"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.14.tgz#1b6e8362ef8c5ecb5da799901f39297e3054773a"
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
has-value@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
dependencies:
get-value "^2.0.3"
has-values "^0.1.4"
isobject "^2.0.0"
has-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
dependencies:
get-value "^2.0.6"
has-values "^1.0.0"
isobject "^3.0.0"
has-values@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
has-values@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
dependencies:
is-number "^3.0.0"
kind-of "^4.0.0"
ignore@^3.3.5:
version "3.3.10"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
indexed-filter@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/indexed-filter/-/indexed-filter-1.0.3.tgz#7911439191cac588188464640a8db4f6b324973d"
dependencies:
append-type "^1.0.1"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
inspect-with-kind@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz#fce151d4ce89722c82ca8e9860bb96f9167c316c"
dependencies:
kind-of "^6.0.2"
is-accessor-descriptor@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
dependencies:
kind-of "^3.0.2"
is-accessor-descriptor@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
dependencies:
kind-of "^6.0.0"
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
dependencies:
kind-of "^3.0.2"
is-data-descriptor@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
dependencies:
kind-of "^6.0.0"
is-descriptor@^0.1.0:
version "0.1.6"
resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
dependencies:
is-accessor-descriptor "^0.1.6"
is-data-descriptor "^0.1.4"
kind-of "^5.0.0"
is-descriptor@^1.0.0, is-descriptor@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
dependencies:
is-accessor-descriptor "^1.0.0"
is-data-descriptor "^1.0.0"
kind-of "^6.0.2"
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
is-extendable@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
dependencies:
is-plain-object "^2.0.4"
is-extglob@^2.1.0, is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
dependencies:
is-extglob "^2.1.0"
is-glob@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
dependencies:
is-extglob "^2.1.1"
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
dependencies:
kind-of "^3.0.2"
is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
dependencies:
isobject "^3.0.1"
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
isarray@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
isobject@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
dependencies:
isarray "1.0.0"
isobject@^3.0.0, isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
optionalDependencies:
graceful-fs "^4.1.6"
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
dependencies:
is-buffer "^1.1.5"
kind-of@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
dependencies:
is-buffer "^1.1.5"
kind-of@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
kind-of@^6.0.0, kind-of@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
dependencies:
object-visit "^1.0.0"
merge2@^1.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5"
micromatch@^3.1.10:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
dependencies:
arr-diff "^4.0.0"
array-unique "^0.3.2"
braces "^2.3.1"
define-property "^2.0.2"
extend-shallow "^3.0.2"
extglob "^2.0.4"
fragment-cache "^0.2.1"
kind-of "^6.0.2"
nanomatch "^1.2.9"
object.pick "^1.3.0"
regex-not "^1.0.0"
snapdragon "^0.8.1"
to-regex "^3.0.2"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
brace-expansion "^1.1.7"
mixin-deep@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
dependencies:
for-in "^1.0.2"
is-extendable "^1.0.1"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
dependencies:
arr-diff "^4.0.0"
array-unique "^0.3.2"
define-property "^2.0.2"
extend-shallow "^3.0.2"
fragment-cache "^0.2.1"
is-windows "^1.0.2"
kind-of "^6.0.2"
object.pick "^1.3.0"
regex-not "^1.0.0"
snapdragon "^0.8.1"
to-regex "^3.0.1"
object-copy@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
dependencies:
copy-descriptor "^0.1.0"
define-property "^0.2.5"
kind-of "^3.0.3"
object-visit@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
dependencies:
isobject "^3.0.0"
object.pick@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
dependencies:
isobject "^3.0.1"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
wrappy "1"
pascalcase@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
path-dirname@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
dependencies:
pify "^3.0.0"
pify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
dependencies:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
repeat-element@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
rmfr@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/rmfr/-/rmfr-2.0.0.tgz#8a42e81332550b3f0019b8fb8ab245bea81b6d1c"
dependencies:
assert-valid-glob-opts "^1.0.0"
glob "^7.1.2"
graceful-fs "^4.1.11"
inspect-with-kind "^1.0.4"
rimraf "^2.6.2"
safe-regex@^1.1.0:
version "1.1.0"
resolved "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
dependencies:
ret "~0.1.10"
set-value@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
dependencies:
extend-shallow "^2.0.1"
is-extendable "^0.1.1"
is-plain-object "^2.0.1"
to-object-path "^0.3.0"
set-value@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
dependencies:
extend-shallow "^2.0.1"
is-extendable "^0.1.1"
is-plain-object "^2.0.3"
split-string "^3.0.1"
signal-exit@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
dependencies:
define-property "^1.0.0"
isobject "^3.0.0"
snapdragon-util "^3.0.1"
snapdragon-util@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
dependencies:
kind-of "^3.2.0"
snapdragon@^0.8.1:
version "0.8.2"
resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
dependencies:
base "^0.11.1"
debug "^2.2.0"
define-property "^0.2.5"
extend-shallow "^2.0.1"
map-cache "^0.2.2"
source-map "^0.5.6"
source-map-resolve "^0.5.0"
use "^3.1.0"
source-map-resolve@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
dependencies:
atob "^2.1.1"
decode-uri-component "^0.2.0"
resolve-url "^0.2.1"
source-map-url "^0.4.0"
urix "^0.1.0"
source-map-url@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
dependencies:
extend-shallow "^3.0.0"
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
dependencies:
define-property "^0.2.5"
object-copy "^0.1.0"
supports-color@^5.2.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
dependencies:
has-flag "^3.0.0"
to-object-path@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
dependencies:
kind-of "^3.0.2"
to-regex-range@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
dependencies:
is-number "^3.0.0"
repeat-string "^1.6.1"
to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
dependencies:
define-property "^2.0.2"
extend-shallow "^3.0.2"
regex-not "^1.0.2"
safe-regex "^1.1.0"
union-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
dependencies:
arr-union "^3.1.0"
get-value "^2.0.6"
is-extendable "^0.1.1"
set-value "^0.4.3"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
unset-value@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
dependencies:
has-value "^0.3.1"
isobject "^3.0.0"
urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
validate-glob-opts@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/validate-glob-opts/-/validate-glob-opts-1.0.2.tgz#ef9f98977d965537ea4f51fa7d5799e9c6ebca91"
dependencies:
array-to-sentence "^1.1.0"
indexed-filter "^1.0.0"
inspect-with-kind "^1.0.4"
is-plain-obj "^1.1.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"

View File

@@ -0,0 +1,41 @@
![now](https://assets.zeit.co/image/upload/v1542240976/repositories/now-cli/now-cli-repo-banner-v2.png)
[![Build Status](https://circleci.com/gh/zeit/now-cli.svg?&style=shield)](https://circleci.com/gh/zeit/workflows/now-cli)
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit)
**Note**: The [canary](https://github.com/zeit/now-cli/tree/canary) branch is under heavy development the stable release branch is [master](https://github.com/zeit/now-cli/tree/master).
## Usage
To install the latest version of Now CLI, visit [zeit.co/download](https://zeit.co/download) or run this command:
```
npm i -g now
```
To quickly start a new project, run the following commands:
```
now init # Pick an example project to clone
cd <PROJECT> # Change directory to the newly created project
now dev # Run locally during development
now # Deploy to the cloud
```
## Documentation
For details on how to use Now CLI, check out our [documentation](https://zeit.co/docs).
## Caught a Bug?
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device
2. Install dependencies with `yarn install`
3. Compile the code: `yarn build`
4. Link the package to the global module directory: `yarn link`
5. You can now start using `now` anywhere inside the command line
As always, you should use `yarn test` to run the tests and see if your changes have broken anything.
## How to Create a Release
If you have write access to this repository, you can read more about how to publish a release [here](https://github.com/zeit/now-cli/wiki/Creating-a-Release).

View File

@@ -0,0 +1,200 @@
{
"name": "now",
"version": "16.1.2",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
"repository": "zeit/now-cli",
"scripts": {
"test": "yarn test-lint",
"preinstall": "node ./scripts/preinstall.js",
"test-unit": "nyc ava test/*unit.js --serial --fail-fast --verbose",
"test-integration": "ava test/integration.js --serial --fail-fast",
"test-integration-now-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
"test-lint": "eslint . --ext .js,.ts",
"prepublishOnly": "yarn build",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"build": "ts-node ./scripts/build.ts",
"build-dev": "ts-node ./scripts/build.ts --dev",
"format-modified": "prettier --parser typescript --write --single-quote `git diff --name-only HEAD * | grep -e \".*\\.ts$\" -e \".*\\.js$\" | xargs echo`"
},
"nyc": {
"include": [
"src/**"
],
"extension": [
".js",
".ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"text",
"html"
],
"sourceMap": true,
"instrument": true,
"all": true
},
"git": {
"pre-commit": [
"test-lint",
"format-modified"
]
},
"bin": {
"now": "./dist/index.js"
},
"files": [
"dist",
"scripts/preinstall.js"
],
"ava": {
"compileEnhancements": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register/transpile-only",
"esm"
]
},
"engines": {
"node": ">= 8.11"
},
"devDependencies": {
"@now/build-utils": "0.9.13",
"@now/go": "latest",
"@now/next": "latest",
"@now/node": "latest",
"@now/php": "latest",
"@now/static-build": "latest",
"@sentry/node": "5.5.0",
"@types/ansi-escapes": "3.0.0",
"@types/ansi-regex": "4.0.0",
"@types/async-retry": "1.2.1",
"@types/bytes": "3.0.0",
"@types/debug": "0.0.31",
"@types/dotenv": "6.1.1",
"@types/escape-html": "0.0.20",
"@types/execa": "0.9.0",
"@types/fs-extra": "5.0.5",
"@types/glob": "7.1.1",
"@types/http-proxy": "1.16.2",
"@types/load-json-file": "2.0.7",
"@types/micro": "7.3.3",
"@types/mime-types": "2.1.0",
"@types/minimatch": "3.0.3",
"@types/mri": "1.1.0",
"@types/ms": "0.7.30",
"@types/node": "11.11.0",
"@types/node-fetch": "2.1.4",
"@types/npm-package-arg": "6.1.0",
"@types/pluralize": "0.0.29",
"@types/progress": "2.0.3",
"@types/psl": "1.1.0",
"@types/semver": "6.0.1",
"@types/tar-fs": "1.16.1",
"@types/text-table": "0.2.0",
"@types/universal-analytics": "0.4.2",
"@types/which": "1.3.1",
"@types/write-json-file": "2.2.1",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.1.0",
"@zeit/dockerignore": "0.0.5",
"@zeit/fun": "0.9.1",
"@zeit/git-hooks": "0.1.4",
"@zeit/ncc": "0.18.5",
"@zeit/source-map-support": "0.6.2",
"alpha-sort": "2.0.1",
"ansi-escapes": "3.0.0",
"ansi-regex": "3.0.0",
"arg": "2.0.0",
"async-listen": "1.2.0",
"async-retry": "1.1.3",
"async-sema": "2.1.4",
"ava": "2.2.0",
"bytes": "3.0.0",
"cache-or-tmp-directory": "1.0.0",
"chalk": "2.4.2",
"chokidar": "2.1.6",
"clipboardy": "2.1.0",
"codecov": "3.1.0",
"cpy": "7.2.0",
"credit-card": "3.0.1",
"date-fns": "1.29.0",
"death": "1.1.0",
"debug": "3.1.0",
"deployment-type": "1.0.1",
"docker-file-parser": "1.0.2",
"dot": "1.1.2",
"dotenv": "4.0.0",
"download": "6.2.5",
"email-prompt": "0.3.2",
"email-validator": "1.1.1",
"epipebomb": "1.0.0",
"escape-html": "1.0.3",
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-config-prettier": "4.1.0",
"eslint-import-resolver-typescript": "1.1.1",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4",
"esm": "3.1.4",
"execa": "1.0.0",
"fetch-h2": "2.0.3",
"fs-extra": "7.0.1",
"glob": "7.1.2",
"http-proxy": "1.17.0",
"ini": "1.3.4",
"inquirer": "3.3.0",
"is-url": "1.2.2",
"jaro-winkler": "0.2.8",
"jsonlines": "0.1.1",
"load-json-file": "3.0.0",
"micro": "9.1.2",
"mime-types": "2.1.24",
"minimatch": "3.0.4",
"mri": "1.1.0",
"ms": "2.1.1",
"node-fetch": "1.7.3",
"npm-package-arg": "6.1.0",
"nyc": "13.2.0",
"ora": "3.4.0",
"pcre-to-regexp": "0.0.5",
"pluralize": "7.0.0",
"pre-commit": "1.2.2",
"prettier": "1.16.2",
"printf": "0.2.5",
"progress": "2.0.3",
"promisepipe": "3.0.0",
"psl": "1.1.31",
"qr-image": "3.2.0",
"raw-body": "2.4.1",
"read-pkg": "2.0.0",
"rx-lite-aggregates": "4.0.8",
"semver": "5.5.0",
"serve-handler": "6.1.1",
"sinon": "4.4.2",
"strip-ansi": "5.2.0",
"stripe": "5.1.0",
"tar-fs": "1.16.3",
"test-listen": "1.1.0",
"text-table": "0.2.0",
"then-sleep": "1.0.1",
"through2": "2.0.3",
"title": "3.4.1",
"tmp-promise": "1.0.3",
"ts-node": "8.3.0",
"typescript": "3.2.4",
"universal-analytics": "0.4.20",
"update-check": "1.5.3",
"utility-types": "2.1.0",
"which": "1.3.1",
"which-promise": "1.0.0",
"write-json-file": "2.2.0",
"yarn": "1.17.3"
}
}

View File

@@ -36,7 +36,7 @@ async function createBuildersTarball() {
} }
} }
const yarn = join(dirRoot, 'node_modules/yarn/bin/yarn.js'); const yarn = join(dirRoot, '../../node_modules/yarn/bin/yarn.js');
await execa(process.execPath, [yarn, 'add', '--no-lockfile', ...builders], { await execa(process.execPath, [yarn, 'add', '--no-lockfile', ...builders], {
cwd: buildersDir, cwd: buildersDir,
stdio: 'inherit' stdio: 'inherit'
@@ -61,7 +61,7 @@ async function main() {
// `fsevents` feature using `useFsEvents: false`, so delete the module here so // `fsevents` feature using `useFsEvents: false`, so delete the module here so
// that it is not compiled by ncc, which makes the npm package size larger // that it is not compiled by ncc, which makes the npm package size larger
// than necessary. // than necessary.
await remove(join(dirRoot, 'node_modules/fsevents')); await remove(join(dirRoot, '../../node_modules/fsevents'));
// Compile the `doT.js` template files for `now dev` // Compile the `doT.js` template files for `now dev`
console.log(); console.log();
@@ -92,7 +92,7 @@ async function main() {
// get compiled into the final ncc bundle file, however, we want them to be // get compiled into the final ncc bundle file, however, we want them to be
// present in the npm package because the contents of those files are involved // present in the npm package because the contents of those files are involved
// with `fun`'s cache invalidation mechanism and they need to be shasum'd. // with `fun`'s cache invalidation mechanism and they need to be shasum'd.
const runtimes = join(dirRoot, 'node_modules/@zeit/fun/dist/src/runtimes'); const runtimes = join(dirRoot, '../../node_modules/@zeit/fun/dist/src/runtimes');
const dest = join(dirRoot, 'dist/runtimes'); const dest = join(dirRoot, 'dist/runtimes');
await cpy('**/*', dest, { parents: true, cwd: runtimes }); await cpy('**/*', dest, { parents: true, cwd: runtimes });

View File

@@ -7,7 +7,7 @@ async function main() {
// Compile the `doT.js` template files for `now dev` // Compile the `doT.js` template files for `now dev`
const templatesDir = join(dirRoot, 'src/util/dev/templates'); const templatesDir = join(dirRoot, 'src/util/dev/templates');
const dotPacker = join(dirRoot, 'node_modules/dot/bin/dot-packer'); const dotPacker = join(dirRoot, '../../node_modules/dot/bin/dot-packer');
await execa(process.execPath, [dotPacker], { await execa(process.execPath, [dotPacker], {
cwd: templatesDir, cwd: templatesDir,
stdio: 'inherit' stdio: 'inherit'
@@ -18,7 +18,7 @@ async function main() {
// Prettier // Prettier
console.log('\nMaking the compiled template functions prettier...'); console.log('\nMaking the compiled template functions prettier...');
const prettier = join(dirRoot, 'node_modules/prettier/bin-prettier.js'); const prettier = join(dirRoot, '../../node_modules/prettier/bin-prettier.js');
await execa( await execa(
process.execPath, process.execPath,
[prettier, '--write', '--single-quote', ...compiledFiles], [prettier, '--write', '--single-quote', ...compiledFiles],

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