Compare commits

...

56 Commits

Author SHA1 Message Date
Steven
9767682006 Publish
- @now/build-utils@0.7.0
 - @now/go@0.5.2
 - @now/next@0.5.0
 - @now/node-bridge@1.2.1
 - @now/node-server@0.8.0
 - @now/node@0.10.0
 - @now/optipng@0.6.3
 - @now/python@0.2.8
 - @now/ruby@0.1.0
 - @now/rust@0.2.7
 - @now/static-build@0.6.0
2019-06-24 17:02:17 -04:00
Steven
3285b31721 Revert "[now-node] Fix express not overriding helpers properties" (#659)
This reverts commit 7a7d8a55fbbe4f8ee89ce50a3c62816fc95f28f5.
2019-06-24 17:00:18 -04:00
Steven
70353c7fc0 [now-build-utils] Fail if engines is invalid (#658)
* [now-build-utils] Fail if engines does not match

* Add test to throw when invalid semver

* Apply suggestions from leo

Co-Authored-By: Leo Lamprecht <mindrun@icloud.com>

* Fix ts error

* Print range so its clear what to add to engines
2019-06-24 15:50:48 -04:00
Nathan Cahill
f85cf99325 support local pipfile (#652) 2019-06-24 15:50:37 -04:00
Luc
8b14a46d04 Add git diff to publishing steps in readme (#646)
* add `git diff canary` to publishing to stable

* Improve readme
2019-06-24 15:50:32 -04:00
Steven
383cbfd82f [now-build-utils] Remove enginesMatch export (#650)
* [now-build-utils] Remove `enginesMatch` export

* Remove unused import
2019-06-24 15:50:23 -04:00
Steven
81e268a3c9 Add support for node 10 in all builders (#649)
* Add support for node 10 in all builders

* Fix meta undefined
2019-06-24 15:50:16 -04:00
Steven
ac8b33213b [now-build-utils] Enhance node version selection (#648)
* [now-build-utils] Enhance node version selection

* Fix test
2019-06-24 15:50:10 -04:00
piousdeer
de12e7b8c8 [now-static-build] Use cross-spawn and npm for now dev (#639)
* [now-static-build] Use `cross-spawn` and npm

* [now-static-build] Add `cross-spawn` dependency
2019-06-24 15:50:04 -04:00
ywg-jean
b9346603f0 Proposes a contributing guideline (#644)
* proposes a contributing guideline

the guideline outlines the process to open a pull request on this
repository and offers guidance on interpreting test errors.

* Wording improvement

As per @styfle suggestion

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

* Reword reference to code of conduct

As per @styfle's suggestion.

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

* fix local developement section title

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

* Improves project description wording

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

* Proper setup instructions

The ones provided initially were incomplete (might explain why I had some issues :) )

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

* updates description of tests

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

* Improve explanation of integration tests

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

* Update ncc bug qualification section

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

* fixes typo

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

* clarifies when and why to run ncc manually

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

* clarifies how to access to test deployements and associated logs

Co-Authored-By: Steven <steven@ceriously.com>
2019-06-24 15:50:00 -04:00
Luc
0b793dfc35 [now-node] Fix express not overriding helpers properties (#638)
* add breaking test

* fix test

* fix tests (bis)

* be explicit about @ts-ignore

* be more specific in test name
2019-06-24 15:49:52 -04:00
Steven
9dd672c383 [now-node] Change PATH to use node10 (#637)
* [now-node] Change PATH to use node10

* Fallback to node8
2019-06-24 15:49:46 -04:00
Steven
1b743aeea8 [now-ruby] Fix ruby publish step (#642) 2019-06-24 15:49:41 -04:00
Steven
d4af4b9f5c Run prettier (#635) 2019-06-24 15:49:35 -04:00
Steven
b734ca3e01 [now-build-utils] Add spawnOpts param to runNpmInstall() (#634) 2019-06-24 15:49:27 -04:00
Luc
f81d753104 [now-go] Fix failing build when go.mod exists in a subfolder (#633)
* add test

* fix test

* fix 14-go-mod-sub test fixtures

* add index.go to make fixtures correct
2019-06-24 15:49:21 -04:00
Steven
db31b9a207 [tests] Bump typescript to 3.5.2 (#631) 2019-06-24 15:49:07 -04:00
Nathan Cahill
b80b5182e6 [now-ruby] Add @now/ruby Builder (#454)
* add @now/ruby

* changes from feedback

* remove mm

* increase timeout

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

* address changes

* fix linting errors

* support arrays in includeFiles

* undo type change
2019-06-24 15:48:56 -04:00
Steven
268a7c2b81 [now-node] Enable node10.x runtime (#630) 2019-06-24 15:48:48 -04:00
Sophearak Tha
667a16c996 [now-go] add support for nested packages (#623)
* add support for nested packages

* properly clone array
2019-06-24 15:48:20 -04:00
Steven
7b851f81c0 Publish
- @now/build-utils@0.6.0
2019-06-17 14:25:30 -04:00
Steven
80fbbcd194 [now-build-utils] Add enginesMatch export (#618)
* Add engineSatisifies for node

* Rename to enginesMatch

* Add test for node10

* Minor refactor

* Add tests for engines, uses semver.intersect()

* Revert @now/node, new PR later
2019-06-17 14:22:22 -04:00
luc
3108332043 Publish
- @now/build-utils@0.5.8
 - @now/md@0.5.4
 - @now/next@0.4.2
 - @now/node-bridge@1.2.0
 - @now/node-server@0.7.5
 - @now/node@0.9.0
 - @now/python@0.2.7
 - @now/static-build@0.5.9
2019-06-17 18:19:38 +02:00
Steven
7509c82c32 [tests] Fix yarn.lock (#628) 2019-06-17 16:40:33 +02:00
Steven
c4f5a5b48d [now-node] Remove layers (#617) 2019-06-17 16:05:31 +02:00
Luc
05314da810 [now-node] Fix faulty behavior when request's body is empty and content-type is application/json (#615)
* add test for empty body and `application/json`

* fix test
2019-06-17 16:01:16 +02:00
Steven
5f1cf714c1 [tests] Run against all commits in the branch (#611) 2019-06-17 16:01:09 +02:00
Steven
2623e2e799 [now-node] Bump layer versions (#610) 2019-06-17 16:01:00 +02:00
Steven
bac1da09d4 [now-build-utils] Add types for Layers (#604)
* [now-build-utils] Add PrepareLayersOptions

* [now-build-utils] Add types for layers

* Add layer types to lambda

* Add any values to a layer function

* Change props to optional

* Update layers per call

* Add missing getFiles function

* Fix types

* Improve error message

* Change createLambda() api

* Remove node-gyp hack

* [now-layer-node] Add bootstrap

* Add fallback to empty object

* Add config.useLayers

* Remove config, change to layers check

* Fix typo

* Add deprecation message
2019-06-17 16:00:47 +02:00
Luc
5b57f1a3ac [now-node] Improve @now/node helpers (#609)
* lazy load everything 

* do not read charset in content-type to set encoding

* add tests

* add tests for express compat

* add test for `res.status().send()`

* update after PR comments
2019-06-17 16:00:39 +02:00
Nathan Rajlich
2e95dd5329 [now-md] Fix shouldServe() logic (#608)
* [now-md] Fix `shouldServe()` logic

Since this builder is not a 1-1 mapping of the input -> output names,
the default `shouldServe()` function needs to be augmented such that
the `.html` requestPath is "considered like" a `.md` file.

* Remove "ends with .html" optimization

It breaks index files, i.e. `GET /`
2019-06-17 16:00:30 +02:00
Joe Haddad
215f6367d6 [now-next] Humanize message about now-build (#607)
Some users read this message as a warning that required action (the captain caps lock WARNING didn't help).

Users should never need to define a `now-build` script unless they grow out of running `next build`.
2019-06-17 16:00:24 +02:00
Joe Haddad
e8cd348a79 [now-next] Fix erroneous .next folder message (#606)
We now store the `.next/cache` folder so we need to check for the `static` folder to know if the user uploaded something they should've excluded.
2019-06-17 16:00:17 +02:00
Luc
168f373641 [now-node] Assign type for req.headers.cookie (#603) 2019-06-17 16:00:00 +02:00
Luc
8c3174be29 [now-node] Move @types/node to dependencies (#602)
* move @types/node to dependencies

* fix types
2019-06-17 15:59:44 +02:00
Luc
898de78b63 [now-node] Make helpers opt-out instead of opt-in (#601)
* set helpers to true by default

* update tests
2019-06-17 15:58:00 +02:00
Luc
26e33c1c4b Pin now-node-bridge dependencies in now-next and now-node-server (#600)
* pin dependencies in now-next

* pin dependencies in now-node-server

* regenerate yarn.lock
2019-06-17 15:57:33 +02:00
Luc
c2f95de3ec [now-node] Helpers compatibility with express, micro, etc (#594)
* copy bridge into now-node

* pass body buffer to listener

* only send addon when helpers are added

* ship bridge.js to deployment

* remove raw-body deps

* fix not waiting for server `listening` event

* add test for express compat

* add test for micro compat

* update now-node-bridge

* remove unnecessary yarn.lock

* fix wrong replacement in launcher.ts

* fix listener not defined

* fix unit tests

* add "test" for exports

* add console.log

* add test in node-bridge

* log error before throwing

* revert now-node-bridge to canary state

* remove unused code

* refactor consumeProxyRequests -> consumeEvent

* remove ts-jest

* update tests

* do not transform body to string if not necessary

* fix tests

* remove jest from deps in now-node

* x-bridge-reqid -> x-now-bridge-request-id

* add test for consumeEvent

* do not expose request id header to the client

* pin node-bridge version

* update node-bridge deps to 1.2.0-canary.1

* update yarn.lock

* add await for user's listener

* refactor

* pass async function to `Server`
2019-06-17 15:56:09 +02:00
Luc
6a7de860db [now-node-bridge] Make normalized events consumable by server (#598)
* add `consumeProxyRequest` to bridge

* refactor

* fix tests

* Update packages/now-node-bridge/test/bridge.test.js

Co-Authored-By: Leo Lamprecht <mindrun@icloud.com>

* Update packages/now-node-bridge/src/bridge.ts

Co-Authored-By: Leo Lamprecht <mindrun@icloud.com>
2019-06-17 15:28:05 +02:00
Steven
acb8cadafe [now-static-build] Should fail build when distDir is empty (#597) 2019-06-17 15:27:58 +02:00
Kai Richard König
1a8df7080d [now-python] Do not append query string to path (#580)
* Do not append query string to path - fixes \#545

* Avoid creating multiple parsed urls

* Fix missing whitespace

* Fix handler

* Remove unused imports
2019-06-17 15:27:50 +02:00
JJ Kasper
5a92826eb0 [now-next] Update appending .html to exported routes (#592) 2019-06-17 15:27:24 +02:00
Steven
e083aa3750 Publish
- @now/node@0.8.1
2019-06-08 14:56:27 -04:00
Steven
941f675657 [now-node] Change helpers to opt-in (#591)
* [now-node] Change helpers to opt-in

* Fix tests

* Fix tests
2019-06-08 14:55:56 -04:00
Steven
6fad726abb Publish
- @now/build-utils@0.5.7
 - @now/node@0.8.0
2019-06-08 10:38:13 -04:00
Luc
dd22051d6b [now-node] Add tests for types exports (#590)
* helpers.test.js -> helpers.test.ts

* import NowRequest and NowResponse in test

* fix addHelpers return type

* add comments
2019-06-08 10:36:05 -04:00
Luc
7e86cb403f [now-node] Add Express-like API (#577)
* first iteration of express-like api for @now/node

* fix error when config is undefined

* add integration test for helpers

* add `res.status()` to helpers integration test

* fix error caused by config values being strings

* add helpers opt-out integration test

* add `helpers.js` to deployed files

* add boolean and number to config values types

* update config.helpers to work with boolean

* add unit tests

* fix type error in config type

* add unit test for req.body

* ignore errors not generated in helpers

* Update packages/now-node/test/fixtures/15-helpers/no-helpers/index.js

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

* update config type

* use ternary instead of filtering with `Boolean`

* add probe in 15-helpers

* add probe for ts function

* ncc `helpers.js`

* fix Config type

* fix @now/rust type issue

* add comment in build.sh

* test that content-type header is correctly added

* add missing tsconfig.json in fixtures

* add `body` to fix `Invalid JSON` errors

* Revert "add `body` to fix `Invalid JSON` errors"

This reverts commit 9b2ff55409501140f0d7411d121fc3a4dfd34ccc.

* make `helpers` false by default

* add method POST to probe for helpers

* Revert "make `helpers` false by default"

This reverts commit d029a432a0bf2463e1613e6cfd76929ce6e45073.

* replace POST requests by GET in probes

* remove unnecessary comments

* destructure in parseQuery

* keep @now/rust unchanged

* Request -> NowRequest and Response -> NowResponse

* improve config types

* add NowListener type

* export NowRequest and NowResponse

* generate `.d.ts` files

* add types to helpers/ts fixtures

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

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

* Update packages/now-node/build.sh

Co-Authored-By: Steven <steven@ceriously.com>
2019-06-08 10:35:45 -04:00
Nathan Rajlich
d19d557738 [now-node] Add watch array for Builder v2 API (#582)
* [now-node] Add `watch` array

For `now dev`

* Use ncc-watcher from npm registry

* Update `yarn.lock`

* Fix integration tests

* Apply @styfle's suggestions

* Update `@zeit/ncc-watcher` to v1.0.3
2019-06-08 10:35:23 -04:00
Steven
e4281f698c Publish
- @now/rust@0.2.6
2019-06-07 12:56:50 -04:00
Steven
86ff681c6d Use prettier on .json files (#588) 2019-06-07 12:56:21 -04:00
Steven
ba97a7cf19 [now-rust] Move ts files into /src (#585) 2019-06-07 12:56:14 -04:00
Luc
0a94397700 [now-rust] config.includeFiles can be string[] (#584) 2019-06-07 12:56:04 -04:00
Luc
5c8e2f2ccc Fix content-type: application/json header being added even if body is undefined (#583) 2019-06-07 12:55:47 -04:00
Steven
35d56a34cb Publish
- @now/python@0.2.6
2019-06-06 17:19:25 -04:00
Steven
9dfd37e135 Update publish instructions in the README (#579)
* Update README.md

* Fix typo

* Add prettier to markdown files

* Fix table

* Another cherry pick
2019-06-06 16:58:57 -04:00
Steven
6f815f2645 Fix python (#581)
This change never made it to master
2019-06-06 16:58:28 -04:00
214 changed files with 4154 additions and 965 deletions

View File

@@ -6,12 +6,10 @@
/packages/now-build-utils/src/*.js
/packages/now-build-utils/src/fs/*.js
/packages/now-node/dist/*
/packages/now-layer-node/dist/*
/packages/now-layer-npm/dist/*
/packages/now-layer-yarn/dist/*
/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/*

View File

@@ -1,3 +0,0 @@
{
"eslint.enable": false
}

85
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,85 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue or [spectrum](https://spectrum.chat/zeit) with the owners of this repository before submitting a Pull Request.
Please read our [code of conduct](CODE_OF_CONDUCT.md) and follow it in all your interactions with the project.
## Local development
This project is configured in a monorepo pattern where one repo contains multiple npm packages. Dependencies are installed and managed with `yarn`, not `npm` CLI.
To get started, execute the following:
```
git clone https://github.com/zeit/now-builders
yarn install
yarn bootstrap
yarn build
yarn lint
yarn test
```
Make sure all the tests pass before making changes.
## Verifying your change
Once you are done with your changes (we even suggest doing it along the way ), make sure all the test still run by running
```
yarn build && yarn test
```
from the root of the project.
If any test fails, make sure to fix it along with your changes. See [Interpreting test errors](#Interpreting-test-errors) for more information about how the tests are executed, especially the integration tests.
## Pull Request Process
Once you are confident that your changes work properly, open a pull request on the main repository.
The pull request will be reviewed by the maintainers and the tests will be checked by our continuous integration platform.
## Interpreting test errors
There are 2 kinds of tests in this repository Unit tests and Integration tests.
Unit tests are run locally with `jest` and execute quickly because they are testing the smallest units of code.
### Integration tests
Integration tests create deployments to your ZEIT account using the `test` project name. After each test is deployed, the `probes` key is used to check if the response is the expected value. If the value doesn't match, you'll see a message explaining the difference. If the deployment failed to build, you'll see a more generic message like the following:
```
[Error: Fetched page https://test-8ashcdlew.now.sh/root.js does not contain hello Root!. Instead it contains An error occurred with this application.
NO_STATUS_CODE_FRO Response headers:
cache-control=s-maxage=0
connection=close
content-type=text/plain; charset=utf-8
date=Wed, 19 Jun 2019 18:01:37 GMT
server=now
strict-transport-security=max-age=63072000
transfer-encoding=chunked
x-now-id=iad1:hgtzj-1560967297876-44ae12559f95
x-now-trace=iad1]
```
In such cases you can visit the URL of the failed deployment and append `/_logs` so see the build error. In the case above, that would be https://test-8ashcdlew.now.sh/_logs
The logs of this deployment will contain the actual error which may help you to understand what went wrong.
### @zeit/ncc integration
Some of the builders use `@zeit/ncc` to bundle files before deployment. If you suspect an error with the bundling mechanism, you can run the `ncc` CLI with a couple modifications to the test.
For example if an error occurred in `now-node/test/fixtures/08-assets`
```
cd packages/now-node/test/fixtures/08-assets
yarn install
echo 'require("http").createServer(module.exports).listen(3000)' >> index.js
npx @zeit/ncc@0.20.1 build index.js --source-map
node dist
```
This will compile the test with the specific version of `ncc` and run the resulting file. If it fails here, then there is likely a bug in `ncc` and not the Builder.

View File

@@ -2,29 +2,50 @@
This is a monorepo containing the [Official Builders](https://zeit.co/docs/v2/deployments/builders/overview) provided by the ZEIT team.
There are two branches:
## Channels
- canary - published to npm as `canary` dist-tag, eg `@now/node@canary`
- master - published to npm as `latest` dist-tag, eg `@now/node@latest`
There are two Channels:
| Channel | Git Branch | npm dist-tag | use example |
| ------- | ---------- | ------------ | ------------------ |
| Canary | `canary` | `@canary` | `@now/node@canary` |
| Stable | `master` | `@latest` | `@now/node@latest` |
All PRs should be submitted to the `canary` branch.
Once a PR is merged into the `canary` branch, it should be published to npm immediately using the Canary Channel.
### Publishing to npm
Run the following command to publish modified builders to npm:
For the stable channel use:
```
yarn publish-stable
```
For the canary channel use:
For the Canary Channel, publish the modified Builders to npm with the following:
```
yarn publish-canary
```
GitHub Actions will take care of publishing the updated packages to npm from there.
For the Stable Channel, you must do the following:
- Cherry pick each commit from canary to master
- Verify that you are *in-sync* with canary (with the exception of the `version` line in `package.json`)
- Deploy the modified Builders
```
git checkout master
git pull # make sure you're up to date
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 ...
git diff origin/canary
yarn publish-stable
```
After running this publish step, GitHub Actions will take care of publishing the modified Builder packages to npm.
If for some reason GitHub Actions fails to publish the npm package, you may do so
manually by running `npm publish` from the package directory. Make sure to
use `npm publish --tag canary` if you are publishing a canary release!
### Contributing
See the [Contribution guidelines for this project](CONTRIBUTING.md), it also contains guidance on interpreting tests failures.

View File

@@ -1,13 +1,15 @@
const childProcess = require('child_process');
const path = require('path');
const { execSync } = require('child_process');
const { relative } = require('path');
const command = 'git diff HEAD~1 --name-only';
const diff = childProcess.execSync(command).toString();
const branch = execSync('git branch | grep "*" | cut -d " " -f2').toString();
console.log(`Running tests on branch "${branch}"`);
const base = branch === 'master' ? 'HEAD~1' : 'origin/canary';
const diff = execSync(`git diff ${base} --name-only`).toString();
const changed = diff
.split('\n')
.filter(item => Boolean(item) && item.includes('packages/'))
.map(item => path.relative('packages', item).split('/')[0]);
.map(item => relative('packages', item).split('/')[0]);
const matches = [];

View File

@@ -32,6 +32,14 @@
"*.ts": [
"prettier --write",
"git add"
],
"*.json": [
"prettier --write",
"git add"
],
"*.md": [
"prettier --write",
"git add"
]
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@now/build-utils",
"version": "0.5.6",
"version": "0.7.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -9,8 +9,12 @@
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-build-utils"
},
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublishOnly": "tsc"
},
"dependencies": {
"@types/cross-spawn": "6.0.0",
"async-retry": "1.2.3",
"async-sema": "2.1.4",
"cross-spawn": "6.0.5",
@@ -18,24 +22,21 @@
"fs-extra": "7.0.0",
"glob": "7.1.3",
"into-stream": "5.0.0",
"memory-fs": "0.4.1",
"multistream": "2.1.1",
"node-fetch": "2.2.0",
"semver": "6.1.1",
"yazl": "2.4.3"
},
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublish": "tsc"
},
"devDependencies": {
"@types/async-retry": "^1.2.1",
"@types/cross-spawn": "6.0.0",
"@types/end-of-stream": "^1.4.0",
"@types/fs-extra": "^5.0.5",
"@types/glob": "^7.1.1",
"@types/node-fetch": "^2.1.6",
"@types/semver": "6.0.0",
"@types/yazl": "^2.4.1",
"execa": "^1.0.0",
"typescript": "3.3.4000"
"typescript": "3.5.2"
}
}

View File

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

View File

@@ -3,6 +3,9 @@ import fs from 'fs-extra';
import path from 'path';
import spawn from 'cross-spawn';
import { SpawnOptions } from 'child_process';
import { deprecate } from 'util';
import { Meta, PackageJson, NodeVersion } from '../types';
import { getSupportedNodeVersion } from './node-version';
function spawnAsync(
command: string,
@@ -51,11 +54,32 @@ export async function runShellScript(fsPath: string) {
return true;
}
async function scanParentDirs(destPath: string, scriptName?: string) {
export function getSpawnOptions(
meta: Meta,
nodeVersion: NodeVersion
): SpawnOptions {
const opts = {
env: { ...process.env },
};
if (!meta.isDev) {
opts.env.PATH = `/node${nodeVersion.major}/bin:${opts.env.PATH}`;
}
return opts;
}
export async function getNodeVersion(destPath: string): Promise<NodeVersion> {
const { packageJson } = await scanParentDirs(destPath, true);
const range = packageJson && packageJson.engines && packageJson.engines.node;
return getSupportedNodeVersion(range);
}
async function scanParentDirs(destPath: string, readPackageJson = false) {
assert(path.isAbsolute(destPath));
let hasScript = false;
let hasPackageLockJson = false;
let packageJson: PackageJson | undefined;
let currentDestPath = destPath;
// eslint-disable-next-line no-constant-condition
@@ -64,12 +88,9 @@ async function scanParentDirs(destPath: string, scriptName?: string) {
// eslint-disable-next-line no-await-in-loop
if (await fs.pathExists(packageJsonPath)) {
// eslint-disable-next-line no-await-in-loop
const packageJson = JSON.parse(
await fs.readFile(packageJsonPath, 'utf8')
);
hasScript = Boolean(
packageJson.scripts && scriptName && packageJson.scripts[scriptName]
);
if (readPackageJson) {
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
}
// eslint-disable-next-line no-await-in-loop
hasPackageLockJson = await fs.pathExists(
path.join(currentDestPath, 'package-lock.json')
@@ -82,12 +103,13 @@ async function scanParentDirs(destPath: string, scriptName?: string) {
currentDestPath = newDestPath;
}
return { hasScript, hasPackageLockJson };
return { hasPackageLockJson, packageJson };
}
export async function installDependencies(
export async function runNpmInstall(
destPath: string,
args: string[] = []
args: string[] = [],
spawnOpts?: SpawnOptions
) {
assert(path.isAbsolute(destPath));
@@ -95,30 +117,22 @@ export async function installDependencies(
console.log(`installing to ${destPath}`);
const { hasPackageLockJson } = await scanParentDirs(destPath);
const opts = {
env: {
...process.env,
// This is a little hack to force `node-gyp` to build for the
// Node.js version that `@now/node` and `@now/node-server` use
npm_config_target: '8.10.0',
},
stdio: 'pipe',
};
const opts = spawnOpts || { env: process.env };
if (hasPackageLockJson) {
commandArgs = args.filter(a => a !== '--prefer-offline');
await spawnAsync(
'npm',
['install', '--unsafe-perm'].concat(commandArgs),
commandArgs.concat(['install', '--unsafe-perm']),
destPath,
opts as SpawnOptions
opts
);
} else {
await spawnAsync(
'yarn',
['--ignore-engines', '--cwd', destPath].concat(commandArgs),
commandArgs.concat(['--ignore-engines', '--cwd', destPath]),
destPath,
opts as SpawnOptions
opts
);
}
}
@@ -129,9 +143,15 @@ export async function runPackageJsonScript(
opts?: SpawnOptions
) {
assert(path.isAbsolute(destPath));
const { hasScript, hasPackageLockJson } = await scanParentDirs(
const { packageJson, hasPackageLockJson } = await scanParentDirs(
destPath,
scriptName
true
);
const hasScript = Boolean(
packageJson &&
packageJson.scripts &&
scriptName &&
packageJson.scripts[scriptName]
);
if (!hasScript) return false;
@@ -151,4 +171,11 @@ export async function runPackageJsonScript(
return true;
}
export const runNpmInstall = installDependencies;
/**
* @deprecate installDependencies() is deprecated.
* Please use runNpmInstall() instead.
*/
export const installDependencies = deprecate(
runNpmInstall,
'installDependencies() is deprecated. Please use runNpmInstall() instead.'
);

View File

@@ -20,6 +20,8 @@ import {
runPackageJsonScript,
runNpmInstall,
runShellScript,
getNodeVersion,
getSpawnOptions,
} from './fs/run-user-scripts';
import streamToBuffer from './fs/stream-to-buffer';
import shouldServe from './should-serve';
@@ -42,6 +44,8 @@ export {
runPackageJsonScript,
runNpmInstall,
runShellScript,
getNodeVersion,
getSpawnOptions,
streamToBuffer,
AnalyzeOptions,
BuildOptions,

View File

@@ -16,7 +16,14 @@ export interface Files {
}
export interface Config {
[key: string]: string;
[key: string]: string | string[] | boolean | number | undefined;
maxLambdaSize?: string;
includeFiles?: string | string[];
bundle?: boolean;
ldsflags?: string;
helpers?: boolean;
rust?: string;
debug?: boolean;
}
export interface Meta {
@@ -155,3 +162,28 @@ export interface ShouldServeOptions {
*/
config: Config;
}
export interface PackageJson {
name: string;
version: string;
engines?: {
[key: string]: string;
node: string;
npm: string;
};
scripts?: {
[key: string]: string;
};
dependencies?: {
[key: string]: string;
};
devDependencies?: {
[key: string]: string;
};
}
export interface NodeVersion {
major: number;
range: string;
runtime: string;
}

View File

@@ -6,6 +6,10 @@ const execa = require('execa');
const assert = require('assert');
const { glob, download } = require('../');
const { createZip } = require('../dist/lambda');
const {
getSupportedNodeVersion,
defaultSelection,
} = require('../dist/fs/node-version');
const {
packAndDeploy,
@@ -64,6 +68,52 @@ it('should create zip files with symlinks properly', async () => {
assert(aStat.isFile());
});
it('should only match supported node versions', () => {
expect(getSupportedNodeVersion('10.x')).resolves.toHaveProperty('major', 10);
expect(getSupportedNodeVersion('8.10.x')).resolves.toHaveProperty('major', 8);
expect(getSupportedNodeVersion('8.11.x')).rejects.toThrow();
expect(getSupportedNodeVersion('6.x')).rejects.toThrow();
expect(getSupportedNodeVersion('999.x')).rejects.toThrow();
expect(getSupportedNodeVersion('foo')).rejects.toThrow();
expect(getSupportedNodeVersion('')).resolves.toBe(defaultSelection);
expect(getSupportedNodeVersion(null)).resolves.toBe(defaultSelection);
expect(getSupportedNodeVersion(undefined)).resolves.toBe(defaultSelection);
});
it('should match all semver ranges', () => {
// See https://docs.npmjs.com/files/package.json#engines
expect(getSupportedNodeVersion('10.0.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('10.x')).resolves.toHaveProperty('major', 10);
expect(getSupportedNodeVersion('>=10')).resolves.toHaveProperty('major', 10);
expect(getSupportedNodeVersion('>=10.3.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('8.5.0 - 10.5.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('>=9.0.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('>=9.5.0 <=10.5.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('~10.5.0')).resolves.toHaveProperty(
'major',
10,
);
expect(getSupportedNodeVersion('^10.5.0')).resolves.toHaveProperty(
'major',
10,
);
});
// own fixtures
const fixturesPath = path.resolve(__dirname, 'fixtures');

View File

@@ -128,18 +128,18 @@ Learn more: https://zeit.co/docs/v2/deployments/official-builders/go-now-go/#ent
const entrypointDirname = dirname(downloadedFiles[entrypoint].fsPath);
let isGoModExist = false;
let goModPath = '';
let goModPathArr: string[] = [];
let isGoModInRootDir = false;
for (const file of Object.keys(downloadedFiles)) {
const fileDirname = dirname(downloadedFiles[file].fsPath);
if (file === 'go.mod') {
isGoModExist = true;
isGoModInRootDir = true;
goModPath = fileDirname;
goModPathArr = goModPath.split(sep);
} else if (file.includes('go.mod')) {
isGoModExist = true;
if (entrypointDirname === fileDirname) {
isGoModExist = true;
goModPath = fileDirname;
goModPathArr = goModPath.split(sep);
break;
}
}
}
@@ -202,14 +202,28 @@ Learn more: https://zeit.co/docs/v2/deployments/official-builders/go-now-go/#ent
if (isGoModExist) {
const goModContents = await readFile(join(goModPath, 'go.mod'), 'utf8');
const usrModName = goModContents.split('\n')[0].split(' ')[1];
if (entrypointArr.length > 1 && isGoModInRootDir) {
let cleanPackagePath = [...entrypointArr];
cleanPackagePath.pop();
goPackageName = `${usrModName}/${cleanPackagePath.join('/')}`;
} else {
goPackageName = `${usrModName}/${packageName}`;
}
}
const mainModGoContents = modMainGoContents
.replace('__NOW_HANDLER_PACKAGE_NAME', goPackageName)
.replace('__NOW_HANDLER_FUNC_NAME', goFuncName);
if (goModPathArr.length > 1) {
if (meta.isDev && isGoModExist && isGoModInRootDir) {
await writeFile(
join(dirname(downloadedFiles['now.json'].fsPath), mainModGoFileName),
mainModGoContents
);
} else if (isGoModExist && isGoModInRootDir) {
await writeFile(join(srcPath, mainModGoFileName), mainModGoContents);
} else if (isGoModExist && !isGoModInRootDir) {
// using `go.mod` path to write main__mod__.go
await writeFile(join(goModPath, mainModGoFileName), mainModGoContents);
} else {
@@ -252,21 +266,28 @@ Learn more: https://zeit.co/docs/v2/deployments/official-builders/go-now-go/#ent
throw err;
}
if (meta.isDev) {
let entrypointDir = entrypointDirname;
if (goModPathArr.length > 1) {
entrypointDir = goModPath;
let baseGoModPath = '';
if (meta.isDev && isGoModExist && isGoModInRootDir) {
baseGoModPath = dirname(downloadedFiles['now.json'].fsPath);
} else if (isGoModExist && isGoModInRootDir) {
baseGoModPath = srcPath;
} else if (isGoModExist && !isGoModInRootDir) {
baseGoModPath = goModPath;
} else {
baseGoModPath = entrypointDirname;
}
const isGoModBk = await pathExists(join(entrypointDir, 'go.mod.bk'));
if (meta.isDev) {
const isGoModBk = await pathExists(join(baseGoModPath, 'go.mod.bk'));
if (isGoModBk) {
await move(
join(entrypointDir, 'go.mod.bk'),
join(entrypointDir, 'go.mod'),
join(baseGoModPath, 'go.mod.bk'),
join(baseGoModPath, 'go.mod'),
{ overwrite: true }
);
await move(
join(entrypointDir, 'go.sum.bk'),
join(entrypointDir, 'go.sum'),
join(baseGoModPath, 'go.sum.bk'),
join(baseGoModPath, 'go.sum'),
{ overwrite: true }
);
}
@@ -283,8 +304,7 @@ Learn more: https://zeit.co/docs/v2/deployments/official-builders/go-now-go/#ent
console.log('Running `go build`...');
const destPath = join(outDir, 'handler');
const isGoModInRootDir = goModPathArr.length === 1;
const baseGoModPath = isGoModInRootDir ? entrypointDirname : goModPath;
try {
let src = [join(baseGoModPath, mainModGoFileName)];

View File

@@ -1,6 +1,6 @@
{
"name": "@now/go",
"version": "0.5.1",
"version": "0.5.2",
"license": "MIT",
"repository": {
"type": "git",
@@ -31,6 +31,6 @@
"@types/fs-extra": "^5.0.5",
"@types/node-fetch": "^2.3.0",
"@types/tar": "^4.0.0",
"typescript": "^3.4.2"
"typescript": "3.5.2"
}
}

View File

@@ -0,0 +1,12 @@
package nested
import (
"fmt"
"net/http"
"with-nested/shared"
)
// Handler func
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, shared.Say("lol:RANDOMNESS_PLACEHOLDER"))
}

View File

@@ -0,0 +1,3 @@
module with-nested
go 1.12

View File

@@ -0,0 +1,5 @@
{
"version": 2,
"builds": [{ "src": "api/nested/*.go", "use": "@now/go" }],
"probes": [{ "path": "/api/nested", "mustContain": "RANDOMNESS_PLACEHOLDER" }]
}

View File

@@ -0,0 +1,6 @@
package shared
// Say func
func Say(text string) string {
return text
}

View File

@@ -0,0 +1,11 @@
package handler
import (
"fmt"
"net/http"
)
// Handler func
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello:RANDOMNESS_PLACEHOLDER")
}

View File

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

View File

@@ -0,0 +1,3 @@
module other-folder
go 1.12

View File

@@ -0,0 +1,11 @@
package handler
import (
"fmt"
"net/http"
)
// Handler func
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello:RANDOMNESS_PLACEHOLDER")
}

View File

@@ -1 +0,0 @@
/dist

View File

@@ -1,32 +0,0 @@
{
"name": "@now/layer-node",
"version": "0.0.2",
"main": "./dist/src/index",
"license": "MIT",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-layer-node"
},
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublishOnly": "tsc"
},
"dependencies": {
"fs-extra": "7.0.1",
"node-fetch": "2.6.0",
"promisepipe": "3.0.0",
"stream-to-promise": "2.2.0",
"tar": "4.4.6",
"yauzl-promise": "2.1.3"
},
"devDependencies": {
"@types/tar": "4.0.0",
"@types/yauzl-promise": "2.1.0",
"typescript": "3.3.3"
}
}

View File

@@ -1,37 +0,0 @@
import { tmpdir } from 'os';
import { join } from 'path';
import { glob, Files } from '@now/build-utils';
import { mkdir, remove, pathExists } from 'fs-extra';
import { install } from './install';
interface BuildLayerConfig {
runtimeVersion: string;
platform: string;
arch: string;
}
interface BuildLayerResult {
files: Files;
entrypoint: string;
}
export async function buildLayer({
runtimeVersion,
platform,
arch,
}: BuildLayerConfig): Promise<BuildLayerResult> {
const dir = join(
tmpdir(),
`now-layer-node-${runtimeVersion}-${platform}-${arch}`
);
const exists = await pathExists(dir);
if (exists) {
await remove(dir);
}
await mkdir(dir);
const { entrypoint } = await install(dir, runtimeVersion, platform, arch);
const files = await glob('{bin/node,bin/node.exe,include/**}', {
cwd: dir,
});
return { files, entrypoint };
}

View File

@@ -1,68 +0,0 @@
import { basename, join } from 'path';
import fetch from 'node-fetch';
import { extract } from 'tar';
import pipe from 'promisepipe';
import { createWriteStream } from 'fs-extra';
import { unzip, zipFromFile } from './unzip';
export async function install(
dest: string,
version: string,
platform: string,
arch: string
) {
const tarballUrl = getUrl(version, platform, arch);
console.log('Downloading from ' + tarballUrl);
console.log('Downloading to ' + dest);
const res = await fetch(tarballUrl);
if (!res.ok) {
throw new Error(`HTTP request failed: ${res.status}`);
}
let entrypoint: string;
if (platform === 'win32') {
// Put it in the `bin` dir for consistency with the tarballs
const finalDest = join(dest, 'bin');
const zipName = basename(tarballUrl);
const zipPath = join(dest, zipName);
await pipe(
res.body,
createWriteStream(zipPath)
);
const zipFile = await zipFromFile(zipPath);
await unzip(zipFile, finalDest, { strip: 1 });
entrypoint = join('bin', 'node.exe');
} else {
const extractStream = extract({ strip: 1, C: dest });
if (!extractStream.destroy) {
// If there is an error in promisepipe,
// it expects a destroy method
extractStream.destroy = () => {};
}
await pipe(
res.body,
extractStream
);
entrypoint = join('bin', 'node');
}
return { entrypoint };
}
export function getUrl(
version: string,
platform: string = process.platform,
arch: string = process.arch
): string {
let ext: string;
let plat: string;
if (platform === 'win32') {
ext = 'zip';
plat = 'win';
} else {
ext = 'tar.gz';
plat = platform;
}
return `https://nodejs.org/dist/v${version}/node-v${version}-${plat}-${arch}.${ext}`;
}

View File

@@ -1,96 +0,0 @@
import { tmpdir } from 'os';
import pipe from 'promisepipe';
import { dirname, join } from 'path';
import { createWriteStream, mkdirp, symlink, unlink } from 'fs-extra';
import streamToPromise from 'stream-to-promise';
import {
Entry,
ZipFile,
open as zipFromFile,
fromBuffer as zipFromBuffer,
} from 'yauzl-promise';
export { zipFromFile, zipFromBuffer, ZipFile };
export async function unzipToTemp(
data: Buffer | string,
tmpDir: string = tmpdir()
): Promise<string> {
const dir = join(
tmpDir,
`zeit-fun-${Math.random()
.toString(16)
.substring(2)}`
);
let zip: ZipFile;
if (Buffer.isBuffer(data)) {
zip = await zipFromBuffer(data);
} else {
zip = await zipFromFile(data);
}
await unzip(zip, dir);
await zip.close();
return dir;
}
interface UnzipOptions {
strip?: number;
}
export async function unzip(
zipFile: ZipFile,
dir: string,
opts: UnzipOptions = {}
): Promise<void> {
let entry: Entry;
const strip = opts.strip || 0;
while ((entry = await zipFile.readEntry()) !== null) {
const fileName =
strip === 0
? entry.fileName
: entry.fileName
.split('/')
.slice(strip)
.join('/');
const destPath = join(dir, fileName);
if (/\/$/.test(entry.fileName)) {
await mkdirp(destPath);
} else {
const [entryStream] = await Promise.all([
entry.openReadStream(),
// ensure parent directory exists
mkdirp(dirname(destPath)),
]);
const mode = entry.externalFileAttributes >>> 16;
if (isSymbolicLink(mode)) {
const linkDest = String(await streamToPromise(entryStream));
await symlink(linkDest, destPath);
} else {
const octal = mode & 4095 /* 07777 */;
const modeOctal = ('0000' + octal.toString(8)).slice(-4);
const modeVal = parseInt(modeOctal, 8);
try {
await unlink(destPath);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
const destStream = createWriteStream(destPath, {
mode: modeVal,
});
await pipe(
entryStream,
destStream
);
}
}
}
}
const S_IFMT = 61440; /* 0170000 type of file */
const S_IFLNK = 40960; /* 0120000 symbolic link */
export function isSymbolicLink(mode: number): boolean {
return (mode & S_IFMT) === S_IFLNK;
}

View File

@@ -1,54 +0,0 @@
/* global jest, expect, it */
jest.setTimeout(30 * 1000);
const { buildLayer } = require('../');
describe('buildLayer', () => {
it('should get node 10 and metadata for windows', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '10.16.0',
platform: 'win32',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(names.size).toBeGreaterThan(0);
expect(entrypoint).toBe('bin/node.exe');
expect(names.has('bin/node.exe')).toBeTruthy();
expect(names.has('bin/npm.cmd')).toBeFalsy();
expect(names.has('bin/npx.cmd')).toBeFalsy();
expect(names.has('bin/node_modules')).toBeFalsy();
});
it('should get node 10 and metadata for macos', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '10.16.0',
platform: 'darwin',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(names.size).toBeGreaterThan(0);
expect(entrypoint).toBe('bin/node');
expect(names.has('bin/node')).toBeTruthy();
expect(names.has('bin/npm')).toBeFalsy();
expect(names.has('bin/npx')).toBeFalsy();
expect(names.has('lib/node_modules')).toBeFalsy();
});
it('should get node 10 and metadata for linux', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '10.16.0',
platform: 'linux',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(names.size).toBeGreaterThan(0);
expect(entrypoint).toBe('bin/node');
expect(names.has('bin/node')).toBeTruthy();
expect(names.has('include/node/node.h')).toBeTruthy();
expect(names.has('bin/npm')).toBeFalsy();
expect(names.has('bin/npx')).toBeFalsy();
expect(names.has('lib/node_modules')).toBeFalsy();
});
});

View File

@@ -1,4 +0,0 @@
declare module 'promisepipe' {
import { Stream } from 'stream';
export default function pipe(...args: Stream[]): Promise<void>;
}

View File

@@ -1,6 +0,0 @@
declare module 'stream-to-promise' {
import { Stream } from 'stream';
export default function streamToPromise(
stream: NodeJS.ReadableStream
): Promise<string>;
}

View File

@@ -1 +0,0 @@
/dist

View File

@@ -1,29 +0,0 @@
{
"name": "@now/layer-npm",
"version": "0.0.2",
"main": "./dist/src/index",
"license": "MIT",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-layer-npm"
},
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublishOnly": "tsc"
},
"dependencies": {
"fs-extra": "7.0.1",
"node-fetch": "2.6.0",
"promisepipe": "3.0.0",
"tar": "4.4.6"
},
"devDependencies": {
"@types/tar": "4.0.0",
"typescript": "3.3.3"
}
}

View File

@@ -1,37 +0,0 @@
import { tmpdir } from 'os';
import { join } from 'path';
import { glob, Files } from '@now/build-utils';
import { mkdir, remove, pathExists } from 'fs-extra';
import { install } from './install';
interface BuildLayerConfig {
runtimeVersion: string;
platform: string;
arch: string;
}
interface BuildLayerResult {
files: Files;
entrypoint: string;
}
export async function buildLayer({
runtimeVersion,
platform,
arch,
}: BuildLayerConfig): Promise<BuildLayerResult> {
const dir = join(
tmpdir(),
`now-layer-npm-${runtimeVersion}-${platform}-${arch}`
);
const exists = await pathExists(dir);
if (exists) {
await remove(dir);
}
await mkdir(dir);
const { entrypoint } = await install(dir, runtimeVersion);
const files = await glob('{bin/**,lib/**,node_modules/**}', {
cwd: dir,
});
return { files, entrypoint };
}

View File

@@ -1,29 +0,0 @@
import { join } from 'path';
import fetch from 'node-fetch';
import { extract } from 'tar';
import pipe from 'promisepipe';
export async function install(dest: string, version: string) {
const tarballUrl = `https://registry.npmjs.org/npm/-/npm-${version}.tgz`;
console.log('Downloading from ' + tarballUrl);
console.log('Downloading to ' + dest);
const res = await fetch(tarballUrl);
if (!res.ok) {
throw new Error(`HTTP request failed: ${res.status}`);
}
const extractStream = extract({ strip: 1, C: dest });
if (!extractStream.destroy) {
// If there is an error in promisepipe,
// it expects a destroy method
extractStream.destroy = () => {};
}
await pipe(
res.body,
extractStream
);
const pathToManifest = join(dest, 'package.json');
const manifest = require(pathToManifest);
const entrypoint = manifest.bin.npm;
return { entrypoint };
}

View File

@@ -1,50 +0,0 @@
/* global jest, expect, it */
jest.setTimeout(30 * 1000);
const { buildLayer } = require('../');
describe('buildLayer', () => {
it('should get npm 6 but not npm for windows', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '6.9.0',
platform: 'win32',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(entrypoint).toBe('./bin/npm-cli.js');
expect(names.size).toBeGreaterThan(0);
expect(names.has('bin/npm.cmd')).toBeTruthy();
expect(names.has('bin/npx.cmd')).toBeTruthy();
expect(names.has('README.md')).toBeFalsy();
});
it('should get npm 6 but not npm for macos', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '6.9.0',
platform: 'darwin',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(entrypoint).toBe('./bin/npm-cli.js');
expect(names.size).toBeGreaterThan(0);
expect(names.has('bin/npm')).toBeTruthy();
expect(names.has('bin/npx')).toBeTruthy();
expect(names.has('README.md')).toBeFalsy();
});
it('should get npm 6 but not npm for linux', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '6.9.0',
platform: 'linux',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(entrypoint).toBe('./bin/npm-cli.js');
expect(names.size).toBeGreaterThan(0);
expect(names.has('bin/npm')).toBeTruthy();
expect(names.has('bin/npx')).toBeTruthy();
expect(names.has('README.md')).toBeFalsy();
});
});

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"declaration": false,
"esModuleInterop": true,
"lib": ["esnext"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"types": ["node"],
"strict": true,
"target": "esnext"
}
}

View File

@@ -1,4 +0,0 @@
declare module 'promisepipe' {
import { Stream } from 'stream';
export default function pipe(...args: Stream[]): Promise<void>;
}

View File

@@ -1 +0,0 @@
/dist

View File

@@ -1,29 +0,0 @@
{
"name": "@now/layer-yarn",
"version": "0.0.2",
"main": "./dist/src/index",
"license": "MIT",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-layer-yarn"
},
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublishOnly": "tsc"
},
"dependencies": {
"fs-extra": "7.0.1",
"node-fetch": "2.6.0",
"promisepipe": "3.0.0",
"tar": "4.4.6"
},
"devDependencies": {
"@types/tar": "4.0.0",
"typescript": "3.3.3"
}
}

View File

@@ -1,37 +0,0 @@
import { tmpdir } from 'os';
import { join } from 'path';
import { glob, Files } from '@now/build-utils';
import { mkdir, remove, pathExists } from 'fs-extra';
import { install } from './install';
interface BuildLayerConfig {
runtimeVersion: string;
platform: string;
arch: string;
}
interface BuildLayerResult {
files: Files;
entrypoint: string;
}
export async function buildLayer({
runtimeVersion,
platform,
arch,
}: BuildLayerConfig): Promise<BuildLayerResult> {
const dir = join(
tmpdir(),
`now-layer-yarn-${runtimeVersion}-${platform}-${arch}`
);
const exists = await pathExists(dir);
if (exists) {
await remove(dir);
}
await mkdir(dir);
const { entrypoint } = await install(dir, runtimeVersion);
const files = await glob('{bin/**,lib/**}', {
cwd: dir,
});
return { files, entrypoint };
}

View File

@@ -1,29 +0,0 @@
import { join } from 'path';
import fetch from 'node-fetch';
import { extract } from 'tar';
import pipe from 'promisepipe';
export async function install(dest: string, version: string) {
const tarballUrl = `https://registry.npmjs.org/yarn/-/yarn-${version}.tgz`;
console.log('Downloading from ' + tarballUrl);
console.log('Downloading to ' + dest);
const res = await fetch(tarballUrl);
if (!res.ok) {
throw new Error(`HTTP request failed: ${res.status}`);
}
const extractStream = extract({ strip: 1, C: dest });
if (!extractStream.destroy) {
// If there is an error in promisepipe,
// it expects a destroy method
extractStream.destroy = () => {};
}
await pipe(
res.body,
extractStream
);
const pathToManifest = join(dest, 'package.json');
const manifest = require(pathToManifest);
const entrypoint = manifest.bin.yarn;
return { entrypoint };
}

View File

@@ -1,49 +0,0 @@
/* global jest, expect, it */
jest.setTimeout(30 * 1000);
const { buildLayer } = require('../');
describe('buildLayer', () => {
it('should get yarn for windows', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '1.16.0',
platform: 'win32',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(entrypoint).toBe('./bin/yarn.js');
expect(names.size).toBeGreaterThan(0);
expect(names.has('bin/yarn.cmd')).toBeTruthy();
expect(names.has('lib/cli.js')).toBeTruthy();
});
it('should get yarn for macos', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '1.16.0',
platform: 'darwin',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(entrypoint).toBe('./bin/yarn.js');
expect(names.size).toBeGreaterThan(0);
expect(names.has('bin/yarn')).toBeTruthy();
expect(names.has('lib/cli.js')).toBeTruthy();
expect(names.has('README.md')).toBeFalsy();
});
it('should get yarn for linux', async () => {
const { files, entrypoint } = await buildLayer({
runtimeVersion: '1.16.0',
platform: 'linux',
arch: 'x64',
});
const names = new Set(Object.keys(files));
expect(names).toBeTruthy();
expect(entrypoint).toBe('./bin/yarn.js');
expect(names.size).toBeGreaterThan(0);
expect(names.has('bin/yarn')).toBeTruthy();
expect(names.has('lib/cli.js')).toBeTruthy();
expect(names.has('README.md')).toBeFalsy();
});
});

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"declaration": false,
"esModuleInterop": true,
"lib": ["esnext"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"types": ["node"],
"strict": true,
"target": "esnext"
}
}

View File

@@ -1,4 +0,0 @@
declare module 'promisepipe' {
import { Stream } from 'stream';
export default function pipe(...args: Stream[]): Promise<void>;
}

View File

@@ -39,4 +39,7 @@ exports.build = async ({ files, entrypoint, config }) => {
return { [replacedEntrypoint]: result };
};
exports.shouldServe = shouldServe;
exports.shouldServe = (options) => {
const requestPath = options.requestPath.replace(/\.html$/, '.md');
return shouldServe({ ...options, requestPath });
};

View File

@@ -1,6 +1,6 @@
{
"name": "@now/md",
"version": "0.5.3",
"version": "0.5.4",
"license": "MIT",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@now/next",
"version": "0.4.1",
"version": "0.5.0",
"license": "MIT",
"main": "./dist/index",
"scripts": {
@@ -14,7 +14,7 @@
"directory": "packages/now-next"
},
"dependencies": {
"@now/node-bridge": "^1.1.4",
"@now/node-bridge": "^1.2.1",
"fs-extra": "^7.0.0",
"get-port": "^5.0.0",
"resolve-from": "^5.0.0",
@@ -28,6 +28,6 @@
"@types/resolve-from": "^5.0.1",
"@types/semver": "^6.0.0",
"jest": "^24.7.1",
"typescript": "^3.4.3"
"typescript": "3.5.2"
}
}

View File

@@ -21,6 +21,8 @@ import {
PrepareCacheOptions,
runNpmInstall,
runPackageJsonScript,
getNodeVersion,
getSpawnOptions,
} from '@now/build-utils';
import nextLegacyVersions from './legacy-versions';
@@ -166,11 +168,14 @@ export const build = async ({
const entryDirectory = path.dirname(entrypoint);
const entryPath = path.join(workPath, entryDirectory);
const dotNext = path.join(entryPath, '.next');
const dotNextStatic = path.join(entryPath, '.next/static');
console.log(`${name} Downloading user files...`);
await download(files, workPath, meta);
const nodeVersion = await getNodeVersion(entryPath);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
const pkg = await readPackageJson(entryPath);
const nextVersion = getNextVersion(pkg);
@@ -190,7 +195,7 @@ export const build = async ({
// If this is the initial build, we want to start the server
if (!urls[entrypoint]) {
console.log(`${name} Installing dependencies...`);
await runNpmInstall(entryPath, ['--prefer-offline']);
await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts);
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'development';
@@ -227,7 +232,7 @@ export const build = async ({
};
}
if (await pathExists(dotNext)) {
if (await pathExists(dotNextStatic)) {
console.warn(
'WARNING: You should not upload the `.next` directory. See https://zeit.co/docs/v2/deployments/official-builders/next-js-now-next/ for more details.'
);
@@ -259,14 +264,15 @@ export const build = async ({
console.log('normalized package.json result: ', packageJson);
await writePackageJson(entryPath, packageJson);
} else if (!pkg.scripts || !pkg.scripts['now-build']) {
console.warn(
'WARNING: "now-build" script not found. Adding \'"now-build": "next build"\' to "package.json" automatically'
console.log(
'Your application is being built using `next build`. ' +
'If you need to define a different build step, please create a `now-build` script in your `package.json` ' +
'(e.g. `{ "scripts": { "now-build": "npm run prepare && next build" } }`).'
);
pkg.scripts = {
'now-build': 'next build',
...(pkg.scripts || {}),
};
console.log('normalized package.json result: ', pkg);
await writePackageJson(entryPath, pkg);
}
@@ -276,20 +282,22 @@ export const build = async ({
}
console.log('installing dependencies...');
await runNpmInstall(entryPath, ['--prefer-offline']);
await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts);
console.log('running user script...');
const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128;
await runPackageJsonScript(entryPath, 'now-build', {
env: {
...process.env,
NODE_OPTIONS: `--max_old_space_size=${memoryToConsume}`,
},
} as SpawnOptions);
const buildSpawnOptions = { ...spawnOpts };
const env = { ...buildSpawnOptions.env } as any;
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
await runPackageJsonScript(entryPath, 'now-build', buildSpawnOptions);
if (isLegacy) {
console.log('running npm install --production...');
await runNpmInstall(entryPath, ['--prefer-offline', '--production']);
await runNpmInstall(
entryPath,
['--prefer-offline', '--production'],
spawnOpts
);
}
if (process.env.NPM_AUTH_TOKEN) {
@@ -510,7 +518,7 @@ export const build = async ({
).map(route => {
// make sure .html is added to dest for now until
// outputting static files to clean routes is available
if (staticPages[`${route.dest}.html`]) {
if (staticPages[`${route.dest}.html`.substr(1)]) {
route.dest = `${route.dest}.html`;
}
return route;

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node-bridge",
"version": "1.1.4",
"version": "1.2.1",
"license": "MIT",
"main": "./index.js",
"repository": {
@@ -21,6 +21,6 @@
"@types/aws-lambda": "8.10.19",
"@types/node": "11.9.4",
"jest": "24.1.0",
"typescript": "3.3.3"
"typescript": "3.5.2"
}
}

View File

@@ -95,9 +95,13 @@ export class Bridge {
private server: Server | null;
private listening: Promise<AddressInfo>;
private resolveListening: (info: AddressInfo) => void;
private events: { [key: string]: NowProxyRequest } = {};
private reqIdSeed: number = 1;
private shouldStoreEvents: boolean = false;
constructor(server?: Server) {
constructor(server?: Server, shouldStoreEvents: boolean = false) {
this.server = null;
this.shouldStoreEvents = shouldStoreEvents;
if (server) {
this.setServer(server);
}
@@ -144,15 +148,16 @@ export class Bridge {
context.callbackWaitsForEmptyEventLoop = false;
const { port } = await this.listening;
const { isApiGateway, method, path, headers, body } = normalizeEvent(event);
const normalizedEvent = normalizeEvent(event);
const { isApiGateway, method, path, headers, body } = normalizedEvent;
const opts = {
hostname: '127.0.0.1',
port,
path,
method,
headers,
};
if (this.shouldStoreEvents) {
const reqId = `${this.reqIdSeed++}`;
this.events[reqId] = normalizedEvent;
headers['x-now-bridge-request-id'] = reqId;
}
const opts = { hostname: '127.0.0.1', port, path, method, headers };
// eslint-disable-next-line consistent-return
return new Promise((resolve, reject) => {
@@ -192,4 +197,10 @@ export class Bridge {
req.end();
});
}
consumeEvent(reqId: string) {
const event = this.events[reqId];
delete this.events[reqId];
return event;
}
}

View File

@@ -83,3 +83,41 @@ test('`NowProxyEvent` normalizing', async () => {
server.close();
});
test('consumeEvent', async () => {
const mockListener = jest.fn((req, res) => {
res.end('hello');
});
const server = new Server(mockListener);
const bridge = new Bridge(server, true);
bridge.listen();
const context = { callbackWaitsForEmptyEventLoop: true };
await bridge.launcher(
{
Action: 'Invoke',
body: JSON.stringify({
method: 'POST',
headers: { foo: 'baz' },
path: '/nowproxy',
body: 'body=1',
}),
},
context
);
const headers = mockListener.mock.calls[0][0].headers;
const reqId = headers['x-now-bridge-request-id'];
expect(reqId).toBeTruthy();
const event = bridge.consumeEvent(reqId);
expect(event.body.toString()).toBe('body=1');
// an event can't be consumed multiple times
// to avoid memory leaks
expect(bridge.consumeEvent(reqId)).toBeUndefined();
server.close();
});

View File

@@ -1,15 +1,17 @@
const { createLambda } = require('@now/build-utils/lambda.js'); // eslint-disable-line import/no-extraneous-dependencies
const download = require('@now/build-utils/fs/download.js'); // eslint-disable-line import/no-extraneous-dependencies
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 fs = require('fs-extra');
const glob = require('@now/build-utils/fs/glob.js'); // eslint-disable-line import/no-extraneous-dependencies
const path = require('path');
const {
FileBlob,
FileFsRef,
download,
createLambda,
glob,
runNpmInstall,
runPackageJsonScript,
} = require('@now/build-utils/fs/run-user-scripts.js'); // eslint-disable-line import/no-extraneous-dependencies
const { shouldServe } = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
getNodeVersion,
getSpawnOptions,
shouldServe,
} = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
/** @typedef { import('@now/build-utils/file-ref') } FileRef */
/** @typedef {{[filePath: string]: FileRef}} Files */
@@ -38,8 +40,15 @@ async function downloadInstallAndBundle(
console.log("installing dependencies for user's code...");
const entrypointFsDirname = path.join(workPath, path.dirname(entrypoint));
await runNpmInstall(entrypointFsDirname, npmArguments);
return [downloadedFiles, entrypointFsDirname];
const nodeVersion = await getNodeVersion(entrypointFsDirname);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
await runNpmInstall(entrypointFsDirname, npmArguments, spawnOpts);
return {
downloadedFiles,
entrypointFsDirname,
spawnOpts,
nodeVersion,
};
}
async function compile(workPath, downloadedFiles, entrypoint, config) {
@@ -101,9 +110,14 @@ exports.config = {
* @returns {Promise<Files>}
*/
exports.build = async ({
files, entrypoint, config, workPath, meta,
files, entrypoint, config, workPath, meta = {},
}) => {
const [downloadedFiles, entrypointFsDirname] = await downloadInstallAndBundle(
const {
downloadedFiles,
entrypointFsDirname,
spawnOptions,
nodeVersion,
} = await downloadInstallAndBundle(
{
files,
entrypoint,
@@ -114,7 +128,7 @@ exports.build = async ({
);
console.log('running user script...');
await runPackageJsonScript(entrypointFsDirname, 'now-build');
await runPackageJsonScript(entrypointFsDirname, 'now-build', spawnOptions);
console.log('preparing lambda files...');
let preparedFiles;
@@ -146,7 +160,7 @@ exports.build = async ({
const lambda = await createLambda({
files: { ...preparedFiles, ...launcherFiles },
handler: 'launcher.launcher',
runtime: 'nodejs8.10',
runtime: nodeVersion.runtime,
});
return { [entrypoint]: lambda };

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node-server",
"version": "0.7.4",
"version": "0.8.0",
"license": "MIT",
"repository": {
"type": "git",
@@ -8,7 +8,7 @@
"directory": "packages/now-node-server"
},
"dependencies": {
"@now/node-bridge": "^1.1.4",
"@now/node-bridge": "^1.2.1",
"@zeit/ncc": "0.18.5",
"fs-extra": "7.0.1"
},

View File

@@ -1,2 +1,3 @@
/dist
/src/bridge.d.ts
/test/fixtures/**/types.d.ts

View File

@@ -0,0 +1,45 @@
declare function ncc(
entrypoint: string,
options?: ncc.NccOptions
): ncc.NccResult;
declare namespace ncc {
export interface NccOptions {
watch?: any;
sourceMap?: boolean;
sourceMapRegister?: boolean;
}
export interface Asset {
source: Buffer;
permissions: number;
}
export interface Assets {
[name: string]: Asset;
}
export interface BuildResult {
err: Error | null | undefined;
code: string;
map: string | undefined;
assets: Assets | undefined;
permissions: number | undefined;
}
export type HandlerFn = (params: BuildResult) => void;
export type HandlerCallback = (fn: HandlerFn) => void;
export type RebuildFn = () => void;
export type RebuildCallback = (fn: RebuildFn) => void;
export type CloseCallback = () => void;
export interface NccResult {
handler: HandlerCallback;
rebuild: RebuildCallback;
close: CloseCallback;
}
}
declare module '@zeit/ncc' {
export = ncc;
}

View File

@@ -9,4 +9,16 @@ if [ ! -e "$bridge_defs" ]; then
fi
cp -v "$bridge_defs" src
# build ts files
tsc
# bundle helpers.ts with ncc
rm dist/helpers.js
ncc build src/helpers.ts -o dist/helpers
mv dist/helpers/index.js dist/helpers.js
rm -rf dist/helpers
# todo: improve
# copy type file for ts test
cp dist/types.d.ts test/fixtures/15-helpers/ts/types.d.ts

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node",
"version": "0.7.4",
"version": "0.10.0",
"license": "MIT",
"main": "./dist/index",
"repository": {
@@ -9,8 +9,10 @@
"directory": "packages/now-node"
},
"dependencies": {
"@now/node-bridge": "^1.1.4",
"@now/node-bridge": "^1.2.1",
"@types/node": "*",
"@zeit/ncc": "0.18.5",
"@zeit/ncc-watcher": "1.0.3",
"fs-extra": "7.0.1"
},
"scripts": {
@@ -22,8 +24,13 @@
"dist"
],
"devDependencies": {
"@types/node": "11.9.4",
"jest": "24.1.0",
"typescript": "3.3.3"
"@types/content-type": "1.1.3",
"@types/cookie": "0.3.3",
"@types/test-listen": "1.1.0",
"content-type": "1.0.4",
"cookie": "0.4.0",
"node-fetch": "2.6.0",
"test-listen": "1.1.0",
"typescript": "3.5.2"
}
}

View File

@@ -0,0 +1,3 @@
// we intentionally import these types here
// to test that they are exported from index
import { NowRequest, NowResponse } from './index';

View File

@@ -0,0 +1,204 @@
import {
NowRequest,
NowResponse,
NowRequestCookies,
NowRequestQuery,
NowRequestBody,
} from './types';
import { Stream } from 'stream';
import { Server } from 'http';
import { Bridge } from './bridge';
function getBodyParser(req: NowRequest, body: Buffer) {
return function parseBody(): NowRequestBody {
if (!req.headers['content-type']) {
return undefined;
}
const { parse: parseCT } = require('content-type');
const { type } = parseCT(req.headers['content-type']);
if (type === 'application/json') {
try {
return JSON.parse(body.toString());
} catch (error) {
throw new ApiError(400, 'Invalid JSON');
}
}
if (type === 'application/octet-stream') {
return body;
}
if (type === 'application/x-www-form-urlencoded') {
const { parse: parseQS } = require('querystring');
// remark : querystring.parse does not produce an iterable object
// https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
return parseQS(body.toString());
}
if (type === 'text/plain') {
return body.toString();
}
return undefined;
};
}
function getQueryParser({ url = '/' }: NowRequest) {
return function parseQuery(): NowRequestQuery {
const { URL } = require('url');
// we provide a placeholder base url because we only want searchParams
const params = new URL(url, 'https://n').searchParams;
const query: { [key: string]: string | string[] } = {};
for (const [key, value] of params) {
query[key] = value;
}
return query;
};
}
function getCookieParser(req: NowRequest) {
return function parseCookie(): NowRequestCookies {
const header: undefined | string | string[] = req.headers.cookie;
if (!header) {
return {};
}
const { parse } = require('cookie');
return parse(Array.isArray(header) ? header.join(';') : header);
};
}
function sendStatusCode(res: NowResponse, statusCode: number): NowResponse {
res.statusCode = statusCode;
return res;
}
function sendData(res: NowResponse, body: any): NowResponse {
if (body === null) {
res.end();
return res;
}
const contentType = res.getHeader('Content-Type');
if (Buffer.isBuffer(body)) {
if (!contentType) {
res.setHeader('Content-Type', 'application/octet-stream');
}
res.setHeader('Content-Length', body.length);
res.end(body);
return res;
}
if (body instanceof Stream) {
if (!contentType) {
res.setHeader('Content-Type', 'application/octet-stream');
}
body.pipe(res);
return res;
}
let str = body;
// Stringify JSON body
if (typeof body === 'object' || typeof body === 'number') {
str = JSON.stringify(body);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
}
res.setHeader('Content-Length', Buffer.byteLength(str));
res.end(str);
return res;
}
function sendJson(res: NowResponse, jsonBody: any): NowResponse {
// Set header to application/json
res.setHeader('Content-Type', 'application/json; charset=utf-8');
// Use send to handle request
return res.send(jsonBody);
}
export class ApiError extends Error {
readonly statusCode: number;
constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
}
}
export function sendError(
res: NowResponse,
statusCode: number,
message: string
) {
res.statusCode = statusCode;
res.statusMessage = message;
res.end();
}
function setLazyProp<T>(req: NowRequest, prop: string, getter: () => T) {
const opts = { configurable: true, enumerable: true };
const optsReset = { ...opts, writable: true };
Object.defineProperty(req, prop, {
...opts,
get: () => {
const value = getter();
// we set the property on the object to avoid recalculating it
Object.defineProperty(req, prop, { ...optsReset, value });
return value;
},
set: value => {
Object.defineProperty(req, prop, { ...optsReset, value });
},
});
}
export function createServerWithHelpers(
listener: (req: NowRequest, res: NowResponse) => void | Promise<void>,
bridge: Bridge
) {
const server = new Server(async (_req, _res) => {
const req = _req as NowRequest;
const res = _res as NowResponse;
try {
const reqId = req.headers['x-now-bridge-request-id'];
// don't expose this header to the client
delete req.headers['x-now-bridge-request-id'];
if (typeof reqId !== 'string') {
throw new ApiError(500, 'Internal Server Error');
}
const event = bridge.consumeEvent(reqId);
setLazyProp<NowRequestCookies>(req, 'cookies', getCookieParser(req));
setLazyProp<NowRequestQuery>(req, 'query', getQueryParser(req));
setLazyProp<NowRequestBody>(req, 'body', getBodyParser(req, event.body));
res.status = statusCode => sendStatusCode(res, statusCode);
res.send = data => sendData(res, data);
res.json = data => sendJson(res, data);
await listener(req, res);
} catch (err) {
if (err instanceof ApiError) {
sendError(res, err.statusCode, err.message);
} else {
throw err;
}
}
});
return server;
}

View File

@@ -1,5 +1,7 @@
import { join, dirname, sep } from 'path';
import { readFile } from 'fs-extra';
import { Assets, NccOptions } from '@zeit/ncc';
import { join, dirname, relative, sep } from 'path';
import { NccWatcher, WatcherResult } from '@zeit/ncc-watcher';
import {
glob,
download,
@@ -10,10 +12,13 @@ import {
createLambda,
runNpmInstall,
runPackageJsonScript,
getNodeVersion,
getSpawnOptions,
PrepareCacheOptions,
BuildOptions,
shouldServe,
} from '@now/build-utils';
export { NowRequest, NowResponse } from './types';
interface CompilerConfig {
includeFiles?: string | string[];
@@ -23,8 +28,25 @@ interface DownloadOptions {
files: Files;
entrypoint: string;
workPath: string;
meta?: Meta;
npmArguments?: string[];
meta: Meta;
}
const watchers: Map<string, NccWatcher> = new Map();
function getWatcher(entrypoint: string, options: NccOptions): NccWatcher {
let watcher = watchers.get(entrypoint);
if (!watcher) {
watcher = new NccWatcher(entrypoint, options);
watchers.set(entrypoint, watcher);
}
return watcher;
}
function toBuffer(data: string | Buffer): Buffer {
if (typeof data === 'string') {
return Buffer.from(data, 'utf8');
}
return data;
}
async function downloadInstallAndBundle({
@@ -32,32 +54,66 @@ async function downloadInstallAndBundle({
entrypoint,
workPath,
meta,
npmArguments = [],
}: DownloadOptions) {
console.log('downloading user files...');
const downloadedFiles = await download(files, workPath, meta);
console.log("installing dependencies for user's code...");
const entrypointFsDirname = join(workPath, dirname(entrypoint));
await runNpmInstall(entrypointFsDirname, npmArguments);
const nodeVersion = await getNodeVersion(entrypointFsDirname);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
await runNpmInstall(entrypointFsDirname, ['--prefer-offline'], spawnOpts);
const entrypointPath = downloadedFiles[entrypoint].fsPath;
return { entrypointPath, entrypointFsDirname };
return { entrypointPath, entrypointFsDirname, nodeVersion, spawnOpts };
}
async function compile(
workPath: string,
entrypointPath: string,
entrypoint: string,
config: CompilerConfig
): Promise<Files> {
config: CompilerConfig,
{ isDev, filesChanged, filesRemoved }: Meta
): Promise<{ preparedFiles: Files; watch: string[] }> {
const input = entrypointPath;
const inputDir = dirname(input);
const rootIncludeFiles = inputDir.split(sep).pop() || '';
const options: NccOptions = {
sourceMap: true,
sourceMapRegister: true,
};
let code: string;
let map: string | undefined;
let assets: Assets | undefined;
let watch: string[] = [];
if (isDev) {
const watcher = getWatcher(entrypointPath, options);
const result = await watcher.build(
Array.isArray(filesChanged)
? filesChanged.map(f => join(workPath, f))
: undefined,
Array.isArray(filesRemoved)
? filesRemoved.map(f => join(workPath, f))
: undefined
);
code = result.code;
map = result.map;
assets = result.assets;
watch = [...result.files, ...result.dirs, ...result.missing]
.filter(f => f.startsWith(workPath))
.map(f => relative(workPath, f));
} else {
const ncc = require('@zeit/ncc');
const { code, map, assets } = await ncc(input, {
const result = await ncc(input, {
sourceMap: true,
sourceMapRegister: true,
});
code = result.code;
map = result.map;
assets = result.assets;
}
if (!assets) assets = {};
if (config && config.includeFiles) {
const includeFiles =
@@ -81,7 +137,7 @@ async function compile(
}
assets[fullPath] = {
source: data,
source: toBuffer(data),
permissions: mode,
};
}
@@ -89,11 +145,15 @@ async function compile(
}
const preparedFiles: Files = {};
// move all user code to 'user' subdirectory
preparedFiles[entrypoint] = new FileBlob({ data: code });
if (map) {
preparedFiles[`${entrypoint.replace('.ts', '.js')}.map`] = new FileBlob({
data: map,
data: toBuffer(map),
});
}
// move all user code to 'user' subdirectory
// eslint-disable-next-line no-restricted-syntax
for (const assetName of Object.keys(assets)) {
const { source: data, permissions: mode } = assets[assetName];
@@ -101,9 +161,11 @@ async function compile(
preparedFiles[join(dirname(entrypoint), assetName)] = blob2;
}
return preparedFiles;
return { preparedFiles, watch };
}
export const version = 2;
export const config = {
maxLambdaSize: '5mb',
};
@@ -113,47 +175,76 @@ export async function build({
entrypoint,
workPath,
config,
meta,
meta = {},
}: BuildOptions) {
const shouldAddHelpers = !(config && config.helpers === false);
const {
entrypointPath,
entrypointFsDirname,
nodeVersion,
spawnOpts,
} = await downloadInstallAndBundle({
files,
entrypoint,
workPath,
meta,
npmArguments: ['--prefer-offline'],
});
console.log('running user script...');
await runPackageJsonScript(entrypointFsDirname, 'now-build');
await runPackageJsonScript(entrypointFsDirname, 'now-build', spawnOpts);
console.log('compiling entrypoint with ncc...');
const preparedFiles = await compile(entrypointPath, entrypoint, config);
const { preparedFiles, watch } = await compile(
workPath,
entrypointPath,
entrypoint,
config,
meta
);
const launcherPath = join(__dirname, 'launcher.js');
let launcherData = await readFile(launcherPath, 'utf8');
launcherData = launcherData.replace(
'// PLACEHOLDER',
'// PLACEHOLDER:shouldStoreProxyRequests',
shouldAddHelpers ? 'shouldStoreProxyRequests = true;' : ''
);
launcherData = launcherData.replace(
'// PLACEHOLDER:setServer',
[
`listener = require("./${entrypoint}");`,
`let listener = require("./${entrypoint}");`,
'if (listener.default) listener = listener.default;',
shouldAddHelpers
? 'const server = require("./helpers").createServerWithHelpers(listener, bridge);'
: 'const server = require("http").createServer(listener);',
'bridge.setServer(server);',
].join(' ')
);
const launcherFiles = {
const launcherFiles: Files = {
'launcher.js': new FileBlob({ data: launcherData }),
'bridge.js': new FileFsRef({ fsPath: require('@now/node-bridge') }),
};
if (shouldAddHelpers) {
launcherFiles['helpers.js'] = new FileFsRef({
fsPath: join(__dirname, 'helpers.js'),
});
}
const lambda = await createLambda({
files: { ...preparedFiles, ...launcherFiles },
files: {
...preparedFiles,
...launcherFiles,
},
handler: 'launcher.launcher',
runtime: 'nodejs8.10',
runtime: nodeVersion.runtime,
});
return { [entrypoint]: lambda };
const output = { [entrypoint]: lambda };
const result = { output, watch };
return result;
}
export async function prepareCache({ workPath }: PrepareCacheOptions) {

View File

@@ -1,7 +1,9 @@
import { Server } from 'http';
import { Bridge } from './bridge';
let listener;
let shouldStoreProxyRequests: boolean = false;
// PLACEHOLDER:shouldStoreProxyRequests
const bridge = new Bridge(undefined, shouldStoreProxyRequests);
if (!process.env.NODE_ENV) {
process.env.NODE_ENV =
@@ -9,7 +11,7 @@ if (!process.env.NODE_ENV) {
}
try {
// PLACEHOLDER
// PLACEHOLDER:setServer
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
console.error(err.message);
@@ -23,8 +25,6 @@ try {
}
}
const server = new Server(listener);
const bridge = new Bridge(server);
bridge.listen();
exports.launcher = bridge.launcher;

View File

@@ -0,0 +1,17 @@
import { ServerResponse, IncomingMessage } from 'http';
export type NowRequestCookies = { [key: string]: string };
export type NowRequestQuery = { [key: string]: string | string[] };
export type NowRequestBody = any;
export type NowRequest = IncomingMessage & {
query: NowRequestQuery;
cookies: NowRequestCookies;
body: NowRequestBody;
};
export type NowResponse = ServerResponse & {
send: (body: any) => NowResponse;
json: (body: any) => NowResponse;
status: (statusCode: number) => NowResponse;
};

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
};

View File

@@ -0,0 +1,3 @@
{
"name": "missing-engines-key-on-purpose"
}

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
};

View File

@@ -0,0 +1,5 @@
{
"engines": {
"node": "10.5.0"
}
}

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
};

View File

@@ -0,0 +1,5 @@
{
"engines": {
"node": ">=10.0.0"
}
}

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
};

View File

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

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [{ "src": "**/*.js", "use": "@now/node" }],
"probes": [
{ "path": "/empty", "mustContain": "RANDOMNESS_PLACEHOLDER:8" },
{ "path": "/exact", "mustContain": "RANDOMNESS_PLACEHOLDER:10" },
{ "path": "/greater", "mustContain": "RANDOMNESS_PLACEHOLDER:10" },
{ "path": "/major", "mustContain": "RANDOMNESS_PLACEHOLDER:10" },
{ "path": "/range", "mustContain": "RANDOMNESS_PLACEHOLDER:10" }
]
}

View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
};

View File

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

View File

@@ -0,0 +1,20 @@
/* eslint-disable prefer-destructuring */
const express = require('express');
const app = express();
module.exports = app;
app.use(express.json());
app.all('*', (req, res) => {
res.status(200);
let who = 'anonymous';
if (req.body && req.body.who) {
who = req.body.who;
}
res.send(`hello ${who}:RANDOMNESS_PLACEHOLDER`);
});

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"express": "4.17.1"
}
}

View File

@@ -0,0 +1,17 @@
/* eslint-disable prefer-destructuring */
module.exports = (req, res) => {
res.status(200);
let who = 'anonymous';
if (req.body && req.body.who) {
who = req.body.who;
} else if (req.query.who) {
who = req.query.who;
} else if (req.cookies.who) {
who = req.cookies.who;
}
res.send(`hello ${who}:RANDOMNESS_PLACEHOLDER`);
};

View File

@@ -0,0 +1,14 @@
/* eslint-disable prefer-destructuring */
const { json, send } = require('micro');
module.exports = async (req, res) => {
const body = await json(req);
let who = 'anonymous';
if (body && body.who) {
who = body.who;
}
send(res, 200, `hello ${who}:RANDOMNESS_PLACEHOLDER`);
};

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"micro": "9.3.4"
}
}

View File

@@ -0,0 +1,5 @@
module.exports = (req, res) => {
const areHelpersAvailable = typeof req.query !== 'undefined';
res.end(`${areHelpersAvailable ? 'yes' : 'no'}:RANDOMNESS_PLACEHOLDER`);
};

View File

@@ -0,0 +1,55 @@
{
"version": 2,
"builds": [
{ "src": "index.js", "use": "@now/node" },
{ "src": "ts/index.ts", "use": "@now/node" },
{ "src": "express-compat/index.js", "use": "@now/node" },
{ "src": "micro-compat/index.js", "use": "@now/node" },
{
"src": "no-helpers/index.js",
"use": "@now/node",
"config": { "helpers": false }
}
],
"probes": [
{
"path": "/",
"mustContain": "hello anonymous:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/?who=bill",
"mustContain": "hello bill:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/",
"method": "POST",
"body": { "who": "john" },
"mustContain": "hello john:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/",
"headers": { "cookie": "who=chris" },
"mustContain": "hello chris:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/ts",
"mustContain": "hello:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/express-compat",
"method": "POST",
"body": { "who": "sara" },
"mustContain": "hello sara:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/micro-compat",
"method": "POST",
"body": { "who": "katie" },
"mustContain": "hello katie:RANDOMNESS_PLACEHOLDER"
},
{
"path": "/no-helpers/",
"mustContain": "no:RANDOMNESS_PLACEHOLDER"
}
]
}

View File

@@ -0,0 +1,6 @@
import { NowRequest, NowResponse } from './types';
export default function listener(req: NowRequest, res: NowResponse) {
res.status(200);
res.send('hello:RANDOMNESS_PLACEHOLDER');
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"sourceMap": true,
"lib": ["esnext"],
"target": "esnext",
"module": "commonjs"
},
"include": ["index.ts"]
}

View File

@@ -0,0 +1,317 @@
/* global beforeAll, beforeEach, afterAll, expect, it, jest */
const fetch = require('node-fetch');
const listen = require('test-listen');
const qs = require('querystring');
const { createServerWithHelpers } = require('../dist/helpers');
const mockListener = jest.fn((req, res) => {
res.send('hello');
});
const consumeEventMock = jest.fn(() => ({}));
const mockBridge = { consumeEvent: consumeEventMock };
let server;
let url;
const nowProps = [
['query', 0],
['cookies', 0],
['body', 0],
['status', 1],
['send', 1],
['json', 1],
];
async function fetchWithProxyReq(_url, opts = {}) {
if (opts.body) {
// eslint-disable-next-line
opts = { ...opts, body: Buffer.from(opts.body) };
}
consumeEventMock.mockImplementationOnce(() => opts);
return fetch(_url, {
...opts,
headers: { ...opts.headers, 'x-now-bridge-request-id': '2' },
});
}
beforeAll(async () => {
server = createServerWithHelpers(mockListener, mockBridge);
url = await listen(server);
});
beforeEach(() => {
mockListener.mockClear();
consumeEventMock.mockClear();
});
afterAll(async () => {
await server.close();
});
it('should call consumeEvent with the correct reqId', async () => {
await fetchWithProxyReq(`${url}/`);
expect(consumeEventMock).toHaveBeenLastCalledWith('2');
});
it('should not expose the request id header', async () => {
await fetchWithProxyReq(`${url}/`, { headers: { 'x-test-header': 'ok' } });
const [{ headers }] = mockListener.mock.calls[0];
expect(headers['x-now-bridge-request-id']).toBeUndefined();
expect(headers['x-test-header']).toBe('ok');
});
it('req.query should reflect querystring in the url', async () => {
await fetchWithProxyReq(`${url}/?who=bill&where=us`);
expect(mockListener.mock.calls[0][0].query).toMatchObject({
who: 'bill',
where: 'us',
});
});
it('req.query should be {} when there is no querystring', async () => {
await fetchWithProxyReq(url);
const [{ query }] = mockListener.mock.calls[0];
expect(Object.keys(query).length).toBe(0);
});
it('req.cookies should reflect req.cookie header', async () => {
await fetchWithProxyReq(url, {
headers: {
cookie: 'who=bill; where=us',
},
});
expect(mockListener.mock.calls[0][0].cookies).toMatchObject({
who: 'bill',
where: 'us',
});
});
it('req.body should be undefined by default', async () => {
await fetchWithProxyReq(url);
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
});
it('req.body should be undefined if content-type is not defined', async () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
});
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
});
it('req.body should be a string when content-type is `text/plain`', async () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
headers: { 'content-type': 'text/plain' },
});
expect(mockListener.mock.calls[0][0].body).toBe('hello');
});
it('req.body should be a buffer when content-type is `application/octet-stream`', async () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
headers: { 'content-type': 'application/octet-stream' },
});
const [{ body }] = mockListener.mock.calls[0];
const str = body.toString();
expect(Buffer.isBuffer(body)).toBe(true);
expect(str).toBe('hello');
});
it('req.body should be an object when content-type is `application/x-www-form-urlencoded`', async () => {
const obj = { who: 'mike' };
await fetchWithProxyReq(url, {
method: 'POST',
body: qs.encode(obj),
headers: { 'content-type': 'application/x-www-form-urlencoded' },
});
expect(mockListener.mock.calls[0][0].body).toMatchObject(obj);
});
it('req.body should be an object when content-type is `application/json`', async () => {
const json = {
who: 'bill',
where: 'us',
};
await fetchWithProxyReq(url, {
method: 'POST',
body: JSON.stringify(json),
headers: { 'content-type': 'application/json' },
});
expect(mockListener.mock.calls[0][0].body).toMatchObject(json);
});
it('should throw error when body is empty and content-type is `application/json`', async () => {
mockListener.mockImplementation((req, res) => {
console.log(req.body);
res.end();
});
const res = await fetchWithProxyReq(url, {
method: 'POST',
body: '',
headers: { 'content-type': 'application/json' },
});
expect(res.status).toBe(400);
});
it('should not recalculate req properties twice', async () => {
const bodySpy = jest.fn(() => {});
mockListener.mockImplementation((req, res) => {
bodySpy(req.body, req.query, req.cookies);
bodySpy(req.body, req.query, req.cookies);
res.end();
});
await fetchWithProxyReq(`${url}/?who=bill`, {
method: 'POST',
body: JSON.stringify({ who: 'mike' }),
headers: { 'content-type': 'application/json', cookie: 'who=jim' },
});
// here we test that bodySpy is called twice with exactly the same arguments
for (let i = 0; i < 3; i += 1) {
expect(bodySpy.mock.calls[0][i]).toBe(bodySpy.mock.calls[1][i]);
}
});
it('should be able to overwrite request properties', async () => {
const spy = jest.fn(() => {});
mockListener.mockImplementation((...args) => {
nowProps.forEach(([prop, n]) => {
/* eslint-disable */
args[n][prop] = 'ok';
args[n][prop] = 'ok2';
spy(args[n][prop]);
});
args[1].end();
});
await fetchWithProxyReq(url);
nowProps.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
});
// we test that properties are configurable
// because expressjs (or some other api frameworks) needs that to work
it('should be able to reconfig request properties', async () => {
const spy = jest.fn(() => {});
mockListener.mockImplementation((...args) => {
nowProps.forEach(([prop, n]) => {
// eslint-disable-next-line
Object.defineProperty(args[n], prop, { value: 'ok' });
Object.defineProperty(args[n], prop, { value: 'ok2' });
spy(args[n][prop]);
});
args[1].end();
});
await fetchWithProxyReq(url);
nowProps.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
});
it('should be able to try/catch parse errors', async () => {
const bodySpy = jest.fn(() => {});
mockListener.mockImplementation((req, res) => {
try {
if (req.body === undefined) res.status(400);
} catch (error) {
bodySpy(error);
} finally {
res.end();
}
});
await fetchWithProxyReq(url, {
method: 'POST',
body: '{"wrong":"json"',
headers: { 'content-type': 'application/json' },
});
expect(bodySpy).toHaveBeenCalled();
const [error] = bodySpy.mock.calls[0];
expect(error.message).toMatch(/invalid json/i);
expect(error.statusCode).toBe(400);
});
it('res.send() should send text', async () => {
mockListener.mockImplementation((req, res) => {
res.send('hello world');
});
const res = await fetchWithProxyReq(url);
expect(await res.text()).toBe('hello world');
});
it('res.json() should send json', async () => {
mockListener.mockImplementation((req, res) => {
res.json({ who: 'bill' });
});
const res = await fetchWithProxyReq(url);
const contentType = res.headers.get('content-type') || '';
expect(contentType.includes('application/json')).toBe(true);
expect(await res.json()).toMatchObject({ who: 'bill' });
});
it('res.status() should set the status code', async () => {
mockListener.mockImplementation((req, res) => {
res.status(404);
res.end();
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(404);
});
it('res.status().send() should work', async () => {
mockListener.mockImplementation((req, res) => {
res.status(404).send('notfound');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(404);
expect(await res.text()).toBe('notfound');
});
it('res.status().json() should work', async () => {
mockListener.mockImplementation((req, res) => {
res.status(404).json({ error: 'not found' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(404);
expect(await res.json()).toMatchObject({ error: 'not found' });
});

View File

@@ -7,7 +7,8 @@
"module": "commonjs",
"outDir": "dist",
"sourceMap": false,
"declaration": false
"declaration": true,
"typeRoots": ["./@types", "./node_modules/@types"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]

View File

@@ -1,6 +1,6 @@
{
"name": "@now/optipng",
"version": "0.6.2",
"version": "0.6.3",
"license": "MIT",
"main": "./dist/index",
"files": [
@@ -21,6 +21,6 @@
},
"devDependencies": {
"@types/node": "11.9.4",
"typescript": "3.3.3"
"typescript": "3.5.2"
}
}

View File

@@ -68,8 +68,9 @@ elif 'app' in __now_variables:
if isinstance(body, string_types):
body = to_bytes(body, charset='utf-8')
path = unquote(payload['path'])
query = urlparse(path).query
url = urlparse(unquote(payload['path']))
query = url.query
path = url.path
environ = {
'CONTENT_LENGTH': str(len(body)),

View File

@@ -1,7 +1,7 @@
{
"name": "@now/python",
"version": "0.2.5",
"main": "index.js",
"version": "0.2.8",
"main": "./dist/index.js",
"license": "MIT",
"files": [
"dist",
@@ -15,7 +15,7 @@
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublish": "tsc"
"prepublishOnly": "tsc"
},
"dependencies": {
"execa": "^1.0.0",
@@ -23,6 +23,6 @@
},
"devDependencies": {
"@types/execa": "^0.9.0",
"typescript": "3.3.4000"
"typescript": "3.5.2"
}
}

View File

@@ -104,7 +104,6 @@ export const build = async ({
workPath = destNow;
}
const foundLockFile = 'Pipfile.lock' in downloadedFiles;
const pyUserBase = await getWriteableDirectory();
process.env.PYTHONUSERBASE = pyUserBase;
const pipPath = 'pip3';
@@ -129,17 +128,25 @@ export const build = async ({
await pipInstall(pipPath, workPath, 'werkzeug');
await pipInstall(pipPath, workPath, 'requests');
if (foundLockFile) {
let fsFiles = await glob('**', workPath);
const entryDirectory = dirname(entrypoint);
const pipfileLockDir = fsFiles[join(entryDirectory, 'Pipfile.lock')]
? join(workPath, entryDirectory)
: fsFiles['Pipfile.lock']
? workPath
: null;
if (pipfileLockDir) {
console.log('found "Pipfile.lock"');
// Install pipenv.
await pipInstallUser(pipPath, ' pipenv_to_requirements');
await pipenvInstall(pyUserBase, workPath);
await pipenvInstall(pyUserBase, pipfileLockDir);
}
const fsFiles = await glob('**', workPath);
const entryDirectory = dirname(entrypoint);
fsFiles = await glob('**', workPath);
const requirementsTxt = join(entryDirectory, 'requirements.txt');
if (fsFiles[requirementsTxt]) {

View File

@@ -0,0 +1,12 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
cowpy = "*"
[requires]
python_version = "3.7"

View File

@@ -0,0 +1,29 @@
{
"_meta": {
"hash": {
"sha256": "841b49dd1836f7373490faa2a1d6abb4ecd53c0b45e8196df7c6e852e21f458b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"cowpy": {
"hashes": [
"sha256:1bdc61d107df02fd34a9241f2220d0704a01d8ce16bed8bff3512a34a0efa56a",
"sha256:91a861bfbfa644dfdba5b2250d141b2227a94f61d4dcbeaf7653524d048935a9"
],
"index": "pypi",
"version": "==1.1.0"
}
},
"develop": {}
}

View File

@@ -0,0 +1,13 @@
from http.server import BaseHTTPRequestHandler
from cowpy import cow
class handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
message = cow.Cowacter().milk('pip:RANDOMNESS_PLACEHOLDER')
self.wfile.write(message.encode())
return

View File

@@ -0,0 +1,5 @@
{
"version": 2,
"builds": [{ "src": "**/**.py", "use": "@now/python" }],
"probes": [{ "path": "/", "mustContain": "pip:RANDOMNESS_PLACEHOLDER" }]
}

View File

@@ -0,0 +1,12 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
cowpy = "*"
[requires]
python_version = "3.7"

View File

@@ -0,0 +1,29 @@
{
"_meta": {
"hash": {
"sha256": "841b49dd1836f7373490faa2a1d6abb4ecd53c0b45e8196df7c6e852e21f458b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"cowpy": {
"hashes": [
"sha256:1bdc61d107df02fd34a9241f2220d0704a01d8ce16bed8bff3512a34a0efa56a",
"sha256:91a861bfbfa644dfdba5b2250d141b2227a94f61d4dcbeaf7653524d048935a9"
],
"index": "pypi",
"version": "==1.1.0"
}
},
"develop": {}
}

View File

@@ -0,0 +1,13 @@
from http.server import BaseHTTPRequestHandler
from cowpy import cow
class handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
message = cow.Cowacter().milk('pip:RANDOMNESS_PLACEHOLDER')
self.wfile.write(message.encode())
return

View File

@@ -0,0 +1,5 @@
{
"version": 2,
"builds": [{ "src": "**/**.py", "use": "@now/python" }],
"probes": [{ "path": "/local", "mustContain": "pip:RANDOMNESS_PLACEHOLDER" }]
}

View File

@@ -1,7 +1,8 @@
from flask import Flask, Response, __version__
from flask import Flask, Response, request, __version__
app = Flask(__name__)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
return Response("path=%s" %(path), mimetype='text/html')
qs = request.args.to_dict()
return Response("path: %s query: %s" %(path, qs), mimetype='text/html')

View File

@@ -1,7 +1,8 @@
from flask import Flask, Response, __version__
from flask import Flask, Response, request, __version__
app = Flask(__name__)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
return Response("path=%s" %(path), mimetype='text/html')
qs = request.args.to_dict()
return Response("path: %s query: %s" %(path, qs), mimetype='text/html')

View File

@@ -3,7 +3,10 @@
"builds": [{ "src": "*.py", "use": "@now/python" }],
"routes": [{ "src": "/another", "dest": "custom.py" }],
"probes": [
{ "path": "/?hello=/", "mustContain": "path=?hello=/" },
{ "path": "/another?hello=/", "mustContain": "path=another?hello=/" }
{ "path": "/?hello=/", "mustContain": "path: query: {'hello': '/'}" },
{
"path": "/another?hello=/",
"mustContain": "path: another query: {'hello': '/'}"
}
]
}

1
packages/now-ruby/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/dist

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