Compare commits

...

36 Commits

Author SHA1 Message Date
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
78 changed files with 1196 additions and 896 deletions

View File

@@ -6,9 +6,6 @@
/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/*

View File

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

View File

@@ -2,28 +2,41 @@
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 cherry pick each commit from canary to master and then 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 ...
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

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.6.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,22 +22,19 @@
"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"

View File

@@ -3,6 +3,8 @@ import fs from 'fs-extra';
import path from 'path';
import spawn from 'cross-spawn';
import { SpawnOptions } from 'child_process';
import { deprecate } from 'util';
import { intersects } from 'semver';
function spawnAsync(
command: string,
@@ -51,11 +53,41 @@ export async function runShellScript(fsPath: string) {
return true;
}
async function scanParentDirs(destPath: string, scriptName?: string) {
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 async function enginesMatch(
destPath: string,
nodeVersion: string
): Promise<boolean> {
const { packageJson } = await scanParentDirs(destPath, true);
const engineVersion =
packageJson && packageJson.engines && packageJson.engines.node;
return intersects(nodeVersion, engineVersion || '0.0.0');
}
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 +96,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,43 +111,36 @@ async function scanParentDirs(destPath: string, scriptName?: string) {
currentDestPath = newDestPath;
}
return { hasScript, hasPackageLockJson };
return { hasPackageLockJson, packageJson };
}
export async function installDependencies(
destPath: string,
args: string[] = []
) {
export async function runNpmInstall(destPath: string, args: string[] = []) {
assert(path.isAbsolute(destPath));
let commandArgs = args;
console.log(`installing to ${destPath}`);
const { hasPackageLockJson } = await scanParentDirs(destPath);
const opts = {
const opts: SpawnOptions = {
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',
};
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 +151,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 +179,11 @@ export async function runPackageJsonScript(
return true;
}
export const runNpmInstall = installDependencies;
/**
* installDependencies() is deprecated.
* Please use runNpmInstall() instead.
*/
export const installDependencies = deprecate(
runNpmInstall,
'installDependencies() is deprecated. Please use runNpmInstall() instead.'
);

View File

@@ -20,6 +20,7 @@ import {
runPackageJsonScript,
runNpmInstall,
runShellScript,
enginesMatch,
} from './fs/run-user-scripts';
import streamToBuffer from './fs/stream-to-buffer';
import shouldServe from './should-serve';
@@ -42,6 +43,7 @@ export {
runPackageJsonScript,
runNpmInstall,
runShellScript,
enginesMatch,
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 {

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,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,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.4.2",
"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.0",
"fs-extra": "^7.0.0",
"get-port": "^5.0.0",
"resolve-from": "^5.0.0",

View File

@@ -166,7 +166,7 @@ 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);
@@ -227,7 +227,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 +259,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);
}
@@ -510,7 +511,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.0",
"license": "MIT",
"main": "./index.js",
"repository": {

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,6 +1,6 @@
{
"name": "@now/node-server",
"version": "0.7.4",
"version": "0.7.5",
"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.0",
"@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.9.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.0",
"@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",
"@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.3.3"
}
}

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,
@@ -14,6 +16,7 @@ import {
BuildOptions,
shouldServe,
} from '@now/build-utils';
export { NowRequest, NowResponse } from './types';
interface CompilerConfig {
includeFiles?: string | string[];
@@ -23,8 +26,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 +52,64 @@ 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);
await runNpmInstall(entrypointFsDirname, ['--prefer-offline']);
const entrypointPath = downloadedFiles[entrypoint].fsPath;
return { entrypointPath, entrypointFsDirname };
}
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 ncc = require('@zeit/ncc');
const { code, map, assets } = await ncc(input, {
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 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 +133,7 @@ async function compile(
}
assets[fullPath] = {
source: data,
source: toBuffer(data),
permissions: mode,
};
}
@@ -89,11 +141,15 @@ async function compile(
}
const preparedFiles: Files = {};
// move all user code to 'user' subdirectory
preparedFiles[entrypoint] = new FileBlob({ data: code });
preparedFiles[`${entrypoint.replace('.ts', '.js')}.map`] = new FileBlob({
data: map,
});
if (map) {
preparedFiles[`${entrypoint.replace('.ts', '.js')}.map`] = new FileBlob({
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 +157,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,8 +171,10 @@ export async function build({
entrypoint,
workPath,
config,
meta,
meta = {},
}: BuildOptions) {
const shouldAddHelpers = !(config && config.helpers === false);
const {
entrypointPath,
entrypointFsDirname,
@@ -123,37 +183,62 @@ export async function build({
entrypoint,
workPath,
meta,
npmArguments: ['--prefer-offline'],
});
console.log('running user script...');
await runPackageJsonScript(entrypointFsDirname, 'now-build');
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',
});
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,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

@@ -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.7",
"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",

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,7 @@
"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': '/'}" }
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@now/rust",
"version": "0.2.5",
"version": "0.2.6",
"license": "MIT",
"main": "./dist/index",
"repository": {

View File

@@ -119,7 +119,10 @@ async function buildWholeProject(
return lambdas;
}
async function gatherExtraFiles(globMatcher: string, entrypoint: string) {
async function gatherExtraFiles(
globMatcher: string | string[] | undefined,
entrypoint: string
) {
if (!globMatcher) return {};
console.log('gathering extra files for the fs...');
@@ -176,7 +179,7 @@ async function buildSingleFile(
rustEnv: Record<string, string>
) {
console.log('building single file');
const launcherPath = path.join(__dirname, 'launcher.rs');
const launcherPath = path.join(__dirname, '..', 'launcher.rs');
let launcherData = await fs.readFile(launcherPath, 'utf8');
const entrypointPath = downloadedFiles[entrypoint].fsPath;

View File

@@ -2,7 +2,9 @@ const path = require('path');
const { spawn } = require('child_process');
const getPort = require('get-port');
const { timeout } = require('promise-timeout');
const { existsSync, readFileSync } = require('fs');
const {
existsSync, readFileSync, statSync, readdirSync,
} = require('fs');
const {
glob,
download,
@@ -11,11 +13,28 @@ const {
runShellScript,
} = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
function validateDistDir(distDir) {
function validateDistDir(distDir, isDev) {
const hash = isDev
? '#local-development'
: '#configuring-the-build-output-directory';
const docsUrl = `https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build${hash}`;
const distDirName = path.basename(distDir);
if (!existsSync(distDir)) {
const message = `Build was unable to create the distDir: ${distDirName}.`
+ '\nMake sure you mentioned the correct dist directory: https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build/#local-development';
const message = `Build was unable to create the distDir: "${distDirName}".`
+ `\nMake sure you configure the the correct distDir: ${docsUrl}`;
throw new Error(message);
}
const stat = statSync(distDir);
if (!stat.isDirectory()) {
const message = `Build failed because distDir is not a directory: "${distDirName}".`
+ `\nMake sure you configure the the correct distDir: ${docsUrl}`;
throw new Error(message);
}
const contents = readdirSync(distDir);
if (contents.length === 0) {
const message = `Build failed because distDir is empty: "${distDirName}".`
+ `\nMake sure you configure the the correct distDir: ${docsUrl}`;
throw new Error(message);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@now/static-build",
"version": "0.5.8",
"version": "0.5.9",
"license": "MIT",
"repository": {
"type": "git",

View File

@@ -0,0 +1,9 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@now/static-build"
}
]
}

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"now-build": "mkdir dist"
}
}

View File

@@ -0,0 +1,9 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@now/static-build"
}
]
}

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"example": "echo 'nothing here'"
}
}

View File

@@ -18,10 +18,15 @@ beforeAll(async () => {
});
const fixturesPath = path.resolve(__dirname, 'fixtures');
const testsThatFailToBuild = new Set([
'04-wrong-dist-dir',
'05-empty-dist-dir',
'06-missing-script',
]);
// eslint-disable-next-line no-restricted-syntax
for (const fixture of fs.readdirSync(fixturesPath)) {
if (fixture === '04-wrong-dist-dir') {
if (testsThatFailToBuild.has(fixture)) {
// eslint-disable-next-line no-loop-func
it(`should not build ${fixture}`, async () => {
try {

View File

@@ -12,7 +12,7 @@ it(
'Should build the airtable folder',
async () => {
const { buildResult } = await runBuildForFolder('airtable');
expect(buildResult['index.js']).toBeDefined();
expect(buildResult.output['index.js']).toBeDefined();
},
TWO_MINUTES,
);
@@ -21,7 +21,7 @@ it(
'Should build the aws-sdk folder',
async () => {
const { buildResult } = await runBuildForFolder('aws-sdk');
expect(buildResult['index.js']).toBeDefined();
expect(buildResult.output['index.js']).toBeDefined();
},
TWO_MINUTES,
);
@@ -30,7 +30,7 @@ it(
'Should build the axios folder',
async () => {
const { buildResult } = await runBuildForFolder('axios');
expect(buildResult['index.js']).toBeDefined();
expect(buildResult.output['index.js']).toBeDefined();
},
TWO_MINUTES,
);
@@ -39,7 +39,7 @@ it(
'Should build the mongoose folder',
async () => {
const { buildResult } = await runBuildForFolder('mongoose');
expect(buildResult['index.js']).toBeDefined();
expect(buildResult.output['index.js']).toBeDefined();
},
TWO_MINUTES,
);

View File

@@ -82,13 +82,13 @@ async function testDeployment (
for (const probe of nowJson.probes || []) {
console.log('testing', JSON.stringify(probe));
const probeUrl = `https://${deploymentUrl}${probe.path}`;
const { text, resp } = await fetchDeploymentUrl(probeUrl, {
method: probe.method,
body: probe.body ? JSON.stringify(probe.body) : undefined,
headers: {
'content-type': 'application/json',
},
});
const fetchOpts = { method: probe.method, headers: { ...probe.headers } };
if (probe.body) {
fetchOpts.headers['content-type'] = 'application/json';
fetchOpts.body = JSON.stringify(probe.body);
}
const { text, resp } = await fetchDeploymentUrl(probeUrl, fetchOpts);
if (probe.mustContain) {
if (!text.includes(probe.mustContain)) {
await fs.writeFile(path.join(__dirname, 'failed-page.txt'), text);

148
yarn.lock
View File

@@ -940,6 +940,16 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/content-type@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.3.tgz#3688bd77fc12f935548eef102a4e34c512b03a07"
integrity sha512-pv8VcFrZ3fN93L4rTNIbbUzdkzjEyVMp5mPVjsFfOYTDOZMZiZ8P1dhu+kEv3faYyKzZgLlSvnyQNFg+p/v5ug==
"@types/cookie@0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
"@types/cross-spawn@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.0.tgz#320aaf1d1a12979f1b84fe7a5590a7e860bf3a80"
@@ -1079,7 +1089,7 @@
dependencies:
resolve-from "*"
"@types/semver@^6.0.0":
"@types/semver@6.0.0", "@types/semver@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.0.0.tgz#86ba89f02a414e39c68d02b351872e4ed31bd773"
integrity sha512-OO0srjOGH99a4LUN2its3+r6CBYcplhJ466yLqs+zvAWgphCpS8hYZEZ797tRDP/QKcqTdb/YCN6ifASoAWkrQ==
@@ -1094,13 +1104,20 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
"@types/tar@4.0.0", "@types/tar@^4.0.0":
"@types/tar@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.0.tgz#e3239d969eeb693a012200613860d0eb871c94f0"
integrity sha512-YybbEHNngcHlIWVCYsoj7Oo1JU9JqONuAlt1LlTH/lmL8BMhbzdFUgReY87a05rY1j8mfK47Del+TCkaLAXwLw==
dependencies:
"@types/node" "*"
"@types/test-listen@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/test-listen/-/test-listen-1.1.0.tgz#db7e5b0e277b4a3ee7b90770dae1a41b186458af"
integrity sha512-y6ZfbSzYHniCeY6ZAzsQjSAdJInNVoEz4Uhsb81W+RCoNYA59yoG/+XbqPqCPj2KCU3Wa6RFWSozutkGIHIsNQ==
dependencies:
"@types/node" "*"
"@types/uglify-js@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
@@ -1124,22 +1141,6 @@
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==
"@types/yauzl-promise@2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/yauzl-promise/-/yauzl-promise-2.1.0.tgz#a813ca317cd897f286fdfefb9b202463d315313a"
integrity sha512-7PkQ5UtElDsanzjdUQzXnstCqxx6KAOTMURuHwOuqC6YO2WaYQ6ItLnLy3TiEVNQMO/pD+QSOcfnAkeSX4hsTA==
dependencies:
"@types/events" "*"
"@types/node" "*"
"@types/yauzl" "*"
"@types/yauzl@*":
version "2.9.1"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"
integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==
dependencies:
"@types/node" "*"
"@types/yazl@^2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/yazl/-/yazl-2.4.1.tgz#0441a6ee151bf8be9307a2318b89df50f174ea00"
@@ -1158,7 +1159,14 @@
globby "8.0.0"
signal-exit "3.0.2"
"@zeit/ncc@0.18.5":
"@zeit/ncc-watcher@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@zeit/ncc-watcher/-/ncc-watcher-1.0.3.tgz#c4569d13db7b80a96a7672047c069400e69913ae"
integrity sha512-367wm9bGMBCwT8kHQckw8UY/iYhC9rdzj357FBoM0SjVvWv53yziEmd12RkeF1EsBs0F2a1uBYdUaUvGo6LfkA==
dependencies:
"@zeit/ncc" "^0.18.5"
"@zeit/ncc@0.18.5", "@zeit/ncc@^0.18.5":
version "0.18.5"
resolved "https://registry.yarnpkg.com/@zeit/ncc/-/ncc-0.18.5.tgz#5687df6c32f1a2e2486aa110b3454ccebda4fb9c"
integrity sha512-F+SbvEAh8rchiRXqQbmD1UmbePY7dCOKTbvfFtbVbK2xMH/tyri5YKfNxXKK7eL9EWkkbqB3NTVQO6nokApeBA==
@@ -1282,11 +1290,6 @@ any-observable@^0.3.0:
resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"
integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==
any-promise@^1.1.0, any-promise@~1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
anymatch@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
@@ -2424,6 +2427,11 @@ contains-path@^0.1.0:
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
content-type@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
conventional-changelog-angular@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.2.tgz#39d945635e03b6d0c9d4078b1df74e06163dc66a"
@@ -2514,6 +2522,11 @@ convert-source-map@^1.1.0, convert-source-map@^1.1.1, convert-source-map@^1.4.0,
dependencies:
safe-buffer "~5.1.1"
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
copy-concurrently@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
@@ -3055,25 +3068,11 @@ end-of-stream@1.4.1, end-of-stream@^1.0.0, end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
end-of-stream@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.1.0.tgz#e9353258baa9108965efc41cb0ef8ade2f3cfb07"
integrity sha1-6TUyWLqpEIll78QcsO+K3i88+wc=
dependencies:
once "~1.3.0"
err-code@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=
errno@^0.1.3:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
dependencies:
prr "~1.0.1"
error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@@ -3288,11 +3287,6 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
events-intercept@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/events-intercept/-/events-intercept-2.0.0.tgz#adbf38681c5a4b2011c41ee41f61a34cba448897"
integrity sha1-rb84aBxaSyARxB7kH2GjTLpEiJc=
exec-series@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/exec-series/-/exec-series-1.0.3.tgz#6d257a9beac482a872c7783bc8615839fc77143a"
@@ -6876,14 +6870,6 @@ memoizeasync@1.0.0:
lru-cache "2.5.0"
passerror "1.1.1"
memory-fs@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
dependencies:
errno "^0.1.3"
readable-stream "^2.0.1"
meow@^3.1.0, meow@^3.3.0, meow@^3.5.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
@@ -7488,13 +7474,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
once@~1.3.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20"
integrity sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=
dependencies:
wrappy "1"
onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
@@ -8035,11 +8014,6 @@ promise-timeout@1.3.0:
resolved "https://registry.yarnpkg.com/promise-timeout/-/promise-timeout-1.3.0.tgz#d1c78dd50a607d5f0a5207410252a3a0914e1014"
integrity sha512-5yANTE0tmi5++POym6OgtFmwfDvOXABD9oj/jLQr5GPEyuNEb7jH4wbbANJceJid49jwhi1RddxnhnEAb/doqg==
promisepipe@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/promisepipe/-/promisepipe-3.0.0.tgz#c9b6e5aa861ef5fcce6134f6f75e14f8f30bd3b2"
integrity sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==
prompts@^0.1.9:
version "0.1.14"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2"
@@ -8082,11 +8056,6 @@ protoduck@^5.0.1:
dependencies:
genfun "^5.0.0"
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -8763,6 +8732,11 @@ semver-truncate@^1.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
semver@6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b"
integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==
semver@^4.0.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
@@ -9132,22 +9106,6 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
stream-to-array@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353"
integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M=
dependencies:
any-promise "^1.1.0"
stream-to-promise@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/stream-to-promise/-/stream-to-promise-2.2.0.tgz#b1edb2e1c8cb11289d1b503c08d3f2aef51e650f"
integrity sha1-se2y4cjLESidG1A8CNPyrvUeZQ8=
dependencies:
any-promise "~1.3.0"
end-of-stream "~1.1.0"
stream-to-array "~2.3.0"
string-argv@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736"
@@ -9457,6 +9415,11 @@ test-exclude@^5.0.0:
read-pkg-up "^4.0.0"
require-main-filename "^1.0.1"
test-listen@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/test-listen/-/test-listen-1.1.0.tgz#2ba614d96c3bc9157469003027b42a495dd83b6a"
integrity sha512-OyEVi981C1sb9NX1xayfgZls3p8QTDRwp06EcgxSgd1kktaENBW8dO15i8v/7Fi15j0IYQctJzk5J+hyEBId2w==
text-extensions@^1.0.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
@@ -10312,22 +10275,7 @@ yargs@^12.0.1, yargs@^12.0.2:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
yauzl-clone@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/yauzl-clone/-/yauzl-clone-1.0.4.tgz#8bc6d293b17cc98802bbbed2e289d18e7697c96c"
integrity sha1-i8bSk7F8yYgCu77S4onRjnaXyWw=
dependencies:
events-intercept "^2.0.0"
yauzl-promise@2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/yauzl-promise/-/yauzl-promise-2.1.3.tgz#17467845db89fc6592ca987ca2ecfee8c381ae3d"
integrity sha1-F0Z4RduJ/GWSyph8ouz+6MOBrj0=
dependencies:
yauzl "^2.9.1"
yauzl-clone "^1.0.4"
yauzl@2.10.0, yauzl@^2.2.1, yauzl@^2.9.1:
yauzl@2.10.0, yauzl@^2.2.1:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=