mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
14 Commits
@now/pytho
...
@now/pytho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
489ec1dfa5 | ||
|
|
d3f92d7143 | ||
|
|
3072b044ef | ||
|
|
3b4968657f | ||
|
|
cafae4c800 | ||
|
|
64952d24f1 | ||
|
|
72758b6e0d | ||
|
|
4f80bc74d5 | ||
|
|
a6e62ed61c | ||
|
|
d8eecd6172 | ||
|
|
0e70608511 | ||
|
|
da0de150df | ||
|
|
a58c35fb9e | ||
|
|
fe88a69ab7 |
@@ -13,3 +13,4 @@
|
||||
/packages/now-go/*
|
||||
/packages/now-rust/dist/*
|
||||
/packages/now-ruby/dist/*
|
||||
/packages/now-static-build/dist/*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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)`,
|
||||
);
|
||||
|
||||
|
||||
@@ -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 .",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/go",
|
||||
"version": "0.5.2",
|
||||
"version": "0.5.3",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/node-bridge",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"license": "MIT",
|
||||
"main": "./index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/python",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.9",
|
||||
"main": "./dist/index.js",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
|
||||
12
packages/now-python/test/fixtures/11-asgi/Pipfile
vendored
Normal file
12
packages/now-python/test/fixtures/11-asgi/Pipfile
vendored
Normal 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
207
packages/now-python/test/fixtures/11-asgi/Pipfile.lock
generated
vendored
Normal 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": {}
|
||||
}
|
||||
8
packages/now-python/test/fixtures/11-asgi/index.py
vendored
Normal file
8
packages/now-python/test/fixtures/11-asgi/index.py
vendored
Normal 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")
|
||||
11
packages/now-python/test/fixtures/11-asgi/now.json
vendored
Normal file
11
packages/now-python/test/fixtures/11-asgi/now.json
vendored
Normal 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
1
packages/now-static-build/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -1 +0,0 @@
|
||||
/test
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
);
|
||||
}
|
||||
2
packages/now-static-build/test/fixtures/07-nonzero-sh/build.sh
vendored
Executable file
2
packages/now-static-build/test/fixtures/07-nonzero-sh/build.sh
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
echo 'non-zero exit code should fail the build RANDOMNESS_PLACEHOLDER'
|
||||
exit 1
|
||||
4
packages/now-static-build/test/fixtures/07-nonzero-sh/now.json
vendored
Normal file
4
packages/now-static-build/test/fixtures/07-nonzero-sh/now.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "build.sh", "use": "@now/static-build" }]
|
||||
}
|
||||
11
packages/now-static-build/test/fixtures/11-cowsay-build/now.json
vendored
Normal file
11
packages/now-static-build/test/fixtures/11-cowsay-build/now.json
vendored
Normal 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" }
|
||||
]
|
||||
}
|
||||
8
packages/now-static-build/test/fixtures/11-cowsay-build/package.json
vendored
Normal file
8
packages/now-static-build/test/fixtures/11-cowsay-build/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"cowsay": "^1.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "mkdir dist && cowsay cow:RANDOMNESS_PLACEHOLDER > dist/index.txt"
|
||||
}
|
||||
}
|
||||
8
packages/now-static-build/test/fixtures/11-cowsay-build/subdirectory/package.json
vendored
Normal file
8
packages/now-static-build/test/fixtures/11-cowsay-build/subdirectory/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"yodasay": "^1.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "mkdir dist && yodasay yoda:RANDOMNESS_PLACEHOLDER > dist/index.txt"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
18
packages/now-static-build/tsconfig.json
Normal file
18
packages/now-static-build/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user