Compare commits

...

14 Commits

Author SHA1 Message Date
Andy Bitz
489ec1dfa5 Publish
- @now/go@0.5.3
 - @now/next@0.5.1
 - @now/node-bridge@1.2.2
 - @now/node-server@0.8.1
 - @now/node@0.10.1
 - @now/python@0.2.9
 - @now/static-build@0.6.1
2019-06-28 18:32:41 +02:00
Steven
d3f92d7143 [now-static-build] Add missing random to test (#674) 2019-06-28 18:28:38 +02:00
Andy
3072b044ef [now-static-build] Use build and dev command if there is no… (#673)
* [now-static-build] Use `build` and `dev` command if there is no `now-` version

* Fix default dev command

* Get correct command

* Added type

* Add getCommands and replace now-dev occurrences

* Linting

* Added test for build

* Adjusted test

* Adjusted message

* Adjusted tests
2019-06-28 18:28:30 +02:00
Steven
3b4968657f [now-static-build] Improve msg when missing script (#672)
* [now-static-build] Improve msg when missing script

* Add 07-nonzero-sh test fixture

* Use uppercase

* Print bash script name
2019-06-28 18:28:24 +02:00
Steven
cafae4c800 [now-static-build] Use typescript (#671)
* [now-static-build] Use typescript

* Add tsconfig and add missing types

* Move to /src folder

* Fix type error

* Remove accidental commit of tsconfig.json
2019-06-28 18:28:17 +02:00
Luc
64952d24f1 [now-node] Fix res.send() and res.json() helpers (#669)
* fix res.send/res.json discrepancies with express

* add tests and refactor

* throw error on res.json(undefined)

* re-add streams

* do not console.warn

* move hello to fixtures-helpers folder

* be more explicit about accepted types

* setDefaultCT -> setContentType

* improve error messages

* make `fixtures-helpers/hello` appear in the code

* refactor setContentType

* set correct `content-length` header

* use PassThrough stream to remove fixture

* remove `.only` in test
2019-06-28 18:28:10 +02:00
Luc
72758b6e0d [tests] Improve git diff in jest config and refactor (#663)
* improve git diff in jest config

* refactor jest.config.js

* try with empty testMatch

* Revert "try with empty testMatch"

This reverts commit ec69a03cc7953a8e6e2d7b2f3ba2bb08d2fcbfa5.

* Update jest.config.js

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

* trim branch name

* move trim up

* add empty files to test behaviour

* Remove empty files
2019-06-28 18:28:03 +02:00
JJ Kasper
4f80bc74d5 Update now-next for new dynamic syntax (#660) 2019-06-28 18:27:55 +02:00
Sophearak Tha
a6e62ed61c [now-go] Ignore vendor folder when looking for go.mod (#593)
* Ignore `vendor` folder when looking for `go.mod`

* add warning message

* Apply suggestions from code review

Co-Authored-By: Steven <steven@ceriously.com>
2019-06-28 18:27:46 +02:00
Luc
d8eecd6172 Revert "Only run github action on 'release' event (#662)" (#665)
This reverts commit 5ff2c37147eb203f5199ffe4d4aa9480e3ebaa85.
2019-06-28 18:27:37 +02:00
Luc
0e70608511 Only run github action on 'release' event (#662)
* only run github action on 'release' event

* only run master build on 'release' event
2019-06-28 18:27:25 +02:00
Luc
da0de150df add --exact to lerna version (#664) 2019-06-28 18:27:19 +02:00
Luc
a58c35fb9e [now-node-bridge] Use a callback on server.listen() method rather than server.on('listening') (#661)
* rely on the listen method rather than events

* add empty files to trigger tests suites

* improve types

* remove empty files
2019-06-28 18:27:11 +02:00
Nathan Cahill
fe88a69ab7 [now-python] Add Python ASGI support (#654)
* add python asgi support

* Update packages/now-python/test/fixtures/11-asgi/index.py

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

* Update packages/now-python/test/fixtures/11-asgi/now.json

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

* Update packages/now-python/test/fixtures/11-asgi/now.json

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

* add raw_path
2019-06-28 18:27:02 +02:00
35 changed files with 1276 additions and 449 deletions

View File

@@ -13,3 +13,4 @@
/packages/now-go/*
/packages/now-rust/dist/*
/packages/now-ruby/dist/*
/packages/now-static-build/dist/*

View File

@@ -6,7 +6,7 @@ Please read our [code of conduct](CODE_OF_CONDUCT.md) and follow it in all your
## 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.
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:
@@ -42,7 +42,7 @@ The pull request will be reviewed by the maintainers and the tests will be check
## 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

View File

@@ -24,8 +24,9 @@ yarn publish-canary
```
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`)
- Verify that you are _in-sync_ with canary (with the exception of the `version` line in `package.json`)
- Deploy the modified Builders
```

View File

@@ -1,33 +1,29 @@
const { execSync } = require('child_process');
const { relative } = require('path');
const branch = execSync('git branch | grep "*" | cut -d " " -f2').toString();
const branch = execSync('git branch | grep "*" | cut -d " " -f2')
.toString()
.trim();
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 gitPath = branch === 'master' ? 'HEAD~1' : 'origin/canary...HEAD';
const diff = execSync(`git diff ${gitPath} --name-only`).toString();
const changed = diff
.split('\n')
.filter(item => Boolean(item) && item.includes('packages/'))
.map(item => relative('packages', item).split('/')[0]);
const matches = [];
const matches = Array.from(new Set(changed));
if (changed.length > 0) {
console.log('The following packages have changed:');
changed.map((item) => {
matches.push(item);
console.log(item);
return null;
});
} else {
if (matches.length === 0) {
matches.push('now-node');
console.log(`No packages changed, defaulting to ${matches[0]}`);
} else {
console.log('The following packages have changed:');
console.log(matches.join('\n'));
}
const testMatch = Array.from(new Set(matches)).map(
const testMatch = matches.map(
item => `**/${item}/**/?(*.)+(spec|test).[jt]s?(x)`,
);

View File

@@ -12,8 +12,8 @@
"scripts": {
"lerna": "lerna",
"bootstrap": "lerna bootstrap",
"publish-stable": "git checkout master && git pull && lerna version",
"publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary",
"publish-stable": "git checkout master && git pull && lerna version --exact",
"publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --exact",
"publish-from-github": "./.circleci/publish.sh",
"build": "./.circleci/build.sh",
"lint": "eslint .",

View File

@@ -61,6 +61,18 @@ export async function build({
await initPrivateGit(process.env.GIT_CREDENTIALS);
}
if (process.env.GO111MODULE) {
console.log(`\nManually assigning 'GO111MODULE' is not recommended.
By default:
- 'GO111MODULE=on' If entrypoint package name is not 'main'
- 'GO111MODULE=off' If entrypoint package name is 'main'
We highly recommend you leverage Go Modules in your project.
Learn more: https://github.com/golang/go/wiki/Modules
`);
}
console.log('Downloading user files...');
const entrypointArr = entrypoint.split(sep);
@@ -135,7 +147,7 @@ Learn more: https://zeit.co/docs/v2/deployments/official-builders/go-now-go/#ent
isGoModExist = true;
isGoModInRootDir = true;
goModPath = fileDirname;
} else if (file.includes('go.mod')) {
} else if (file.endsWith('go.mod') && !file.endsWith('vendor')) {
if (entrypointDirname === fileDirname) {
isGoModExist = true;
goModPath = fileDirname;
@@ -161,6 +173,10 @@ Learn more: https://zeit.co/docs/v2/deployments/official-builders/go-now-go/#ent
`Found exported function "${handlerFunctionName}" in "${entrypoint}"`
);
if (!isGoModExist && 'vendor' in downloadedFiles) {
throw new Error('`go.mod` is required to use a `vendor` directory.');
}
// check if package name other than main
// using `go.mod` way building the handler
const packageName = parsedAnalyzed.packageName;

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@now/next",
"version": "0.5.0",
"version": "0.5.1",
"license": "MIT",
"main": "./dist/index",
"scripts": {
@@ -14,7 +14,7 @@
"directory": "packages/now-next"
},
"dependencies": {
"@now/node-bridge": "^1.2.1",
"@now/node-bridge": "1.2.2",
"fs-extra": "^7.0.0",
"get-port": "^5.0.0",
"resolve-from": "^5.0.0",

View File

@@ -40,6 +40,7 @@ import {
validateEntrypoint,
normalizePage,
getDynamicRoutes,
isDynamicRoute,
} from './utils';
interface BuildParamsMeta {
@@ -414,7 +415,7 @@ export const build = async ({
const pathname = page.replace(/\.html$/, '');
if (pathname.startsWith('$') || pathname.includes('/$')) {
if (isDynamicRoute(pathname)) {
dynamicPages.push(pathname);
}
@@ -461,7 +462,7 @@ export const build = async ({
const pathname = page.replace(/\.js$/, '');
if (pathname.startsWith('$') || pathname.includes('/$')) {
if (isDynamicRoute(pathname)) {
dynamicPages.push(normalizePage(pathname));
}

View File

@@ -9,6 +9,14 @@ export interface EnvConfig {
[name: string]: string | undefined;
}
// Identify /[param]/ in route string
const TEST_DYNAMIC_ROUTE = /\/\[[^\/]+?\](?=\/|$)/;
function isDynamicRoute(route: string): boolean {
route = route.startsWith('/') ? route : `/${route}`;
return TEST_DYNAMIC_ROUTE.test(route);
}
/**
* Validate if the entrypoint is allowed to be used
*/
@@ -226,7 +234,7 @@ function getRoutes(
continue;
}
if (pageName.startsWith('$') || pageName.includes('/$')) {
if (isDynamicRoute(pageName)) {
dynamicPages.push(normalizePage(pageName));
}
@@ -351,4 +359,5 @@ export {
stringMap,
syncEnvVars,
normalizePage,
isDynamicRoute,
};

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node-bridge",
"version": "1.2.1",
"version": "1.2.2",
"license": "MIT",
"main": "./index.js",
"repository": {

View File

@@ -27,6 +27,16 @@ export interface NowProxyResponse {
encoding: string;
}
interface ServerLike {
listen: (
opts: {
host?: string;
port?: number;
},
callback: (this: Server | null) => void
) => Server | void;
}
/**
* If the `http.Server` handler function throws an error asynchronously,
* then it ends up being an unhandled rejection which doesn't kill the node
@@ -92,14 +102,14 @@ function normalizeEvent(
}
export class Bridge {
private server: Server | null;
private server: ServerLike | 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, shouldStoreEvents: boolean = false) {
constructor(server?: ServerLike, shouldStoreEvents: boolean = false) {
this.server = null;
this.shouldStoreEvents = shouldStoreEvents;
if (server) {
@@ -116,18 +126,8 @@ export class Bridge {
});
}
setServer(server: Server) {
setServer(server: ServerLike) {
this.server = server;
server.once('listening', () => {
const addr = server.address();
if (typeof addr === 'string') {
throw new Error(`Unexpected string for \`server.address()\`: ${addr}`);
} else if (!addr) {
throw new Error('`server.address()` returned `null`');
} else {
this.resolveListening(addr);
}
});
}
listen() {
@@ -135,10 +135,35 @@ export class Bridge {
throw new Error('Server has not been set!');
}
return this.server.listen({
host: '127.0.0.1',
port: 0,
});
const resolveListening = this.resolveListening;
return this.server.listen(
{
host: '127.0.0.1',
port: 0,
},
function listeningCallback() {
if (!this || typeof this.address !== 'function') {
throw new Error(
'Missing server.address() function on `this` in server.listen()'
);
}
const addr = this.address();
if (!addr) {
throw new Error('`server.address()` returned `null`');
}
if (typeof addr === 'string') {
throw new Error(
`Unexpected string for \`server.address()\`: ${addr}`
);
}
resolveListening(addr);
}
);
}
async launcher(

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node",
"version": "0.10.0",
"version": "0.10.1",
"license": "MIT",
"main": "./dist/index",
"repository": {
@@ -9,7 +9,7 @@
"directory": "packages/now-node"
},
"dependencies": {
"@now/node-bridge": "^1.2.1",
"@now/node-bridge": "1.2.2",
"@types/node": "*",
"@zeit/ncc": "0.18.5",
"@zeit/ncc-watcher": "1.0.3",

View File

@@ -73,56 +73,88 @@ function getCookieParser(req: NowRequest) {
};
}
function sendStatusCode(res: NowResponse, statusCode: number): NowResponse {
function status(res: NowResponse, statusCode: number): NowResponse {
res.statusCode = statusCode;
return res;
}
function sendData(res: NowResponse, body: any): NowResponse {
if (body === null) {
function setContentHeaders(
res: NowResponse,
type: string,
length?: number
): void {
if (!res.getHeader('content-type')) {
res.setHeader('content-type', type);
}
if (length !== undefined) {
res.setHeader('content-length', length);
}
}
function send(res: NowResponse, body: any) {
const t = typeof body;
if (body === null || t === 'undefined') {
res.end();
return res;
}
const contentType = res.getHeader('Content-Type');
if (t === 'string') {
setContentHeaders(
res,
'text/plain; charset=utf-8',
Buffer.byteLength(body)
);
res.end(body);
return res;
}
if (Buffer.isBuffer(body)) {
if (!contentType) {
res.setHeader('Content-Type', 'application/octet-stream');
}
res.setHeader('Content-Length', body.length);
setContentHeaders(res, 'application/octet-stream', body.length);
res.end(body);
return res;
}
if (body instanceof Stream) {
if (!contentType) {
res.setHeader('Content-Type', 'application/octet-stream');
}
setContentHeaders(res, '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');
switch (t) {
case 'boolean':
case 'number':
case 'bigint':
case 'object':
return json(res, body);
}
res.setHeader('Content-Length', Buffer.byteLength(str));
res.end(str);
return res;
throw new Error(
'`body` is not a valid string, object, boolean, number, Stream, or Buffer'
);
}
function sendJson(res: NowResponse, jsonBody: any): NowResponse {
// Set header to application/json
res.setHeader('Content-Type', 'application/json; charset=utf-8');
function json(res: NowResponse, jsonBody: any): NowResponse {
switch (typeof jsonBody) {
case 'object':
case 'boolean':
case 'number':
case 'bigint':
case 'string':
const body = JSON.stringify(jsonBody);
setContentHeaders(
res,
'application/json; charset=utf-8',
Buffer.byteLength(body)
);
res.end(body);
return res;
}
// Use send to handle request
return res.send(jsonBody);
throw new Error(
'`jsonBody` is not a valid object, boolean, string, number, or null'
);
}
export class ApiError extends Error {
@@ -186,9 +218,9 @@ export function createServerWithHelpers(
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);
res.status = statusCode => status(res, statusCode);
res.send = body => send(res, body);
res.json = jsonBody => json(res, jsonBody);
await listener(req, res);
} catch (err) {

View File

@@ -12,6 +12,6 @@ export type NowRequest = IncomingMessage & {
export type NowResponse = ServerResponse & {
send: (body: any) => NowResponse;
json: (body: any) => NowResponse;
json: (jsonBody: any) => NowResponse;
status: (statusCode: number) => NowResponse;
};

View File

@@ -1,28 +1,17 @@
/* global beforeAll, beforeEach, afterAll, expect, it, jest */
/* global beforeEach, afterEach, 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 mockListener = jest.fn();
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
@@ -37,281 +26,572 @@ async function fetchWithProxyReq(_url, opts = {}) {
});
}
beforeAll(async () => {
beforeEach(async () => {
mockListener.mockClear();
consumeEventMock.mockClear();
mockListener.mockImplementation((req, res) => {
res.send('hello');
});
consumeEventMock.mockImplementation(() => ({}));
server = createServerWithHelpers(mockListener, mockBridge);
url = await listen(server);
});
beforeEach(() => {
mockListener.mockClear();
consumeEventMock.mockClear();
});
afterAll(async () => {
afterEach(async () => {
await server.close();
});
it('should call consumeEvent with the correct reqId', async () => {
await fetchWithProxyReq(`${url}/`);
describe('contract with @now/node-bridge', () => {
test('should call consumeEvent with the correct reqId', async () => {
await fetchWithProxyReq(`${url}/`);
expect(consumeEventMock).toHaveBeenLastCalledWith('2');
});
expect(consumeEventMock).toHaveBeenLastCalledWith('2');
});
it('should not expose the request id header', async () => {
await fetchWithProxyReq(`${url}/`, { headers: { 'x-test-header': 'ok' } });
test('should not expose the request id header', async () => {
await fetchWithProxyReq(`${url}/`, { headers: { 'x-test-header': 'ok' } });
const [{ headers }] = mockListener.mock.calls[0];
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',
expect(headers['x-now-bridge-request-id']).toBeUndefined();
expect(headers['x-test-header']).toBe('ok');
});
});
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);
});
describe('all helpers', () => {
const nowHelpers = [
['query', 0],
['cookies', 0],
['body', 0],
['status', 1],
['send', 1],
['json', 1],
];
it('req.cookies should reflect req.cookie header', async () => {
await fetchWithProxyReq(url, {
headers: {
cookie: 'who=bill; where=us',
},
});
test('should not recalculate req properties twice', async () => {
const spy = jest.fn(() => {});
expect(mockListener.mock.calls[0][0].cookies).toMatchObject({
who: 'bill',
where: 'us',
});
});
const nowReqHelpers = nowHelpers.filter(([, i]) => i === 0);
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 {
mockListener.mockImplementation((req, res) => {
spy(...nowReqHelpers.map(h => req[h]));
spy(...nowReqHelpers.map(h => req[h]));
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(spy.mock.calls[0][i]).toBe(spy.mock.calls[1][i]);
}
});
await fetchWithProxyReq(url, {
method: 'POST',
body: '{"wrong":"json"',
headers: { 'content-type': 'application/json' },
test('should be able to overwrite request properties', async () => {
const spy = jest.fn(() => {});
mockListener.mockImplementation((...args) => {
nowHelpers.forEach(([prop, n]) => {
/* eslint-disable */
args[n][prop] = 'ok';
args[n][prop] = 'ok2';
spy(args[n][prop]);
});
args[1].end();
});
await fetchWithProxyReq(url);
nowHelpers.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
});
expect(bodySpy).toHaveBeenCalled();
test('should be able to reconfig request properties', async () => {
const spy = jest.fn(() => {});
const [error] = bodySpy.mock.calls[0];
expect(error.message).toMatch(/invalid json/i);
expect(error.statusCode).toBe(400);
mockListener.mockImplementation((...args) => {
nowHelpers.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);
nowHelpers.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
});
});
it('res.send() should send text', async () => {
mockListener.mockImplementation((req, res) => {
res.send('hello world');
describe('req.query', () => {
test('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',
});
});
const res = await fetchWithProxyReq(url);
expect(await res.text()).toBe('hello world');
test('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('res.json() should send json', async () => {
mockListener.mockImplementation((req, res) => {
res.json({ who: 'bill' });
describe('req.cookies', () => {
test('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',
});
});
});
describe('req.body', () => {
test('req.body should be undefined by default', async () => {
await fetchWithProxyReq(url);
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
});
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();
test('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);
});
const res = await fetchWithProxyReq(url);
test('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(res.status).toBe(404);
});
it('res.status().send() should work', async () => {
mockListener.mockImplementation((req, res) => {
res.status(404).send('notfound');
expect(mockListener.mock.calls[0][0].body).toBe('hello');
});
const res = await fetchWithProxyReq(url);
test('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' },
});
expect(res.status).toBe(404);
expect(await res.text()).toBe('notfound');
});
const [{ body }] = mockListener.mock.calls[0];
it('res.status().json() should work', async () => {
mockListener.mockImplementation((req, res) => {
res.status(404).json({ error: 'not found' });
const str = body.toString();
expect(Buffer.isBuffer(body)).toBe(true);
expect(str).toBe('hello');
});
const res = await fetchWithProxyReq(url);
test('req.body should be an object when content-type is `application/x-www-form-urlencoded`', async () => {
const obj = { who: 'mike' };
expect(res.status).toBe(404);
expect(await res.json()).toMatchObject({ error: 'not found' });
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);
});
test('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);
});
test('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);
});
test('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);
});
});
describe('res.send()', () => {
test('res.send() should set body to ""', async () => {
mockListener.mockImplementation((req, res) => {
res.send();
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(await res.text()).toBe('');
});
test('res.send(null) should set body to ""', async () => {
mockListener.mockImplementation((req, res) => {
res.send(null);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(await res.text()).toBe('');
});
test('res.send(undefined) should set body to ""', async () => {
mockListener.mockImplementation((req, res) => {
res.send(undefined);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(await res.text()).toBe('');
});
test('res.send(String) should send as text/plain', async () => {
mockListener.mockImplementation((req, res) => {
res.send('hey');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('text/plain; charset=utf-8');
expect(await res.text()).toBe('hey');
});
test('res.send(String) should not override Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('Content-Type', 'text/html');
res.send('hey');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('text/html');
expect(await res.text()).toBe('hey');
});
test('res.send(String) should set Content-Length', async () => {
mockListener.mockImplementation((req, res) => {
res.send('½ + ¼ = ¾');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(Number(res.headers.get('content-length'))).toBe(12);
expect(await res.text()).toBe('½ + ¼ = ¾');
});
test('res.send(Buffer) should send as octet-stream', async () => {
mockListener.mockImplementation((req, res) => {
res.send(Buffer.from('hello'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('application/octet-stream');
expect(await res.text()).toBe('hello');
});
test('res.send(Buffer) should not override Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('Content-Type', 'text/plain');
res.send(Buffer.from('hello'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('text/plain');
expect(await res.text()).toBe('hello');
});
test('res.send(Buffer) should set Content-Length', async () => {
mockListener.mockImplementation((req, res) => {
res.send(Buffer.from('½ + ¼ = ¾'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(Number(res.headers.get('content-length'))).toBe(12);
expect(await res.text()).toBe('½ + ¼ = ¾');
});
test('res.send(Object) should send as application/json', async () => {
mockListener.mockImplementation((req, res) => {
res.send({ name: 'tobi' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('{"name":"tobi"}');
});
test('res.send(Stream) should send as application/octet-stream', async () => {
const { PassThrough } = require('stream');
mockListener.mockImplementation((req, res) => {
const stream = new PassThrough();
res.send(stream);
stream.push('hello');
stream.end();
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('application/octet-stream');
expect(await res.text()).toBe('hello');
});
test('res.send() should send be chainable', async () => {
const spy = jest.fn();
mockListener.mockImplementation((req, res) => {
spy(res, res.send('hello'));
});
await fetchWithProxyReq(url);
const [a, b] = spy.mock.calls[0];
expect(a).toBe(b);
});
});
describe('res.json()', () => {
test('res.json() should not override previous Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('Content-Type', 'application/vnd.example+json');
res.json({ hello: 'world' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/vnd.example+json'
);
expect(await res.text()).toBe('{"hello":"world"}');
});
test('res.json() should send as application/json', async () => {
mockListener.mockImplementation((req, res) => {
res.json({ hello: 'world' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('{"hello":"world"}');
});
test('res.json() should set Content-Length and Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.json({ hello: '½ + ¼ = ¾' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(Number(res.headers.get('content-length'))).toBe(24);
expect(await res.text()).toBe('{"hello":"½ + ¼ = ¾"}');
});
test('res.json(null) should respond with json for null', async () => {
mockListener.mockImplementation((req, res) => {
res.json(null);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('null');
});
test('res.json() should throw an error', async () => {
let _err;
mockListener.mockImplementation((req, res) => {
try {
res.json();
} catch (err) {
_err = err;
} finally {
res.end();
}
});
await fetchWithProxyReq(url);
expect(_err).toBeDefined();
expect(_err.message).toMatch(/not a valid object/);
});
test('res.json(undefined) should throw an error', async () => {
let _err;
mockListener.mockImplementation((req, res) => {
try {
res.json(undefined);
} catch (err) {
_err = err;
} finally {
res.end();
}
});
await fetchWithProxyReq(url);
expect(_err).toBeDefined();
expect(_err.message).toMatch(/not a valid object/);
});
test('res.json(Number) should respond with json for number', async () => {
mockListener.mockImplementation((req, res) => {
res.json(300);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('300');
});
test('res.json(String) should respond with json for string', async () => {
mockListener.mockImplementation((req, res) => {
res.json('str');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('"str"');
});
test('res.json(Array) should respond with json for array', async () => {
mockListener.mockImplementation((req, res) => {
res.json(['foo', 'bar', 'baz']);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('["foo","bar","baz"]');
});
test('res.json(Object) should respond with json for object', async () => {
mockListener.mockImplementation((req, res) => {
res.json({ name: 'tobi' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('{"name":"tobi"}');
});
test('res.json() should send be chainable', async () => {
const spy = jest.fn();
mockListener.mockImplementation((req, res) => {
spy(res, res.json({ hello: 'world' }));
});
await fetchWithProxyReq(url);
const [a, b] = spy.mock.calls[0];
expect(a).toBe(b);
});
});
describe('res.status()', () => {
test('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);
});
test('res.status() should be chainable', async () => {
const spy = jest.fn();
mockListener.mockImplementation((req, res) => {
spy(res, res.status(404));
res.end();
});
await fetchWithProxyReq(url);
const [a, b] = spy.mock.calls[0];
expect(a).toBe(b);
});
});

View File

@@ -2,11 +2,11 @@ from http.server import BaseHTTPRequestHandler
import base64
import json
import inspect
import __NOW_HANDLER_FILENAME
__now_variables = dir(__NOW_HANDLER_FILENAME)
if 'handler' in __now_variables or 'Handler' in __now_variables:
base = __NOW_HANDLER_FILENAME.handler if ('handler' in __now_variables) else __NOW_HANDLER_FILENAME.Handler
if not issubclass(base, BaseHTTPRequestHandler):
@@ -47,76 +47,202 @@ if 'handler' in __now_variables or 'Handler' in __now_variables:
'body': res.text,
}
elif 'app' in __now_variables:
print('using Web Server Gateway Interface (WSGI)')
import sys
from urllib.parse import urlparse, unquote
from werkzeug._compat import BytesIO
from werkzeug._compat import string_types
from werkzeug._compat import to_bytes
from werkzeug._compat import wsgi_encoding_dance
from werkzeug.datastructures import Headers
from werkzeug.wrappers import Response
def now_handler(event, context):
payload = json.loads(event['body'])
if not inspect.iscoroutinefunction(__NOW_HANDLER_FILENAME.app.__call__):
print('using Web Server Gateway Interface (WSGI)')
import sys
from urllib.parse import urlparse, unquote
from werkzeug._compat import BytesIO
from werkzeug._compat import string_types
from werkzeug._compat import to_bytes
from werkzeug._compat import wsgi_encoding_dance
from werkzeug.datastructures import Headers
from werkzeug.wrappers import Response
def now_handler(event, context):
payload = json.loads(event['body'])
headers = Headers(payload.get('headers', {}))
headers = Headers(payload.get('headers', {}))
body = payload.get('body', '')
if body != '':
body = payload.get('body', '')
if body != '':
if payload.get('encoding') == 'base64':
body = base64.b64decode(body)
if isinstance(body, string_types):
body = to_bytes(body, charset='utf-8')
url = urlparse(unquote(payload['path']))
query = url.query
path = url.path
environ = {
'CONTENT_LENGTH': str(len(body)),
'CONTENT_TYPE': headers.get('content-type', ''),
'PATH_INFO': path,
'QUERY_STRING': query,
'REMOTE_ADDR': headers.get(
'x-forwarded-for', headers.get(
'x-real-ip', payload.get(
'true-client-ip', ''))),
'REQUEST_METHOD': payload['method'],
'SERVER_NAME': headers.get('host', 'lambda'),
'SERVER_PORT': headers.get('x-forwarded-port', '80'),
'SERVER_PROTOCOL': 'HTTP/1.1',
'event': event,
'context': context,
'wsgi.errors': sys.stderr,
'wsgi.input': BytesIO(body),
'wsgi.multiprocess': False,
'wsgi.multithread': False,
'wsgi.run_once': False,
'wsgi.url_scheme': headers.get('x-forwarded-proto', 'http'),
'wsgi.version': (1, 0),
}
for key, value in environ.items():
if isinstance(value, string_types) and key != 'QUERY_STRING':
environ[key] = wsgi_encoding_dance(value)
for key, value in headers.items():
key = 'HTTP_' + key.upper().replace('-', '_')
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
environ[key] = value
response = Response.from_app(__NOW_HANDLER_FILENAME.app, environ)
return_dict = {
'statusCode': response.status_code,
'headers': dict(response.headers)
}
if response.data:
return_dict['body'] = base64.b64encode(response.data).decode('utf-8')
return_dict['encoding'] = 'base64'
return return_dict
else:
print('using Asynchronous Server Gateway Interface (ASGI)')
import asyncio
import enum
from urllib.parse import urlparse, unquote, urlencode
class ASGICycleState(enum.Enum):
REQUEST = enum.auto()
RESPONSE = enum.auto()
class ASGICycle:
def __init__(self, scope):
self.scope = scope
self.body = b''
self.state = ASGICycleState.REQUEST
self.app_queue = None
self.response = {}
def __call__(self, app, body):
"""
Receives the application and any body included in the request, then builds the
ASGI instance using the connection scope.
Runs until the response is completely read from the application.
"""
loop = asyncio.new_event_loop()
self.app_queue = asyncio.Queue(loop=loop)
self.put_message({'type': 'http.request', 'body': body, 'more_body': False})
asgi_instance = app(self.scope, self.receive, self.send)
asgi_task = loop.create_task(asgi_instance)
loop.run_until_complete(asgi_task)
return self.response
def put_message(self, message):
self.app_queue.put_nowait(message)
async def receive(self):
"""
Awaited by the application to receive messages in the queue.
"""
message = await self.app_queue.get()
return message
async def send(self, message):
"""
Awaited by the application to send messages to the current cycle instance.
"""
message_type = message['type']
if self.state is ASGICycleState.REQUEST:
if message_type != 'http.response.start':
raise RuntimeError(
f"Expected 'http.response.start', received: {message_type}"
)
status_code = message['status']
headers = {k: v for k, v in message.get('headers', [])}
self.on_request(headers, status_code)
self.state = ASGICycleState.RESPONSE
elif self.state is ASGICycleState.RESPONSE:
if message_type != 'http.response.body':
raise RuntimeError(
f"Expected 'http.response.body', received: {message_type}"
)
body = message.get('body', b'')
more_body = message.get('more_body', False)
# The body must be completely read before returning the response.
self.body += body
if not more_body:
self.on_response()
self.put_message({'type': 'http.disconnect'})
def on_request(self, headers, status_code):
self.response['statusCode'] = status_code
self.response['headers'] = {k.decode(): v.decode() for k, v in headers.items()}
def on_response(self):
if self.body:
self.response['body'] = base64.b64encode(self.body).decode('utf-8')
self.response['encoding'] = 'base64'
def now_handler(event, context):
payload = json.loads(event['body'])
headers = payload.get('headers', {})
body = payload.get('body', b'')
if payload.get('encoding') == 'base64':
body = base64.b64decode(body)
if isinstance(body, string_types):
body = to_bytes(body, charset='utf-8')
elif not isinstance(body, bytes):
body = body.encode()
url = urlparse(unquote(payload['path']))
query = url.query
path = url.path
url = urlparse(unquote(payload['path']))
query = url.query.encode()
path = url.path
environ = {
'CONTENT_LENGTH': str(len(body)),
'CONTENT_TYPE': headers.get('content-type', ''),
'PATH_INFO': path,
'QUERY_STRING': query,
'REMOTE_ADDR': headers.get(
'x-forwarded-for', headers.get(
'x-real-ip', payload.get(
'true-client-ip', ''))),
'REQUEST_METHOD': payload['method'],
'SERVER_NAME': headers.get('host', 'lambda'),
'SERVER_PORT': headers.get('x-forwarded-port', '80'),
'SERVER_PROTOCOL': 'HTTP/1.1',
'event': event,
'context': context,
'wsgi.errors': sys.stderr,
'wsgi.input': BytesIO(body),
'wsgi.multiprocess': False,
'wsgi.multithread': False,
'wsgi.run_once': False,
'wsgi.url_scheme': headers.get('x-forwarded-proto', 'http'),
'wsgi.version': (1, 0),
}
scope = {
'server': (headers.get('host', 'lambda'), headers.get('x-forwarded-port', 80)),
'client': (headers.get(
'x-forwarded-for', headers.get(
'x-real-ip', payload.get(
'true-client-ip', ''))), 0),
'scheme': headers.get('x-forwarded-proto', 'http'),
'root_path': '',
'query_string': query,
'headers': [[k.lower().encode(), v.encode()] for k, v in headers.items()],
'type': 'http',
'http_version': '1.1',
'method': payload['method'],
'path': path,
'raw_path': path.encode(),
}
for key, value in environ.items():
if isinstance(value, string_types) and key != 'QUERY_STRING':
environ[key] = wsgi_encoding_dance(value)
asgi_cycle = ASGICycle(scope)
response = asgi_cycle(__NOW_HANDLER_FILENAME.app, body)
return response
for key, value in headers.items():
key = 'HTTP_' + key.upper().replace('-', '_')
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
environ[key] = value
response = Response.from_app(__NOW_HANDLER_FILENAME.app, environ)
return_dict = {
'statusCode': response.status_code,
'headers': dict(response.headers)
}
if response.data:
return_dict['body'] = base64.b64encode(response.data).decode('utf-8')
return_dict['encoding'] = 'base64'
return return_dict
else:
print('Missing variable `handler` or `app` in file __NOW_HANDLER_FILENAME.py')
print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python')

View File

@@ -1,6 +1,6 @@
{
"name": "@now/python",
"version": "0.2.8",
"version": "0.2.9",
"main": "./dist/index.js",
"license": "MIT",
"files": [

View File

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

207
packages/now-python/test/fixtures/11-asgi/Pipfile.lock generated vendored Normal file
View File

@@ -0,0 +1,207 @@
{
"_meta": {
"hash": {
"sha256": "93dcd591e5690d3a71cb02979cbe317e83e3c03ec020867bf1554a480ef5cd8a"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
],
"version": "==0.4.0"
},
"certifi": {
"hashes": [
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
],
"version": "==2019.6.16"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"h11": {
"hashes": [
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
],
"version": "==0.8.1"
},
"h2": {
"hashes": [
"sha256:c8f387e0e4878904d4978cd688a3195f6b169d49b1ffa572a3d347d7adc5e09f",
"sha256:fd07e865a3272ac6ef195d8904de92dc7b38dc28297ec39cfa22716b6d62e6eb"
],
"version": "==3.1.0"
},
"hpack": {
"hashes": [
"sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
],
"version": "==3.0.0"
},
"httpcore": {
"hashes": [
"sha256:96f910b528d47b683242ec207050c7bbaa99cd1b9a07f78ea80cf61e55556b58"
],
"version": "==0.3.0"
},
"httptools": {
"hashes": [
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
],
"version": "==0.0.13"
},
"hyperframe": {
"hashes": [
"sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
"sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
],
"version": "==5.2.0"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"multidict": {
"hashes": [
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
],
"version": "==4.5.2"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"version": "==2.22.0"
},
"requests-async": {
"hashes": [
"sha256:8731420451383196ecf2fd96082bfc8ae5103ada90aba185888499d7784dde6f"
],
"version": "==0.5.0"
},
"rfc3986": {
"hashes": [
"sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405",
"sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18"
],
"version": "==1.3.2"
},
"sanic": {
"hashes": [
"sha256:cc64978266025afb0e7c0f8be928e2b81670c5d58ddac290d04c9d0da6ec2112",
"sha256:ebd806298782400db811ea9d63e8096e835e67f0b5dc5e66e507532984a82bb3"
],
"index": "pypi",
"version": "==19.6.0"
},
"ujson": {
"hashes": [
"sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"
],
"markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
"version": "==1.35"
},
"urllib3": {
"hashes": [
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
],
"version": "==1.25.3"
},
"uvloop": {
"hashes": [
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
],
"markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
"version": "==0.12.2"
},
"websockets": {
"hashes": [
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
],
"version": "==6.0"
}
},
"develop": {}
}

View File

@@ -0,0 +1,8 @@
from sanic import Sanic
from sanic import response
app = Sanic()
@app.route("/")
async def index(request):
return response.text("asgi:RANDOMNESS_PLACEHOLDER")

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [
{
"src": "index.py",
"use": "@now/python",
"config": { "maxLambdaSize": "10mb" }
}
],
"probes": [{ "path": "/", "mustContain": "asgi:RANDOMNESS_PLACEHOLDER" }]
}

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

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

View File

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

View File

@@ -1,18 +1,28 @@
{
"name": "@now/static-build",
"version": "0.6.0",
"version": "0.6.1",
"license": "MIT",
"main": "./dist/index",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-static-build"
},
"scripts": {
"test": "jest"
"build": "tsc",
"test": "tsc && jest",
"prepublishOnly": "tsc"
},
"dependencies": {
"cross-spawn": "6.0.5",
"get-port": "5.0.0",
"promise-timeout": "1.3.0"
},
"devDependencies": {
"@types/promise-timeout": "1.3.0",
"typescript": "3.5.2"
}
}

View File

@@ -1,11 +1,9 @@
const path = require('path');
const spawn = require('cross-spawn');
const getPort = require('get-port');
const { timeout } = require('promise-timeout');
const {
existsSync, readFileSync, statSync, readdirSync,
} = require('fs');
const {
import path from 'path';
import spawn from 'cross-spawn';
import getPort from 'get-port';
import { timeout } from 'promise-timeout';
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
import {
glob,
download,
runNpmInstall,
@@ -13,42 +11,72 @@ const {
runShellScript,
getNodeVersion,
getSpawnOptions,
} = require('@now/build-utils'); // eslint-disable-line import/no-extraneous-dependencies
Files,
BuildOptions,
} from '@now/build-utils';
function validateDistDir(distDir, isDev) {
interface PackageJson {
scripts?: {
[key: string]: string;
};
}
function validateDistDir(distDir: string, isDev: boolean | undefined) {
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 configure the the correct distDir: ${docsUrl}`;
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}`;
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}`;
const message =
`Build failed because distDir is empty: "${distDirName}".` +
`\nMake sure you configure the the correct distDir: ${docsUrl}`;
throw new Error(message);
}
}
exports.version = 2;
function getCommand(pkg: PackageJson, cmd: string) {
const scripts = (pkg && pkg.scripts) || {};
const nowCmd = `now-${cmd}`;
if (scripts[nowCmd]) {
return nowCmd;
}
if (scripts[cmd]) {
return cmd;
}
return nowCmd;
}
export const version = 2;
const nowDevScriptPorts = new Map();
exports.build = async ({
files, entrypoint, workPath, config, meta = {},
}) => {
console.log('downloading user files...');
export async function build({
files,
entrypoint,
workPath,
config,
meta = {},
}: BuildOptions) {
console.log('Downloading user files...');
await download(files, workPath, meta);
const mountpoint = path.dirname(entrypoint);
@@ -58,25 +86,38 @@ exports.build = async ({
const distPath = path.join(
workPath,
path.dirname(entrypoint),
(config && config.distDir) || 'dist',
(config && (config.distDir as string)) || 'dist'
);
const entrypointName = path.basename(entrypoint);
if (entrypointName.endsWith('.sh')) {
console.log(`Running build script "${entrypoint}"`);
await runShellScript(path.join(workPath, entrypoint));
validateDistDir(distPath, meta.isDev);
return glob('**', distPath, mountpoint);
}
if (entrypointName === 'package.json') {
await runNpmInstall(entrypointFsDirname, ['--prefer-offline'], spawnOpts);
const pkgPath = path.join(workPath, entrypoint);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
let output = {};
const routes = [];
let output: Files = {};
const routes: { src: string; dest: string }[] = [];
const devScript = getCommand(pkg, 'dev');
if (meta.isDev && pkg.scripts && pkg.scripts['now-dev']) {
if (meta.isDev && pkg.scripts && pkg.scripts[devScript]) {
let devPort = nowDevScriptPorts.get(entrypoint);
if (typeof devPort === 'number') {
console.log('`now-dev` server already running for %j', entrypoint);
console.log(
'`%s` server already running for %j',
devScript,
entrypoint
);
} else {
// Run the `now-dev` script out-of-bounds, since it is assumed that
// Run the `now-dev` or `dev` script out-of-bounds, since it is assumed that
// it will launch a dev server that never "completes"
devPort = await getPort();
nowDevScriptPorts.set(entrypoint, devPort);
@@ -84,7 +125,7 @@ exports.build = async ({
cwd: entrypointFsDirname,
env: { ...process.env, PORT: String(devPort) },
};
const child = spawn('npm', ['run', 'now-dev'], opts);
const child = spawn('npm', ['run', devScript], opts);
child.on('exit', () => nowDevScriptPorts.delete(entrypoint));
child.stdout.setEncoding('utf8');
child.stdout.pipe(process.stdout);
@@ -96,8 +137,8 @@ exports.build = async ({
// for this builder.
try {
await timeout(
new Promise((resolve) => {
const checkForPort = (data) => {
new Promise(resolve => {
const checkForPort = (data: string) => {
// Check the logs for the URL being printed with the port number
// (i.e. `http://localhost:47521`).
if (data.indexOf(`:${devPort}`) !== -1) {
@@ -107,11 +148,11 @@ exports.build = async ({
child.stdout.on('data', checkForPort);
child.stderr.on('data', checkForPort);
}),
5 * 60 * 1000,
5 * 60 * 1000
);
} catch (err) {
throw new Error(
`Failed to detect a server running on port ${devPort}.\nDetails: https://err.sh/zeit/now-builders/now-static-build-failed-to-detect-a-server`,
`Failed to detect a server running on port ${devPort}.\nDetails: https://err.sh/zeit/now-builders/now-static-build-failed-to-detect-a-server`
);
}
@@ -128,37 +169,31 @@ exports.build = async ({
});
} else {
if (meta.isDev) {
console.log('WARN: "now-dev" script is missing from package.json');
console.log('WARN: "${devScript}" script is missing from package.json');
console.log(
'See the local development docs: https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build/#local-development',
'See the local development docs: https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build/#local-development'
);
}
// Run the `now-build` script and wait for completion to collect the build
// outputs
console.log('running user "now-build" script from `package.json`...');
if (
!(await runPackageJsonScript(
entrypointFsDirname,
'now-build',
spawnOpts,
))
) {
const buildScript = getCommand(pkg, 'build');
console.log(`Running "${buildScript}" script in "${entrypoint}"`);
const found = await runPackageJsonScript(
entrypointFsDirname,
buildScript,
spawnOpts
);
if (!found) {
throw new Error(
`An error running "now-build" script in "${entrypoint}"`,
`Missing required "${buildScript}" script in "${entrypoint}"`
);
}
validateDistDir(distPath);
validateDistDir(distPath, meta.isDev);
output = await glob('**', distPath, mountpoint);
}
const watch = [path.join(mountpoint.replace(/^\.\/?/, ''), '**/*')];
return { routes, watch, output };
}
if (path.extname(entrypoint) === '.sh') {
await runShellScript(path.join(workPath, entrypoint));
validateDistDir(distPath);
return glob('**', distPath, mountpoint);
}
throw new Error('Proper build script must be specified as entrypoint');
};
throw new Error(
`Build "src" is "${entrypoint}" but expected "package.json" or "build.sh"`
);
}

View File

@@ -0,0 +1,2 @@
echo 'non-zero exit code should fail the build RANDOMNESS_PLACEHOLDER'
exit 1

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"cowsay": "^1.3.1"
},
"scripts": {
"build": "mkdir dist && cowsay cow:RANDOMNESS_PLACEHOLDER > dist/index.txt"
}
}

View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"yodasay": "^1.1.6"
},
"scripts": {
"build": "mkdir dist && yodasay yoda:RANDOMNESS_PLACEHOLDER > dist/index.txt"
}
}

View File

@@ -22,6 +22,7 @@ const testsThatFailToBuild = new Set([
'04-wrong-dist-dir',
'05-empty-dist-dir',
'06-missing-script',
'07-nonzero-sh',
]);
// eslint-disable-next-line no-restricted-syntax

View File

@@ -0,0 +1,18 @@
{
"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

@@ -1061,6 +1061,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14"
integrity sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==
"@types/promise-timeout@1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@types/promise-timeout/-/promise-timeout-1.3.0.tgz#90649ff6f48c1ead9de142e6dd9f62f8c5a54022"
integrity sha512-AtVKSZUtpBoZ4SshXJk5JcTXJllinHKKx615lsRNJUsbbFlI0AI8drlnoiQ+PNvjkeoF9Y8fJUh6UO2khsIBZw==
"@types/prop-types@*":
version "15.7.0"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.0.tgz#4c48fed958d6dcf9487195a0ef6456d5f6e0163a"