Compare commits

...

80 Commits

Author SHA1 Message Date
Steven
99410a0c06 Publish
- @now/build-utils@0.9.11
 - @now/go@0.5.8
 - @now/next@0.5.9
 - @now/node@0.12.4
 - @now/python@0.2.14
 - @now/ruby@0.1.4
 - @now/static-build@0.9.5
2019-08-07 08:28:33 -07:00
Timothy
9df2e3a62d [docs] Add Builders Developer Reference (#888)
* Add Builders developer reference

* Updates to the README
2019-08-07 08:27:59 -07:00
Steven
3cc786412d [now-bash] Move source code to external repository (#887)
* [now-bash] Remove source code

* Change tests to run from root instead of dist
2019-08-07 08:27:51 -07:00
Steven
973229e02e [now-node][now-next][now-static-build] Enable node 10 for zero c… (#879) 2019-08-07 08:27:10 -07:00
Steven
1493b0657b [now-build-utils] Default to Node 10 for zero config (#773)
* [now-build-utils] Default to Node 10

* Revert back to 8

* Enable node 10 for zero config

* Add silent flag
2019-08-07 08:26:56 -07:00
Steven
66df087faa [now-python] Use ncc before publishing to npm (#875) 2019-08-07 08:26:43 -07:00
Steven
4401799eb3 [now-ruby] Use ncc before publishing to npm (#877) 2019-08-07 08:26:37 -07:00
Steven
313cad8e20 [now-go] Use ncc before publishing to npm (#876)
* [now-go] Use ncc before publishing to npm

* Add missing main
2019-08-07 08:26:15 -07:00
Steven
3a51773693 [now-php][now-rust] Remove deprecated community builders (#871)
* Remove now-rust

* Remove now-php

* Remove now-php-bridge

* Remove unused eslintignore

* Change a comment
2019-08-07 08:25:56 -07:00
Andy Bitz
ddab628034 Publish
- @now/build-utils@0.9.10
2019-08-06 19:33:36 +02:00
Steven
2c10cdcbca [now-build-utils] Fix TS globbing for zero config (#886)
* [now-build-utils] Fix TS globbing for zero config

* Fix tests

* [now-build-utils] Fix TS globbing for zero config
2019-08-06 19:30:13 +02:00
Steven
c30eac53f1 [now-build-utils] Fix TS globbing for zero config (#885)
* [now-build-utils] Fix TS globbing for zero config

* Fix tests
2019-08-06 19:30:07 +02:00
Andy Bitz
2d3e32e95b Publish
- @now/build-utils@0.9.9
2019-08-06 03:01:43 +02:00
Andy
bd8d41cadc [now-build-utils] Fix 404 for index routes (#882) 2019-08-06 03:01:06 +02:00
Andy Bitz
0a6e7d8e23 Publish
- @now/static-build@0.9.4
2019-08-05 16:12:58 +02:00
Andy
e0a8cb5011 [now-static-build] Choose public dir when there is no dist dir (#874)
* [now-static-build] Choose `public` dir when there is no `dist` dir

* Fix `path.join`
2019-08-05 16:12:30 +02:00
Andy Bitz
bcdc27139f Publish
- @now/build-utils@0.9.8
2019-08-03 00:31:02 +02:00
Andy
8cfaef7e6c [now-build-utils] Fix 404 route (#872) 2019-08-03 00:30:33 +02:00
Andy Bitz
79c096b80e Publish
- @now/build-utils@0.9.7
2019-08-02 22:32:16 +02:00
Andy
2cacb95c7d [now-build-utils] Disable directory listing for api routes (#870) 2019-08-02 22:26:37 +02:00
Steven
0b54093ca2 Publish
- @now/go@0.5.7
 - @now/node@0.12.3
 - @now/python@0.2.13
 - @now/routing-utils@1.2.1
 - @now/rust@0.2.10
 - @now/static-build@0.9.3
2019-08-01 16:10:17 -07:00
Steven
8c7371e093 Revert "[now-build-utils][now-static-build] Fix spawn options fo… (#867)
This reverts commit cf640a619e35c8cebaf6655799f273ba11bbd6e6.
2019-08-01 16:06:00 -07:00
Matthew Sweeney
e59c0f59f5 [now-static-build] Add sapper Optimization and Test (#865)
* add sapper optimization and test

* remove test dir

* remove test scripts
2019-08-01 15:28:16 -07:00
Steven
21b115a788 Update CODEOWNERS (#866) 2019-08-01 15:28:07 -07:00
Connor Davis
c5df4f7f9e [now-routing-utils] Make Now Routing Utils a Public Library (#860)
* Make Now Routing Utils a Public Library

* Suggestions

* Cleanup

* Update lockfile

* Add to dist to eslint ignore

* v1.2.0-canary.0

* v1.2.0-canary.1

* v1.2.1-canary.0

* Use optional dep instead of peer dep
2019-08-01 15:28:02 -07:00
Igor Klopov
2429451115 [now-rust] install rust in npm install of the builder (#863) 2019-08-01 15:27:45 -07:00
Andy
21789cdbf1 [now-build-utils][now-static-build] Fix spawn options for windows (#864)
* [now-build-utils][now-static-build] Fix spawn options for windows

* Fix type

* Fix types
2019-08-01 15:27:39 -07:00
Igor Klopov
f74ed2aff1 [now-go] install golang in npm install step (#859)
* [now-go] install golang in npm install step

* update

* move logic into now-install script

* remove unneeded items from gitignore

* now-postinstall
2019-08-01 15:27:28 -07:00
Steven
2f83e6375a [now-node] Bump node-file-trace to 0.2.6 (#861) 2019-08-01 15:27:23 -07:00
Kai Richard König
99a7b8f1f7 [now-python] Use pipfile2req to convert pipfile to requirements.txt (#857)
* Use a more robust library to convert pipfile - closes #841

* Renaming for clarity

* Suppress warnings since we know it will be invoked manually

Co-Authored-By: Steven <steven@ceriously.com>

* Apply the flag while installing not while generating the requirements.

Co-Authored-By: Steven <steven@ceriously.com>
2019-08-01 15:27:17 -07:00
Steven
3fa7b80bde Publish
- @now/build-utils@0.9.6
 - @now/next@0.5.8
 - @now/node@0.12.2
2019-07-31 08:30:21 -04:00
Steven
d012e73166 [now-node] Bump node-file-trace to 0.2.5 (#852) 2019-07-31 08:29:09 -04:00
Andy
d6c46da37f [now-build-utils] Remove @now/nuxt from default builders (#850) 2019-07-31 08:29:04 -04:00
Andy
1508932ad6 [now-static-build] Allow setting the tag for zero-config builders (#842)
* [now-static-build] Allow to set the `tag` for zero-config builders

This reverts commit 32b6f1d6a9047920d6cc40385de711022f77c4d4.

* Remove unsupported builders

* Fix setting the tag
2019-07-31 08:28:58 -04:00
Steven
873b099f53 [now-node] Bump node-file-trace to 0.2.4 (#849) 2019-07-31 08:28:52 -04:00
Steven
6546d3b67d [now-build-utils] Fix require file path for legacy builders (#846)
* [now-build-utils] Fix require file path

* Add more tests
2019-07-31 08:28:46 -04:00
Steven
34ad4ac33a [now-next] Fix dev-server bundle output (#847)
* [now-next] Fix dev-server bundle

* [now-next] Fix bundle output dev-server
2019-07-31 08:28:39 -04:00
Steven
01d0b017af Publish
- @now/build-utils@0.9.5
 - @now/next@0.5.7
 - @now/node@0.12.1
 - @now/static-build@0.9.2
2019-07-30 07:38:20 -04:00
Steven
db88ad4b32 [master] Clean up package.json to match canary 2019-07-30 07:28:41 -04:00
Andy Bitz
4b846c3c88 Revert "[now-static-build] Allow to set the tag for zero-config builders"
This reverts commit d74d5141e4cb2c4e1e8a50bd66cad8d36f57e734.
2019-07-30 07:24:49 -04:00
Andy Bitz
2731435e3b [now-static-build] Allow to set the tag for zero-config builders 2019-07-30 07:24:43 -04:00
Steven
f18e1a6bd4 [now-static-build] Use ncc before publishing to npm (#839) 2019-07-30 07:24:31 -04:00
Steven
167d7bedec [now-next] Use ncc before publishing to npm (#832)
* [now-next] Change bridge build step

* Remove dependency on @now/node-bridge

* Test: print deploymentUrl

* Rename build script

* Use ncc before publishing

* Move deps to devDependencies
2019-07-30 07:24:11 -04:00
Sophearak Tha
cd0b1d61d1 [now-build-utils] Add debug() function (#831)
* Add `debug()` function

* Apply suggestions from code review

Co-Authored-By: Steven <steven@ceriously.com>

* Update packages/now-build-utils/src/debug.ts

Co-Authored-By: Steven <steven@ceriously.com>

* Apply suggestions from code review

Co-Authored-By: Steven <steven@ceriously.com>
2019-07-30 07:21:44 -04:00
Steven
86dc3708d2 [now-node] Fix 18-nested-tsconfig tests (#830)
* [now-node] Fix nested-tsconfig tests

* Remove unnecessary test
2019-07-30 07:21:19 -04:00
paulogdm
1e79eae029 Update _error.js route to return 404 status (#828) 2019-07-30 07:21:07 -04:00
paulogdm
d0c954210a Update types.ts (#829) 2019-07-30 07:20:55 -04:00
Steven
67c3481779 [now-build-utils] Use ncc before publishing to npm (#820)
* [now-build-utils] Use ncc before publish to npm

* Test all foreign fixtures

* Revert tests, but keep now-next
2019-07-30 07:20:42 -04:00
Steven
67473afd5b [now-node] Bump node-file-trace to 0.2.3 (#819)
* Silence vscode error

* Bump node-file-trace to 0.2.3

* Add test for mixed modules
2019-07-30 07:20:19 -04:00
Steven
3d52610acf [now-node] Fix symlink regression (#813) 2019-07-30 07:20:07 -04:00
Steven
729ba010c4 [now-node] Change TS default to esModuleInterop=true (#789)
* [now-node] Default to esModuleInterop=true

* Add test fixture 19-default-import-ts

* Change back to match ts-node
2019-07-30 07:19:51 -04:00
Steven
a470e563dc [now-node] Add logs with time for each step (#786)
* [now-node] Add logs with time for each step

* Fix typo
2019-07-30 07:19:33 -04:00
ywg-jean
15f674b8b7 [now-node] Yarn workspaces/transpilation followup (#782)
* tests transpilation of yarn workspaces

up until @now/node@0.7.4-canary.28 this used to work out of the box

* tests that typescript workspaces are properly compiled

* update node-file-trace, symlink handling

* update node-file-trace, use index.ts in package.json main

* remove logging!

* adds test with ts files in a subdir

* Updates typescript example to avoid node internal special casing

The initial example ended up passing even with a flawed implementation
because it unknowingly relied on a special case within node module's
resulotion engine which automaticaly resolves index files
within a module if they are located in the same directory as the
module's `package.json`

* upgrade to node-file-trace 0.2.2

* cleanup manual debugging leftovers

* fix yarn lock for node-file-trace 0.2.2

* more cleanup of manual tests

I offer my apologies about this mess
2019-07-30 07:19:15 -04:00
Andy Bitz
1e4e6b68e0 Publish
- @now/build-utils@0.9.4
2019-07-24 22:27:49 +02:00
Andy
2332100cff [now-build-utils] Do not use canary for zero config (#821) 2019-07-24 22:27:34 +02:00
Steven
9cb6f500b0 Publish
- @now/build-utils@0.9.3
 - @now/next@0.5.6
2019-07-23 14:45:11 -04:00
Joe Haddad
79a2bfde35 [now-next] Add immutable header to Next.js static assets (#810)
* Add immutable header to Next.js static assets

* Verify builder injects immutable header

* Use incorrect type

* Add missing field

* Add missing else clause
2019-07-23 14:43:29 -04:00
Luis Fernando Alvarez D
86a659e5c5 Normalize slash for nested paths (#788)
* Fixed slash issue in nested folders for Windows

* Added a test for dynamic routes in dev
2019-07-23 14:43:18 -04:00
Andy Bitz
0f00110db7 Publish
- @now/static-build@0.9.1
2019-07-22 20:51:23 +02:00
Andy
03ca6975ed [now-static-build] Use dev command in zero config when there is no (#808)
command
2019-07-22 20:50:25 +02:00
Andy
001813c529 [now-static-build] Adjust error message (#807) 2019-07-22 20:50:16 +02:00
Leo Lamprecht
570ef4824b Publish
- @now/static-build@0.9.0
2019-07-22 18:12:18 +00:00
Leo Lamprecht
ae9a43a0bd [now-static-build] Optimized Docusarus 2.0 (#809)
* Optimized Docusarus 2.0

* Better name for framework

* Added Docusaurus 1.0 test
2019-07-22 18:11:40 +00:00
Andy Bitz
ffaa5eaa17 Publish
- @now/build-utils@0.9.2
2019-07-21 20:31:50 +02:00
Andy
7f41a23a43 [now-build-utils] Improve builder detection (#803)
* [now-build-utils] Improve builder detection

* Changed message

* Change type

* Allow standalone public

* Removed unused interface

* Adjust test

* Adjust test
2019-07-21 20:31:21 +02:00
Leo Lamprecht
e80a1a5340 Publish
- @now/static-build@0.8.0
2019-07-20 13:46:54 +00:00
Leo Lamprecht
7eadaf889a Added zero config support for Ember (#801)
* Added zero config support for Ember

* Renamed dir

* Renamed dir

* Removed useless tests

* Trigger production build

* Fixed ember example
2019-07-20 13:38:07 +00:00
Andy Bitz
0823cc1005 Publish
- @now/static-build@0.7.5
2019-07-19 22:26:01 +02:00
Andy
11be2bf349 [now-static-build] Improve error message for the output directory for zero config (#795)
* [now-static-build] Improve error message for the output directory for zero config

* Add link
2019-07-19 22:24:58 +02:00
Andy Bitz
e8f31aebeb Publish
- @now/build-utils@0.9.1
2019-07-19 15:55:07 +02:00
Andy
162d27a38f [now-build-utils] Display link for missing build script warning (#792)
* [now-build-utils] Display link for missing `build` script warning

* Add link
2019-07-19 15:54:52 +02:00
Andy
355c007dbb [now-build-utils] Add nuxt builder detection (#798)
* [now-build-utils] Add nuxt builder detection

* Add tests

* Fix version
2019-07-19 15:54:47 +02:00
Andy Bitz
f946463cab Publish
- @now/static-build@0.7.4
2019-07-19 12:44:25 +02:00
Andy
a1e768ecc1 [now-static-build] Replace dist with public as output directory (#793)
* [now-static-build] Replace `dist` with `public` as output directory

* Fix test

* Revert "Fix test"

This reverts commit 351c84c7bea1313742c165f753e7481fc7c3c0fa.

* Revert "[now-static-build] Replace `dist` with `public` as output directory"

This reverts commit daedfac17729645d9b409d7df3da6b0fb5d3b56a.

* Apply only to zero config

* Fix path
2019-07-19 12:43:51 +02:00
Andy Bitz
539f9135dd Publish
- @now/build-utils@0.9.0
2019-07-19 00:35:45 +02:00
Andy
abd3d84d4c [now-build-utils] Improve static builds detection (#787)
* [now-build-utils] Improve static builds detection

* Don't deconstruct

* Allow null for package.json
2019-07-19 00:29:47 +02:00
Steven
0274893d31 Publish
- @now/bash@1.0.3
2019-07-18 10:32:09 -04:00
Steven
0ec767e0d0 Enhance readme publish steps (#783) 2019-07-18 10:19:34 -04:00
Steven
e6296fa06b Ignore yarn.lock changes on publish (#784) 2019-07-18 10:19:26 -04:00
Igor Klopov
35def42263 [now-bash] Respect meta.isDev flag (#778)
* switch from SRC to DIST, pass 'meta' to 'download'

* mkdir true directory of entrypoint

* lint fixes

* added a test

* Delete yarn-error.log
2019-07-18 10:19:18 -04:00
278 changed files with 27113 additions and 5278 deletions

View File

@@ -28,4 +28,7 @@ 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

View File

@@ -9,9 +9,8 @@
/packages/now-next/dist/*
/packages/now-node-bridge/*
/packages/now-python/dist/*
/packages/now-optipng/dist/*
/packages/now-go/*
/packages/now-rust/dist/*
/packages/now-ruby/dist/*
/packages/now-static-build/dist/*
/packages/now-static-build/test/fixtures/**
/packages/now-routing-utils/dist/*

4
.github/CODEOWNERS vendored
View File

@@ -2,10 +2,12 @@
# https://help.github.com/en/articles/about-code-owners
* @styfle
/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-rust @styfle @mike-engel @anmonteiro
/packages/now-ruby @styfle @coetry @nathancahill
/packages/now-static-build @styfle @AndyBitz
/packages/now-routing-utils @dav-is

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$/, '')
```

View File

@@ -1,15 +1,15 @@
# now-builders
This is a monorepo containing the [Official Builders](https://zeit.co/docs/v2/deployments/builders/overview) provided by the ZEIT team.
This is a monorepo containing the [Official Builders](https://zeit.co/docs/v2/advanced/builders) provided by the ZEIT team.
## Channels
There are two Channels:
| Channel | Git Branch | npm dist-tag | use example |
| ------- | ---------- | ------------ | ------------------ |
| Canary | `canary` | `@canary` | `@now/node@canary` |
| Stable | `master` | `@latest` | `@now/node@latest` |
| 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 should be submitted to the `canary` branch.
@@ -30,14 +30,23 @@ For the Stable Channel, you must do the following:
- Deploy the modified Builders
```
git checkout master
git pull # make sure you're up to date
# 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>
# ... etc ...
# Verify the only difference is "version" in package.json
git diff origin/canary
# Ship it
yarn publish-stable
```
@@ -50,3 +59,7 @@ use `npm publish --tag canary` if you are publishing a canary release!
### Contributing
See the [Contribution guidelines for this project](CONTRIBUTING.md), it also contains guidance on interpreting tests failures.
### Creating Your Own Builder
To create your own Builder, see [the Builder's Developer Reference](DEVELOPING_A_BUILDER.md).

View File

@@ -44,6 +44,7 @@
},
"devDependencies": {
"@types/node": "*",
"@zeit/ncc": "0.20.4",
"async-retry": "1.2.3",
"buffer-replace": "1.0.0",
"codecov": "3.2.0",

View File

@@ -1,32 +0,0 @@
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

View File

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

View File

@@ -1,16 +0,0 @@
#!/bin/bash
set -euo pipefail
cd "$LAMBDA_TASK_ROOT"
# Configure `import`
export IMPORT_CACHE="$LAMBDA_TASK_ROOT/.import-cache"
export PATH="$IMPORT_CACHE/bin:$PATH"
# Load `import` and runtime
# shellcheck disable=SC1090
. "$(which import)"
# shellcheck disable=SC1090
. "$IMPORT_CACHE/runtime.sh"
# Load user code and process events in a loop forever
_lambda_runtime_init

View File

@@ -1,40 +0,0 @@
#!/bin/bash
set -euo pipefail
# `import` debug logs are always enabled during build
export IMPORT_DEBUG=1
# Install `import`
IMPORT_BIN="$IMPORT_CACHE/bin/import"
mkdir -p "$(dirname "$IMPORT_BIN")"
curl -sfLS https://import.pw > "$IMPORT_BIN"
chmod +x "$IMPORT_BIN"
# For now only the entrypoint file is copied into the lambda
mkdir -p "$(dirname "$ENTRYPOINT")"
cp "$SRC/$ENTRYPOINT" "$ENTRYPOINT"
# Copy in the runtime
cp "$BUILDER/runtime.sh" "$IMPORT_CACHE"
cp "$BUILDER/bootstrap" .
# Load `import`
. "$(which import)"
# Cache runtime and user dependencies
echo "Caching imports in \"$ENTRYPOINT\"…"
. "$IMPORT_CACHE/runtime.sh"
. "$ENTRYPOINT"
echo "Done caching imports"
# Run user build script
if declare -f build > /dev/null; then
echo "Running \`build\` function in \"$ENTRYPOINT\"…"
build "$@"
fi
# Ensure the entrypoint defined a `handler` function
if ! declare -f handler > /dev/null; then
echo "ERROR: A \`handler\` function must be defined in \"$ENTRYPOINT\"!" >&2
exit 1
fi

View File

@@ -1,80 +0,0 @@
const execa = require('execa');
const { join } = require('path');
const snakeCase = require('snake-case');
const {
glob,
download,
createLambda,
getWriteableDirectory,
shouldServe,
} = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
// From this list: https://import.pw/importpw/import/docs/config.md
const allowedConfigImports = new Set([
'CACHE',
'CURL_OPTS',
'DEBUG',
'RELOAD',
'SERVER',
]);
exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest;
exports.build = async ({
workPath, files, entrypoint, config,
}) => {
const srcDir = await getWriteableDirectory();
console.log('downloading files...');
await download(files, srcDir);
const configEnv = Object.keys(config).reduce((o, v) => {
const name = snakeCase(v).toUpperCase();
if (allowedConfigImports.has(name)) {
o[`IMPORT_${name}`] = config[v]; // eslint-disable-line no-param-reassign
}
return o;
}, {});
if (config && config.import) {
Object.keys(config.import).forEach((key) => {
const name = snakeCase(key).toUpperCase();
// eslint-disable-next-line no-param-reassign
configEnv[`IMPORT_${name}`] = config.import[key];
});
}
const IMPORT_CACHE = `${workPath}/.import-cache`;
const env = Object.assign({}, process.env, configEnv, {
PATH: `${IMPORT_CACHE}/bin:${process.env.PATH}`,
IMPORT_CACHE,
SRC: srcDir,
BUILDER: __dirname,
ENTRYPOINT: entrypoint,
});
const builderPath = join(__dirname, 'builder.sh');
await execa(builderPath, [entrypoint], {
env,
cwd: workPath,
stdio: 'inherit',
});
const lambda = await createLambda({
files: await glob('**', workPath),
handler: entrypoint, // not actually used in `bootstrap`
runtime: 'provided',
environment: Object.assign({}, configEnv, {
SCRIPT_FILENAME: entrypoint,
}),
});
return {
[entrypoint]: lambda,
};
};
exports.shouldServe = shouldServe;

View File

@@ -1,25 +0,0 @@
{
"name": "@now/bash",
"version": "1.0.2",
"description": "Now 2.0 builder for HTTP endpoints written in Bash",
"main": "index.js",
"author": "Nathan Rajlich <nate@zeit.co>",
"license": "MIT",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/bash-now-bash",
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-bash"
},
"files": [
"builder.sh",
"runtime.sh",
"bootstrap",
"index.js",
"package.json"
],
"dependencies": {
"execa": "^1.0.0",
"snake-case": "^2.1.0"
}
}

View File

@@ -1,119 +0,0 @@
#!/bin/bash
import "static-binaries@1.0.0"
static_binaries jq
# These get reset upon each request
_STATUS_CODE="$(mktemp)"
_HEADERS="$(mktemp)"
_lambda_runtime_api() {
local endpoint="$1"
shift
curl -sfLS "http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/$endpoint" "$@"
}
_lambda_runtime_init() {
# Initialize user code
# shellcheck disable=SC1090
. "$SCRIPT_FILENAME" || {
local exit_code="$?"
local error_message="Initialization failed for '$SCRIPT_FILENAME' (exit code $exit_code)"
echo "$error_message" >&2
local error='{"errorMessage":"'"$error_message"'"}'
_lambda_runtime_api "init/error" -X POST -d "$error"
exit "$exit_code"
}
# Process events
while true; do _lambda_runtime_next; done
}
_lambda_runtime_next() {
echo 200 > "$_STATUS_CODE"
echo '{"content-type":"text/plain; charset=utf8"}' > "$_HEADERS"
local headers
headers="$(mktemp)"
# Get an event
local event
event="$(mktemp)"
_lambda_runtime_api invocation/next -D "$headers" | jq --raw-output --monochrome-output '.body' > "$event"
local request_id
request_id="$(grep -Fi Lambda-Runtime-Aws-Request-Id "$headers" | tr -d '[:space:]' | cut -d: -f2)"
rm -f "$headers"
# Execute the handler function from the script
local body
body="$(mktemp)"
# Stdin of the `handler` function is the HTTP request body.
# Need to use a fifo here instead of bash <() because Lambda
# errors with "/dev/fd/63 not found" for some reason :/
local stdin
stdin="$(mktemp -u)"
mkfifo "$stdin"
_lambda_runtime_body < "$event" > "$stdin" &
local exit_code=0
handler "$event" < "$stdin" > "$body" || exit_code="$?"
rm -f "$event" "$stdin"
if [ "$exit_code" -eq 0 ]; then
# Send the response
jq --raw-input --raw-output --compact-output --slurp --monochrome-output \
--arg statusCode "$(cat "$_STATUS_CODE")" \
--argjson headers "$(cat "$_HEADERS")" \
'{statusCode:$statusCode|tonumber, headers:$headers, encoding:"base64", body:.|@base64}' < "$body" \
| _lambda_runtime_api "invocation/$request_id/response" -X POST -d @- > /dev/null
rm -f "$body" "$_HEADERS"
else
local error_message="Invocation failed for 'handler' function in '$SCRIPT_FILENAME' (exit code $exit_code)"
echo "$error_message" >&2
_lambda_runtime_api "invocation/$request_id/error" -X POST -d '{"errorMessage":"'"$error_message"'"}' > /dev/null
fi
}
_lambda_runtime_body() {
local event
event="$(cat)"
if [ "$(jq --raw-output '.body | type' <<< "$event")" = "string" ]; then
if [ "$(jq --raw-output '.encoding' <<< "$event")" = "base64" ]; then
jq --raw-output '.body' <<< "$event" | base64 --decode
else
# assume plain-text body
jq --raw-output '.body' <<< "$event"
fi
fi
}
# Set the response status code.
http_response_code() {
echo "$1" > "$_STATUS_CODE"
}
# Sets a response header.
# Overrides existing header if it has already been set.
http_response_header() {
local name="$1"
local value="$2"
local tmp
tmp="$(mktemp)"
jq \
--arg name "$name" \
--arg value "$value" \
'.[$name] = $value' < "$_HEADERS" > "$tmp"
mv -f "$tmp" "$_HEADERS"
}
http_response_redirect() {
http_response_code "${2:-302}"
http_response_header "location" "$1"
}
http_response_json() {
http_response_header "content-type" "application/json; charset=utf8"
}

View File

@@ -1,146 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
semver "^5.5.0"
shebang-command "^1.2.0"
which "^1.2.9"
end-of-stream@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
dependencies:
once "^1.4.0"
execa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
dependencies:
cross-spawn "^6.0.0"
get-stream "^4.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
get-stream@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
dependencies:
pump "^3.0.0"
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
lower-case@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==
dependencies:
lower-case "^1.1.1"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
dependencies:
path-key "^2.0.0"
once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
semver@^5.5.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
dependencies:
shebang-regex "^1.0.0"
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
signal-exit@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
snake-case@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f"
integrity sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8=
dependencies:
no-case "^2.2.0"
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

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

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

View File

@@ -1 +1 @@
module.exports = require('../dist/fs/get-writable-directory').default;
module.exports = require('../dist/index').getWriteableDirectory;

View File

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

View File

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

View File

@@ -1 +1 @@
module.exports = require('../dist/fs/run-user-scripts');
module.exports = require('../dist/index');

View File

@@ -1 +1 @@
module.exports = require('../dist/fs/stream-to-buffer').default;
module.exports = require('../dist/index').streamToBuffer;

View File

@@ -1,6 +1,6 @@
{
"name": "@now/build-utils",
"version": "0.8.9",
"version": "0.9.11",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -11,23 +11,9 @@
"directory": "packages/now-build-utils"
},
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublishOnly": "tsc"
},
"dependencies": {
"async-retry": "1.2.3",
"async-sema": "2.1.4",
"cross-spawn": "6.0.5",
"end-of-stream": "1.4.1",
"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",
"yazl": "2.4.3"
"build": "./build.sh",
"test": "./build.sh && jest",
"prepublishOnly": "./build.sh"
},
"devDependencies": {
"@types/async-retry": "^1.2.1",
@@ -39,7 +25,19 @@
"@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",
"typescript": "3.5.2"
"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

@@ -1,110 +0,0 @@
import { PackageJson, Builder, Config } from './types';
import minimatch from 'minimatch';
const src: string = 'package.json';
const config: Config = { zeroConfig: true };
// Static builders are special cased in `@now/static-build`
const BUILDERS = new Map<string, Builder>([
['next', { src, use: '@now/next', config }],
]);
const API_BUILDERS: Builder[] = [
{ src: 'api/**/*.js', use: '@now/node@canary', config },
{ src: 'api/**/*.ts', use: '@now/node@canary', config },
{ src: 'api/**/*.rs', use: '@now/rust', config },
{ src: 'api/**/*.go', use: '@now/go', config },
{ src: 'api/**/*.php', use: '@now/php', config },
{ src: 'api/**/*.py', use: '@now/python', config },
{ src: 'api/**/*.rb', use: '@now/ruby', config },
{ src: 'api/**/*.sh', use: '@now/bash', config },
];
interface Warning {
code: string;
message: string;
}
export async function detectBuilder(
pkg: PackageJson
): Promise<{
builder: null | Builder;
warnings: null | Warning[];
}> {
let warnings: null | Warning[] = null;
const scripts = pkg.scripts || {};
if (!scripts.build) {
warnings = [
{
code: 'missing_build_script',
message:
'Your `package.json` file is missing a `build` property inside the `script` property',
},
];
}
for (const [dependency, builder] of BUILDERS) {
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
// Return the builder when a dependency matches
if (deps[dependency]) {
return { builder, warnings };
}
}
// If there is no `build` and `now-build` script
// we'll not select `@now/static-build`
// since it would fail
if (!scripts.build) {
return { builder: null, warnings };
}
// By default we'll choose the `static-build` builder
const builder = { src, use: '@now/static-build', config };
return { builder, warnings };
}
// 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 the file does not match any builder we also
// don't want to create a route e.g. `package.json`
if (API_BUILDERS.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);
}
export async function detectApiBuilders(
files: string[]
): Promise<Builder[] | null> {
const builds = files
.sort(sortFiles)
.filter(ignoreApiFilter)
.map(file => {
const result = API_BUILDERS.find(
({ src }): boolean => minimatch(file, src)
);
return result ? { ...result, src: file } : null;
});
const finishedBuilds = builds.filter(Boolean);
return finishedBuilds.length > 0 ? (finishedBuilds as Builder[]) : null;
}

View File

@@ -0,0 +1,173 @@
import { PackageJson, Builder, Config } from './types';
import minimatch from 'minimatch';
interface ErrorResponse {
code: string;
message: string;
}
interface Options {
tag?: 'canary' | 'latest';
}
const src: string = 'package.json';
const config: Config = { zeroConfig: true };
// Static builders are special cased in `@now/static-build`
const BUILDERS = new Map<string, Builder>([
['next', { src, use: '@now/next', config }],
]);
const API_BUILDERS: Builder[] = [
{ 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 },
];
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',
};
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 BUILDERS) {
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 (API_BUILDERS.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 = API_BUILDERS.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((builder: Builder) => {
// @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

@@ -1,6 +1,16 @@
import { Route } from './types';
import { Route, Builder } from './types';
import { parse as parsePath } from 'path';
import { ignoreApiFilter, sortFiles } from './detect-builder';
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('/');
@@ -46,8 +56,14 @@ function createRouteFromPath(filePath: string): Route {
query.push(`${name}=$${counter++}`);
return `([^\\/]+)`;
} else if (isLast) {
const { name: fileName } = parsePath(segment);
return fileName;
const { name: fileName, ext } = parsePath(segment);
const isIndex = fileName === 'index';
// Either filename with extension, filename without extension
// or nothing when the filename is `index`
return `(${escapeName(fileName)}|${escapeName(fileName)}${escapeName(
ext
)})${isIndex ? '?' : ''}`;
}
return segment;
@@ -167,12 +183,12 @@ function sortFilesBySegmentCount(fileA: string, fileB: string): number {
return 0;
}
export async function detectApiRoutes(
files: string[]
): Promise<{
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 };
}
@@ -232,5 +248,39 @@ export async function detectApiRoutes(
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

@@ -4,7 +4,7 @@ import path from 'path';
import spawn from 'cross-spawn';
import { SpawnOptions } from 'child_process';
import { deprecate } from 'util';
import { Meta, PackageJson, NodeVersion } from '../types';
import { Meta, PackageJson, NodeVersion, Config } from '../types';
import { getSupportedNodeVersion } from './node-version';
function spawnAsync(
@@ -75,13 +75,23 @@ export function getSpawnOptions(
export async function getNodeVersion(
destPath: string,
minNodeVersion?: string
minNodeVersion?: string,
config?: Config
): Promise<NodeVersion> {
const { packageJson } = await scanParentDirs(destPath, true);
const range =
(packageJson && packageJson.engines && packageJson.engines.node) ||
minNodeVersion;
return getSupportedNodeVersion(range, typeof minNodeVersion !== 'undefined');
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) {

View File

@@ -16,8 +16,9 @@ import {
} from './fs/run-user-scripts';
import streamToBuffer from './fs/stream-to-buffer';
import shouldServe from './should-serve';
import { detectBuilder, detectApiBuilders } from './detect-builder';
import { detectApiRoutes } from './detect-routes';
import { detectBuilders } from './detect-builders';
import { detectRoutes } from './detect-routes';
import debug from './debug';
export {
FileBlob,
@@ -38,9 +39,9 @@ export {
getSpawnOptions,
streamToBuffer,
shouldServe,
detectBuilder,
detectApiBuilders,
detectApiRoutes,
detectBuilders,
detectRoutes,
debug,
};
export * from './types';

View File

@@ -23,6 +23,8 @@ export interface Route {
headers?: {
[key: string]: string;
};
continue?: boolean;
status?: number;
}
export interface Config {

View File

@@ -4,7 +4,9 @@ const fs = require('fs-extra');
// eslint-disable-next-line import/no-extraneous-dependencies
const execa = require('execa');
const assert = require('assert');
const { glob, download } = require('../');
const {
glob, download, detectBuilders, detectRoutes,
} = require('../');
const { createZip } = require('../dist/lambda');
const {
getSupportedNodeVersion,
@@ -16,12 +18,6 @@ const {
testDeployment,
} = require('../../../test/lib/deployment/test-deployment.js');
const {
detectBuilder,
detectApiBuilders,
detectApiRoutes,
} = require('../dist');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
@@ -120,6 +116,36 @@ it('should match all semver ranges', () => {
);
});
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');
@@ -139,7 +165,7 @@ for (const fixture of fs.readdirSync(fixturesPath)) {
// few foreign tests
const buildersToTestWith = ['now-node', 'now-static-build'];
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
// eslint-disable-next-line no-restricted-syntax
for (const builder of buildersToTestWith) {
@@ -151,7 +177,7 @@ for (const builder of buildersToTestWith) {
// 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', '03-env-vars'].includes(fixture)) {
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(
@@ -165,109 +191,399 @@ for (const builder of buildersToTestWith) {
}
}
it('Test `detectBuilder`', async () => {
it('Test `detectBuilders`', async () => {
{
const pkg = { dependencies: { next: '1.0.0' } };
const { builder, warnings } = await detectBuilder(pkg);
expect(builder.use).toBe('@now/next');
expect(warnings.length).toBe(1);
}
{
const pkg = { devDependencies: { next: '1.0.0' } };
const { builder } = await detectBuilder(pkg);
expect(builder.use).toBe('@now/next');
}
{
const pkg = {};
const { builder } = await detectBuilder(pkg);
expect(builder).toBe(null);
}
});
it('Test `detectApiBuilders`', async () => {
{
const files = ['package.json', 'api/user.js', 'api/team.js'];
const builders = await detectApiBuilders(files);
expect(builders[0].use).toBe('@now/node@canary');
}
{
const files = ['package.json', 'api/user.go', 'api/team.js'];
const builders = await detectApiBuilders(files);
expect(builders.some(({ use }) => use === '@now/go')).toBeTruthy();
expect(builders.some(({ use }) => use === '@now/node@canary')).toBeTruthy();
}
{
const files = ['package.json'];
const builders = await detectApiBuilders(files);
// 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);
}
{
const files = [
'api/users/[id].js',
'api/_utils/handler.js',
'api/users/.helper.js',
'api/teams/_helper.js',
];
const builders = await detectApiBuilders(files);
expect(builders.length).toBe(1);
}
});
it('Test `detectApiRoutes`', async () => {
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { defaultRoutes } = await detectApiRoutes(files);
expect(defaultRoutes.length).toBe(2);
expect(defaultRoutes[0].dest).toBe('/api/team.js');
expect(defaultRoutes[1].dest).toBe('/api/user.go');
// 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);
}
{
const files = ['api/user.go', 'api/user.js'];
const { error } = await detectApiRoutes(files);
expect(error.code).toBe('conflicting_file_path');
// 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);
}
{
const files = ['api/[user].go', 'api/[team]/[id].js'];
const { error } = await detectApiRoutes(files);
expect(error.code).toBe('conflicting_file_path');
// 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);
}
{
const files = ['api/[team]/[team].js'];
const { error } = await detectApiRoutes(files);
expect(error.code).toBe('conflicting_path_segment');
// static file
const files = ['index.html'];
const { builders, errors } = await detectBuilders(files);
expect(builders).toBe(null);
expect(errors).toBe(null);
}
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { defaultRoutes } = await detectApiRoutes(files);
expect(defaultRoutes.length).toBe(2);
// 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 detectApiBuilders(files);
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);
}
});
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);
}
});

View File

@@ -1,6 +1,6 @@
node_modules
dist
*.log
/go
/analyze
*.js
!util/install.js

View File

@@ -1,5 +0,0 @@
*.ts
test
tsconfig.json
package-lock.json
yarn.lock

2
packages/now-go/build.sh Executable file
View File

@@ -0,0 +1,2 @@
ncc build index.ts -o dist
ncc build install.ts -o dist/install

View File

@@ -0,0 +1,6 @@
import { downloadGo } from './go-helpers';
downloadGo().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -1,7 +1,8 @@
{
"name": "@now/go",
"version": "0.5.6",
"version": "0.5.8",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/go-now-go",
"repository": {
"type": "git",
@@ -9,29 +10,25 @@
"directory": "packages/now-go"
},
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublish": "tsc"
"build": "./build.sh",
"test": "./build.sh && jest",
"prepublish": "./build.sh",
"now-postinstall": "node dist/install/index.js"
},
"files": [
"*.js",
"main.go",
"main__mod__.go",
"util"
"dist"
],
"dependencies": {
"debug": "^4.1.1",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"node-fetch": "^2.2.1",
"tar": "4.4.6"
},
"devDependencies": {
"@types/debug": "^4.1.3",
"@types/execa": "^0.9.0",
"@types/fs-extra": "^5.0.5",
"@types/node-fetch": "^2.3.0",
"@types/tar": "^4.0.0",
"debug": "^4.1.1",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"node-fetch": "^2.2.1",
"tar": "4.4.6",
"typescript": "3.5.2"
}
}

View File

@@ -1,2 +1,2 @@
/dist
/src/now__bridge.d.ts
/src/now__bridge.ts

16
packages/now-next/build.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
set -euo pipefail
bridge_defs="$(dirname $(pwd))/now-node-bridge/src/bridge.ts"
cp -v "$bridge_defs" src/now__bridge.ts
tsc
ncc build src/dev-server.ts -o dist/dev
mv dist/dev/index.js dist/dev-server.js
rm -rf dist/dev
ncc build src/index.ts -o dist/main
mv dist/main/index.js dist/index.js
rm -rf dist/main

View File

@@ -1,11 +0,0 @@
#!/bin/bash
set -euo pipefail
bridge_entrypoint="$(node -p 'require.resolve("@now/node-bridge")')"
bridge_defs="$(dirname "$bridge_entrypoint")/bridge.d.ts"
if [ ! -e "$bridge_defs" ]; then
yarn install --cwd "$bridge_entrypoint"
fi
cp -v "$bridge_defs" src/now__bridge.d.ts

View File

@@ -1,33 +1,30 @@
{
"name": "@now/next",
"version": "0.5.5",
"version": "0.5.9",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/next-js-now-next",
"scripts": {
"build": "./getBridgeTypes.sh && tsc",
"test": "npm run build && jest",
"prepublish": "yarn run build"
"build": "./build.sh",
"test": "./build.sh && jest",
"prepublishOnly": "./build.sh"
},
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-next"
},
"dependencies": {
"@now/node-bridge": "1.2.3",
"fs-extra": "^7.0.0",
"get-port": "^5.0.0",
"resolve-from": "^5.0.0",
"semver": "^5.6.0"
},
"files": [
"dist"
],
"devDependencies": {
"@types/next-server": "^8.0.0",
"@types/resolve-from": "^5.0.1",
"@types/semver": "^6.0.0",
"@types/next-server": "8.0.0",
"@types/resolve-from": "5.0.1",
"@types/semver": "6.0.0",
"fs-extra": "7.0.0",
"get-port": "5.0.0",
"resolve-from": "5.0.0",
"semver": "6.1.1",
"typescript": "3.5.2"
}
}

View File

@@ -1,4 +1,4 @@
import { ChildProcess, fork, SpawnOptions } from 'child_process';
import { ChildProcess, fork } from 'child_process';
import {
pathExists,
readFile,
@@ -16,33 +16,34 @@ import {
FileBlob,
FileFsRef,
Files,
getNodeVersion,
getSpawnOptions,
glob,
Lambda,
PrepareCacheOptions,
runNpmInstall,
runPackageJsonScript,
getNodeVersion,
getSpawnOptions,
Route,
} from '@now/build-utils';
import createServerlessConfig from './create-serverless-config';
import nextLegacyVersions from './legacy-versions';
import {
EnvConfig,
excludeFiles,
filesFromDirectory,
getDynamicRoutes,
getNextConfig,
getPathsInside,
getRoutes,
includeOnlyEntryDirectory,
isDynamicRoute,
normalizePackageJson,
filesFromDirectory,
normalizePage,
stringMap,
syncEnvVars,
validateEntrypoint,
normalizePage,
getDynamicRoutes,
isDynamicRoute,
} from './utils';
import createServerlessConfig from './create-serverless-config';
interface BuildParamsMeta {
isDev: boolean | undefined;
@@ -155,9 +156,10 @@ export const build = async ({
files,
workPath,
entrypoint,
config,
meta = {} as BuildParamsMeta,
}: BuildParamsType): Promise<{
routes?: ({ src?: string; dest?: string } | { handle: string })[];
routes: Route[];
output: Files;
watch?: string[];
childProcesses: ChildProcess[];
@@ -178,7 +180,7 @@ export const build = async ({
await createServerlessConfig(workPath);
}
const nodeVersion = await getNodeVersion(entryPath);
const nodeVersion = await getNodeVersion(entryPath, undefined, config);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
if (!nextVersion) {
@@ -301,7 +303,7 @@ export const build = async ({
await unlinkFile(path.join(entryPath, '.npmrc'));
}
const exportedPageRoutes: { src: string; dest: string }[] = [];
const exportedPageRoutes: Route[] = [];
const lambdas: { [key: string]: Lambda } = {};
const staticPages: { [key: string]: FileFsRef } = {};
const dynamicPages: string[] = [];
@@ -330,7 +332,7 @@ export const build = async ({
);
const launcherFiles = {
'now__bridge.js': new FileFsRef({
fsPath: require('@now/node-bridge'),
fsPath: path.join(__dirname, 'now__bridge.js'),
}),
};
const nextFiles: { [key: string]: FileFsRef } = {
@@ -394,7 +396,7 @@ export const build = async ({
console.log('preparing lambda files...');
const launcherFiles = {
'now__bridge.js': new FileFsRef({
fsPath: require('@now/node-bridge'),
fsPath: path.join(__dirname, 'now__bridge.js'),
}),
'now__launcher.js': new FileFsRef({
fsPath: path.join(__dirname, 'launcher.js'),
@@ -536,6 +538,16 @@ export const build = async ({
routes: [
// Static exported pages (.html rewrites)
...exportedPageRoutes,
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
// Next.js page lambdas, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
@@ -547,6 +559,7 @@ export const build = async ({
{
src: path.join('/', entryDirectory, '.*'),
dest: path.join('/', entryDirectory, '_error'),
status: 404,
},
]),
],

View File

@@ -3,8 +3,8 @@ if (!process.env.NODE_ENV) {
process.env.NOW_REGION === 'dev1' ? 'development' : 'production';
}
const { Server } = require('http');
const { Bridge } = require('./now__bridge');
import { Server } from 'http';
import { Bridge } from './now__bridge';
const page = require('./page');
// page.render is for React rendering

View File

@@ -228,7 +228,7 @@ function getRoutes(
const relativeToPages = path.relative('pages', relativePath);
const extension = path.extname(relativeToPages);
const pageName = relativeToPages.replace(extension, '');
const pageName = relativeToPages.replace(extension, '').replace(/\\/g, '/');
if (pageName.startsWith('_')) {
continue;

View File

@@ -0,0 +1,5 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
};

View File

@@ -0,0 +1,12 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@now/next" }],
"probes": [
{
"path": "/_next/static/testing-build-id/pages/index.js",
"responseHeaders": {
"cache-control": "public,max-age=31536000,immutable"
}
}
]
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "latest",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1 @@
export default () => 'Hi';

View File

@@ -22,6 +22,24 @@ describe('build meta dev', () => {
export default () => 'Index page'
`,
}),
'pages/nested/[param].js': new FileBlob({
mode: 0o777,
data: `
export default () => 'Dynamic page'
`,
}),
'pages/nested/page.tsx': new FileBlob({
mode: 0o777,
data: `
export default () => 'Nested page'
`,
}),
'pages/api/test.js': new FileBlob({
mode: 0o777,
data: `
export default (req, res) => res.status(200).end('API Route')
`,
}),
// This file should be omitted because `pages/index.js` will use the same route
'public/index': new FileBlob({
mode: 0o777,
@@ -39,9 +57,14 @@ describe('build meta dev', () => {
"now-build": "next build"
},
"dependencies": {
"next": "8",
"next": "9",
"react": "16",
"react-dom": "16"
},
"devDependencies": {
"@types/node": "12",
"@types/react": "16",
"typescript": "3"
}
}
`,
@@ -80,11 +103,20 @@ describe('build meta dev', () => {
{ src: '/static/(.*)', dest: 'http://localhost:5000/static/$1' },
{ src: '/index', dest: 'http://localhost:5000/index' },
{ src: '/', dest: 'http://localhost:5000/' },
{ src: '/nested/page', dest: 'http://localhost:5000/nested/page' },
{ src: '/api/test', dest: 'http://localhost:5000/api/test' },
{
src: '^/(nested\\/([^\\/]+?)(?:\\/)?)$',
dest: 'http://localhost:5000/$1',
},
{ src: '/data.txt', dest: 'http://localhost:5000/data.txt' },
]);
expect(watch).toEqual([
'next.config.js',
'pages/index.js',
'pages/nested/[param].js',
'pages/nested/page.tsx',
'pages/api/test.js',
'public/index',
'public/data.txt',
'package.json',

View File

@@ -0,0 +1,13 @@
describe('export', () => {
it('should require by path main', async () => {
const main = require('@now/next');
expect(main).toBeDefined();
});
it('should require by path dev-server relative to index', async () => {
const index = require('@now/next/dist/index.js');
const server = require('@now/next/dist/dev-server.js');
expect(index).toBeDefined();
expect(server).toBeDefined();
});
});

View File

@@ -6,7 +6,7 @@ const {
normalizePackageJson,
getNextConfig,
} = require('@now/next/dist/utils');
const FileRef = require('@now/build-utils/file-ref'); // eslint-disable-line import/no-extraneous-dependencies
const { FileRef } = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
describe('getNextConfig', () => {
const workPath = path.join(__dirname, 'fixtures');

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node",
"version": "0.12.0",
"version": "0.12.4",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/node-js-now-node",
@@ -28,7 +28,7 @@
"@types/etag": "1.8.0",
"@types/test-listen": "1.1.0",
"@zeit/ncc": "0.20.4",
"@zeit/node-file-trace": "0.2.1",
"@zeit/node-file-trace": "0.2.6",
"content-type": "1.0.4",
"cookie": "0.4.0",
"etag": "1.8.1",

View File

@@ -16,6 +16,7 @@ import {
PrepareCacheOptions,
BuildOptions,
shouldServe,
Config,
} from '@now/build-utils';
export { NowRequest, NowResponse } from './types';
import { makeLauncher } from './launcher';
@@ -32,6 +33,7 @@ interface DownloadOptions {
files: Files;
entrypoint: string;
workPath: string;
config: Config;
meta: Meta;
}
@@ -53,16 +55,25 @@ async function downloadInstallAndBundle({
files,
entrypoint,
workPath,
config,
meta,
}: DownloadOptions) {
console.log('downloading user files...');
const downloadTime = Date.now();
const downloadedFiles = await download(files, workPath, meta);
console.log(`download complete [${Date.now() - downloadTime}ms]`);
console.log("installing dependencies for user's code...");
const installTime = Date.now();
const entrypointFsDirname = join(workPath, dirname(entrypoint));
const nodeVersion = await getNodeVersion(entrypointFsDirname);
const nodeVersion = await getNodeVersion(
entrypointFsDirname,
undefined,
config
);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
await runNpmInstall(entrypointFsDirname, ['--prefer-offline'], spawnOpts);
console.log(`install complete [${Date.now() - installTime}ms]`);
const entrypointPath = downloadedFiles[entrypoint].fsPath;
return { entrypointPath, entrypointFsDirname, nodeVersion, spawnOpts };
@@ -72,8 +83,7 @@ async function compile(
workPath: string,
entrypointPath: string,
entrypoint: string,
config: CompilerConfig,
{ isDev, filesChanged, filesRemoved }: Meta
config: CompilerConfig
): Promise<{
preparedFiles: Files;
shouldAddSourcemapSupport: boolean;
@@ -162,6 +172,7 @@ async function compile(
const { fileList, esmFileList } = await nodeFileTrace([...inputFiles], {
base: workPath,
ts: true,
mixedModules: true,
ignore: config.excludeFiles,
readFile(fsPath: string): Buffer | string | null {
const relPath = relative(workPath, fsPath);
@@ -175,9 +186,12 @@ async function compile(
source = compileTypeScript(fsPath, source.toString());
}
const { mode } = lstatSync(fsPath);
if (isSymbolicLink(mode))
throw new Error('Internal error: Unexpected symlink.');
const entry = new FileBlob({ data: source, mode });
let entry: File;
if (isSymbolicLink(mode)) {
entry = new FileFsRef({ fsPath, mode });
} else {
entry = new FileBlob({ data: source, mode });
}
fsCache.set(relPath, entry);
sourceCache.set(relPath, source);
return source.toString();
@@ -290,20 +304,24 @@ export async function build({
files,
entrypoint,
workPath,
config,
meta,
});
console.log('running user script...');
const runScriptTime = Date.now();
await runPackageJsonScript(entrypointFsDirname, 'now-build', spawnOpts);
console.log(`script complete [${Date.now() - runScriptTime}ms]`);
console.log('tracing input files...');
const traceTime = Date.now();
const { preparedFiles, shouldAddSourcemapSupport, watch } = await compile(
workPath,
entrypointPath,
entrypoint,
config,
meta
config
);
console.log(`trace complete [${Date.now() - traceTime}ms]`);
const launcherFiles: Files = {
[`${LAUNCHER_FILENAME}.js`]: new FileBlob({

View File

@@ -8,7 +8,7 @@ import _ts from 'typescript';
*/
/**
* Debugging `ts-node`.
* Debugging.
*/
const shouldDebug = false;
const debug = shouldDebug
@@ -453,11 +453,18 @@ function fixConfig(ts: TSCommon, config: _ts.ParsedCommandLine) {
delete config.options.tsBuildInfoFile;
delete config.options.incremental;
// Target ES5 output by default (instead of ES3).
// Target esnext output by default (instead of ES3).
// This will prevent TS from polyfill/downlevel emit.
if (config.options.target === undefined) {
config.options.target = ts.ScriptTarget.ESNext;
}
// When mixing TS with JS, its best to enable this flag.
// This is useful when no `tsconfig.json` is supplied.
if (config.options.esModuleInterop === undefined) {
config.options.esModuleInterop = true;
}
// Target CommonJS, always!
config.options.module = ts.ModuleKind.CommonJS;

View File

@@ -3,13 +3,15 @@ import { parse } from 'url';
const func = (req: IncomingMessage, res: ServerResponse) => {
if (req.url) {
const url = parse(req.url);
const { pathname, search } = parse(req.url);
const location = pathname
? pathname.replace(/\/+/g, '/') + (search ? search : '')
: '/';
/*
res.writeHead(302, {
Location: url.pathname
? url.pathname.replace(/\/+/g, '/') + (url.search ? url.search : '')
: '/',
});
res.end();
Location: location
});*/
res.end(`double-redirect:RANDOMNESS_PLACEHOLDER:${location}`);
}
};

View File

@@ -29,7 +29,7 @@
"mustContain": "root:RANDOMNESS_PLACEHOLDER"
},
{
"path": "//",
"path": "/pricing/",
"mustContain": "trailing-redirect:RANDOMNESS_PLACEHOLDER"
}
]

View File

@@ -0,0 +1,8 @@
//@ts-ignore test will compile during deployment
import express from 'express';
const router = express.Router();
export default function handler(req: any, res: any) {
if (router && req) res.end('default-import:RANDOMNESS_PLACEHOLDER');
else res.end('failed to fetch default import');
}

View File

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

View File

@@ -0,0 +1,6 @@
{
"dependencies": {
"@types/express": "4.17.0",
"express": "4.17.1"
}
}

View File

@@ -0,0 +1,11 @@
import { dep1 } from './js/ecmascript-module';
const { dep2 } = require('./js/commonjs-module');
module.exports = (req, res) => {
if (req && typeof dep1 === 'string' && typeof dep2 === 'string') {
res.end('mixed-modules:js:RANDOMNESS_PLACEHOLDER');
} else {
res.end('import failed');
}
};

View File

@@ -0,0 +1,11 @@
import { IncomingMessage, ServerResponse } from 'http';
import { dep1 } from './ts/ecmascript-module';
const { dep2 } = require('./ts/commonjs-module');
module.exports = (req: IncomingMessage, res: ServerResponse) => {
if (req && typeof dep1 === 'string' && typeof dep2 === 'string') {
res.end('mixed-modules:ts:RANDOMNESS_PLACEHOLDER');
} else {
res.end('import failed');
}
};

View File

@@ -0,0 +1 @@
module.exports = { dep2: 'dep2' };

View File

@@ -0,0 +1,2 @@
export const dep1 = 'dep1';
export const another = 'another';

View File

@@ -0,0 +1,5 @@
{
"engines": {
"node": "10.x"
}
}

View File

@@ -0,0 +1,14 @@
{
"version": 2,
"builds": [{ "src": "entrypoint**", "use": "@now/node" }],
"probes": [
{
"path": "/entrypoint.js",
"mustContain": "mixed-modules:js:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/entrypoint.ts",
"mustContain": "mixed-modules:ts:RANDOMNESS_PLACEHOLDER"
}
]
}

View File

@@ -0,0 +1 @@
module.exports = { dep2: 'dep2' };

View File

@@ -0,0 +1 @@
export const dep1 = 'dep1';

View File

@@ -0,0 +1,5 @@
{
"engines": {
"node": "10.x"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@builders-typescript-test/lib",
"main": "index.ts",
"main": "src/index",
"version": "1.0.0",
"devDependencies": {
"typescript": "^3.4.5"

View File

@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["./**/*.ts"]
"include": ["src/**/*.ts"]
}

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,20 +0,0 @@
FROM library/centos:6.8
RUN yum -y install wget git
RUN rpm -Uvh https://mirror.webtatic.com/yum/el6/latest.rpm
RUN yum -y install php71w-cli php71w-fpm php71w-mbstring php71w-mysql php71w-opcache
RUN yum -y install epel-release
RUN yum -y install patchelf
RUN mkdir -p /root/app/public
WORKDIR /root/app
COPY ./php.ini /root/app/php.ini
COPY ./php-fpm.ini /root/app/php-fpm.ini
COPY ./test.php /root/app/test.php
COPY ./test.sh /root/app/test.sh
RUN patchelf --set-rpath '$ORIGIN' /usr/bin/php
RUN patchelf --set-rpath '$ORIGIN' /usr/sbin/php-fpm
RUN patchelf --set-rpath '$ORIGIN' /usr/lib64/php/modules/mysqli.so
CMD ["/bin/bash", "test.sh"]

View File

@@ -1,15 +0,0 @@
rm -rf ../native
mkdir -p ../native/modules
docker rmi now-php-docker-image --force
docker build . -t now-php-docker-image
docker run now-php-docker-image
docker run now-php-docker-image /bin/cat /usr/sbin/php-fpm > ../native/php-fpm
docker run now-php-docker-image /bin/cat /root/app/php.ini > ../native/php.ini
docker run now-php-docker-image /bin/cat /root/app/php-fpm.ini > ../native/php-fpm.ini
docker run now-php-docker-image /bin/cat /usr/lib64/php/modules/curl.so > ../native/modules/curl.so
docker run now-php-docker-image /bin/cat /usr/lib64/php/modules/json.so > ../native/modules/json.so
docker run now-php-docker-image /bin/cat /usr/lib64/php/modules/mbstring.so > ../native/modules/mbstring.so
docker run now-php-docker-image /bin/cat /usr/lib64/php/modules/mysqli.so > ../native/modules/mysqli.so
docker run now-php-docker-image /bin/cat /usr/lib64/mysql/libmysqlclient.so.16 > ../native/modules/libmysqlclient.so.16
docker run now-php-docker-image /bin/cat /usr/lib64/php/modules/opcache.so > ../native/modules/opcache.so
chmod +x ../native/php-fpm

View File

@@ -1,9 +0,0 @@
[global]
error_log=/tmp/fpm-error.log
[www]
listen=0.0.0.0:9000
pm=static
pm.max_children=1
catch_workers_output=yes
clear_env=no

View File

@@ -1,8 +0,0 @@
extension=/root/app/modules/curl.so
extension=/root/app/modules/json.so
extension=/root/app/modules/mbstring.so
extension=/root/app/modules/mysqli.so
zend_extension=/root/app/modules/opcache.so
opcache.enable_cli=1
mysqli.max_links=10
mysqli.max_persistent=10

View File

@@ -1,4 +0,0 @@
<?php
mysqli_connect();
print('php_sapi_name=' . php_sapi_name() . PHP_EOL);
print('opcache_enabled=' . opcache_get_status()['opcache_enabled'] . PHP_EOL);

View File

@@ -1,18 +0,0 @@
mkdir -p /root/app/modules
cp /usr/bin/php /root/app/php
cp /usr/sbin/php-fpm /root/app/php-fpm
cp /usr/lib64/php/modules/curl.so /root/app/modules/curl.so
cp /usr/lib64/php/modules/json.so /root/app/modules/json.so
cp /usr/lib64/php/modules/mbstring.so /root/app/modules/mbstring.so
cp /usr/lib64/php/modules/mysqli.so /root/app/modules/mysqli.so
cp /usr/lib64/mysql/libmysqlclient.so.16 /root/app/modules/libmysqlclient.so.16
cp /usr/lib64/php/modules/opcache.so /root/app/modules/opcache.so
rm -rf $(which php)
rm -rf $(which php-fpm)
rm -rf /usr/lib64/php
rm -rf /usr/lib64/mysql
rm -rf /etc/php.d
./php-fpm --help
./php -c php.ini test.php
echo "if you see 'can't connect to local mysql' - it is good - mysql library is found and used"
echo "if you see 'call to undefined function mysqli_connect' - it is bad - something went wrong"

View File

@@ -1,149 +0,0 @@
/* eslint-disable no-bitwise,no-use-before-define */
const assert = require('assert');
const { freeParser } = require('_http_common');
const { spawn } = require('child_process');
const createConnection = require('./connection.js');
const { MSG_TYPE, PROTOCOL_STATUS } = require('./consts.js');
const { whenPortOpens } = require('./port.js');
const { HTTPParser } = process.binding('http_parser');
const BEGIN_REQUEST_DATA_KEEP_CONN = Buffer.from('\0\x01\x01\0\0\0\0\0'); // FCGI_ROLE_RESPONDER && FCGI_KEEP_CONN
const MESSAGE_FCGI_STDOUT = `message-${MSG_TYPE.FCGI_STDOUT}`;
const MESSAGE_FCGI_STDERR = `message-${MSG_TYPE.FCGI_STDERR}`;
const MESSAGE_FCGI_END_REQUEST = `message-${MSG_TYPE.FCGI_END_REQUEST}`;
let curReqId = 0;
let connection;
async function startPhp() {
assert(!connection);
const child = spawn(
'./php-fpm',
['-c', 'php.ini', '--fpm-config', 'php-fpm.ini', '--nodaemonize'],
{
stdio: 'inherit',
cwd: '/var/task/native',
},
);
child.on('exit', () => {
console.error('php exited');
process.exit(1);
});
child.on('error', (error) => {
console.error(error);
process.exit(1);
});
await whenPortOpens(9000, 400);
const newConnection = createConnection({
_host: '127.0.0.1',
_port: 9000,
});
await new Promise((resolve, reject) => {
function onError() {
cleanup();
reject();
}
function onConnect() {
connection = newConnection;
cleanup();
resolve();
}
newConnection.on('error', onError);
newConnection.on('connect', onConnect);
function cleanup() {
newConnection.removeListener('error', onError);
newConnection.removeListener('connect', onConnect);
}
});
}
async function query({ params, stdin }) {
if (!connection) {
await startPhp();
}
return new Promise((resolve) => {
assert(connection);
const chunks = [Buffer.from('HTTP/1.1 200 MAKES-PARSER-WORK\n')];
function onError(error) {
console.error(error);
process.exit(1);
}
function onStdout(reqId, data) {
chunks.push(data);
}
function onStderr(reqId, data) {
console.error(data.toString().trim());
}
function onEndRequest(reqId, data) {
const protocolStatus = data.readUInt8(4, true);
if (protocolStatus !== PROTOCOL_STATUS.FCGI_REQUEST_COMPLETE) {
console.error('protocolStatus', protocolStatus);
process.exit(1);
}
const response = Buffer.concat(chunks);
const parser = new HTTPParser(HTTPParser.RESPONSE);
let tuples = [];
parser[HTTPParser.kOnHeadersComplete | 0] = (major, minor, t) => {
tuples = t;
};
let body;
parser[HTTPParser.kOnBody | 0] = (b, start, len) => {
body = b.slice(start, start + len);
};
parser.execute(response);
freeParser(parser);
cleanup();
resolve({ tuples, body });
}
connection.on('error', onError);
connection.on(MESSAGE_FCGI_STDOUT, onStdout);
connection.on(MESSAGE_FCGI_STDERR, onStderr);
connection.on(MESSAGE_FCGI_END_REQUEST, onEndRequest);
function cleanup() {
connection.removeListener('error', onError);
connection.removeListener(MESSAGE_FCGI_STDOUT, onStdout);
connection.removeListener(MESSAGE_FCGI_STDERR, onStderr);
connection.removeListener(MESSAGE_FCGI_END_REQUEST, onEndRequest);
}
curReqId += 1;
// TODO these things have callbacks. what to do with them?
connection.send(
MSG_TYPE.FCGI_BEGIN_REQUEST,
curReqId,
BEGIN_REQUEST_DATA_KEEP_CONN,
);
connection.send(MSG_TYPE.FCGI_PARAMS, curReqId, params);
connection.send(MSG_TYPE.FCGI_PARAMS, curReqId, null);
if (stdin) connection.send(MSG_TYPE.FCGI_STDIN, curReqId, stdin);
connection.send(MSG_TYPE.FCGI_STDIN, curReqId, null);
});
}
module.exports = {
query,
};
/*
(async function() {
console.log(await query({ params: {
REQUEST_METHOD: 'GET', SCRIPT_FILENAME: '/phpinfo.php'
} }));
})();
*/

View File

@@ -1,41 +0,0 @@
/* eslint-disable consistent-return */
const net = require('net');
function whenPortOpensCallback(port, attempts, cb) {
const client = net.connect(port, '127.0.0.1');
client.on('error', (error) => {
if (!attempts) return cb(error);
setTimeout(() => {
whenPortOpensCallback(port, attempts - 1, cb);
}, 50);
});
client.on('connect', () => {
client.destroy();
cb();
});
}
function isPortOpen(port) {
return new Promise((resolve) => {
whenPortOpensCallback(port, 0, (error) => {
if (error) return resolve(false);
resolve(true);
});
});
}
function whenPortOpens(port, attempts) {
return new Promise((resolve, reject) => {
whenPortOpensCallback(port, attempts, (error) => {
if (error) return reject(error);
resolve();
});
});
}
module.exports = {
isPortOpen,
whenPortOpensCallback,
whenPortOpens,
};

View File

@@ -1,43 +0,0 @@
const FileBlob = require('@now/build-utils/file-blob.js'); // eslint-disable-line import/no-extraneous-dependencies
const FileFsRef = require('@now/build-utils/file-fs-ref.js'); // eslint-disable-line import/no-extraneous-dependencies
const glob = require('@now/build-utils/fs/glob.js'); // eslint-disable-line import/no-extraneous-dependencies
const path = require('path');
async function getFiles() {
const files = await glob('native/**', __dirname);
const phpConfig = await FileBlob.fromStream({
stream: files['native/php.ini'].toStream(),
});
phpConfig.data = phpConfig.data
.toString()
.replace(/\/root\/app\/modules/g, '/var/task/native/modules');
files['native/php.ini'] = phpConfig;
Object.assign(files, {
'fastcgi/connection.js': new FileFsRef({
fsPath: require.resolve('fastcgi-client/lib/connection.js'),
}),
'fastcgi/consts.js': new FileFsRef({
fsPath: require.resolve('fastcgi-client/lib/consts.js'),
}),
'fastcgi/stringifykv.js': new FileFsRef({
fsPath: require.resolve('fastcgi-client/lib/stringifykv.js'),
}),
'fastcgi/index.js': new FileFsRef({
fsPath: path.join(__dirname, 'fastcgi/index.js'),
}),
'fastcgi/port.js': new FileFsRef({
fsPath: path.join(__dirname, 'fastcgi/port.js'),
}),
'launcher.js': new FileFsRef({
fsPath: path.join(__dirname, 'launcher.js'),
}),
});
return files;
}
module.exports = {
getFiles,
};

View File

@@ -1,150 +0,0 @@
/* eslint-disable prefer-template */
const assert = require('assert');
const fs = require('fs');
const { join: pathJoin } = require('path');
const { parse: parseUrl } = require('url');
const { query } = require('./fastcgi/index.js');
function normalizeEvent(event) {
if (event.Action === 'Invoke') {
const invokeEvent = JSON.parse(event.body);
const {
method, path, headers, encoding,
} = invokeEvent;
let { body } = invokeEvent;
if (body) {
if (encoding === 'base64') {
body = Buffer.from(body, encoding);
} else if (encoding === undefined) {
body = Buffer.from(body);
} else {
throw new Error(`Unsupported encoding: ${encoding}`);
}
}
return {
method,
path,
headers,
body,
};
}
const {
httpMethod: method, path, headers, body,
} = event;
return {
method,
path,
headers,
body,
};
}
function isDirectory(p) {
return new Promise((resolve) => {
fs.stat(p, (error, s) => {
if (error) {
resolve(false);
return;
}
if (s.isDirectory()) {
resolve(true);
return;
}
resolve(false);
});
});
}
async function transformFromAwsRequest({
method, path, headers, body,
}) {
const { pathname, search, query: queryString } = parseUrl(path);
let requestUri = pathname + (search || '');
let filename = pathJoin(
'/var/task/user',
process.env.NOW_ENTRYPOINT || pathname,
);
if (await isDirectory(filename)) {
if (!filename.endsWith('/')) {
filename += '/';
requestUri = pathname + '/' + (search || '');
}
filename += 'index.php';
}
const params = {};
params.REQUEST_METHOD = method;
params.REQUEST_URI = requestUri;
params.QUERY_STRING = queryString || ''; // can be null
params.SCRIPT_FILENAME = filename;
params.SERVER_PROTOCOL = 'HTTP/1.1';
params.SERVER_PORT = 443;
params.HTTPS = 'on';
// eslint-disable-next-line no-restricted-syntax
for (const [k, v] of Object.entries(headers)) {
const camel = k.toUpperCase().replace(/-/g, '_');
params[`HTTP_${camel}`] = v;
if (camel === 'HOST') {
params.SERVER_NAME = v;
} else if (['CONTENT_TYPE', 'CONTENT_LENGTH'].includes(camel)) {
params[camel] = v; // without HOST_ prepended
}
}
return { params, stdin: body };
}
function transformToAwsResponse({ tuples, body }) {
let statusCode = 200;
const headers = {};
// eslint-disable-next-line no-param-reassign
if (!body) body = Buffer.alloc(0);
assert(Buffer.isBuffer(body));
for (let i = 0; i < tuples.length; i += 2) {
const k = tuples[i].toLowerCase();
const v = tuples[i + 1];
if (k === 'status') {
statusCode = Number(v.split(' ')[0]); // '408 Request Timeout'
} else {
if (!headers[k]) headers[k] = [];
headers[k].push(v);
}
}
return {
statusCode,
headers,
body: body.toString('base64'),
encoding: 'base64',
};
}
async function launcher(event) {
const awsRequest = normalizeEvent(event);
const input = await transformFromAwsRequest(awsRequest);
const output = await query(input);
return transformToAwsResponse(output);
}
exports.launcher = launcher;
/*
(async function() {
console.log(await launcher({
httpMethod: 'GET',
path: '/phpinfo.php'
}));
})();
*/

View File

@@ -1,9 +0,0 @@
[global]
error_log=/tmp/fpm-error.log
[www]
listen=0.0.0.0:9000
pm=static
pm.max_children=1
catch_workers_output=yes
clear_env=no

View File

@@ -1,8 +0,0 @@
extension=/root/app/modules/curl.so
extension=/root/app/modules/json.so
extension=/root/app/modules/mbstring.so
extension=/root/app/modules/mysqli.so
zend_extension=/root/app/modules/opcache.so
opcache.enable_cli=1
mysqli.max_links=10
mysqli.max_persistent=10

View File

@@ -1,13 +0,0 @@
{
"name": "@now/php-bridge",
"version": "0.5.3",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-php-bridge"
},
"dependencies": {
"fastcgi-client": "0.0.1"
}
}

View File

@@ -1 +0,0 @@
/test

View File

@@ -1,55 +0,0 @@
const {
createLambda,
rename,
glob,
download,
shouldServe,
} = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
const path = require('path');
const { getFiles } = require('@now/php-bridge');
exports.build = async ({
files, entrypoint, workPath, config, meta,
}) => {
// Download all files to workPath
const downloadedFiles = await download(files, workPath, meta);
let includedFiles = {};
if (config && config.includeFiles) {
// Find files for each glob
// eslint-disable-next-line no-restricted-syntax
for (const pattern of config.includeFiles) {
// eslint-disable-next-line no-await-in-loop
const matchedFiles = await glob(pattern, workPath);
Object.assign(includedFiles, matchedFiles);
}
// explicit and always include the entrypoint
Object.assign(includedFiles, {
[entrypoint]: files[entrypoint],
});
} else {
// Backwards compatibility
includedFiles = downloadedFiles;
}
console.log('Included files:', Object.keys(includedFiles));
const userFiles = rename(includedFiles, name => path.join('user', name));
const bridgeFiles = await getFiles();
// TODO config.extensions. OR php.ini from user
delete bridgeFiles['native/modules/mysqli.so'];
delete bridgeFiles['native/modules/libmysqlclient.so.16'];
const lambda = await createLambda({
files: { ...userFiles, ...bridgeFiles },
handler: 'launcher.launcher',
runtime: 'nodejs8.10',
environment: {
NOW_ENTRYPOINT: entrypoint,
},
});
return { [entrypoint]: lambda };
};
exports.shouldServe = shouldServe;

View File

@@ -1,17 +0,0 @@
{
"name": "@now/php",
"version": "0.5.7",
"license": "MIT",
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/php-now-php",
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-php"
},
"dependencies": {
"@now/php-bridge": "^0.5.3"
},
"scripts": {
"test": "jest"
}
}

View File

@@ -1,2 +0,0 @@
<?php
print('cow:RANDOMNESS_PLACEHOLDER');

View File

@@ -1,11 +0,0 @@
{
"version": 2,
"builds": [
{ "src": "index.php", "use": "@now/php" },
{ "src": "subdirectory/index.php", "use": "@now/php" }
],
"probes": [
{ "path": "/", "mustContain": "cow:RANDOMNESS_PLACEHOLDER" },
{ "path": "/subdirectory/", "mustContain": "yoda:RANDOMNESS_PLACEHOLDER" }
]
}

View File

@@ -1,2 +0,0 @@
<?php
print('yoda:RANDOMNESS_PLACEHOLDER');

View File

@@ -1,2 +0,0 @@
<?php
print($_ENV['RANDOMNESS_ENV_VAR'] . ':env');

View File

@@ -1,5 +0,0 @@
{
"version": 2,
"builds": [{ "src": "env/index.php", "use": "@now/php" }],
"probes": [{ "path": "/env", "mustContain": "RANDOMNESS_PLACEHOLDER:env" }]
}

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