├── .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 |
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 |
11 | {props.children}
12 |
13 | )
14 |
15 | describe("Button", function () {
16 | it("renders", function () {
17 | const rendered = mount(Click me )
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 |
--------------------------------------------------------------------------------