├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── sample ├── basic │ ├── README.md │ ├── cowsay.js │ ├── greet.ts │ ├── package.json │ ├── print.ts │ └── tsconfig.json ├── mocha-enzyme │ ├── README.md │ ├── package.json │ ├── test.tsx │ └── tsconfig.json ├── mocha │ ├── README.md │ ├── package.json │ ├── test.ts │ └── tsconfig.json └── tape │ ├── README.md │ ├── package.json │ └── test.js ├── src ├── bundle.ts ├── chrome-location.ts ├── cli.ts ├── fs.ts ├── host-bindings.ts ├── plugins.ts ├── puppeteer.ts ├── script-error.ts ├── server.ts ├── temporary.ts └── types.ts ├── tsconfig.json ├── tslint.json └── types ├── envify.d.ts └── serve-handler.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | dist/ 3 | !dist/*.d.ts 4 | node_modules/ 5 | .DS_Store 6 | Thumbs.db 7 | *.log 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Andy Wermke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🤖  puppet-run

2 | 3 |

4 | npm (tag) 5 |

6 | 7 | Run any JavaScript / TypeScript code in a headless browser using [Puppeteer](https://github.com/GoogleChrome/puppeteer) and pipe its output to your terminal 🔥 8 | 9 | Transparently bundles input files, so you can use `require()` and ES module imports. You can even simulate connectivity issues and serve static files. Great for testing client-side code in an actual browser! 10 | 11 | How does it relate to [Karma](https://karma-runner.github.io)? It's everything that Karma is not: It's small, it's fast and trivial to set up. 12 | 13 | 🚀  Runs any script in a headless Chrome browser
14 | 📦  Zero-config transparent bundling
15 | 💡  Supports TypeScript, ES modules & JSX out of the box
16 | 🖥  Pipes console output and errors to host shell
17 | ⚙️  Uses custom Babel, TypeScript, ... config if present
18 | 19 | 20 | ## Installation 21 | 22 | ```sh 23 | npm install puppet-run 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Basics 29 | 30 | Running `puppet-run` from the command line is simple. We can use npm's [npx tool](https://blog.npmjs.org/post/162869356040/introducing-npx-an-npm-package-runner) for convenience. 31 | 32 | ```sh 33 | npx puppet-run [] 34 | 35 | # without npx 36 | node ./node_modules/.bin/puppet-run [] 37 | ``` 38 | 39 | Pass any JavaScript or TypeScript file to `puppet-run` as an entrypoint. It will be transpiled by Babel and bundled using `browserify`. It normally works out-of-the-box with zero configuration. 40 | 41 | ```sh 42 | npx puppet-run [...puppet-run options] ./path/to/script.js [...script options] 43 | ``` 44 | 45 | ### Run mocha tests 46 | 47 | ```sh 48 | npm install puppet-run-plugin-mocha 49 | npx puppet-run --plugin=mocha [...mocha options] ./path/to/*.test.js 50 | ``` 51 | 52 | ### Print help texts 53 | 54 | ```sh 55 | npx puppet-run --help 56 | ``` 57 | 58 | To print a plugin's help text: 59 | 60 | ```sh 61 | npx puppet-run --plugin=mocha --help 62 | ``` 63 | 64 | 65 | ## Example 66 | 67 | ```js 68 | // sample.js 69 | 70 | // Everything logged here will be piped to your host terminal 71 | console.log(`I am being run in a browser: ${navigator.userAgent}`) 72 | 73 | // Explicitly terminate the script when you are done 74 | puppet.exit() 75 | ``` 76 | 77 | Don't forget to call `puppet.exit()` when the script is done, so `puppet-run` knows that the script has finished. You can also exit with a non-zero exit code using `puppet.exit(statusCode: number)`. 78 | 79 | Check out the "Scripting API" section below if you want to learn more about the globally available `puppet` object. 80 | 81 | Let's run the sample script! 82 | 83 | ```sh 84 | npx puppet-run ./sample.js 85 | ``` 86 | 87 | You should now see the output of the script on your terminal: 88 | 89 | ``` 90 | I am being run in a browser: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36 91 | ``` 92 | 93 | Have fun! 94 | 95 | 96 | ## Plugins 97 | 98 | Plugins make it easy to integrate your script with testing frameworks. 99 | 100 | Check out the 👉 [plugins repository](https://github.com/andywer/puppet-run-plugins) to see what's on offer. 101 | 102 | 103 | ## Scripting API 104 | 105 | The script runner will inject a `puppet` object into the browser window's global scope. It contains a couple of useful functions. 106 | 107 | #### `puppet.argv: string[]` 108 | 109 | Contains all the command line arguments and options passed to `puppet-run` after the script file path. 110 | 111 | #### `puppet.exit(exitCode?: number = 0)` 112 | 113 | Causes the script to end. The `puppet-run` process will exit with the exit code you pass here. 114 | 115 | The exit code defaults to zero. 116 | 117 | #### `puppet.setOfflineMode(takeOffline: boolean = true)` 118 | 119 | Puts the browser in offline mode and closes all active connections if called with `true` or no arguments. Call it with `false` to bring the browser back online. 120 | 121 | 122 | ## More features 123 | 124 | ### Environment variables 125 | 126 | You can access all environment variables of the host shell in your scripts as `process.env.*`. 127 | 128 | ### Source Maps 129 | 130 | If an error is thrown, you will see the error and stack trace in your host shell. The stack trace will reference your source file lines, not the line in the bundle file that is actually served to the browser under the hood. 131 | 132 | 133 | ## Samples 134 | 135 | Have a look at the samples in the [`sample`](./sample) directory: 136 | 137 | - [Simple Testing](./sample/basic) 138 | - [Simple Mocha Test](./sample/mocha) 139 | - [React / Enzyme Test](./sample/mocha-enzyme) 140 | - [Tape Test](./sample/tape) 141 | 142 | 143 | ## Test framework support 144 | 145 | If you want to run tests in the browser using puppet-run, check out this list first: 146 | 147 | #### ✅ Mocha 148 | 149 | Works like a charm, see [`sample/mocha`](./sample/mocha) or [`sample/mocha-enzyme`](./sample/mocha-enzyme). They use the [Mocha Plugin](https://github.com/andywer/puppet-run-plugins/tree/master/packages/puppet-run-plugin-mocha). 150 | 151 | #### ✅ Tape 152 | 153 | Works like a charm, see [`sample/tape`](./sample/tape). 154 | 155 | #### ❌ AVA 156 | 157 | Currently not possible, since it's testing library and test runner code are too tightly coupled. 158 | 159 | #### ❔ Jest 160 | 161 | Didn't try yet. 162 | 163 | 164 | ## License 165 | 166 | MIT 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-run", 3 | "version": "0.11.4", 4 | "license": "MIT", 5 | "description": "Run anything JavaScript in a headless Chrome from your command line.", 6 | "author": "Andy Wermke (https://github.com/andywer)", 7 | "repository": "github:andywer/puppet-run", 8 | "bin": "./dist/cli.js", 9 | "scripts": { 10 | "prebuild": "rimraf dist/", 11 | "build": "tsc", 12 | "test": "tslint --project .", 13 | "prepare": "npm run build" 14 | }, 15 | "keywords": [ 16 | "puppeteer", 17 | "headless", 18 | "chrome", 19 | "parcel", 20 | "bundler", 21 | "javascript", 22 | "script", 23 | "testing" 24 | ], 25 | "dependencies": { 26 | "@babel/core": "^7.9.6", 27 | "@babel/preset-env": "^7.9.6", 28 | "@babel/preset-react": "^7.9.4", 29 | "@babel/preset-typescript": "^7.9.0", 30 | "babelify": "^10.0.0", 31 | "browserify": "^16.5.1", 32 | "chai": "^4.2.0", 33 | "chalk": "^2.4.2", 34 | "dedent": "^0.7.0", 35 | "del": "^6.0.0", 36 | "@goto-bus-stop/envify": "^5.0.0", 37 | "get-port": "^4.2.0", 38 | "meow": "^5.0.0", 39 | "minimist": "^1.2.5", 40 | "mkdirp": "^0.5.1", 41 | "nanoid": "^2.1.11", 42 | "ora": "^3.4.0", 43 | "puppeteer-core": "^1.20.0", 44 | "serve-handler": "^6.1.2", 45 | "sourcemapped-stacktrace": "^1.1.11", 46 | "which": "^1.3.1" 47 | }, 48 | "devDependencies": { 49 | "@types/babelify": "^7.3.6", 50 | "@types/browserify": "^12.0.36", 51 | "@types/debug": "0.0.30", 52 | "@types/dedent": "^0.7.0", 53 | "@types/del": "^4.0.0", 54 | "@types/meow": "^5.0.0", 55 | "@types/mkdirp": "^0.5.2", 56 | "@types/nanoid": "^2.1.0", 57 | "@types/node": "^10.17.24", 58 | "@types/ora": "^1.3.5", 59 | "@types/puppeteer-core": "^1.9.0", 60 | "@types/rimraf": "^2.0.4", 61 | "@types/which": "^1.3.2", 62 | "lint-staged": "^7.2.2", 63 | "prettier": "^1.19.1", 64 | "rimraf": "^3.0.2", 65 | "ts-node": "^7.0.1", 66 | "tslint": "^5.20.1", 67 | "tslint-config-prettier": "^1.18.0", 68 | "typescript": "^3.9.3" 69 | }, 70 | "files": [ 71 | "dist/**" 72 | ], 73 | "prettier": { 74 | "semi": false, 75 | "printWidth": 120 76 | }, 77 | "lint-staged": { 78 | "*": [ 79 | "prettier --write", 80 | "git add" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /sample/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic Samples 2 | 3 | Run the sample test: 4 | 5 | ```sh 6 | npm install 7 | npm run cowsay 8 | npm run greet 9 | npm run greet newbie 10 | ``` 11 | 12 | The npm scripts will simply run: 13 | 14 | ```sh 15 | puppet-run ./cowsay 16 | puppet-run ./greet 17 | puppet-run ./greet newbie 18 | ``` 19 | -------------------------------------------------------------------------------- /sample/basic/cowsay.js: -------------------------------------------------------------------------------- 1 | // cowsays.js 2 | import * as cowsay from "cowsay" 3 | 4 | const text = window.atob("SSBsaXZlIGluIGEgYnJvd3NlciBub3c=") 5 | console.log(cowsay.say({ text })) 6 | 7 | puppet.exit() 8 | -------------------------------------------------------------------------------- /sample/basic/greet.ts: -------------------------------------------------------------------------------- 1 | import print from "./print" 2 | 3 | declare const puppet: { 4 | argv: string[], 5 | exit (exitCode?: number): void 6 | } 7 | 8 | if (puppet.argv.length === 0) { 9 | print("Hello World") 10 | } else { 11 | print(`Hello, ${puppet.argv[0]}!`) 12 | } 13 | 14 | puppet.exit(0) 15 | -------------------------------------------------------------------------------- /sample/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-run-sample-basic", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "cowsay": "puppet-run ./cowsay.js", 7 | "greet": "puppet-run ./greet.ts" 8 | }, 9 | "devDependencies": { 10 | "puppet-run": "^0.3.0" 11 | }, 12 | "dependencies": { 13 | "cowsay": "^1.3.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample/basic/print.ts: -------------------------------------------------------------------------------- 1 | // This file is to show that bundling works only 2 | // See ./greet.ts 3 | 4 | export default function print (message: string) { 5 | console.log(message) 6 | } 7 | -------------------------------------------------------------------------------- /sample/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es5", 8 | "module": "commonjs", 9 | "lib": ["es2015"], 10 | "outDir": "dist", 11 | "declaration": true, 12 | "strict": true 13 | }, 14 | "include": [ 15 | "./*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /sample/mocha-enzyme/README.md: -------------------------------------------------------------------------------- 1 | # Mocha & Enzyme Sample 2 | 3 | Run the sample test: 4 | 5 | ```sh 6 | npm install 7 | npm test 8 | ``` 9 | 10 | It will simply run: 11 | 12 | ```sh 13 | puppet-run plugin:mocha ./test 14 | ``` 15 | -------------------------------------------------------------------------------- /sample/mocha-enzyme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-run-sample-mocha-enzyme", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "puppet-run plugin:mocha ./test" 7 | }, 8 | "devDependencies": { 9 | "@types/chai": "^4.1.7", 10 | "@types/enzyme": "^3.1.15", 11 | "@types/enzyme-adapter-react-16": "^1.0.3", 12 | "@types/mocha": "^5.2.5", 13 | "chai": "^4.2.0", 14 | "enzyme": "^3.7.0", 15 | "enzyme-adapter-react-16": "^1.7.0", 16 | "mocha": "^5.2.0", 17 | "puppet-run": "^0.3.0", 18 | "puppet-run-plugin-mocha": "^0.1.1" 19 | }, 20 | "dependencies": { 21 | "react": "^16.6.3", 22 | "react-dom": "^16.6.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/mocha-enzyme/test.tsx: -------------------------------------------------------------------------------- 1 | import { expect } from "chai" 2 | import Enzyme, { mount } from "enzyme" 3 | import Adapter from "enzyme-adapter-react-16" 4 | import React from "react" 5 | 6 | Enzyme.configure({ adapter: new Adapter() }) 7 | Object.assign(window as any, { React }) 8 | 9 | const Button = (props: { children: React.ReactNode }) => ( 10 | 13 | ) 14 | 15 | describe("Button", function () { 16 | it("renders", function () { 17 | const rendered = mount() 18 | expect(rendered.type()).to.equal(Button) 19 | expect(rendered.text()).to.equal("Click me") 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /sample/mocha-enzyme/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es5", 8 | "module": "commonjs", 9 | "jsx": "react", 10 | "lib": ["dom", "es2015"], 11 | "outDir": "dist", 12 | "declaration": true, 13 | "strict": true 14 | }, 15 | "include": [ 16 | "./*.tsx" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /sample/mocha/README.md: -------------------------------------------------------------------------------- 1 | # Mocha Sample 2 | 3 | Run the sample test: 4 | 5 | ```sh 6 | npm install 7 | npm test 8 | ``` 9 | 10 | It will simply run: 11 | 12 | ```sh 13 | puppet-run plugin:mocha ./test.ts 14 | ``` 15 | -------------------------------------------------------------------------------- /sample/mocha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-run-sample-mocha", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "puppet-run plugin:mocha ./test.ts" 7 | }, 8 | "devDependencies": { 9 | "@types/chai": "^4.1.7", 10 | "@types/mocha": "^5.2.5", 11 | "chai": "^4.2.0", 12 | "mocha": "^5.2.0", 13 | "puppet-run": "^0.3.0", 14 | "puppet-run-plugin-mocha": "^0.1.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/mocha/test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai" 2 | 3 | describe("1 + 2", function () { 4 | it("equals 3", function () { 5 | expect(1 + 2).to.equal(3) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /sample/mocha/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es5", 8 | "module": "commonjs", 9 | "lib": ["es2015"], 10 | "outDir": "dist", 11 | "declaration": true, 12 | "strict": true 13 | }, 14 | "include": [ 15 | "./*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /sample/tape/README.md: -------------------------------------------------------------------------------- 1 | # Tape Sample 2 | 3 | Run the sample test: 4 | 5 | ```sh 6 | npm install 7 | npm test 8 | ``` 9 | 10 | It will simply run: 11 | 12 | ```sh 13 | puppet-run ./test.js 14 | ``` 15 | -------------------------------------------------------------------------------- /sample/tape/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-run-sample-tape", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "puppet-run ./test.js" 7 | }, 8 | "devDependencies": { 9 | "puppet-run": "^0.3.0", 10 | "tape": "^4.9.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/tape/test.js: -------------------------------------------------------------------------------- 1 | import test from "tape" 2 | 3 | test.onFinish(() => puppet.exit(0)) 4 | test.onFailure(() => puppet.exit(1)) 5 | 6 | test("1 + 2 = 3", t => { 7 | t.plan(1) 8 | t.is(1 + 2, 3) 9 | }) 10 | -------------------------------------------------------------------------------- /src/bundle.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as path from "path" 3 | import babelify from "babelify" 4 | import browserify from "browserify" 5 | import envify from "@goto-bus-stop/envify" 6 | import mkdirp from "mkdirp" 7 | import nanoid from "nanoid" 8 | import { TemporaryFileCache } from "./temporary" 9 | import { Entrypoint } from "./types" 10 | 11 | export async function createBundle (entry: Entrypoint, cache: TemporaryFileCache): Promise { 12 | // TODO: Use persistent cache 13 | 14 | const servePath = (entry.servePath || `${path.basename(entry.sourcePath)}-${nanoid(6)}`).replace(/\.(jsx?|tsx?)/i, ".js") 15 | const bundleFilePath = path.join(cache, servePath) 16 | const extensions = ["", ".js", ".jsx", ".ts", ".tsx", ".json"] 17 | 18 | mkdirp.sync(path.dirname(bundleFilePath)) 19 | 20 | await new Promise(resolve => { 21 | const stream = browserify({ 22 | debug: true, // enables inline sourcemaps 23 | entries: [entry.sourcePath], 24 | extensions 25 | }) 26 | .transform(babelify.configure({ 27 | cwd: __dirname, 28 | extensions, 29 | presets: [ 30 | "@babel/preset-typescript", 31 | "@babel/preset-react", 32 | "@babel/preset-env" 33 | ], 34 | root: process.cwd() 35 | } as any)) 36 | .transform(envify) 37 | .bundle() 38 | .pipe(fs.createWriteStream(bundleFilePath)) 39 | 40 | stream.on("finish", resolve) 41 | }) 42 | 43 | return { 44 | servePath, 45 | sourcePath: bundleFilePath 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/chrome-location.ts: -------------------------------------------------------------------------------- 1 | // Original source: 2 | 3 | import * as fs from "fs" 4 | import * as os from "os" 5 | import * as path from "path" 6 | import which from "which" 7 | 8 | const osx = process.platform === 'darwin' 9 | const win = process.platform === 'win32' 10 | const other = !osx && !win 11 | 12 | export function getChromeLocation () { 13 | if (other) { 14 | try { 15 | return which.sync('google-chrome') 16 | } catch(e) { 17 | return null 18 | } 19 | } else if (osx) { 20 | const regPath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' 21 | const altPath = path.join(os.homedir(), regPath.slice(1)) 22 | 23 | return fs.existsSync(regPath) 24 | ? regPath 25 | : altPath 26 | } else { 27 | const suffix = '\\Google\\Chrome\\Application\\chrome.exe'; 28 | const prefixes = [ 29 | process.env.LOCALAPPDATA 30 | , process.env.PROGRAMFILES 31 | , process.env['PROGRAMFILES(X86)'] 32 | ] 33 | 34 | for (const prefix of prefixes) { 35 | const exe = prefix + suffix 36 | if (fs.existsSync(exe)) { 37 | return exe 38 | } 39 | } 40 | } 41 | return null 42 | } 43 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import getPort from "get-port" 4 | import meow from "meow" 5 | import minimist from "minimist" 6 | import ora from "ora" 7 | import path from "path" 8 | import { createBundle } from "./bundle" 9 | import { copyFiles, dedupeSourceFiles, resolveDirectoryEntrypoints } from "./fs" 10 | import { loadPlugin, printPluginHelp, resolveEntrypoints } from "./plugins" 11 | import { spawnPuppet } from "./puppeteer" 12 | import { serveDirectory } from "./server" 13 | import { clearTemporaryFileCache, createTemporaryFileCache, writeBlankHtmlPage } from "./temporary" 14 | import { Entrypoint } from "./types" 15 | 16 | const cli = meow(` 17 | Usage 18 | $ puppet-run <./entrypoint> [...more entrypoints] [-- <...script arguments>] 19 | $ puppet-run <./entrypoint>: [...more entrypoints] [-- <...script args>] 20 | $ puppet-run --plugin= [<...entrypoints>] [-- <...script arguments>] 21 | 22 | Options 23 | --help Show this help. 24 | --inspect Run in actual Chrome window and keep it open. 25 | --bundle <./file>[:] Bundle and serve additional files, but don't inject them. 26 | --p , --port Serve on this port. Defaults to random port. 27 | --plugin Load and apply plugin . 28 | --serve <./file>[:] Serve additional files next to bundle. 29 | 30 | Example 31 | $ puppet-run ./sample/cowsays.js 32 | $ puppet-run ./sample/greet.ts newbie 33 | $ puppet-run --plugin=mocha ./sample/mocha-test.ts 34 | `, { 35 | autoHelp: false 36 | }) 37 | 38 | function ensureArray (arg: string | string[] | undefined): string[] { 39 | if (!arg) { 40 | return [] 41 | } else if (Array.isArray(arg)) { 42 | return arg 43 | } else { 44 | return [arg] 45 | } 46 | } 47 | 48 | function parseEntrypointArg (arg: string): Entrypoint { 49 | const [sourcePath, servePath] = arg.split(":") 50 | return { 51 | servePath, 52 | sourcePath 53 | } 54 | } 55 | 56 | async function withSpinner(promise: Promise): Promise { 57 | const spinner = ora("Bundling code").start() 58 | 59 | try { 60 | const result = await promise 61 | spinner.succeed("Bundling done.") 62 | return result 63 | } catch (error) { 64 | spinner.fail("Bundling failed.") 65 | throw error // re-throw 66 | } 67 | } 68 | 69 | const argsSeparatorIndex = process.argv.indexOf("--") 70 | const runnerOptionArgs = argsSeparatorIndex > -1 ? process.argv.slice(2, argsSeparatorIndex) : process.argv.slice(2) 71 | const scriptArgs = argsSeparatorIndex > -1 ? process.argv.slice(argsSeparatorIndex + 1) : [] 72 | 73 | const runnerOptions = minimist(runnerOptionArgs) 74 | 75 | const pluginNames = Array.isArray(runnerOptions.plugin || []) 76 | ? runnerOptions.plugin || [] 77 | : [runnerOptions.plugin] 78 | 79 | const plugins = pluginNames.map(loadPlugin) 80 | 81 | if (runnerOptionArgs.indexOf("--help") > -1 && plugins.length > 0) { 82 | printPluginHelp(plugins[0], scriptArgs) 83 | process.exit(0) 84 | } else if (process.argv.length === 2 || runnerOptionArgs.indexOf("--help") > -1) { 85 | cli.showHelp() 86 | process.exit(0) 87 | } 88 | 89 | async function run() { 90 | let exitCode = 0 91 | 92 | const headless = runnerOptionArgs.indexOf("--inspect") > -1 ? false : true 93 | const port = runnerOptions.p || runnerOptions.port 94 | ? parseInt(runnerOptions.p || runnerOptions.port, 10) 95 | : await getPort() 96 | 97 | const additionalBundleEntries = await resolveDirectoryEntrypoints( 98 | ensureArray(runnerOptions.bundle).map(parseEntrypointArg), 99 | filenames => dedupeSourceFiles(filenames, true) 100 | ) 101 | const additionalFilesToServe = await resolveDirectoryEntrypoints(ensureArray(runnerOptions.serve).map(parseEntrypointArg)) 102 | 103 | const entrypointArgs = runnerOptions._ 104 | const entrypoints = await resolveEntrypoints(plugins, entrypointArgs.map(parseEntrypointArg), scriptArgs) 105 | 106 | const temporaryCache = createTemporaryFileCache() 107 | 108 | try { 109 | const serverURL = `http://localhost:${port}/` 110 | writeBlankHtmlPage(path.join(temporaryCache, "index.html")) 111 | 112 | const allBundles = await withSpinner( 113 | Promise.all([...entrypoints, ...additionalBundleEntries].map(entrypoint => { 114 | return createBundle(entrypoint, temporaryCache) 115 | })) 116 | ) 117 | 118 | const startupBundles = allBundles.slice(0, entrypoints.length) 119 | const lazyBundles = allBundles.slice(entrypoints.length) 120 | 121 | await copyFiles([...additionalFilesToServe, ...lazyBundles], temporaryCache) 122 | 123 | const closeServer = await serveDirectory(temporaryCache, port) 124 | const puppet = await spawnPuppet(startupBundles.map(entry => entry.servePath!), serverURL, { headless }) 125 | await puppet.run(scriptArgs, plugins) 126 | 127 | exitCode = await puppet.waitForExit() 128 | await puppet.close() 129 | closeServer() 130 | } catch (error) { 131 | if (headless) { 132 | throw error 133 | } else { 134 | // tslint:disable-next-line:no-console 135 | console.error(error) 136 | await new Promise(resolve => process.on("SIGINT", resolve)) 137 | } 138 | } finally { 139 | if (process.env.KEEP_TEMP_CACHE) { 140 | // tslint:disable-next-line:no-console 141 | console.log(`Temporary cache written to: ${temporaryCache}`) 142 | } else { 143 | clearTemporaryFileCache(temporaryCache) 144 | } 145 | } 146 | 147 | if (exitCode > 0) { 148 | // tslint:disable-next-line:no-console 149 | console.log(`Script exited with exit code ${exitCode}.`) 150 | } 151 | process.exit(exitCode) 152 | } 153 | 154 | run().catch(error => { 155 | // tslint:disable-next-line:no-console 156 | console.error(error) 157 | process.exit(1) 158 | }) 159 | -------------------------------------------------------------------------------- /src/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as path from "path" 3 | import mkdirp from "mkdirp" 4 | import * as util from "util" 5 | import { Entrypoint } from "./types" 6 | 7 | const passthrough = (thing: T) => thing 8 | 9 | const readdir = util.promisify(fs.readdir) 10 | const stat = util.promisify(fs.stat) 11 | 12 | function flatten(nested: T[][]): T[] { 13 | return nested.reduce( 14 | (flattened, subarray) => [...flattened, ...subarray], 15 | [] 16 | ) 17 | } 18 | 19 | export function copyFile (from: string, to: string) { 20 | if (path.resolve(from) === path.resolve(to)) return 21 | 22 | return new Promise(resolve => { 23 | const input = fs.createReadStream(from) 24 | const output = fs.createWriteStream(to) 25 | 26 | input.once("end", resolve) 27 | input.pipe(output) 28 | }) 29 | } 30 | 31 | export async function copyFiles(filesToServe: Entrypoint[], destinationDirectory: string) { 32 | return Promise.all(filesToServe.map( 33 | async ({ servePath, sourcePath }) => { 34 | const servingPath = servePath || path.basename(sourcePath) 35 | const destinationFilePath = path.resolve(destinationDirectory, servingPath.replace(/^\//, "")) 36 | 37 | if (destinationFilePath.substr(0, destinationDirectory.length) !== destinationDirectory) { 38 | throw new Error(`File would be served outside of destination directory: ${sourcePath} => ${servingPath}`) 39 | } 40 | 41 | mkdirp.sync(path.dirname(destinationFilePath)) 42 | await copyFile(sourcePath, destinationFilePath) 43 | } 44 | )) 45 | } 46 | 47 | export function dedupeSourceFiles(basenames: string[], dropNonSourceFiles?: boolean): string[] { 48 | // We don't want to include a source file and its already transpiled version as input 49 | 50 | const sourceExtensionsRegex = /\.(jsx?|tsx?)$/i 51 | const sourceFileNames = basenames.filter(basename => basename.match(sourceExtensionsRegex)) 52 | const nonSourceFileNames = basenames.filter(basename => sourceFileNames.indexOf(basename) === -1) 53 | 54 | const collidingSourceFileNames = sourceFileNames.reduce<{ [name: string]: string[] }>( 55 | (destructured, filename) => { 56 | const ext = path.extname(filename) 57 | const name = filename.substr(0, filename.length - ext.length) 58 | return { 59 | ...destructured, 60 | [name]: (destructured[name] || []).concat([ext]) 61 | } 62 | }, 63 | {} 64 | ) 65 | 66 | const dedupedSourceFileNames = Object.keys(collidingSourceFileNames).map(name => { 67 | const ext = collidingSourceFileNames[name].sort()[0] 68 | return `${name}${ext}` 69 | }) 70 | 71 | return [ 72 | ...(dropNonSourceFiles ? [] : nonSourceFileNames), 73 | ...dedupedSourceFileNames 74 | ] 75 | } 76 | 77 | export async function resolveDirectoryEntrypoints( 78 | entrypoints: Entrypoint[], 79 | filterFiles: (basenames: string[]) => string[] = passthrough 80 | ): Promise { 81 | const nested = await Promise.all( 82 | entrypoints.map(async entry => { 83 | if ((await stat(entry.sourcePath)).isDirectory()) { 84 | const files = filterFiles(await readdir(entry.sourcePath)) 85 | const subentries = files.map(filename => ({ 86 | servePath: entry.servePath ? path.join(entry.servePath, filename) : undefined, 87 | sourcePath: path.join(entry.sourcePath, filename) 88 | })) 89 | return resolveDirectoryEntrypoints(subentries) 90 | } else { 91 | return [entry] 92 | } 93 | }) 94 | ) 95 | return flatten(nested) 96 | } 97 | -------------------------------------------------------------------------------- /src/host-bindings.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | import chalk from "chalk" 3 | import { ConsoleMessage, Page } from "puppeteer-core" 4 | 5 | export interface PuppetContextConfig { 6 | argv: string[], 7 | plugins: PluginsConfig 8 | } 9 | 10 | const magicLogMessageMarker = "$$$PUPPET_MAGIC_LOG$$$" 11 | 12 | async function consoleMessageToLogArgs (message: ConsoleMessage) { 13 | const args = message.args() 14 | const jsonArgs = await Promise.all(args.map( 15 | arg => arg.jsonValue() 16 | )) 17 | 18 | // Clean-up to enable garbage collection 19 | args.forEach(arg => arg.dispose()) 20 | 21 | return jsonArgs 22 | } 23 | 24 | export function capturePuppetConsole (page: Page) { 25 | page.on("console", async message => { 26 | const type = message.type() 27 | 28 | // Ignore magic messages, since they are control messages 29 | if (message.text().startsWith(magicLogMessageMarker)) return 30 | 31 | const consoleArgs = await consoleMessageToLogArgs(message) 32 | 33 | if (type === "clear") { 34 | return console.clear() 35 | } else if (type === "startGroupCollapsed") { 36 | return console.groupCollapsed() 37 | } else if (type === "endGroup") { 38 | return console.groupEnd() 39 | } 40 | 41 | if (type === "error") { 42 | console.error(...consoleArgs) 43 | } else if (type === "warning") { 44 | console.warn(...consoleArgs) 45 | } else if (type === "debug") { 46 | console.debug(...consoleArgs) 47 | } else if (type === "startGroup") { 48 | console.group(...consoleArgs) 49 | } else { 50 | console.log(...consoleArgs) 51 | } 52 | }) 53 | } 54 | 55 | export function captureFailedRequests(page: Page) { 56 | page.on("requestfailed", request => { 57 | const failure = request.failure() 58 | console.error(chalk.redBright(`Request failed: ${request.method()} ${request.url()}`)) 59 | console.error(chalk.gray(` ${failure ? failure.errorText : "Unknown error"}`)) 60 | }) 61 | page.on("requestfinished", request => { 62 | const response = request.response() 63 | if (response && response.status() >= 400) { 64 | console.error(chalk.redBright(`HTTP ${response.status()} ${request.method()} ${request.url()}`)) 65 | } 66 | }) 67 | } 68 | 69 | export function createPuppetContextConfig (argv: string[], plugins: any = {}) { 70 | return { 71 | argv, 72 | plugins 73 | } 74 | } 75 | 76 | export async function injectPuppetContext (page: Page, contextConfig: PuppetContextConfig) { 77 | await page.addScriptTag({ 78 | content: ` 79 | window.puppet = { 80 | argv: ${JSON.stringify(contextConfig.argv)}, 81 | plugins: ${JSON.stringify(contextConfig.plugins)}, 82 | exit (exitCode = 0) { 83 | console.log(${JSON.stringify(magicLogMessageMarker)}, "exit", exitCode) 84 | }, 85 | setOfflineMode (offline = true) { 86 | return window.setPuppetOfflineMode(offline) 87 | } 88 | }; 89 | ` 90 | }) 91 | await page.exposeFunction("setPuppetOfflineMode", (offlineMode: boolean) => page.setOfflineMode(offlineMode)) 92 | } 93 | 94 | export function subscribeToMagicLogs (page: Page, subscriber: (type: string, args: any[]) => void) { 95 | const handler = async (message: ConsoleMessage) => { 96 | const args = message.args() 97 | if (args.length < 2) return 98 | 99 | const firstArgument = await args[0].jsonValue() 100 | if (firstArgument !== magicLogMessageMarker) return 101 | 102 | const [type, ...otherArgs] = await Promise.all( 103 | args.slice(1).map(arg => arg.jsonValue()) 104 | ) 105 | 106 | // Don't forget to dispose eventually, to enable garbage collection of log message args 107 | args.forEach(arg => arg.dispose()) 108 | 109 | subscriber(type, otherArgs) 110 | } 111 | 112 | page.on("console", handler) 113 | const unsubscribe = () => page.off("console", handler) 114 | 115 | return unsubscribe 116 | } 117 | -------------------------------------------------------------------------------- /src/plugins.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent" 2 | import * as path from "path" 3 | import { Entrypoint, Plugin } from "./types" 4 | 5 | export { Plugin } 6 | 7 | function validatePlugin (plugin: Plugin, packageName: string) { 8 | if (typeof plugin.resolveBundleEntrypoints !== "function") { 9 | throw new Error(`Bad plugin package: ${packageName}\nShould export a function "resolveBundleEntrypoints".`) 10 | } 11 | } 12 | 13 | function loadPluginModule (packageName: string, pluginName: string): any { 14 | const searchPaths = [ 15 | path.join(process.cwd(), "node_modules") 16 | ] 17 | try { 18 | const modulePath = require.resolve(packageName, { paths: searchPaths }) 19 | return require(modulePath) 20 | } catch (error) { 21 | throw new Error( 22 | `Cannot load plugin ${pluginName}. Module could not be loaded: ${packageName}\n\n` + 23 | `Try installing the module:\n\n` + 24 | ` npm install --save-dev ${packageName}\n\n` + 25 | `Caught error: ${error.message}\n` 26 | ) 27 | } 28 | } 29 | 30 | export function loadPlugin (entrypointArgument: string): Plugin { 31 | const pluginName = entrypointArgument.replace(/^plugin:/, "") 32 | 33 | const packageName = pluginName.startsWith("puppet-run-plugin-") 34 | ? pluginName 35 | : `puppet-run-plugin-${pluginName}` 36 | 37 | const loadedModule = loadPluginModule(packageName, pluginName) 38 | validatePlugin(loadedModule, packageName) 39 | 40 | return { 41 | ...loadedModule, 42 | packageName 43 | } 44 | } 45 | 46 | export function printPluginHelp (plugin: Plugin, scriptArgs: string[]) { 47 | if (plugin.help) { 48 | // tslint:disable-next-line:no-console 49 | console.log(dedent( 50 | plugin.help(scriptArgs) 51 | )) 52 | } else { 53 | // tslint:disable-next-line:no-console 54 | console.log(dedent(` 55 | ${plugin.packageName} 56 | 57 | No plugin help available. 58 | `)) 59 | } 60 | } 61 | 62 | export async function resolveEntrypoints(plugins: Plugin[], initialEntrypoints: Entrypoint[], scriptArgs: string[]): Promise { 63 | let entrypoints: Entrypoint[] = initialEntrypoints 64 | 65 | for (const plugin of plugins) { 66 | entrypoints = plugin.resolveBundleEntrypoints 67 | ? await plugin.resolveBundleEntrypoints(entrypoints, scriptArgs) 68 | : entrypoints 69 | } 70 | 71 | return entrypoints 72 | } 73 | 74 | export async function createRuntimeConfig(plugins: Plugin[], scriptArgs: string[]) { 75 | let config: any = {} 76 | 77 | for (const plugin of plugins) { 78 | config = plugin.extendPuppetDotPlugins 79 | ? await plugin.extendPuppetDotPlugins(config, scriptArgs) 80 | : config 81 | } 82 | 83 | return config 84 | } 85 | -------------------------------------------------------------------------------- /src/puppeteer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as path from "path" 3 | import { launch, Page } from "puppeteer-core" 4 | import { URL } from "url" 5 | import { getChromeLocation } from "./chrome-location" 6 | import { 7 | captureFailedRequests, 8 | capturePuppetConsole, 9 | createPuppetContextConfig, 10 | injectPuppetContext, 11 | subscribeToMagicLogs 12 | } from "./host-bindings" 13 | import { createRuntimeConfig, Plugin } from "./plugins" 14 | import ScriptError from "./script-error" 15 | 16 | declare const window: any; 17 | 18 | export interface Puppet { 19 | on: Page["on"], 20 | once: Page["once"], 21 | off: Page["off"], 22 | close (): Promise, 23 | run (argv: string[], plugins?: Plugin[]): Promise, 24 | waitForExit (): Promise 25 | } 26 | 27 | const pendingPagePromises: Array> = [] 28 | 29 | function trackPendingPagePromise (promise: Promise) { 30 | pendingPagePromises.push(promise) 31 | return promise 32 | } 33 | 34 | async function loadBundle (page: Page, bundleFilePath: string, serverURL: string): Promise { 35 | await page.addScriptTag({ 36 | content: fs.readFileSync(require.resolve("sourcemapped-stacktrace/dist/sourcemapped-stacktrace.js"), "utf8") 37 | }) 38 | 39 | await page.addScriptTag({ 40 | url: new URL(bundleFilePath, serverURL).toString() 41 | }) 42 | } 43 | 44 | async function resolveStackTrace (page: Page, stackTrace: string) { 45 | const resolvedStackTrace = await page.evaluate(stack => new Promise(resolve => 46 | window.sourceMappedStackTrace.mapStackTrace(stack, (newStack: string[]) => { 47 | resolve(newStack.join("\n")) 48 | }) 49 | ), stackTrace) 50 | const replacePathInStackTrace = (matched: string, matchedPath: string) => { 51 | return matched.replace(matchedPath, path.relative(process.cwd(), matchedPath)) 52 | } 53 | return resolvedStackTrace.replace(/ at [^\(]+\((\.\.[^:]+):[0-9]/gm, replacePathInStackTrace) 54 | } 55 | 56 | async function resolveToScriptError (page: Page, error: Error) { 57 | if (!error.stack) { 58 | const endOfActualMessageIndex = error.message ? error.message.indexOf("\n at ") : -1 59 | if (endOfActualMessageIndex === -1) { 60 | return new ScriptError(error) 61 | } else { 62 | const messageWithStack = error.message 63 | const messageWithResolvedStack = [ 64 | error.message.substr(0, endOfActualMessageIndex), 65 | await resolveStackTrace(page, messageWithStack) 66 | ].join("\n") 67 | error.message = error.message.substr(0, endOfActualMessageIndex) 68 | return new ScriptError(error, messageWithResolvedStack) 69 | } 70 | } 71 | if (Array.isArray(error.stack)) { 72 | return new ScriptError(error, await resolveStackTrace(page, error.stack.join("\n"))) 73 | } else { 74 | return new ScriptError(error, await resolveStackTrace(page, error.stack)) 75 | } 76 | } 77 | 78 | function createExitPromise (page: Page) { 79 | let exited = false 80 | return new Promise((resolve, reject) => { 81 | subscribeToMagicLogs(page, (type, args) => { 82 | if (type === "exit") { 83 | exited = true 84 | resolve(args[0] as number) 85 | } 86 | }) 87 | // tslint:disable-next-line:no-console 88 | const fail = (error: Error) => exited ? console.error(error) : reject(error) 89 | const handleScriptError = (error: Error) => { 90 | trackPendingPagePromise(resolveToScriptError(page, error)) 91 | .then(scriptError => fail(scriptError)) 92 | .catch(internalError => { 93 | // tslint:disable:no-console 94 | console.error("Internal error while resolving script error:") 95 | console.error(internalError) 96 | // tslint:enable:no-console 97 | fail(error) 98 | }) 99 | } 100 | page.once("error", handleScriptError) 101 | page.once("pageerror", handleScriptError) 102 | }) 103 | } 104 | 105 | export async function spawnPuppet(bundleFilePaths: string[], serverURL: string, options: { headless?: boolean }): Promise { 106 | let puppetExit: Promise 107 | const { headless = true } = options 108 | 109 | const browser = await launch({ 110 | executablePath: getChromeLocation() || undefined, 111 | headless 112 | }) 113 | 114 | const [ page ] = await browser.pages() 115 | 116 | // Navigate to a secure origin first. See 117 | await page.goto(serverURL + "index.html") 118 | 119 | capturePuppetConsole(page) 120 | captureFailedRequests(page) 121 | 122 | return { 123 | async close () { 124 | if (headless) { 125 | await Promise.all(pendingPagePromises) 126 | await browser.close() 127 | } else { 128 | return new Promise(resolve => undefined) 129 | } 130 | }, 131 | async run (argv: string[], plugins: Plugin[] = []) { 132 | const pluginsConfig = await createRuntimeConfig(plugins, argv) 133 | const contextConfig = createPuppetContextConfig(argv, pluginsConfig) 134 | puppetExit = createExitPromise(page) 135 | 136 | await injectPuppetContext(page, contextConfig) 137 | 138 | // Load bundles sequentially 139 | for (const bundlePath of bundleFilePaths) { 140 | await loadBundle(page, bundlePath, serverURL) 141 | } 142 | }, 143 | async waitForExit () { 144 | return puppetExit 145 | }, 146 | off: page.removeListener.bind(page), 147 | on: page.on.bind(page), 148 | once: page.once.bind(page), 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/script-error.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line 2 | interface ScriptError extends Error { } 3 | 4 | // tslint:disable-next-line:no-shadowed-variable 5 | const ScriptError = function ScriptError(this: ScriptError, error: Error, stack?: string) { 6 | Error.call(this, error as any) 7 | Object.defineProperty(this, "message", { 8 | enumerable: false, 9 | value: error.message 10 | }) 11 | Object.defineProperty(this, "stack", { 12 | enumerable: false, 13 | value: stack || error.stack 14 | }) 15 | } as any as { new(error: Error, stack?: string): ScriptError } 16 | 17 | ScriptError.prototype = new Error() 18 | ScriptError.prototype.name = "ScriptError" 19 | 20 | export default ScriptError 21 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http" 2 | import serve from "serve-handler" 3 | 4 | export async function serveDirectory(dirPath: string, port: number) { 5 | const server = http.createServer((req, res) => { 6 | serve(req, res, { 7 | cleanUrls: false, 8 | directoryListing: true, 9 | public: dirPath 10 | }) 11 | }) 12 | await new Promise((resolve, reject) => { 13 | server.listen(port, "127.0.0.1", (error?: Error) => error ? reject(error) : resolve()) 14 | }) 15 | const closeServer = () => server.close() 16 | return closeServer 17 | } 18 | -------------------------------------------------------------------------------- /src/temporary.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as os from "os" 3 | import * as path from "path" 4 | import del from "del" 5 | 6 | export type TemporaryFileCache = string 7 | 8 | export function createTemporaryFileCache (): TemporaryFileCache { 9 | return fs.mkdtempSync(path.join(os.tmpdir(), "puppet-run-cache")) 10 | } 11 | 12 | export function clearTemporaryFileCache (cache: TemporaryFileCache) { 13 | del.sync([cache], { force: true }) 14 | } 15 | 16 | export function writeBlankHtmlPage (filePath: string) { 17 | const content = ` 18 | 19 | 20 | 21 | 22 | 23 | `.trim() 24 | fs.writeFileSync(filePath, content, "utf8") 25 | } 26 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Entrypoint { 2 | servePath?: string 3 | sourcePath: string 4 | } 5 | 6 | export interface Plugin { 7 | packageName: string, 8 | extendPuppetDotPlugins?( 9 | puppetDotPlugins: InputConfig, 10 | scriptArgs: string[] 11 | ): Promise, 12 | help?(scriptArgs: string[]): string, 13 | resolveBundleEntrypoints?(entrypoints: Entrypoint[], scriptArgs: string[]): Promise 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "target": "es5", 7 | "module": "commonjs", 8 | "lib": ["dom", "es2015"], 9 | "outDir": "dist", 10 | "declaration": true, 11 | "strict": true 12 | }, 13 | "include": [ 14 | "./src/cli.ts", 15 | "./types/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-config-prettier" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "interface-name": [ 9 | true, 10 | "never-prefix" 11 | ], 12 | "no-implicit-dependencies": [ 13 | true, 14 | "dev" 15 | ], 16 | "curly": [true, "ignore-same-line"], 17 | "no-object-literal-type-assertion": false, 18 | "no-submodule-imports": false, 19 | "object-literal-sort-keys": false, 20 | "only-arrow-functions": false, 21 | "ordered-imports": false, 22 | "semicolon": false 23 | }, 24 | "rulesDirectory": [] 25 | } 26 | -------------------------------------------------------------------------------- /types/envify.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@goto-bus-stop/envify" { 2 | import stream from "stream" 3 | 4 | function applyEnvify(file: string, argv: any[]): stream.Duplex 5 | 6 | export = applyEnvify 7 | } 8 | 9 | declare module "@goto-bus-stop/envify/custom" { 10 | import stream from "stream" 11 | 12 | function envify(customVars?: { [name: string]: string }): (file: string, argv: any[]) => stream.Duplex 13 | 14 | export = envify 15 | } 16 | -------------------------------------------------------------------------------- /types/serve-handler.d.ts: -------------------------------------------------------------------------------- 1 | declare module "serve-handler" { 2 | import * as Http from "http" 3 | 4 | interface Options { 5 | cleanUrls?: boolean 6 | directoryListing?: boolean | string[] 7 | public?: string 8 | // incomplete 9 | } 10 | 11 | function serve(req: Http.IncomingMessage, res: Http.ServerResponse, options?: Options): Promise 12 | 13 | export = serve 14 | } 15 | --------------------------------------------------------------------------------