Compare commits

..

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

* Add test to throw when invalid semver

* Apply suggestions from leo

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

* Fix ts error

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

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

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

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

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

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

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

* Wording improvement

As per @styfle suggestion

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

* Reword reference to code of conduct

As per @styfle's suggestion.

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

* fix local developement section title

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

* Improves project description wording

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

* Proper setup instructions

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

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

* updates description of tests

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

* Improve explanation of integration tests

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

* Update ncc bug qualification section

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

* fixes typo

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

* clarifies when and why to run ncc manually

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

* clarifies how to access to test deployements and associated logs

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

* fix test

* fix tests (bis)

* be explicit about @ts-ignore

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

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

* fix test

* fix 14-go-mod-sub test fixtures

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

* changes from feedback

* remove mm

* increase timeout

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

* address changes

* fix linting errors

* support arrays in includeFiles

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

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

* Rename to enginesMatch

* Add test for node10

* Minor refactor

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

* Revert @now/node, new PR later
2019-06-17 14:22:22 -04:00
176 changed files with 4307 additions and 575 deletions

View File

@@ -12,3 +12,5 @@
/packages/now-optipng/dist/*
/packages/now-go/*
/packages/now-rust/dist/*
/packages/now-ruby/dist/*
/packages/now-static-build/dist/*

85
CONTRIBUTING.md Normal file
View File

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

View File

@@ -23,7 +23,11 @@ For the Canary Channel, publish the modified Builders to npm with the following:
yarn publish-canary
```
For the Stable Channel, you must cherry pick each commit from canary to master and then deploy the modified Builders:
For the Stable Channel, you must do the following:
- Cherry pick each commit from canary to master
- Verify that you are _in-sync_ with canary (with the exception of the `version` line in `package.json`)
- Deploy the modified Builders
```
git checkout master
@@ -33,6 +37,7 @@ git cherry-pick <PR502_COMMIT_SHA>
git cherry-pick <PR503_COMMIT_SHA>
git cherry-pick <PR504_COMMIT_SHA>
# ... etc ...
git diff origin/canary
yarn publish-stable
```
@@ -41,3 +46,7 @@ After running this publish step, GitHub Actions will take care of publishing the
If for some reason GitHub Actions fails to publish the npm package, you may do so
manually by running `npm publish` from the package directory. Make sure to
use `npm publish --tag canary` if you are publishing a canary release!
### Contributing
See the [Contribution guidelines for this project](CONTRIBUTING.md), it also contains guidance on interpreting tests failures.

View File

@@ -1,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

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

View File

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

View File

@@ -4,6 +4,8 @@ import path from 'path';
import spawn from 'cross-spawn';
import { SpawnOptions } from 'child_process';
import { deprecate } from 'util';
import { Meta, PackageJson, NodeVersion } from '../types';
import { getSupportedNodeVersion } from './node-version';
function spawnAsync(
command: string,
@@ -52,11 +54,32 @@ export async function runShellScript(fsPath: string) {
return true;
}
async function scanParentDirs(destPath: string, scriptName?: string) {
export function getSpawnOptions(
meta: Meta,
nodeVersion: NodeVersion
): SpawnOptions {
const opts = {
env: { ...process.env },
};
if (!meta.isDev) {
opts.env.PATH = `/node${nodeVersion.major}/bin:${opts.env.PATH}`;
}
return opts;
}
export async function getNodeVersion(destPath: string): Promise<NodeVersion> {
const { packageJson } = await scanParentDirs(destPath, true);
const range = packageJson && packageJson.engines && packageJson.engines.node;
return getSupportedNodeVersion(range);
}
async function scanParentDirs(destPath: string, readPackageJson = false) {
assert(path.isAbsolute(destPath));
let hasScript = false;
let hasPackageLockJson = false;
let packageJson: PackageJson | undefined;
let currentDestPath = destPath;
// eslint-disable-next-line no-constant-condition
@@ -65,13 +88,8 @@ async function scanParentDirs(destPath: string, scriptName?: string) {
// eslint-disable-next-line no-await-in-loop
if (await fs.pathExists(packageJsonPath)) {
// eslint-disable-next-line no-await-in-loop
if (scriptName) {
const packageJson = JSON.parse(
await fs.readFile(packageJsonPath, 'utf8')
);
hasScript = Boolean(
packageJson.scripts && scriptName && packageJson.scripts[scriptName]
);
if (readPackageJson) {
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
}
// eslint-disable-next-line no-await-in-loop
hasPackageLockJson = await fs.pathExists(
@@ -85,13 +103,13 @@ async function scanParentDirs(destPath: string, scriptName?: string) {
currentDestPath = newDestPath;
}
return { hasScript, hasPackageLockJson };
return { hasPackageLockJson, packageJson };
}
export async function runNpmInstall(
destPath: string,
args: string[] = [],
cmd?: string
spawnOpts?: SpawnOptions
) {
assert(path.isAbsolute(destPath));
@@ -99,23 +117,19 @@ export async function runNpmInstall(
console.log(`installing to ${destPath}`);
const { hasPackageLockJson } = await scanParentDirs(destPath);
const opts: SpawnOptions = {
env: {
...process.env,
},
};
const opts = spawnOpts || { env: process.env };
if (hasPackageLockJson) {
commandArgs = args.filter(a => a !== '--prefer-offline');
await spawnAsync(
cmd || 'npm',
'npm',
commandArgs.concat(['install', '--unsafe-perm']),
destPath,
opts
);
} else {
await spawnAsync(
cmd || 'yarn',
'yarn',
commandArgs.concat(['--ignore-engines', '--cwd', destPath]),
destPath,
opts
@@ -129,9 +143,15 @@ export async function runPackageJsonScript(
opts?: SpawnOptions
) {
assert(path.isAbsolute(destPath));
const { hasScript, hasPackageLockJson } = await scanParentDirs(
const { packageJson, hasPackageLockJson } = await scanParentDirs(
destPath,
scriptName
true
);
const hasScript = Boolean(
packageJson &&
packageJson.scripts &&
scriptName &&
packageJson.scripts[scriptName]
);
if (!hasScript) return false;
@@ -152,7 +172,7 @@ export async function runPackageJsonScript(
}
/**
* installDependencies() is deprecated.
* @deprecate installDependencies() is deprecated.
* Please use runNpmInstall() instead.
*/
export const installDependencies = deprecate(

View File

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

View File

@@ -162,3 +162,28 @@ export interface ShouldServeOptions {
*/
config: Config;
}
export interface PackageJson {
name: string;
version: string;
engines?: {
[key: string]: string;
node: string;
npm: string;
};
scripts?: {
[key: string]: string;
};
dependencies?: {
[key: string]: string;
};
devDependencies?: {
[key: string]: string;
};
}
export interface NodeVersion {
major: number;
range: string;
runtime: string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,8 @@ import {
PrepareCacheOptions,
runNpmInstall,
runPackageJsonScript,
getNodeVersion,
getSpawnOptions,
} from '@now/build-utils';
import nextLegacyVersions from './legacy-versions';
@@ -38,6 +40,7 @@ import {
validateEntrypoint,
normalizePage,
getDynamicRoutes,
isDynamicRoute,
} from './utils';
interface BuildParamsMeta {
@@ -171,6 +174,9 @@ export const build = async ({
console.log(`${name} Downloading user files...`);
await download(files, workPath, meta);
const nodeVersion = await getNodeVersion(entryPath);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
const pkg = await readPackageJson(entryPath);
const nextVersion = getNextVersion(pkg);
@@ -190,7 +196,7 @@ export const build = async ({
// If this is the initial build, we want to start the server
if (!urls[entrypoint]) {
console.log(`${name} Installing dependencies...`);
await runNpmInstall(entryPath, ['--prefer-offline']);
await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts);
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'development';
@@ -277,20 +283,22 @@ export const build = async ({
}
console.log('installing dependencies...');
await runNpmInstall(entryPath, ['--prefer-offline']);
await runNpmInstall(entryPath, ['--prefer-offline'], spawnOpts);
console.log('running user script...');
const memoryToConsume = Math.floor(os.totalmem() / 1024 ** 2) - 128;
await runPackageJsonScript(entryPath, 'now-build', {
env: {
...process.env,
NODE_OPTIONS: `--max_old_space_size=${memoryToConsume}`,
},
} as SpawnOptions);
const buildSpawnOptions = { ...spawnOpts };
const env = { ...buildSpawnOptions.env } as any;
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
await runPackageJsonScript(entryPath, 'now-build', buildSpawnOptions);
if (isLegacy) {
console.log('running npm install --production...');
await runNpmInstall(entryPath, ['--prefer-offline', '--production']);
await runNpmInstall(
entryPath,
['--prefer-offline', '--production'],
spawnOpts
);
}
if (process.env.NPM_AUTH_TOKEN) {
@@ -407,7 +415,7 @@ export const build = async ({
const pathname = page.replace(/\.html$/, '');
if (pathname.startsWith('$') || pathname.includes('/$')) {
if (isDynamicRoute(pathname)) {
dynamicPages.push(pathname);
}
@@ -454,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.0",
"version": "1.2.2",
"license": "MIT",
"main": "./index.js",
"repository": {
@@ -21,6 +21,6 @@
"@types/aws-lambda": "8.10.19",
"@types/node": "11.9.4",
"jest": "24.1.0",
"typescript": "3.3.3"
"typescript": "3.5.2"
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node-server",
"version": "0.7.5",
"version": "0.8.1",
"license": "MIT",
"repository": {
"type": "git",
@@ -8,7 +8,7 @@
"directory": "packages/now-node-server"
},
"dependencies": {
"@now/node-bridge": "^1.2.0",
"@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.9.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.0",
"@now/node-bridge": "1.2.2",
"@types/node": "*",
"@zeit/ncc": "0.18.5",
"@zeit/ncc-watcher": "1.0.3",
@@ -31,6 +31,6 @@
"cookie": "0.4.0",
"node-fetch": "2.6.0",
"test-listen": "1.1.0",
"typescript": "3.3.3"
"typescript": "3.5.2"
}
}

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,8 @@ import {
createLambda,
runNpmInstall,
runPackageJsonScript,
getNodeVersion,
getSpawnOptions,
PrepareCacheOptions,
BuildOptions,
shouldServe,
@@ -58,10 +60,12 @@ async function downloadInstallAndBundle({
console.log("installing dependencies for user's code...");
const entrypointFsDirname = join(workPath, dirname(entrypoint));
await runNpmInstall(entrypointFsDirname, ['--prefer-offline']);
const nodeVersion = await getNodeVersion(entrypointFsDirname);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
await runNpmInstall(entrypointFsDirname, ['--prefer-offline'], spawnOpts);
const entrypointPath = downloadedFiles[entrypoint].fsPath;
return { entrypointPath, entrypointFsDirname };
return { entrypointPath, entrypointFsDirname, nodeVersion, spawnOpts };
}
async function compile(
@@ -178,6 +182,8 @@ export async function build({
const {
entrypointPath,
entrypointFsDirname,
nodeVersion,
spawnOpts,
} = await downloadInstallAndBundle({
files,
entrypoint,
@@ -186,7 +192,7 @@ export async function build({
});
console.log('running user script...');
await runPackageJsonScript(entrypointFsDirname, 'now-build');
await runPackageJsonScript(entrypointFsDirname, 'now-build', spawnOpts);
console.log('compiling entrypoint with ncc...');
const { preparedFiles, watch } = await compile(
@@ -233,7 +239,7 @@ export async function build({
...launcherFiles,
},
handler: 'launcher.launcher',
runtime: 'nodejs8.10',
runtime: nodeVersion.runtime,
});
const output = { [entrypoint]: lambda };

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

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

View File

@@ -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.7",
"version": "0.2.9",
"main": "./dist/index.js",
"license": "MIT",
"files": [
@@ -23,6 +23,6 @@
},
"devDependencies": {
"@types/execa": "^0.9.0",
"typescript": "3.3.4000"
"typescript": "3.5.2"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@
"routes": [{ "src": "/another", "dest": "custom.py" }],
"probes": [
{ "path": "/?hello=/", "mustContain": "path: query: {'hello': '/'}" },
{ "path": "/another?hello=/", "mustContain": "path: another query: {'hello': '/'}" }
{
"path": "/another?hello=/",
"mustContain": "path: another query: {'hello': '/'}"
}
]
}

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-ruby/.gitignore vendored Normal file
View File

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

220
packages/now-ruby/index.ts Normal file
View File

@@ -0,0 +1,220 @@
import { join, dirname } from 'path';
import execa from 'execa';
import {
ensureDir,
move,
remove,
pathExists,
readFile,
writeFile,
} from 'fs-extra';
import {
download,
getWriteableDirectory,
glob,
createLambda,
BuildOptions,
} from '@now/build-utils';
import { installBundler } from './install-ruby';
const REQUIRED_VENDOR_DIR = 'vendor/bundle/ruby/2.5.0';
async function matchPaths(
configPatterns: string | string[] | undefined,
workPath: string
) {
const patterns =
typeof configPatterns === 'string' ? [configPatterns] : configPatterns;
if (!patterns) {
return [];
}
const patternPaths = await Promise.all(
patterns.map(async pattern => {
const files = await glob(pattern, workPath);
return Object.keys(files);
})
);
return patternPaths.reduce((a, b) => a.concat(b), []);
}
async function bundleInstall(
bundlePath: string,
bundleDir: string,
gemfilePath: string
) {
console.log(`running "bundle install --deployment"...`);
const bundleAppConfig = await getWriteableDirectory();
try {
await execa(
bundlePath,
[
'install',
'--deployment',
'--gemfile',
gemfilePath,
'--path',
bundleDir,
],
{
stdio: 'inherit',
env: {
BUNDLE_SILENCE_ROOT_WARNING: '1',
BUNDLE_APP_CONFIG: bundleAppConfig,
},
}
);
} catch (err) {
console.log(`failed to run "bundle install --deployment"...`);
throw err;
}
}
export const config = {
maxLambdaSize: '5mb',
};
export const build = async ({
workPath,
files,
entrypoint,
config,
}: BuildOptions) => {
console.log('downloading files...');
// eslint-disable-next-line no-param-reassign
files = await download(files, workPath);
const { gemHome, bundlerPath } = await installBundler();
process.env.GEM_HOME = gemHome;
const fsFiles = await glob('**', workPath);
const entryDirectory = dirname(entrypoint);
const fsEntryDirectory = dirname(fsFiles[entrypoint].fsPath);
// check for an existing vendor directory
console.log(
'checking for existing vendor directory at',
'"' + REQUIRED_VENDOR_DIR + '"'
);
const vendorDir = join(workPath, REQUIRED_VENDOR_DIR);
const bundleDir = join(workPath, 'vendor/bundle');
const relativeVendorDir = join(fsEntryDirectory, REQUIRED_VENDOR_DIR);
let hasRootVendorDir = await pathExists(vendorDir);
let hasRelativeVendorDir = await pathExists(relativeVendorDir);
let hasVendorDir = hasRootVendorDir || hasRelativeVendorDir;
if (hasRelativeVendorDir) {
if (hasRootVendorDir) {
console.log(
'found two vendor directories, choosing the vendor directory relative to entrypoint'
);
} else {
console.log('found vendor directory relative to entrypoint');
}
// vendor dir must be at the root for lambda to find it
await move(relativeVendorDir, vendorDir);
} else if (hasRootVendorDir) {
console.log('found vendor directory in project root');
}
await ensureDir(vendorDir);
// no vendor directory, check for Gemfile to install
if (!hasVendorDir) {
const gemFile = join(entryDirectory, 'Gemfile');
if (fsFiles[gemFile]) {
console.log(
'did not find a vendor directory but found a Gemfile, bundling gems...'
);
const gemfilePath = fsFiles[gemFile].fsPath;
// try installing. this won't work if native extesions are required.
// if that's the case, gems should be vendored locally before deploying.
try {
await bundleInstall(bundlerPath, bundleDir, gemfilePath);
} catch (err) {
console.log(
'unable to build gems from Gemfile. vendor the gems locally with "bundle install --deployment" and retry.'
);
throw err;
}
}
} else {
console.log('found vendor directory, skipping "bundle install"...');
}
// try to remove gem cache to slim bundle size
try {
await remove(join(vendorDir, 'cache'));
} catch (e) {}
const originalRbPath = join(__dirname, '..', 'now_init.rb');
const originalNowHandlerRbContents = await readFile(originalRbPath, 'utf8');
// will be used on `require_relative '$here'` or for loading rack config.ru file
// for example, `require_relative 'api/users'`
console.log('entrypoint is', entrypoint);
const userHandlerFilePath = entrypoint.replace(/\.rb$/, '');
const nowHandlerRbContents = originalNowHandlerRbContents.replace(
/__NOW_HANDLER_FILENAME/g,
userHandlerFilePath
);
// in order to allow the user to have `server.rb`, we need our `server.rb` to be called
// somethig else
const nowHandlerRbFilename = 'now__handler__ruby';
await writeFile(
join(workPath, `${nowHandlerRbFilename}.rb`),
nowHandlerRbContents
);
const outputFiles = await glob('**', workPath);
// static analysis is impossible with ruby.
// instead, provide `includeFiles` and `excludeFiles` config options to reduce bundle size.
if (config && (config.includeFiles || config.excludeFiles)) {
const includedPaths = await matchPaths(config.includeFiles, workPath);
const excludedPaths = await matchPaths(
<string | string[]>config.excludeFiles,
workPath
);
for (let i = 0; i < excludedPaths.length; i++) {
// whitelist includeFiles
if (includedPaths.includes(excludedPaths[i])) {
continue;
}
// whitelist handler
if (excludedPaths[i] === `${nowHandlerRbFilename}.rb`) {
continue;
}
// whitelist vendor directory
if (excludedPaths[i].startsWith(REQUIRED_VENDOR_DIR)) {
continue;
}
delete outputFiles[excludedPaths[i]];
}
}
const lambda = await createLambda({
files: outputFiles,
handler: `${nowHandlerRbFilename}.now__handler`,
runtime: 'ruby2.5',
environment: {},
});
return {
[entrypoint]: lambda,
};
};

View File

@@ -0,0 +1,66 @@
import { join } from 'path';
import execa from 'execa';
import { getWriteableDirectory } from '@now/build-utils';
const RUBY_VERSION = '2.5.3';
async function installRuby(version: string = RUBY_VERSION) {
const baseDir = await getWriteableDirectory();
const rubyDir = join(baseDir, 'ruby');
const rubyBuildDir = join(baseDir, 'ruby-build');
await execa(
'yum',
[
'install',
'-y',
'git',
'gcc',
'make',
'tar',
'bzip2',
'readline-devel',
'openssl-devel',
'ruby-devel',
'zlib-devel',
],
{ stdio: 'inherit' }
);
await execa(
'git',
['clone', 'git://github.com/rbenv/ruby-build.git', rubyBuildDir],
{ stdio: 'inherit' }
);
await execa(join(rubyBuildDir, 'bin', 'ruby-build'), [version, rubyDir], {
stdio: 'inherit',
});
return {
gemHome: rubyDir,
rubyPath: join(rubyDir, 'bin', 'ruby'),
gemPath: join(rubyDir, 'bin', 'gem'),
};
}
// downloads and installs `bundler` (respecting
// process.env.GEM_HOME), and returns
// the absolute path to it
export async function installBundler() {
console.log('installing ruby...');
const { gemHome, rubyPath, gemPath } = await installRuby();
console.log('installing bundler...');
await execa(gemPath, ['install', 'bundler', '--no-document'], {
stdio: 'inherit',
env: {
GEM_HOME: gemHome,
},
});
return {
gemHome,
rubyPath,
gemPath,
bundlerPath: join(gemHome, 'bin', 'bundler'),
};
}

87
packages/now-ruby/now_init.rb Executable file
View File

@@ -0,0 +1,87 @@
require 'tmpdir'
require 'webrick'
require 'net/http'
require 'base64'
require 'json'
$entrypoint = '__NOW_HANDLER_FILENAME'
ENV['RAILS_ENV'] ||= 'production'
ENV['RAILS_LOG_TO_STDOUT'] ||= '1'
def rack_handler(httpMethod, path, body, headers)
require 'rack'
app, _ = Rack::Builder.parse_file($entrypoint)
server = Rack::MockRequest.new app
env = headers.transform_keys { |k| k.split('-').join('_').prepend('HTTP_').upcase }
res = server.request(httpMethod, path, env.merge({ :input => body }))
{
:statusCode => res.status,
:headers => res.original_headers,
:body => res.body,
}
end
def webrick_handler(httpMethod, path, body, headers)
require_relative $entrypoint
if not Object.const_defined?('Handler')
return { :statusCode => 500, :body => 'Handler not defined in lambda' }
end
host = '0.0.0.0'
port = 3000
server = WEBrick::HTTPServer.new :BindAddress => host, :Port => port
if Handler.is_a?(Proc)
server.mount_proc '/', Handler
else
server.mount '/', Handler
end
th = Thread.new(server) do |server|
server.start
end
http = Net::HTTP.new(host, port)
res = http.send_request(httpMethod, path, body, headers)
Signal.list.keys.each do |sig|
begin
Signal.trap(sig, cleanup)
rescue
end
end
server.shutdown
Thread.kill(th)
{
:statusCode => res.code.to_i,
:headers => res.each_capitalized.to_h,
:body => res.body,
}
end
def now__handler(event:, context:)
payload = JSON.parse(event['body'])
path = payload['path']
headers = payload['headers']
httpMethod = payload['method']
encoding = payload['encoding']
body = payload['body']
if (not body.nil? and not body.empty?) and (not encoding.nil? and encoding == 'base64')
body = Base64.decode64(body)
end
if $entrypoint.end_with? '.ru'
return rack_handler(httpMethod, path, body, headers)
end
return webrick_handler(httpMethod, path, body, headers)
end

26
packages/now-ruby/package.json Executable file
View File

@@ -0,0 +1,26 @@
{
"name": "@now/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "0.1.0",
"main": "./dist/index",
"files": [
"dist",
"now_init.rb"
],
"repository": {
"type": "git",
"url": "https://github.com/zeit/now-builders.git",
"directory": "packages/now-ruby"
},
"license": "MIT",
"scripts": {
"build": "tsc",
"test": "tsc && jest",
"prepublishOnly": "tsc"
},
"dependencies": {
"execa": "^1.0.0",
"fs-extra": "^7.0.1",
"typescript": "3.5.2"
}
}

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "cowsay", "~> 0.3.0"

View File

@@ -0,0 +1,13 @@
GEM
remote: https://rubygems.org/
specs:
cowsay (0.3.0)
PLATFORMS
ruby
DEPENDENCIES
cowsay (~> 0.3.0)
BUNDLED WITH
2.0.1

View File

@@ -0,0 +1,10 @@
require 'webrick'
require 'cowsay'
class Handler < WEBrick::HTTPServlet::AbstractServlet
def do_GET req, res
res.status = 200
res['Content-Type'] = 'text/plain'
res.body = Cowsay.say('gem:RANDOMNESS_PLACEHOLDER', 'cow')
end
end

View File

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

View File

@@ -0,0 +1,10 @@
require 'webrick'
require 'cowsay'
class Handler < WEBrick::HTTPServlet::AbstractServlet
def do_GET req, res
res.status = 200
res['Content-Type'] = 'text/plain'
res.body = Cowsay.say('gem:RANDOMNESS_PLACEHOLDER', 'cow')
end
end

View File

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

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env ruby
#
# This file was generated by RubyGems.
#
# The application 'cowsay' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
version = ">= 0.a"
str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('cowsay', 'cowsay', version)
else
gem "cowsay", version
load Gem.bin_path("cowsay", "cowsay", version)
end

View File

@@ -0,0 +1,20 @@
*.gem
*.rbc
*.swp
.DS_Store
.bundle
.config
.vagrant
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp

View File

@@ -0,0 +1,4 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in zerobuf.gemspec
gemspec

View File

@@ -0,0 +1,23 @@
Copyright 2012 MoneyDesktop Inc.
Copyright Cowsay contributors https://github.com/moneydesktop/cowsay/contributors
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
# Cowsay
ASCII art avatars emote your messages
## Installation
Add this line to your application's Gemfile:
gem install cowsay
And then execute:
$ cowsay 'Hello world!'
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request

View File

@@ -0,0 +1 @@
require 'bundler/gem_tasks'

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env ruby
require 'cowsay'
require 'optparse'
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: cowsay [-h] [-f cowfile] [-l] [message]"
opts.on("-l", "List available cow files") do |cowfile|
options['list'] = true
end
opts.on("-f COWFILE", "Specify a cow file") do |cowfile|
options['cowfile'] = cowfile
end
end.parse!
if options['list']
puts "Cow files:"
puts Cowsay.character_classes.join(' ')
else
if ARGV.any?
message = ARGV.join(' ')
else
#retrieve any piped input, otherwise use the empty string.
message = STDIN.tty? ? '' : ARGF.read.chomp
end
puts Cowsay.say(message, options['cowfile'])
end

View File

@@ -0,0 +1,22 @@
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'cowsay/version'
Gem::Specification.new do |gem|
gem.name = 'cowsay'
gem.version = Cowsay::VERSION
gem.authors = ['JohnnyT']
gem.email = ['johnnyt@moneydesktop.com']
gem.description = %q{ASCII art avatars emote your messages}
gem.summary = gem.description
gem.homepage = 'https://github.com/moneydesktop/cowsay'
gem.files = `git ls-files`.split($/)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.require_paths = ['lib']
gem.add_development_dependency 'rake'
# gem.add_development_dependency 'rspec-pride'
end

View File

@@ -0,0 +1,28 @@
require 'cowsay/version'
require 'cowsay/character'
module ::Cowsay
module_function # all instance methods are available on the module (class) level
def random_character
random_class = Character.const_get(character_classes[rand(character_classes.length)])
random_class.new
end
def character_classes
@character_classes ||= Character.constants.map { |c| c.to_sym } - [:Base, :Template]
end
def say(message, character)
character ||= 'cow'
if character == 'random'
random_character.say(message)
else
if character_classes.include? character.capitalize.to_sym
Character.const_get(character.capitalize).say(message)
else
puts "No cow file found for #{character}. Use the -l flag to see a list of available cow files."
end
end
end
end

View File

@@ -0,0 +1,9 @@
module Cowsay
module Character
autoload :Base, 'cowsay/character/base'
end
end
Dir[File.expand_path('character/*.rb', File.dirname(__FILE__))].each do |character|
require character
end

View File

@@ -0,0 +1,73 @@
module Cowsay
module Character
class Base
MAX_LINE_LENGTH = 36 unless defined?(MAX_LINE_LENGTH)
def self.say(message)
new.say(message)
end
def initialize
@thoughts = '\\'
end
def say(message)
render_balloon(message) + render_character
end
def template
raise '#template should be subclassed'
end
private
def render_character
template
end
def render_balloon(message)
message_lines = format_message(message)
line_length = message_lines.max{ |a,b| a.length <=> b.length }.length
output_lines = []
output_lines << " #{'_' * (line_length + 2)} "
message_lines.each do |line|
# 'Here is your message: %s' % 'hello world'
# is the same as
# printf('Here is your message: %s', 'hello world')
output_lines << "| %-#{line_length}s |" % line
end
output_lines << " #{'-' * (line_length + 2)} "
output_lines << ''
output_lines.join("\n")
end
def format_message(message)
return [message] if message.length <= MAX_LINE_LENGTH
lines = []
words = message.split(/\s/).reject{ |word| word.length.zero? }
new_line = ''
words.each do |word|
new_line << "#{word} "
if new_line.length > MAX_LINE_LENGTH
lines << new_line.chomp
new_line = ''
end
end
lines << new_line.chomp unless new_line.length.zero?
lines
end
end
end
end

View File

@@ -0,0 +1,31 @@
module Cowsay
module Character
class Beavis < Base
def template
<<-TEMPLATE
#{@thoughts} __------~~-,
#{@thoughts} ,' ,
/ \\
/ :
| '
| |
| |
| _-- |
_| =-. .-. ||
o|/o/ _. |
/ ~ \\ |
(____\@) ___~ |
|_===~~~.` |
_______.--~ |
\\________ |
\\ |
__/-___-- -__
/ _ \\
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,17 @@
module Cowsay
module Character
class Bunny < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts} \\
\\ /\\
( )
.( o ).
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,30 @@
module Cowsay
module Character
class Cheese < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts}
_____ _________
/ \\_/ |
| ||
| ||
| ###\\ /### | |
| 0 \\/ 0 | |
/| | |
/ | < |\\ \\
| /| | | |
| | \\_______/ | | |
| | | / /
/|| /|||
----------------|
| | | |
*** ***
/___\\ /___\\
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,17 @@
module Cowsay
module Character
class Cow < Base
def template
<<-TEMPLATE
#{@thoughts} ^__^
#{@thoughts} (oo)\\_______
(__)\\ )\\/\\
||----w |
|| ||
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,31 @@
module Cowsay
module Character
class Daemon < Base
def template
<<-TEMPLATE
#{@thoughts} , ,
#{@thoughts} /( )`
#{@thoughts} \\ \\___ / |
/- _ `-/ '
(/\\/ \\ \\ /\\
/ / | ` \\
O O ) / |
`-^--'`< '
(_.) _ ) /
`.___/` /
`-----' /
<----. __ / __ \\
<----|====O)))==) \\) /====
<----' `--' `.__,' \\
| |
\\ /
______( (_ / \\______
,' ,-----' | \\
`--{__________) \\/
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,27 @@
module Cowsay
module Character
class Dragon < Base
def template
<<-TEMPLATE
#{@thoughts} / \\ //\\
#{@thoughts} |\\___/| / \\// \\\\
/0 0 \\__ / // | \\ \\
/ / \\/_/ // | \\ \\
\@_^_\@'/ \\/_ // | \\ \\
//_^_/ \\/_ // | \\ \\
( //) | \\/// | \\ \\
( / /) _|_ / ) // | \\ _\\
( // /) '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.
(( / / )) ,-{ _ `-.|.-~-. .~ `.
(( // / )) '/\\ / ~-. _ .-~ .-~^-. \\
(( /// )) `. { } / \\ \\
(( / )) .----~-.\\ \\-' .~ \\ `. \\^-.
///.----..> \\ _ -~ `. ^-` ^-_
///-._ _ _ _ _ _ _}^ - - - - ~ ~-- ,.-~
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,22 @@
module Cowsay
module Character
class Elephant < Base
def template
<<-TEMPLATE
#{@thoughts} /\\ ___ /\\
#{@thoughts} // \\/ \\/ \\\\
(( O O ))
\\\\ / \\ //
\\/ | | \\/
| | | |
| | | |
| o |
| | | |
|m| |m|
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,19 @@
module Cowsay
module Character
class Frogs < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts}
oO)-. .-(Oo
/__ _\\ /_ __\\
\\ \\( | ()~() | )/ /
\\__|\\ | (-___-) | /|__/
' '--' ==`-'== '--' '
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,33 @@
module Cowsay
module Character
class Ghostbusters < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts}
#{@thoughts} __---__
_- /--______
__--( / \\ )XXXXXXXXXXX\\v.
.-XXX( O O )XXXXXXXXXXXXXXX-
/XXX( U ) XXXXXXX\\
/XXXXX( )--_ XXXXXXXXXXX\\
/XXXXX/ ( O ) XXXXXX \\XXXXX\\
XXXXX/ / XXXXXX \\__ \\XXXXX
XXXXXX__/ XXXXXX \\__---->
---___ XXX__/ XXXXXX \\__ /
\\- --__/ ___/\\ XXXXXX / ___--/=
\\-\\ ___/ XXXXXX '--- XXXXXX
\\-\\/XXX\\ XXXXXX /XXXXX
\\XXXXXXXXX \\ /XXXXX/
\\XXXXXX > _/XXXXX/
\\XXXXX--__/ __-- XXXX/
-XXXXXXXX--------------- XXXXXX-
\\XXXXXXXXXXXXXXXXXXXXXXXXXX/
""VXXXXXXXXXXXXXXXXXXV""
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,19 @@
module Cowsay
module Character
class Kitty < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts}
("`-' '-/") .___..--' ' "`-._
` *_ * ) `-. ( ) .`-.__. `)
(_Y_.) ' ._ ) `._` ; `` -. .-'
_.. `--'_..-_/ /--' _ .' ,4
( i l ),-'' ( l i),' ( ( ! .-'
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,19 @@
module Cowsay
module Character
class Koala < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts}
___
{~._.~}
( Y )
()~*~()
(_)-(_)
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,19 @@
module Cowsay
module Character
class Moose < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts} \\_\\_ _/_/
#{@thoughts} \\__/
(oo)\\_______
(__)\\ )\\/\\
||----w |
|| ||
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,29 @@
module Cowsay
module Character
class Ren < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts}
____
/# /_\\_
| |/o\\o\\
| \\\\_/_/
/ |_ |
| ||\\_ ~|
| ||| \\/
| |||_
\\// |
|| |
||_ \\
\\_| o|
/\\___/
/ ||||__
(___)_)
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,20 @@
module Cowsay
module Character
class Sheep < Base
def template
<<-TEMPLATE
#{@thoughts}
#{@thoughts}
__
UooU\\.'\@\@\@\@\@\@`.
\\__/(\@\@\@\@\@\@\@\@\@\@)
(\@\@\@\@\@\@\@\@)
`YY~~~~YY'
|| ||
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,26 @@
module Cowsay
module Character
class Stegosaurus < Base
def template
<<-TEMPLATE
#{@thoughts} . .
#{@thoughts} / `. .' "
#{@thoughts} .---. < > < > .---.
#{@thoughts} | \\ \\ - ~ ~ - / / |
_____ ..-~ ~-..-~
| | \\~~~\\.' `./~~~/
--------- \\__/ \\__/
.' O \\ / / \\ "
(_____, `._.' | } \\/~~~/
`----. / } | / \\__/
`-. | / | / `. ,~~|
~-.__| /_ - ~ ^| /- _ `..-'
| / | / ~-. `-. _ _ _
|_____| |_____| ~ - . _ _ _ _ _>
TEMPLATE
end
end
end
end

View File

@@ -0,0 +1,26 @@
module Cowsay
module Character
class Stimpy < Base
def template
<<-TEMPLATE
#{@thoughts} . _ .
#{@thoughts} |\\_|/__/|
/ / \\/ \\ \\
/__|O||O|__ \\
|/_ \\_/\\_/ _\\ |
| | (____) | ||
\\/\\___/\\__/ //
(_/ ||
| ||
| ||\\
\\ //_/
\\______//
__ || __||
(____(____)
TEMPLATE
end
end
end
end

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