mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
47 Commits
@now/pytho
...
@now/bash@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66954e84fe | ||
|
|
15a21bb28c | ||
|
|
96ca1e1d8c | ||
|
|
587cb52191 | ||
|
|
95422ffd46 | ||
|
|
391a883799 | ||
|
|
43d6960df4 | ||
|
|
5c128003d8 | ||
|
|
2f8fd1b14b | ||
|
|
625553c146 | ||
|
|
3b0ce7bad3 | ||
|
|
489ec1dfa5 | ||
|
|
d3f92d7143 | ||
|
|
3072b044ef | ||
|
|
3b4968657f | ||
|
|
cafae4c800 | ||
|
|
64952d24f1 | ||
|
|
72758b6e0d | ||
|
|
4f80bc74d5 | ||
|
|
a6e62ed61c | ||
|
|
d8eecd6172 | ||
|
|
0e70608511 | ||
|
|
da0de150df | ||
|
|
a58c35fb9e | ||
|
|
fe88a69ab7 | ||
|
|
9767682006 | ||
|
|
3285b31721 | ||
|
|
70353c7fc0 | ||
|
|
f85cf99325 | ||
|
|
8b14a46d04 | ||
|
|
383cbfd82f | ||
|
|
81e268a3c9 | ||
|
|
ac8b33213b | ||
|
|
de12e7b8c8 | ||
|
|
b9346603f0 | ||
|
|
0b793dfc35 | ||
|
|
9dd672c383 | ||
|
|
1b743aeea8 | ||
|
|
d4af4b9f5c | ||
|
|
b734ca3e01 | ||
|
|
f81d753104 | ||
|
|
db31b9a207 | ||
|
|
b80b5182e6 | ||
|
|
268a7c2b81 | ||
|
|
667a16c996 | ||
|
|
7b851f81c0 | ||
|
|
80fbbcd194 |
@@ -12,3 +12,5 @@
|
||||
/packages/now-optipng/dist/*
|
||||
/packages/now-go/*
|
||||
/packages/now-rust/dist/*
|
||||
/packages/now-ruby/dist/*
|
||||
/packages/now-static-build/dist/*
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -7,3 +7,4 @@
|
||||
/packages/now-go @styfle @sophearak
|
||||
/packages/now-python @styfle @sophearak
|
||||
/packages/now-rust @styfle @mike-engel @anmonteiro
|
||||
/packages/now-ruby @styfle @coetry @nathancahill
|
||||
|
||||
85
CONTRIBUTING.md
Normal file
85
CONTRIBUTING.md
Normal 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.
|
||||
11
README.md
11
README.md
@@ -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.
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
const { execSync } = require('child_process');
|
||||
const { relative } = require('path');
|
||||
|
||||
const branch = execSync('git branch | grep "*" | cut -d " " -f2').toString();
|
||||
const branch = execSync('git branch | grep "*" | cut -d " " -f2')
|
||||
.toString()
|
||||
.trim();
|
||||
console.log(`Running tests on branch "${branch}"`);
|
||||
const base = branch === 'master' ? 'HEAD~1' : 'origin/canary';
|
||||
const diff = execSync(`git diff ${base} --name-only`).toString();
|
||||
const gitPath = branch === 'master' ? 'HEAD~1' : 'origin/canary...HEAD';
|
||||
const diff = execSync(`git diff ${gitPath} --name-only`).toString();
|
||||
|
||||
const changed = diff
|
||||
.split('\n')
|
||||
.filter(item => Boolean(item) && item.includes('packages/'))
|
||||
.map(item => relative('packages', item).split('/')[0]);
|
||||
|
||||
const matches = [];
|
||||
const matches = Array.from(new Set(changed));
|
||||
|
||||
if (changed.length > 0) {
|
||||
console.log('The following packages have changed:');
|
||||
|
||||
changed.map((item) => {
|
||||
matches.push(item);
|
||||
console.log(item);
|
||||
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
if (matches.length === 0) {
|
||||
matches.push('now-node');
|
||||
console.log(`No packages changed, defaulting to ${matches[0]}`);
|
||||
} else {
|
||||
console.log('The following packages have changed:');
|
||||
console.log(matches.join('\n'));
|
||||
}
|
||||
|
||||
const testMatch = Array.from(new Set(matches)).map(
|
||||
const testMatch = matches.map(
|
||||
item => `**/${item}/**/?(*.)+(spec|test).[jt]s?(x)`,
|
||||
);
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"scripts": {
|
||||
"lerna": "lerna",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"publish-stable": "git checkout master && git pull && lerna version",
|
||||
"publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary",
|
||||
"publish-stable": "git checkout master && git pull && lerna version --exact",
|
||||
"publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --exact",
|
||||
"publish-from-github": "./.circleci/publish.sh",
|
||||
"build": "./.circleci/build.sh",
|
||||
"lint": "eslint .",
|
||||
|
||||
@@ -13,6 +13,15 @@ exports.config = {
|
||||
maxLambdaSize: '10mb',
|
||||
};
|
||||
|
||||
// From this list: https://import.pw/importpw/import/docs/config.md
|
||||
const allowedConfigImports = new Set([
|
||||
'CACHE',
|
||||
'CURL_OPTS',
|
||||
'DEBUG',
|
||||
'RELOAD',
|
||||
'SERVER',
|
||||
]);
|
||||
|
||||
exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest;
|
||||
|
||||
exports.build = async ({
|
||||
@@ -24,10 +33,23 @@ exports.build = async ({
|
||||
await download(files, srcDir);
|
||||
|
||||
const configEnv = Object.keys(config).reduce((o, v) => {
|
||||
o[`IMPORT_${snakeCase(v).toUpperCase()}`] = config[v]; // eslint-disable-line no-param-reassign
|
||||
const name = snakeCase(v).toUpperCase();
|
||||
|
||||
if (allowedConfigImports.has(name)) {
|
||||
o[`IMPORT_${name}`] = config[v]; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
if (config && config.import) {
|
||||
Object.keys(config.import).forEach((key) => {
|
||||
const name = snakeCase(key).toUpperCase();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
configEnv[`IMPORT_${name}`] = config.import[key];
|
||||
});
|
||||
}
|
||||
|
||||
const IMPORT_CACHE = `${workPath}/.import-cache`;
|
||||
const env = Object.assign({}, process.env, configEnv, {
|
||||
PATH: `${IMPORT_CACHE}/bin:${process.env.PATH}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/bash",
|
||||
"version": "0.2.3",
|
||||
"version": "0.3.0",
|
||||
"description": "Now 2.0 builder for HTTP endpoints written in Bash",
|
||||
"main": "index.js",
|
||||
"author": "Nathan Rajlich <nate@zeit.co>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/build-utils",
|
||||
"version": "0.5.8",
|
||||
"version": "0.7.3",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
45
packages/now-build-utils/src/fs/node-version.ts
Normal file
45
packages/now-build-utils/src/fs/node-version.ts
Normal 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;
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PrepareCacheOptions,
|
||||
ShouldServeOptions,
|
||||
Meta,
|
||||
Config,
|
||||
} from './types';
|
||||
import { Lambda, createLambda } from './lambda';
|
||||
import download, { DownloadedFiles } from './fs/download';
|
||||
@@ -20,6 +21,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,10 +45,13 @@ export {
|
||||
runPackageJsonScript,
|
||||
runNpmInstall,
|
||||
runShellScript,
|
||||
getNodeVersion,
|
||||
getSpawnOptions,
|
||||
streamToBuffer,
|
||||
AnalyzeOptions,
|
||||
BuildOptions,
|
||||
PrepareCacheOptions,
|
||||
ShouldServeOptions,
|
||||
shouldServe,
|
||||
Config,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,13 @@ export interface Files {
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
[key: string]: string | string[] | boolean | number | undefined;
|
||||
[key: string]:
|
||||
| string
|
||||
| string[]
|
||||
| boolean
|
||||
| number
|
||||
| { [key: string]: string }
|
||||
| undefined;
|
||||
maxLambdaSize?: string;
|
||||
includeFiles?: string | string[];
|
||||
bundle?: boolean;
|
||||
@@ -24,6 +30,8 @@ export interface Config {
|
||||
helpers?: boolean;
|
||||
rust?: string;
|
||||
debug?: boolean;
|
||||
zeroConfig?: boolean;
|
||||
import?: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
@@ -162,3 +170,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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)];
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
12
packages/now-go/test/fixtures/13-go-mod-nested/api/nested/index.go
vendored
Normal file
12
packages/now-go/test/fixtures/13-go-mod-nested/api/nested/index.go
vendored
Normal 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"))
|
||||
}
|
||||
3
packages/now-go/test/fixtures/13-go-mod-nested/go.mod
vendored
Normal file
3
packages/now-go/test/fixtures/13-go-mod-nested/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module with-nested
|
||||
|
||||
go 1.12
|
||||
5
packages/now-go/test/fixtures/13-go-mod-nested/now.json
vendored
Normal file
5
packages/now-go/test/fixtures/13-go-mod-nested/now.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "api/nested/*.go", "use": "@now/go" }],
|
||||
"probes": [{ "path": "/api/nested", "mustContain": "RANDOMNESS_PLACEHOLDER" }]
|
||||
}
|
||||
6
packages/now-go/test/fixtures/13-go-mod-nested/shared/shared.go
vendored
Normal file
6
packages/now-go/test/fixtures/13-go-mod-nested/shared/shared.go
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
package shared
|
||||
|
||||
// Say func
|
||||
func Say(text string) string {
|
||||
return text
|
||||
}
|
||||
11
packages/now-go/test/fixtures/14-go-mod-sub/api/index.go
vendored
Normal file
11
packages/now-go/test/fixtures/14-go-mod-sub/api/index.go
vendored
Normal 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")
|
||||
}
|
||||
5
packages/now-go/test/fixtures/14-go-mod-sub/now.json
vendored
Normal file
5
packages/now-go/test/fixtures/14-go-mod-sub/now.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "api/**/*.go", "use": "@now/go" }],
|
||||
"probes": [{ "path": "/api", "mustContain": "RANDOMNESS_PLACEHOLDER" }]
|
||||
}
|
||||
3
packages/now-go/test/fixtures/14-go-mod-sub/other-folder/go.mod
vendored
Normal file
3
packages/now-go/test/fixtures/14-go-mod-sub/other-folder/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module other-folder
|
||||
|
||||
go 1.12
|
||||
11
packages/now-go/test/fixtures/14-go-mod-sub/other-folder/index.go
vendored
Normal file
11
packages/now-go/test/fixtures/14-go-mod-sub/other-folder/index.go
vendored
Normal 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")
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,14 @@ export interface EnvConfig {
|
||||
[name: string]: string | undefined;
|
||||
}
|
||||
|
||||
// Identify /[param]/ in route string
|
||||
const TEST_DYNAMIC_ROUTE = /\/\[[^\/]+?\](?=\/|$)/;
|
||||
|
||||
function isDynamicRoute(route: string): boolean {
|
||||
route = route.startsWith('/') ? route : `/${route}`;
|
||||
return TEST_DYNAMIC_ROUTE.test(route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the entrypoint is allowed to be used
|
||||
*/
|
||||
@@ -226,7 +234,7 @@ function getRoutes(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageName.startsWith('$') || pageName.includes('/$')) {
|
||||
if (isDynamicRoute(pageName)) {
|
||||
dynamicPages.push(normalizePage(pageName));
|
||||
}
|
||||
|
||||
@@ -351,4 +359,5 @@ export {
|
||||
stringMap,
|
||||
syncEnvVars,
|
||||
normalizePage,
|
||||
isDynamicRoute,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/node-bridge",
|
||||
"version": "1.2.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,16 @@ export interface NowProxyResponse {
|
||||
encoding: string;
|
||||
}
|
||||
|
||||
interface ServerLike {
|
||||
listen: (
|
||||
opts: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
},
|
||||
callback: (this: Server | null) => void
|
||||
) => Server | void;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the `http.Server` handler function throws an error asynchronously,
|
||||
* then it ends up being an unhandled rejection which doesn't kill the node
|
||||
@@ -92,14 +102,14 @@ function normalizeEvent(
|
||||
}
|
||||
|
||||
export class Bridge {
|
||||
private server: Server | null;
|
||||
private server: ServerLike | null;
|
||||
private listening: Promise<AddressInfo>;
|
||||
private resolveListening: (info: AddressInfo) => void;
|
||||
private events: { [key: string]: NowProxyRequest } = {};
|
||||
private reqIdSeed: number = 1;
|
||||
private shouldStoreEvents: boolean = false;
|
||||
|
||||
constructor(server?: Server, shouldStoreEvents: boolean = false) {
|
||||
constructor(server?: ServerLike, shouldStoreEvents: boolean = false) {
|
||||
this.server = null;
|
||||
this.shouldStoreEvents = shouldStoreEvents;
|
||||
if (server) {
|
||||
@@ -116,18 +126,8 @@ export class Bridge {
|
||||
});
|
||||
}
|
||||
|
||||
setServer(server: Server) {
|
||||
setServer(server: ServerLike) {
|
||||
this.server = server;
|
||||
server.once('listening', () => {
|
||||
const addr = server.address();
|
||||
if (typeof addr === 'string') {
|
||||
throw new Error(`Unexpected string for \`server.address()\`: ${addr}`);
|
||||
} else if (!addr) {
|
||||
throw new Error('`server.address()` returned `null`');
|
||||
} else {
|
||||
this.resolveListening(addr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
listen() {
|
||||
@@ -135,10 +135,35 @@ export class Bridge {
|
||||
throw new Error('Server has not been set!');
|
||||
}
|
||||
|
||||
return this.server.listen({
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
});
|
||||
const resolveListening = this.resolveListening;
|
||||
|
||||
return this.server.listen(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port: 0,
|
||||
},
|
||||
function listeningCallback() {
|
||||
if (!this || typeof this.address !== 'function') {
|
||||
throw new Error(
|
||||
'Missing server.address() function on `this` in server.listen()'
|
||||
);
|
||||
}
|
||||
|
||||
const addr = this.address();
|
||||
|
||||
if (!addr) {
|
||||
throw new Error('`server.address()` returned `null`');
|
||||
}
|
||||
|
||||
if (typeof addr === 'string') {
|
||||
throw new Error(
|
||||
`Unexpected string for \`server.address()\`: ${addr}`
|
||||
);
|
||||
}
|
||||
|
||||
resolveListening(addr);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async launcher(
|
||||
|
||||
@@ -1,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 };
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/node",
|
||||
"version": "0.9.0",
|
||||
"version": "0.11.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"repository": {
|
||||
@@ -9,11 +9,10 @@
|
||||
"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",
|
||||
"fs-extra": "7.0.1"
|
||||
"@zeit/ncc-watcher": "1.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./build.sh",
|
||||
@@ -26,11 +25,13 @@
|
||||
"devDependencies": {
|
||||
"@types/content-type": "1.1.3",
|
||||
"@types/cookie": "0.3.3",
|
||||
"@types/etag": "1.8.0",
|
||||
"@types/test-listen": "1.1.0",
|
||||
"content-type": "1.0.4",
|
||||
"cookie": "0.4.0",
|
||||
"etag": "1.8.1",
|
||||
"node-fetch": "2.6.0",
|
||||
"test-listen": "1.1.0",
|
||||
"typescript": "3.3.3"
|
||||
"typescript": "3.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
NowRequestQuery,
|
||||
NowRequestBody,
|
||||
} from './types';
|
||||
import { Stream } from 'stream';
|
||||
import { Server } from 'http';
|
||||
import { Bridge } from './bridge';
|
||||
|
||||
@@ -73,56 +72,127 @@ 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 setCharset(type: string, charset: string) {
|
||||
const { parse, format } = require('content-type');
|
||||
const parsed = parse(type);
|
||||
parsed.parameters.charset = charset;
|
||||
return format(parsed);
|
||||
}
|
||||
|
||||
function createETag(body: any, encoding: string | undefined) {
|
||||
const etag = require('etag');
|
||||
const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
|
||||
return etag(buf, { weak: true });
|
||||
}
|
||||
|
||||
function send(req: NowRequest, res: NowResponse, body: any): NowResponse {
|
||||
let chunk: unknown = body;
|
||||
let encoding: string | undefined;
|
||||
|
||||
switch (typeof chunk) {
|
||||
// string defaulting to html
|
||||
case 'string':
|
||||
if (!res.getHeader('content-type')) {
|
||||
res.setHeader('content-type', 'text/html');
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'object':
|
||||
if (chunk === null) {
|
||||
chunk = '';
|
||||
} else if (Buffer.isBuffer(chunk)) {
|
||||
if (!res.getHeader('content-type')) {
|
||||
res.setHeader('content-type', 'application/octet-stream');
|
||||
}
|
||||
} else {
|
||||
return json(req, res, chunk);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// write strings in utf-8
|
||||
if (typeof chunk === 'string') {
|
||||
encoding = 'utf8';
|
||||
|
||||
// reflect this in content-type
|
||||
const type = res.getHeader('content-type');
|
||||
if (typeof type === 'string') {
|
||||
res.setHeader('content-type', setCharset(type, 'utf-8'));
|
||||
}
|
||||
}
|
||||
|
||||
// populate Content-Length
|
||||
let len: number | undefined;
|
||||
if (chunk !== undefined) {
|
||||
if (Buffer.isBuffer(chunk)) {
|
||||
// get length of Buffer
|
||||
len = chunk.length;
|
||||
} else if (typeof chunk === 'string') {
|
||||
if (chunk.length < 1000) {
|
||||
// just calculate length small chunk
|
||||
len = Buffer.byteLength(chunk, encoding);
|
||||
} else {
|
||||
// convert chunk to Buffer and calculate
|
||||
const buf = Buffer.from(chunk, encoding);
|
||||
len = buf.length;
|
||||
chunk = buf;
|
||||
encoding = undefined;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'`body` is not a valid string, object, boolean, number, Stream, or Buffer'
|
||||
);
|
||||
}
|
||||
|
||||
if (len !== undefined) {
|
||||
res.setHeader('content-length', len);
|
||||
}
|
||||
}
|
||||
|
||||
// populate ETag
|
||||
let etag: string | undefined;
|
||||
if (
|
||||
!res.getHeader('etag') &&
|
||||
len !== undefined &&
|
||||
(etag = createETag(chunk, encoding))
|
||||
) {
|
||||
res.setHeader('etag', etag);
|
||||
}
|
||||
|
||||
// strip irrelevant headers
|
||||
if (204 === res.statusCode || 304 === res.statusCode) {
|
||||
res.removeHeader('Content-Type');
|
||||
res.removeHeader('Content-Length');
|
||||
res.removeHeader('Transfer-Encoding');
|
||||
chunk = '';
|
||||
}
|
||||
|
||||
if (req.method === 'HEAD') {
|
||||
// skip body for HEAD
|
||||
res.end();
|
||||
return res;
|
||||
} else {
|
||||
// respond
|
||||
res.end(chunk, encoding);
|
||||
}
|
||||
|
||||
const contentType = res.getHeader('Content-Type');
|
||||
|
||||
if (Buffer.isBuffer(body)) {
|
||||
if (!contentType) {
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
}
|
||||
res.setHeader('Content-Length', body.length);
|
||||
res.end(body);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (body instanceof Stream) {
|
||||
if (!contentType) {
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
}
|
||||
body.pipe(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
let str = body;
|
||||
|
||||
// Stringify JSON body
|
||||
if (typeof body === 'object' || typeof body === 'number') {
|
||||
str = JSON.stringify(body);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
res.setHeader('Content-Length', Buffer.byteLength(str));
|
||||
res.end(str);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function sendJson(res: NowResponse, jsonBody: any): NowResponse {
|
||||
// Set header to application/json
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
function json(req: NowRequest, res: NowResponse, jsonBody: any): NowResponse {
|
||||
const body = JSON.stringify(jsonBody);
|
||||
|
||||
// Use send to handle request
|
||||
return res.send(jsonBody);
|
||||
// content-type
|
||||
if (!res.getHeader('content-type')) {
|
||||
res.setHeader('content-type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
return send(req, res, body);
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
@@ -186,9 +256,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(req, res, body);
|
||||
res.json = jsonBody => json(req, res, jsonBody);
|
||||
|
||||
await listener(req, res);
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { readFile } from 'fs-extra';
|
||||
import { Assets, NccOptions } from '@zeit/ncc';
|
||||
import { join, dirname, relative, sep } from 'path';
|
||||
import { NccWatcher, WatcherResult } from '@zeit/ncc-watcher';
|
||||
@@ -12,11 +11,14 @@ import {
|
||||
createLambda,
|
||||
runNpmInstall,
|
||||
runPackageJsonScript,
|
||||
getNodeVersion,
|
||||
getSpawnOptions,
|
||||
PrepareCacheOptions,
|
||||
BuildOptions,
|
||||
shouldServe,
|
||||
} from '@now/build-utils';
|
||||
export { NowRequest, NowResponse } from './types';
|
||||
import { makeLauncher } from './launcher';
|
||||
|
||||
interface CompilerConfig {
|
||||
includeFiles?: string | string[];
|
||||
@@ -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(
|
||||
@@ -97,7 +101,8 @@ async function compile(
|
||||
assets = result.assets;
|
||||
watch = [...result.files, ...result.dirs, ...result.missing]
|
||||
.filter(f => f.startsWith(workPath))
|
||||
.map(f => relative(workPath, f));
|
||||
.map(f => relative(workPath, f))
|
||||
.concat(Object.keys(assets || {}));
|
||||
} else {
|
||||
const ncc = require('@zeit/ncc');
|
||||
const result = await ncc(input, {
|
||||
@@ -178,6 +183,8 @@ export async function build({
|
||||
const {
|
||||
entrypointPath,
|
||||
entrypointFsDirname,
|
||||
nodeVersion,
|
||||
spawnOpts,
|
||||
} = await downloadInstallAndBundle({
|
||||
files,
|
||||
entrypoint,
|
||||
@@ -186,7 +193,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(
|
||||
@@ -196,28 +203,11 @@ export async function build({
|
||||
config,
|
||||
meta
|
||||
);
|
||||
const launcherPath = join(__dirname, 'launcher.js');
|
||||
let launcherData = await readFile(launcherPath, 'utf8');
|
||||
|
||||
launcherData = launcherData.replace(
|
||||
'// PLACEHOLDER:shouldStoreProxyRequests',
|
||||
shouldAddHelpers ? 'shouldStoreProxyRequests = true;' : ''
|
||||
);
|
||||
|
||||
launcherData = launcherData.replace(
|
||||
'// PLACEHOLDER:setServer',
|
||||
[
|
||||
`let listener = require("./${entrypoint}");`,
|
||||
'if (listener.default) listener = listener.default;',
|
||||
shouldAddHelpers
|
||||
? 'const server = require("./helpers").createServerWithHelpers(listener, bridge);'
|
||||
: 'const server = require("http").createServer(listener);',
|
||||
'bridge.setServer(server);',
|
||||
].join(' ')
|
||||
);
|
||||
|
||||
const launcherFiles: Files = {
|
||||
'launcher.js': new FileBlob({ data: launcherData }),
|
||||
'launcher.js': new FileBlob({
|
||||
data: makeLauncher(entrypoint, shouldAddHelpers),
|
||||
}),
|
||||
'bridge.js': new FileFsRef({ fsPath: require('@now/node-bridge') }),
|
||||
};
|
||||
|
||||
@@ -233,7 +223,7 @@ export async function build({
|
||||
...launcherFiles,
|
||||
},
|
||||
handler: 'launcher.launcher',
|
||||
runtime: 'nodejs8.10',
|
||||
runtime: nodeVersion.runtime,
|
||||
});
|
||||
|
||||
const output = { [entrypoint]: lambda };
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Bridge } from './bridge';
|
||||
export function makeLauncher(
|
||||
entrypoint: string,
|
||||
shouldAddHelpers: boolean
|
||||
): string {
|
||||
return `const { Bridge } = require("./bridge");
|
||||
|
||||
let shouldStoreProxyRequests: boolean = false;
|
||||
// PLACEHOLDER:shouldStoreProxyRequests
|
||||
|
||||
const bridge = new Bridge(undefined, shouldStoreProxyRequests);
|
||||
let bridge;
|
||||
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV =
|
||||
@@ -11,13 +12,35 @@ if (!process.env.NODE_ENV) {
|
||||
}
|
||||
|
||||
try {
|
||||
// PLACEHOLDER:setServer
|
||||
let listener = require("./${entrypoint}");
|
||||
if (listener.default) listener = listener.default;
|
||||
|
||||
if(typeof listener.listen === 'function') {
|
||||
const server = listener;
|
||||
bridge = new Bridge(server);
|
||||
} else if(typeof listener === 'function') {
|
||||
${
|
||||
shouldAddHelpers
|
||||
? [
|
||||
'bridge = new Bridge(undefined, true);',
|
||||
'const server = require("./helpers").createServerWithHelpers(listener, bridge);',
|
||||
'bridge.setServer(server);',
|
||||
].join('\n')
|
||||
: [
|
||||
'const server = require("http").createServer(listener);',
|
||||
'bridge = new Bridge(server);',
|
||||
].join('\n')
|
||||
}
|
||||
} else {
|
||||
console.error('Export in entrypoint is not valid');
|
||||
console.error('Did you forget to export a function or a server?');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
console.error(err.message);
|
||||
console.error(
|
||||
'Did you forget to add it to "dependencies" in `package.json`?'
|
||||
);
|
||||
console.error('Did you forget to add it to "dependencies" in \`package.json\`?');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.error(err);
|
||||
@@ -27,4 +50,5 @@ try {
|
||||
|
||||
bridge.listen();
|
||||
|
||||
exports.launcher = bridge.launcher;
|
||||
exports.launcher = bridge.launcher;`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
3
packages/now-node/test/fixtures/10-node-engines/empty/index.js
vendored
Normal file
3
packages/now-node/test/fixtures/10-node-engines/empty/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = (req, res) => {
|
||||
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
|
||||
};
|
||||
3
packages/now-node/test/fixtures/10-node-engines/empty/package.json
vendored
Normal file
3
packages/now-node/test/fixtures/10-node-engines/empty/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "missing-engines-key-on-purpose"
|
||||
}
|
||||
3
packages/now-node/test/fixtures/10-node-engines/exact/index.js
vendored
Normal file
3
packages/now-node/test/fixtures/10-node-engines/exact/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = (req, res) => {
|
||||
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
|
||||
};
|
||||
5
packages/now-node/test/fixtures/10-node-engines/exact/package.json
vendored
Normal file
5
packages/now-node/test/fixtures/10-node-engines/exact/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": "10.5.0"
|
||||
}
|
||||
}
|
||||
3
packages/now-node/test/fixtures/10-node-engines/greater/index.js
vendored
Normal file
3
packages/now-node/test/fixtures/10-node-engines/greater/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = (req, res) => {
|
||||
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
|
||||
};
|
||||
5
packages/now-node/test/fixtures/10-node-engines/greater/package.json
vendored
Normal file
5
packages/now-node/test/fixtures/10-node-engines/greater/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
3
packages/now-node/test/fixtures/10-node-engines/major/index.js
vendored
Normal file
3
packages/now-node/test/fixtures/10-node-engines/major/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = (req, res) => {
|
||||
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
|
||||
};
|
||||
5
packages/now-node/test/fixtures/10-node-engines/major/package.json
vendored
Normal file
5
packages/now-node/test/fixtures/10-node-engines/major/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": "10.x"
|
||||
}
|
||||
}
|
||||
11
packages/now-node/test/fixtures/10-node-engines/now.json
vendored
Normal file
11
packages/now-node/test/fixtures/10-node-engines/now.json
vendored
Normal 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" }
|
||||
]
|
||||
}
|
||||
3
packages/now-node/test/fixtures/10-node-engines/range/index.js
vendored
Normal file
3
packages/now-node/test/fixtures/10-node-engines/range/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = (req, res) => {
|
||||
res.end(`RANDOMNESS_PLACEHOLDER:${process.versions.node}`);
|
||||
};
|
||||
5
packages/now-node/test/fixtures/10-node-engines/range/package.json
vendored
Normal file
5
packages/now-node/test/fixtures/10-node-engines/range/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": "10.x"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
const express = require('express');
|
||||
|
||||
const app = express();
|
||||
|
||||
module.exports = app;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.all('*', (req, res) => {
|
||||
res.status(200);
|
||||
|
||||
let who = 'anonymous';
|
||||
|
||||
if (req.body && req.body.who) {
|
||||
who = req.body.who;
|
||||
}
|
||||
|
||||
res.send(`hello ${who}:RANDOMNESS_PLACEHOLDER`);
|
||||
});
|
||||
@@ -3,7 +3,6 @@
|
||||
"builds": [
|
||||
{ "src": "index.js", "use": "@now/node" },
|
||||
{ "src": "ts/index.ts", "use": "@now/node" },
|
||||
{ "src": "express-compat/index.js", "use": "@now/node" },
|
||||
{ "src": "micro-compat/index.js", "use": "@now/node" },
|
||||
{
|
||||
"src": "no-helpers/index.js",
|
||||
@@ -35,12 +34,6 @@
|
||||
"path": "/ts",
|
||||
"mustContain": "hello:RANDOMNESS_PLACEHOLDER"
|
||||
},
|
||||
{
|
||||
"path": "/express-compat",
|
||||
"method": "POST",
|
||||
"body": { "who": "sara" },
|
||||
"mustContain": "hello sara:RANDOMNESS_PLACEHOLDER"
|
||||
},
|
||||
{
|
||||
"path": "/micro-compat",
|
||||
"method": "POST",
|
||||
|
||||
9
packages/now-node/test/fixtures/16-servers/express/index.js
vendored
Normal file
9
packages/now-node/test/fixtures/16-servers/express/index.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.all('*', (req, res) => {
|
||||
res.send('hello from express:RANDOMNESS_PLACEHOLDER');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
17
packages/now-node/test/fixtures/16-servers/hapi/index.js
vendored
Normal file
17
packages/now-node/test/fixtures/16-servers/hapi/index.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
const Hapi = require('@hapi/hapi');
|
||||
|
||||
const server = Hapi.server({
|
||||
port: 3000,
|
||||
host: 'localhost',
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/{p*}',
|
||||
handler: () => 'hello from hapi:RANDOMNESS_PLACEHOLDER',
|
||||
});
|
||||
|
||||
// server.listener is a node's http.Server
|
||||
// server does not have the `listen` method so we need to export this instead
|
||||
|
||||
module.exports = server.listener;
|
||||
5
packages/now-node/test/fixtures/16-servers/hapi/package.json
vendored
Normal file
5
packages/now-node/test/fixtures/16-servers/hapi/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@hapi/hapi": "18.3.1"
|
||||
}
|
||||
}
|
||||
7
packages/now-node/test/fixtures/16-servers/index.js
vendored
Normal file
7
packages/now-node/test/fixtures/16-servers/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
const http = require('http');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.end('hello:RANDOMNESS_PLACEHOLDER');
|
||||
});
|
||||
|
||||
module.exports = server;
|
||||
9
packages/now-node/test/fixtures/16-servers/koa/index.js
vendored
Normal file
9
packages/now-node/test/fixtures/16-servers/koa/index.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
const Koa = require('koa');
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx) => {
|
||||
ctx.body = 'hello from koa:RANDOMNESS_PLACEHOLDER';
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
5
packages/now-node/test/fixtures/16-servers/koa/package.json
vendored
Normal file
5
packages/now-node/test/fixtures/16-servers/koa/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"koa": "2.7.0"
|
||||
}
|
||||
}
|
||||
22
packages/now-node/test/fixtures/16-servers/now.json
vendored
Normal file
22
packages/now-node/test/fixtures/16-servers/now.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "**/*.js", "use": "@now/node" }],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/",
|
||||
"mustContain": "hello:RANDOMNESS_PLACEHOLDER"
|
||||
},
|
||||
{
|
||||
"path": "/express",
|
||||
"mustContain": "hello from express:RANDOMNESS_PLACEHOLDER"
|
||||
},
|
||||
{
|
||||
"path": "/koa",
|
||||
"mustContain": "hello from koa:RANDOMNESS_PLACEHOLDER"
|
||||
},
|
||||
{
|
||||
"path": "/hapi",
|
||||
"mustContain": "hello from hapi:RANDOMNESS_PLACEHOLDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ from http.server import BaseHTTPRequestHandler
|
||||
|
||||
import base64
|
||||
import json
|
||||
import inspect
|
||||
|
||||
import __NOW_HANDLER_FILENAME
|
||||
__now_variables = dir(__NOW_HANDLER_FILENAME)
|
||||
|
||||
|
||||
if 'handler' in __now_variables or 'Handler' in __now_variables:
|
||||
base = __NOW_HANDLER_FILENAME.handler if ('handler' in __now_variables) else __NOW_HANDLER_FILENAME.Handler
|
||||
if not issubclass(base, BaseHTTPRequestHandler):
|
||||
@@ -47,76 +47,202 @@ if 'handler' in __now_variables or 'Handler' in __now_variables:
|
||||
'body': res.text,
|
||||
}
|
||||
elif 'app' in __now_variables:
|
||||
print('using Web Server Gateway Interface (WSGI)')
|
||||
import sys
|
||||
from urllib.parse import urlparse, unquote
|
||||
from werkzeug._compat import BytesIO
|
||||
from werkzeug._compat import string_types
|
||||
from werkzeug._compat import to_bytes
|
||||
from werkzeug._compat import wsgi_encoding_dance
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.wrappers import Response
|
||||
def now_handler(event, context):
|
||||
payload = json.loads(event['body'])
|
||||
if not inspect.iscoroutinefunction(__NOW_HANDLER_FILENAME.app.__call__):
|
||||
print('using Web Server Gateway Interface (WSGI)')
|
||||
import sys
|
||||
from urllib.parse import urlparse, unquote
|
||||
from werkzeug._compat import BytesIO
|
||||
from werkzeug._compat import string_types
|
||||
from werkzeug._compat import to_bytes
|
||||
from werkzeug._compat import wsgi_encoding_dance
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.wrappers import Response
|
||||
def now_handler(event, context):
|
||||
payload = json.loads(event['body'])
|
||||
|
||||
headers = Headers(payload.get('headers', {}))
|
||||
headers = Headers(payload.get('headers', {}))
|
||||
|
||||
body = payload.get('body', '')
|
||||
if body != '':
|
||||
body = payload.get('body', '')
|
||||
if body != '':
|
||||
if payload.get('encoding') == 'base64':
|
||||
body = base64.b64decode(body)
|
||||
if isinstance(body, string_types):
|
||||
body = to_bytes(body, charset='utf-8')
|
||||
|
||||
url = urlparse(unquote(payload['path']))
|
||||
query = url.query
|
||||
path = url.path
|
||||
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(body)),
|
||||
'CONTENT_TYPE': headers.get('content-type', ''),
|
||||
'PATH_INFO': path,
|
||||
'QUERY_STRING': query,
|
||||
'REMOTE_ADDR': headers.get(
|
||||
'x-forwarded-for', headers.get(
|
||||
'x-real-ip', payload.get(
|
||||
'true-client-ip', ''))),
|
||||
'REQUEST_METHOD': payload['method'],
|
||||
'SERVER_NAME': headers.get('host', 'lambda'),
|
||||
'SERVER_PORT': headers.get('x-forwarded-port', '80'),
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
'event': event,
|
||||
'context': context,
|
||||
'wsgi.errors': sys.stderr,
|
||||
'wsgi.input': BytesIO(body),
|
||||
'wsgi.multiprocess': False,
|
||||
'wsgi.multithread': False,
|
||||
'wsgi.run_once': False,
|
||||
'wsgi.url_scheme': headers.get('x-forwarded-proto', 'http'),
|
||||
'wsgi.version': (1, 0),
|
||||
}
|
||||
|
||||
for key, value in environ.items():
|
||||
if isinstance(value, string_types) and key != 'QUERY_STRING':
|
||||
environ[key] = wsgi_encoding_dance(value)
|
||||
|
||||
for key, value in headers.items():
|
||||
key = 'HTTP_' + key.upper().replace('-', '_')
|
||||
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
|
||||
environ[key] = value
|
||||
|
||||
response = Response.from_app(__NOW_HANDLER_FILENAME.app, environ)
|
||||
|
||||
return_dict = {
|
||||
'statusCode': response.status_code,
|
||||
'headers': dict(response.headers)
|
||||
}
|
||||
|
||||
if response.data:
|
||||
return_dict['body'] = base64.b64encode(response.data).decode('utf-8')
|
||||
return_dict['encoding'] = 'base64'
|
||||
|
||||
return return_dict
|
||||
else:
|
||||
print('using Asynchronous Server Gateway Interface (ASGI)')
|
||||
import asyncio
|
||||
import enum
|
||||
from urllib.parse import urlparse, unquote, urlencode
|
||||
|
||||
|
||||
class ASGICycleState(enum.Enum):
|
||||
REQUEST = enum.auto()
|
||||
RESPONSE = enum.auto()
|
||||
|
||||
|
||||
class ASGICycle:
|
||||
def __init__(self, scope):
|
||||
self.scope = scope
|
||||
self.body = b''
|
||||
self.state = ASGICycleState.REQUEST
|
||||
self.app_queue = None
|
||||
self.response = {}
|
||||
|
||||
def __call__(self, app, body):
|
||||
"""
|
||||
Receives the application and any body included in the request, then builds the
|
||||
ASGI instance using the connection scope.
|
||||
Runs until the response is completely read from the application.
|
||||
"""
|
||||
loop = asyncio.new_event_loop()
|
||||
self.app_queue = asyncio.Queue(loop=loop)
|
||||
self.put_message({'type': 'http.request', 'body': body, 'more_body': False})
|
||||
|
||||
asgi_instance = app(self.scope, self.receive, self.send)
|
||||
|
||||
asgi_task = loop.create_task(asgi_instance)
|
||||
loop.run_until_complete(asgi_task)
|
||||
return self.response
|
||||
|
||||
def put_message(self, message):
|
||||
self.app_queue.put_nowait(message)
|
||||
|
||||
async def receive(self):
|
||||
"""
|
||||
Awaited by the application to receive messages in the queue.
|
||||
"""
|
||||
message = await self.app_queue.get()
|
||||
return message
|
||||
|
||||
async def send(self, message):
|
||||
"""
|
||||
Awaited by the application to send messages to the current cycle instance.
|
||||
"""
|
||||
message_type = message['type']
|
||||
|
||||
if self.state is ASGICycleState.REQUEST:
|
||||
if message_type != 'http.response.start':
|
||||
raise RuntimeError(
|
||||
f"Expected 'http.response.start', received: {message_type}"
|
||||
)
|
||||
|
||||
status_code = message['status']
|
||||
headers = {k: v for k, v in message.get('headers', [])}
|
||||
|
||||
self.on_request(headers, status_code)
|
||||
self.state = ASGICycleState.RESPONSE
|
||||
|
||||
elif self.state is ASGICycleState.RESPONSE:
|
||||
if message_type != 'http.response.body':
|
||||
raise RuntimeError(
|
||||
f"Expected 'http.response.body', received: {message_type}"
|
||||
)
|
||||
|
||||
body = message.get('body', b'')
|
||||
more_body = message.get('more_body', False)
|
||||
|
||||
# The body must be completely read before returning the response.
|
||||
self.body += body
|
||||
|
||||
if not more_body:
|
||||
self.on_response()
|
||||
self.put_message({'type': 'http.disconnect'})
|
||||
|
||||
def on_request(self, headers, status_code):
|
||||
self.response['statusCode'] = status_code
|
||||
self.response['headers'] = {k.decode(): v.decode() for k, v in headers.items()}
|
||||
|
||||
def on_response(self):
|
||||
if self.body:
|
||||
self.response['body'] = base64.b64encode(self.body).decode('utf-8')
|
||||
self.response['encoding'] = 'base64'
|
||||
|
||||
def now_handler(event, context):
|
||||
payload = json.loads(event['body'])
|
||||
|
||||
headers = payload.get('headers', {})
|
||||
|
||||
body = payload.get('body', b'')
|
||||
if payload.get('encoding') == 'base64':
|
||||
body = base64.b64decode(body)
|
||||
if isinstance(body, string_types):
|
||||
body = to_bytes(body, charset='utf-8')
|
||||
elif not isinstance(body, bytes):
|
||||
body = body.encode()
|
||||
|
||||
url = urlparse(unquote(payload['path']))
|
||||
query = url.query
|
||||
path = url.path
|
||||
url = urlparse(unquote(payload['path']))
|
||||
query = url.query.encode()
|
||||
path = url.path
|
||||
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(body)),
|
||||
'CONTENT_TYPE': headers.get('content-type', ''),
|
||||
'PATH_INFO': path,
|
||||
'QUERY_STRING': query,
|
||||
'REMOTE_ADDR': headers.get(
|
||||
'x-forwarded-for', headers.get(
|
||||
'x-real-ip', payload.get(
|
||||
'true-client-ip', ''))),
|
||||
'REQUEST_METHOD': payload['method'],
|
||||
'SERVER_NAME': headers.get('host', 'lambda'),
|
||||
'SERVER_PORT': headers.get('x-forwarded-port', '80'),
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
'event': event,
|
||||
'context': context,
|
||||
'wsgi.errors': sys.stderr,
|
||||
'wsgi.input': BytesIO(body),
|
||||
'wsgi.multiprocess': False,
|
||||
'wsgi.multithread': False,
|
||||
'wsgi.run_once': False,
|
||||
'wsgi.url_scheme': headers.get('x-forwarded-proto', 'http'),
|
||||
'wsgi.version': (1, 0),
|
||||
}
|
||||
scope = {
|
||||
'server': (headers.get('host', 'lambda'), headers.get('x-forwarded-port', 80)),
|
||||
'client': (headers.get(
|
||||
'x-forwarded-for', headers.get(
|
||||
'x-real-ip', payload.get(
|
||||
'true-client-ip', ''))), 0),
|
||||
'scheme': headers.get('x-forwarded-proto', 'http'),
|
||||
'root_path': '',
|
||||
'query_string': query,
|
||||
'headers': [[k.lower().encode(), v.encode()] for k, v in headers.items()],
|
||||
'type': 'http',
|
||||
'http_version': '1.1',
|
||||
'method': payload['method'],
|
||||
'path': path,
|
||||
'raw_path': path.encode(),
|
||||
}
|
||||
|
||||
for key, value in environ.items():
|
||||
if isinstance(value, string_types) and key != 'QUERY_STRING':
|
||||
environ[key] = wsgi_encoding_dance(value)
|
||||
asgi_cycle = ASGICycle(scope)
|
||||
response = asgi_cycle(__NOW_HANDLER_FILENAME.app, body)
|
||||
return response
|
||||
|
||||
for key, value in headers.items():
|
||||
key = 'HTTP_' + key.upper().replace('-', '_')
|
||||
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
|
||||
environ[key] = value
|
||||
|
||||
response = Response.from_app(__NOW_HANDLER_FILENAME.app, environ)
|
||||
|
||||
return_dict = {
|
||||
'statusCode': response.status_code,
|
||||
'headers': dict(response.headers)
|
||||
}
|
||||
|
||||
if response.data:
|
||||
return_dict['body'] = base64.b64encode(response.data).decode('utf-8')
|
||||
return_dict['encoding'] = 'base64'
|
||||
|
||||
return return_dict
|
||||
else:
|
||||
print('Missing variable `handler` or `app` in file __NOW_HANDLER_FILENAME.py')
|
||||
print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/python",
|
||||
"version": "0.2.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
12
packages/now-python/test/fixtures/04-cowsay-pipfile/Pipfile
vendored
Normal file
12
packages/now-python/test/fixtures/04-cowsay-pipfile/Pipfile
vendored
Normal 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"
|
||||
29
packages/now-python/test/fixtures/04-cowsay-pipfile/Pipfile.lock
generated
vendored
Normal file
29
packages/now-python/test/fixtures/04-cowsay-pipfile/Pipfile.lock
generated
vendored
Normal 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": {}
|
||||
}
|
||||
13
packages/now-python/test/fixtures/04-cowsay-pipfile/index.py
vendored
Normal file
13
packages/now-python/test/fixtures/04-cowsay-pipfile/index.py
vendored
Normal 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
|
||||
5
packages/now-python/test/fixtures/04-cowsay-pipfile/now.json
vendored
Normal file
5
packages/now-python/test/fixtures/04-cowsay-pipfile/now.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "**/**.py", "use": "@now/python" }],
|
||||
"probes": [{ "path": "/", "mustContain": "pip:RANDOMNESS_PLACEHOLDER" }]
|
||||
}
|
||||
12
packages/now-python/test/fixtures/05-cowsay-pipfile-local/local/Pipfile
vendored
Normal file
12
packages/now-python/test/fixtures/05-cowsay-pipfile-local/local/Pipfile
vendored
Normal 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"
|
||||
29
packages/now-python/test/fixtures/05-cowsay-pipfile-local/local/Pipfile.lock
generated
vendored
Normal file
29
packages/now-python/test/fixtures/05-cowsay-pipfile-local/local/Pipfile.lock
generated
vendored
Normal 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": {}
|
||||
}
|
||||
13
packages/now-python/test/fixtures/05-cowsay-pipfile-local/local/index.py
vendored
Normal file
13
packages/now-python/test/fixtures/05-cowsay-pipfile-local/local/index.py
vendored
Normal 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
|
||||
5
packages/now-python/test/fixtures/05-cowsay-pipfile-local/now.json
vendored
Normal file
5
packages/now-python/test/fixtures/05-cowsay-pipfile-local/now.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "**/**.py", "use": "@now/python" }],
|
||||
"probes": [{ "path": "/local", "mustContain": "pip:RANDOMNESS_PLACEHOLDER" }]
|
||||
}
|
||||
@@ -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': '/'}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
12
packages/now-python/test/fixtures/11-asgi/Pipfile
vendored
Normal file
12
packages/now-python/test/fixtures/11-asgi/Pipfile
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
sanic = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
207
packages/now-python/test/fixtures/11-asgi/Pipfile.lock
generated
vendored
Normal file
207
packages/now-python/test/fixtures/11-asgi/Pipfile.lock
generated
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "93dcd591e5690d3a71cb02979cbe317e83e3c03ec020867bf1554a480ef5cd8a"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiofiles": {
|
||||
"hashes": [
|
||||
"sha256:021ea0ba314a86027c166ecc4b4c07f2d40fc0f4b3a950d1868a0f2571c2bbee",
|
||||
"sha256:1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d"
|
||||
],
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
|
||||
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
|
||||
],
|
||||
"version": "==2019.6.16"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208",
|
||||
"sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
},
|
||||
"h2": {
|
||||
"hashes": [
|
||||
"sha256:c8f387e0e4878904d4978cd688a3195f6b169d49b1ffa572a3d347d7adc5e09f",
|
||||
"sha256:fd07e865a3272ac6ef195d8904de92dc7b38dc28297ec39cfa22716b6d62e6eb"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"hpack": {
|
||||
"hashes": [
|
||||
"sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
|
||||
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
|
||||
],
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:96f910b528d47b683242ec207050c7bbaa99cd1b9a07f78ea80cf61e55556b58"
|
||||
],
|
||||
"version": "==0.3.0"
|
||||
},
|
||||
"httptools": {
|
||||
"hashes": [
|
||||
"sha256:e00cbd7ba01ff748e494248183abc6e153f49181169d8a3d41bb49132ca01dfc"
|
||||
],
|
||||
"version": "==0.0.13"
|
||||
},
|
||||
"hyperframe": {
|
||||
"hashes": [
|
||||
"sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40",
|
||||
"sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"
|
||||
],
|
||||
"version": "==5.2.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
||||
],
|
||||
"version": "==4.5.2"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"requests-async": {
|
||||
"hashes": [
|
||||
"sha256:8731420451383196ecf2fd96082bfc8ae5103ada90aba185888499d7784dde6f"
|
||||
],
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"rfc3986": {
|
||||
"hashes": [
|
||||
"sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405",
|
||||
"sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18"
|
||||
],
|
||||
"version": "==1.3.2"
|
||||
},
|
||||
"sanic": {
|
||||
"hashes": [
|
||||
"sha256:cc64978266025afb0e7c0f8be928e2b81670c5d58ddac290d04c9d0da6ec2112",
|
||||
"sha256:ebd806298782400db811ea9d63e8096e835e67f0b5dc5e66e507532984a82bb3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.6.0"
|
||||
},
|
||||
"ujson": {
|
||||
"hashes": [
|
||||
"sha256:f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
|
||||
"version": "==1.35"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
],
|
||||
"version": "==1.25.3"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
"sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573",
|
||||
"sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64",
|
||||
"sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0",
|
||||
"sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5",
|
||||
"sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26",
|
||||
"sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7",
|
||||
"sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115",
|
||||
"sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021",
|
||||
"sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f",
|
||||
"sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe"
|
||||
],
|
||||
"markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
|
||||
"version": "==0.12.2"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136",
|
||||
"sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6",
|
||||
"sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1",
|
||||
"sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538",
|
||||
"sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4",
|
||||
"sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908",
|
||||
"sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0",
|
||||
"sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d",
|
||||
"sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c",
|
||||
"sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d",
|
||||
"sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c",
|
||||
"sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb",
|
||||
"sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf",
|
||||
"sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e",
|
||||
"sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96",
|
||||
"sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584",
|
||||
"sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484",
|
||||
"sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d",
|
||||
"sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559",
|
||||
"sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff",
|
||||
"sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"
|
||||
],
|
||||
"version": "==6.0"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
||||
8
packages/now-python/test/fixtures/11-asgi/index.py
vendored
Normal file
8
packages/now-python/test/fixtures/11-asgi/index.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
return response.text("asgi:RANDOMNESS_PLACEHOLDER")
|
||||
11
packages/now-python/test/fixtures/11-asgi/now.json
vendored
Normal file
11
packages/now-python/test/fixtures/11-asgi/now.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "index.py",
|
||||
"use": "@now/python",
|
||||
"config": { "maxLambdaSize": "10mb" }
|
||||
}
|
||||
],
|
||||
"probes": [{ "path": "/", "mustContain": "asgi:RANDOMNESS_PLACEHOLDER" }]
|
||||
}
|
||||
1
packages/now-ruby/.gitignore
vendored
Normal file
1
packages/now-ruby/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/dist
|
||||
220
packages/now-ruby/index.ts
Normal file
220
packages/now-ruby/index.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
66
packages/now-ruby/install-ruby.ts
Normal file
66
packages/now-ruby/install-ruby.ts
Normal 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
87
packages/now-ruby/now_init.rb
Executable 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
|
||||
28
packages/now-ruby/package.json
Executable file
28
packages/now-ruby/package.json
Executable file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@now/ruby",
|
||||
"author": "Nathan Cahill <nathan@nathancahill.com>",
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"files": [
|
||||
"dist",
|
||||
"now_init.rb"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zeit/now-builders.git",
|
||||
"directory": "packages/now-ruby"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "tsc && jest",
|
||||
"prepublishOnly": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": "^1.0.0",
|
||||
"fs-extra": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "3.5.2"
|
||||
}
|
||||
}
|
||||
5
packages/now-ruby/test/fixtures/01-cowsay/Gemfile
vendored
Normal file
5
packages/now-ruby/test/fixtures/01-cowsay/Gemfile
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "cowsay", "~> 0.3.0"
|
||||
13
packages/now-ruby/test/fixtures/01-cowsay/Gemfile.lock
vendored
Normal file
13
packages/now-ruby/test/fixtures/01-cowsay/Gemfile.lock
vendored
Normal 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
|
||||
10
packages/now-ruby/test/fixtures/01-cowsay/index.rb
vendored
Normal file
10
packages/now-ruby/test/fixtures/01-cowsay/index.rb
vendored
Normal 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
|
||||
5
packages/now-ruby/test/fixtures/01-cowsay/now.json
vendored
Normal file
5
packages/now-ruby/test/fixtures/01-cowsay/now.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "index.rb", "use": "@now/ruby" }],
|
||||
"probes": [{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" }]
|
||||
}
|
||||
10
packages/now-ruby/test/fixtures/02-cowsay-vendored/index.rb
vendored
Normal file
10
packages/now-ruby/test/fixtures/02-cowsay-vendored/index.rb
vendored
Normal 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
|
||||
5
packages/now-ruby/test/fixtures/02-cowsay-vendored/now.json
vendored
Normal file
5
packages/now-ruby/test/fixtures/02-cowsay-vendored/now.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "index.rb", "use": "@now/ruby" }],
|
||||
"probes": [{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" }]
|
||||
}
|
||||
27
packages/now-ruby/test/fixtures/02-cowsay-vendored/vendor/bundle/ruby/2.5.0/bin/cowsay
vendored
Executable file
27
packages/now-ruby/test/fixtures/02-cowsay-vendored/vendor/bundle/ruby/2.5.0/bin/cowsay
vendored
Executable 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
|
||||
BIN
packages/now-ruby/test/fixtures/02-cowsay-vendored/vendor/bundle/ruby/2.5.0/cache/cowsay-0.3.0.gem
vendored
Normal file
BIN
packages/now-ruby/test/fixtures/02-cowsay-vendored/vendor/bundle/ruby/2.5.0/cache/cowsay-0.3.0.gem
vendored
Normal file
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# Specify your gem's dependencies in zerobuf.gemspec
|
||||
gemspec
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
require 'bundler/gem_tasks'
|
||||
28
packages/now-ruby/test/fixtures/02-cowsay-vendored/vendor/bundle/ruby/2.5.0/gems/cowsay-0.3.0/bin/cowsay
vendored
Executable file
28
packages/now-ruby/test/fixtures/02-cowsay-vendored/vendor/bundle/ruby/2.5.0/gems/cowsay-0.3.0/bin/cowsay
vendored
Executable 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,31 @@
|
||||
module Cowsay
|
||||
module Character
|
||||
|
||||
class Beavis < Base
|
||||
def template
|
||||
<<-TEMPLATE
|
||||
#{@thoughts} __------~~-,
|
||||
#{@thoughts} ,' ,
|
||||
/ \\
|
||||
/ :
|
||||
| '
|
||||
| |
|
||||
| |
|
||||
| _-- |
|
||||
_| =-. .-. ||
|
||||
o|/o/ _. |
|
||||
/ ~ \\ |
|
||||
(____\@) ___~ |
|
||||
|_===~~~.` |
|
||||
_______.--~ |
|
||||
\\________ |
|
||||
\\ |
|
||||
__/-___-- -__
|
||||
/ _ \\
|
||||
|
||||
TEMPLATE
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
module Cowsay
|
||||
module Character
|
||||
|
||||
class Bunny < Base
|
||||
def template
|
||||
<<-TEMPLATE
|
||||
#{@thoughts}
|
||||
#{@thoughts} \\
|
||||
\\ /\\
|
||||
( )
|
||||
.( o ).
|
||||
TEMPLATE
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user