├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── example ├── counter.ts ├── example.ts └── server.ts ├── mod.ts ├── runner.ts ├── test ├── count.ts ├── failure.ts ├── test-task.ts └── test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | tmp -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: minimal 2 | 3 | os: 4 | - osx 5 | - linux 6 | 7 | before_install: 8 | - export PATH=$HOME/.deno/bin:$PATH 9 | 10 | install: 11 | - curl -L https://deno.land/x/install/install.py | python 12 | 13 | script: 14 | - deno -v 15 | - mkdir tmp 16 | - deno --allow-run --allow-env test/test-task.ts --cwd=test | tee tmp/result 17 | - deno --allow-run --allow-env test/test.ts 18 | - deno --allow-run --allow-env example/example.ts --cwd=example all 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yosuke Torii 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno-task-runner 2 | 3 | [![Build Status](https://travis-ci.org/jinjor/deno-task-runner.svg?branch=master)](https://travis-ci.org/jinjor/deno-task-runner) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/6kbm7dxgsk7x6wl0?svg=true)](https://ci.appveyor.com/project/jinjor/deno-task-runner) 5 | 6 | Write tasks in deno. 7 | 8 | ## Example 9 | 10 | ```typescript 11 | import { task } from "https://deno.land/x/task_runner/mod.ts"; 12 | 13 | task("prepare", "echo preparing..."); 14 | task("counter", "deno counter.ts"); 15 | task("thumb", "deno https://deno.land/thumb.ts"); 16 | task("all", "$prepare", ["$counter alice", "$counter bob"], "$thumb"); 17 | // ^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ 18 | // 1st task 2nd task (parallel) 3rd task 19 | ``` 20 | 21 | ``` 22 | $ deno example.ts all --allow-run 23 | preparing... 24 | bob 1 25 | alice 1 26 | alice 2 27 | bob 2 28 | alice 3 29 | bob 3 30 | alice 4 31 | bob 4 32 | bob 5 33 | alice 5 34 | 👍 35 | ``` 36 | 37 | ## Watch 38 | 39 | ```typescript 40 | task("compile", "echo changed", "$all").watchSync("src"); 41 | task("dev-server", "echo restarting...", "$server").watch("server"); 42 | ``` 43 | 44 | ## LICENSE 45 | 46 | MIT 47 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - ps: iex (iwr https://deno.land/x/install/install.ps1) 3 | 4 | test_script: 5 | - deno -v 6 | - mkdir tmp 7 | - deno --allow-run --allow-env test/test-task.ts --cwd=test | tee tmp/result 8 | - deno --allow-run --allow-env test/test.ts 9 | - deno --allow-run --allow-env example/example.ts --cwd=example all 10 | 11 | build: off 12 | -------------------------------------------------------------------------------- /example/counter.ts: -------------------------------------------------------------------------------- 1 | import { args } from "deno"; 2 | const name = args[1] || ""; 3 | let i = 0; 4 | const interval = setInterval(() => { 5 | console.log(name, ++i); 6 | if (i >= 5) { 7 | clearInterval(interval); 8 | } 9 | }, 600); 10 | -------------------------------------------------------------------------------- /example/example.ts: -------------------------------------------------------------------------------- 1 | import { task } from "../mod.ts"; 2 | 3 | task("prepare", "echo preparing..."); 4 | task("counter", "deno counter.ts"); 5 | task("thumb", "deno https://deno.land/thumb.ts"); 6 | task("all", "$prepare", ["$counter alice", "$counter bob"], "$thumb"); 7 | task("start", "echo changed", "$all").watchSync("."); 8 | 9 | task("server", "deno server.ts"); 10 | task("dev", "echo restarting...", "$server").watch("."); 11 | -------------------------------------------------------------------------------- /example/server.ts: -------------------------------------------------------------------------------- 1 | const name = Math.floor(Math.random() * 1000); 2 | setInterval(() => { 3 | console.log(`${name} running...`); 4 | }, 600); 5 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { args, exit } from "deno"; 2 | import * as flags from "https://deno.land/x/flags@v0.2.5/index.ts"; 3 | import { TaskRunner, TaskDecorator } from "runner.ts"; 4 | 5 | const globalRunner = new TaskRunner(); 6 | 7 | /** Define a task. 8 | * 9 | * ``` 10 | * task("prepare", "echo preparing..."); 11 | * task("counter", "deno counter.ts"); 12 | * task("thumb", "deno https://deno.land/thumb.ts"); 13 | * task("all", "$prepare", ["$counter alice", "$counter bob"], "$thumb"); 14 | * ^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ 15 | * 1st task 2nd task (parallel) 3rd task 16 | * ``` 17 | * 18 | * - Use $name to call other tasks. You can also pass arguments. 19 | * - Use array to run commands in parallel. 20 | * 21 | * Add file watcher. Usage is the same as https://github.com/jinjor/deno-watch. 22 | * 23 | * ``` 24 | * task("compile", "echo changed", "$all").watchSync("src", options); 25 | * task("dev-server", "echo restarting...", "$server").watch("server"); 26 | * ``` 27 | * 28 | * - `watchSync` waits for running tasks, while `watch` does not. 29 | * - `watch` kills processes if they are running. 30 | */ 31 | export function task( 32 | name: string, 33 | ...rawCommands: (string | string[])[] 34 | ): TaskDecorator { 35 | return globalRunner.task(name, ...rawCommands); 36 | } 37 | 38 | new Promise(resolve => setTimeout(resolve, 0)) 39 | .then(async () => { 40 | const parsedArgs = flags.parse(args); 41 | const cwd = parsedArgs.cwd || "."; 42 | const taskName = parsedArgs._[1]; 43 | const taskArgs = parsedArgs._.splice(2); 44 | if (!taskName) { 45 | throw new Error("Usage: task_file.ts task_name [--cwd]"); 46 | } 47 | await globalRunner.run(taskName, taskArgs, { cwd }); 48 | }) 49 | .catch(e => { 50 | console.error(e); 51 | exit(1); 52 | }); 53 | -------------------------------------------------------------------------------- /runner.ts: -------------------------------------------------------------------------------- 1 | import { ProcessStatus, Closer, Process } from "deno"; 2 | import * as deno from "deno"; 3 | import { 4 | watch, 5 | Options as WatchOptions 6 | } from "https://deno.land/x/watch@1.2.0/mod.ts"; 7 | import * as path from "https://deno.land/x/fs/path.ts"; // should fix later 8 | 9 | type Tasks = { [name: string]: Command }; 10 | interface ResolveContext { 11 | checked: Set; 12 | hasWatcher: boolean; 13 | } 14 | class ProcessError extends Error { 15 | constructor( 16 | public pid: number, 17 | public rid: number, 18 | public status: ProcessStatus, 19 | public taskName?: string 20 | ) { 21 | super("Process exited with status code " + status.code); 22 | } 23 | } 24 | interface Command { 25 | resolveRef(tasks: Tasks, context: ResolveContext): Command; 26 | run(args: string[], context: RunContext): Promise; 27 | } 28 | class Single implements Command { 29 | constructor(public script: string) {} 30 | resolveRef(tasks: Tasks, _: ResolveContext) { 31 | return this; 32 | } 33 | async run(args: string[], { cwd, shell, resources }: RunContext) { 34 | const allArgs = shell 35 | ? [...getShellCommand(), [this.script, ...args].join(" ")] 36 | : [...this.script.split(/\s/), ...args]; 37 | const p = deno.run({ 38 | args: allArgs, 39 | cwd: cwd, 40 | stdout: "inherit", 41 | stderr: "inherit" 42 | }); 43 | const closer = { 44 | close() { 45 | kill(p); 46 | } 47 | }; 48 | resources.add(closer); 49 | const status = await p.status(); 50 | p.close(); 51 | resources.delete(closer); 52 | if (!status.success) { 53 | throw new ProcessError(p.pid, p.rid, status); 54 | } 55 | } 56 | } 57 | 58 | function getShellCommand(): string[] { 59 | let env = deno.env(); 60 | if (deno.platform.os === "win") { 61 | return [env.COMSPEC || "cmd.exe", "/D", "/C"]; 62 | } else { 63 | return [env.SHELL || "/bin/sh", "-c"]; 64 | } 65 | } 66 | 67 | async function kill(p: Process) { 68 | const k = deno.run({ 69 | args: ["kill", `${p.pid}`], 70 | stdout: "inherit", 71 | stderr: "inherit" 72 | }); 73 | await k.status(); 74 | k.close(); 75 | } 76 | 77 | class Ref implements Command { 78 | constructor(public script: string) {} 79 | resolveRef(tasks: Tasks, context: ResolveContext) { 80 | const splitted = this.script.split(/\s/); 81 | const name = splitted[0].slice(1); 82 | const args = splitted.slice(1); 83 | if (!name.length) { 84 | throw new Error("Task name should not be empty."); 85 | } 86 | 87 | let command = tasks[name]; 88 | if (!command) { 89 | throw new Error(`Task "${name}" is not defined.`); 90 | } 91 | if (context.checked.has(name)) { 92 | throw new Error(`Task "${name}" is in a reference loop.`); 93 | } 94 | if (command instanceof Single) { 95 | command = new Single([command.script, ...args].join(" ")); 96 | } 97 | return command.resolveRef(tasks, { 98 | ...context, 99 | checked: new Set(context.checked).add(name) 100 | }); 101 | } 102 | async run(args: string[], context: RunContext) { 103 | throw new Error("Ref should be resolved before running."); 104 | } 105 | } 106 | class Sequence implements Command { 107 | commands: Command[]; 108 | constructor(commands: Command[]) { 109 | this.commands = commands; 110 | } 111 | resolveRef(tasks: Tasks, context: ResolveContext) { 112 | return new Sequence( 113 | this.commands.map(c => { 114 | return c.resolveRef(tasks, context); 115 | }) 116 | ); 117 | } 118 | async run(args: string[], context: RunContext) { 119 | if (args.length) { 120 | throw new Error("Cannot pass args to sequential tasks."); 121 | } 122 | for (let command of this.commands) { 123 | await command.run([], context); 124 | } 125 | } 126 | } 127 | class Parallel implements Command { 128 | commands: Command[]; 129 | constructor(commands: Command[]) { 130 | this.commands = commands; 131 | } 132 | resolveRef(tasks: Tasks, context: ResolveContext) { 133 | return new Parallel( 134 | this.commands.map(c => { 135 | return c.resolveRef(tasks, context); 136 | }) 137 | ); 138 | } 139 | async run(args: string[], context: RunContext) { 140 | if (args.length) { 141 | throw new Error("Cannot pass args to parallel tasks."); 142 | } 143 | await Promise.all(this.commands.map(c => c.run([], context))); 144 | } 145 | } 146 | class SyncWatcher implements Command { 147 | constructor( 148 | public dirs: string[], 149 | public watchOptions: WatchOptions, 150 | public command: Command 151 | ) {} 152 | resolveRef(tasks: Tasks, context: ResolveContext) { 153 | if (context.hasWatcher) { 154 | throw new Error("Nested watchers not supported."); 155 | } 156 | return new SyncWatcher( 157 | this.dirs, 158 | this.watchOptions, 159 | this.command.resolveRef(tasks, { ...context, hasWatcher: true }) 160 | ); 161 | } 162 | async run(args: string[], context: RunContext) { 163 | const dirs_ = this.dirs.map(d => { 164 | return path.join(context.cwd, d); 165 | }); 166 | const childResources = new Set(); 167 | await this.command 168 | .run(args, { ...context, resources: childResources }) 169 | .catch(_ => {}); 170 | for await (const _ of watch(dirs_, this.watchOptions)) { 171 | closeResouces(childResources); 172 | await this.command 173 | .run(args, { ...context, resources: childResources }) 174 | .catch(_ => {}); 175 | } 176 | } 177 | } 178 | class AsyncWatcher implements Command { 179 | constructor( 180 | public dirs: string[], 181 | public watchOptions: WatchOptions, 182 | public command: Command 183 | ) {} 184 | resolveRef(tasks: Tasks, context: ResolveContext) { 185 | if (context.hasWatcher) { 186 | throw new Error("Nested watchers not supported."); 187 | } 188 | return new AsyncWatcher( 189 | this.dirs, 190 | this.watchOptions, 191 | this.command.resolveRef(tasks, { ...context, hasWatcher: true }) 192 | ); 193 | } 194 | async run(args: string[], context: RunContext) { 195 | const dirs_ = this.dirs.map(d => { 196 | return path.join(context.cwd, d); 197 | }); 198 | const childResources = new Set(); 199 | const closer = { 200 | close() { 201 | throw new Error("Nested watchers not supported."); 202 | } 203 | }; 204 | context.resources.add(closer); 205 | this.command 206 | .run(args, { ...context, resources: childResources }) 207 | .catch(_ => {}); 208 | for await (const _ of watch(dirs_, this.watchOptions)) { 209 | closeResouces(childResources); 210 | this.command 211 | .run(args, { ...context, resources: childResources }) 212 | .catch(_ => {}); 213 | } 214 | context.resources.delete(closer); 215 | } 216 | } 217 | 218 | function closeResouces(resources: Set) { 219 | for (let resource of resources) { 220 | resource.close(); 221 | } 222 | resources.clear(); 223 | } 224 | 225 | export class TaskDecorator { 226 | constructor(public tasks: Tasks, public name: string) {} 227 | watchSync(dirs: string | string[], watchOptions = {}) { 228 | if (typeof dirs === "string") { 229 | dirs = [dirs]; 230 | } 231 | this.tasks[this.name] = new SyncWatcher( 232 | dirs, 233 | watchOptions, 234 | this.tasks[this.name] 235 | ); 236 | } 237 | watch(dirs: string | string[], watchOptions = {}) { 238 | if (typeof dirs === "string") { 239 | dirs = [dirs]; 240 | } 241 | this.tasks[this.name] = new AsyncWatcher( 242 | dirs, 243 | watchOptions, 244 | this.tasks[this.name] 245 | ); 246 | } 247 | } 248 | interface RunOptions { 249 | cwd?: string; 250 | shell?: boolean; 251 | } 252 | interface RunContext { 253 | cwd: string; 254 | shell: boolean; 255 | resources: Set; 256 | } 257 | export class TaskRunner { 258 | tasks: Tasks = {}; 259 | task(name: string, ...rawCommands: (string | string[])[]): TaskDecorator { 260 | if (name.split(/\s/).length > 1) { 261 | throw new Error(`Task name "${name}" is invalid.`); 262 | } 263 | if (this.tasks[name]) { 264 | throw new Error(`Task name "${name}" is duplicated.`); 265 | } 266 | this.tasks[name] = makeCommand(rawCommands); 267 | return new TaskDecorator(this.tasks, name); 268 | } 269 | async run(taskName: string, args: string[] = [], options: RunOptions = {}) { 270 | options = { cwd: ".", shell: true, ...options }; 271 | let command = this.tasks[taskName]; 272 | if (!command) { 273 | throw new Error(`Task "${taskName}" not found.`); 274 | } 275 | const resolveContext = { checked: new Set(), hasWatcher: false }; 276 | const context = { 277 | cwd: options.cwd, 278 | shell: options.shell, 279 | resources: new Set() 280 | }; 281 | const resolvedCommand = command.resolveRef(this.tasks, resolveContext); 282 | await resolvedCommand.run(args, context); 283 | } 284 | } 285 | 286 | function makeCommand(rawCommands: (string | string[])[]): Command { 287 | if (rawCommands.length === 0) { 288 | throw new Error("Task needs at least one command."); 289 | } 290 | if (rawCommands.length === 1) { 291 | return makeNonSequenceCommand(rawCommands[0]); 292 | } 293 | return new Sequence(rawCommands.map(makeNonSequenceCommand)); 294 | } 295 | function makeNonSequenceCommand(rawCommand: string | string[]): Command { 296 | if (typeof rawCommand === "string") { 297 | return makeSingleCommand(rawCommand); 298 | } 299 | return new Parallel(rawCommand.map(makeSingleCommand)); 300 | } 301 | function makeSingleCommand(script: string) { 302 | script = script.trim(); 303 | if (!script.trim()) { 304 | throw new Error("Command should not be empty."); 305 | } 306 | if (script.charAt(0) === "$") { 307 | return new Ref(script); 308 | } 309 | return new Single(script); 310 | } 311 | -------------------------------------------------------------------------------- /test/count.ts: -------------------------------------------------------------------------------- 1 | import { args } from "deno"; 2 | const name = args[1]; 3 | const seconds = args.slice(2); 4 | if (!seconds.length) { 5 | seconds[0] = "1"; 6 | } 7 | (async () => { 8 | let i = 0; 9 | let s; 10 | while (seconds.length) { 11 | await new Promise(resolve => setTimeout(resolve, 1000)); 12 | i++; 13 | s = +seconds[0]; 14 | if (s === i) { 15 | console.log(name); 16 | seconds.shift(); 17 | } 18 | if (i > 10) { 19 | throw new Error("Did not match: " + seconds[0]); 20 | } 21 | } 22 | })(); 23 | -------------------------------------------------------------------------------- /test/failure.ts: -------------------------------------------------------------------------------- 1 | import { exit } from "deno"; 2 | console.error("😛"); 3 | exit(1); 4 | -------------------------------------------------------------------------------- /test/test-task.ts: -------------------------------------------------------------------------------- 1 | import { TaskRunner } from "../runner.ts"; 2 | 3 | const runner = new TaskRunner(); 4 | runner.task("hello", "echo hello"); 5 | runner.task("hello2", "$hello alice", "$hello bob"); 6 | runner.task("c", "deno count.ts"); 7 | runner.task("count", "$c start", ["$c foo 1 3 5", "$c bar 2 4"], "$c end"); 8 | runner.task("hello-watch", "echo hello").watch("."); 9 | runner.task("touch", "touch test.ts"); 10 | runner.task( 11 | "shell", 12 | `echo hello > ../tmp/result-from-shell`, 13 | `echo world >> ../tmp/result-from-shell` 14 | ); 15 | 16 | (async () => { 17 | await runner.run("hello", ["world"], { cwd: "test" }); 18 | console.log("===="); 19 | await runner.run("hello2", [], { cwd: "test" }); 20 | console.log("===="); 21 | await runner.run("count", [], { cwd: "test" }); 22 | console.log("===="); 23 | await runner.run("shell", [], { cwd: "test" }); 24 | // console.log("===="); 25 | // await runner.run("hello-watch", [],{ cwd: "test" }); 26 | // await new Promise(resolve => setTimeout(resolve, 1000)); 27 | // await runner.run("touch", [],{ cwd: "test" }); 28 | })(); 29 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "deno"; 2 | import { test } from "https://deno.land/x/testing@v0.2.5/mod.ts"; 3 | import { assertEqual } from "https://deno.land/x/pretty_assert@0.1.4/mod.ts"; 4 | import { TaskRunner } from "../runner.ts"; 5 | 6 | test(async function basics() { 7 | const bytes = await readFile("tmp/result"); 8 | const result = new TextDecoder() 9 | .decode(bytes) 10 | .replace(/\r\n/g, "\n") 11 | .trim(); 12 | const expectation = ` 13 | hello world 14 | ==== 15 | hello alice 16 | hello bob 17 | ==== 18 | start 19 | foo 20 | bar 21 | foo 22 | bar 23 | foo 24 | end 25 | ==== 26 | ` 27 | .replace(/\r\n/g, "\n") 28 | .trim(); 29 | assertEqual(result, expectation); 30 | }); 31 | 32 | test(async function shell() { 33 | const bytes = await readFile("tmp/result-from-shell"); 34 | const result = new TextDecoder() 35 | .decode(bytes) 36 | .replace(/\r\n/g, "\n") 37 | .replace(/\s*\n/g, "\n") // for cmd.exe 38 | .trim(); 39 | const expectation = ` 40 | hello 41 | world 42 | ` 43 | .replace(/\r\n/g, "\n") 44 | .trim(); 45 | assertEqual(result, expectation); 46 | }); 47 | 48 | test(async function errors() { 49 | await throws(async () => { 50 | const runner = new TaskRunner(); 51 | runner.task("foo"); 52 | }); 53 | await throws(async () => { 54 | const runner = new TaskRunner(); 55 | runner.task("a b", "echo hello"); 56 | }); 57 | await throws(async () => { 58 | const runner = new TaskRunner(); 59 | runner.task("hello", "echo hello"); 60 | runner.task("hello", "echo hello again"); 61 | }); 62 | await throws(async () => { 63 | const runner = new TaskRunner(); 64 | runner.task("hello", "echo hello"); 65 | await runner.run("hell"); 66 | }); 67 | await throws(async () => { 68 | const runner = new TaskRunner(); 69 | runner.task("hello", "$echo hello"); 70 | await runner.run("hello"); 71 | }); 72 | await throws(async () => { 73 | const runner = new TaskRunner(); 74 | runner.task("hello", "$hello"); 75 | await runner.run("hello"); 76 | }); 77 | await throws(async () => { 78 | const runner = new TaskRunner(); 79 | runner.task("greeting", "echo hello", "echo bye"); 80 | await runner.run("greeting", ["x"]); 81 | }); 82 | await throws(async () => { 83 | const runner = new TaskRunner(); 84 | runner.task("greeting", ["echo hello", "echo bye"]); 85 | await runner.run("greeting", ["x"]); 86 | }); 87 | await throws(async () => { 88 | const runner = new TaskRunner(); 89 | runner.task("child", ["echo hello"]).watch("."); 90 | runner.task("parent", ["$child"]).watch("."); 91 | await runner.run("parent", []); 92 | }); 93 | await throws(async () => { 94 | const runner = new TaskRunner(); 95 | runner.task( 96 | "failure", 97 | "echo start", 98 | ["deno test/failure.ts", "echo another"], 99 | "echo end" 100 | ); 101 | await runner.run("failure"); 102 | }); 103 | }); 104 | 105 | export async function throws( 106 | f: () => Promise | void, 107 | message?: string 108 | ): Promise { 109 | let thrown = false; 110 | try { 111 | await f(); 112 | } catch (e) { 113 | console.log(e.message); 114 | thrown = true; 115 | } 116 | if (!thrown) { 117 | throw new Error( 118 | message || `Expected \`${funcToString(f)}\` to throw, but it did not.` 119 | ); 120 | } 121 | } 122 | function funcToString(f: Function) { 123 | // index_ts_1.funcname() 124 | return f 125 | .toString() 126 | .replace(/[a-zA-Z0-9]+_(ts|js)_[0-9]+\./g, "") 127 | .replace(/\s+/g, " "); 128 | } 129 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "baseUrl": ".", 5 | "noImplicitAny": true, 6 | "paths": { 7 | "deno": ["../../.deno/deno.d.ts"], 8 | "http://*": ["../../.deno/deps/http/*"], 9 | "https://*": ["../../.deno/deps/https/*"] 10 | } 11 | } 12 | } 13 | --------------------------------------------------------------------------------