import { Sys } from "../deps.ts"; export class ProjectNormalizer { constructor(readonly sys = Sys) { } async isDirectory(path: string): Promise { try { return (await this.sys.stat(path)).isDirectory; } catch { return false; } } async isFile(path: string): Promise { try { return (await this.sys.stat(path)).isFile; } catch { return false; } } async readContent(path: string): Promise { try { return await this.sys.readTextFile(path); } catch { return ""; } } async ensureDirectory(path: string) { if (!await this.isDirectory(path)) { await this.sys.mkdir(path, { recursive: true }); } } async writeJsonFile(path: string, content: any) { await this.sys.writeTextFile( path, JSON.stringify(content), ); await this.formatPath(path); } async writeLines(path: string, lines: string[]) { await this.sys.writeTextFile( path, lines.concat("").join("\n"), ); } async run(...cmd: string[]): Promise { const p = this.sys.run({ cmd, stdout: "piped", stderr: "piped" }); const [status, stdout, stderr] = await Promise.all([ p.status(), p.output(), p.stderrOutput(), ]); if (status.success) { return new TextDecoder().decode(stdout); } else { console.error(new TextDecoder().decode(stderr)); throw new Error(`Command failed: ${cmd.join(" ")}`); } } async formatPath(path: string) { await this.run("./run", "fmt", "-q", path); } async updateRunScript() { await this.writeLines( "run", [ `#!/bin/sh`, `# Simplified run tool for deno commands`, ``, `if test $# -eq 0`, `then`, ` echo "Usage: $0 [file or command]"`, ` exit 1`, `elif echo $1 | grep -q '.*\.ts'`, `then`, ` denocmd=run`, ` denoargs=$1`, ` shift`, `else`, ` denocmd=$1`, ` shift`, `fi`, ``, `denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@"`, `exec deno $denocmd $denoargs`, ], ); await this.sys.chmod("run", 0o755); } async updateDenoDefs() { const defs = await this.run("./run", "types"); await this.sys.writeTextFile("deno.d.ts", defs); } async updateEditorConfig() { await this.writeLines( ".editorconfig", [ `root = true`, ``, `[*.{ts,json}]`, `charset = utf-8`, `end_of_line = lf`, `insert_final_newline = true`, `indent_style = space`, `indent_size = 2`, `trim_trailing_whitespace = true`, ], ); } async updateTsConfig() { await this.writeJsonFile("tsconfig.json", { compilerOptions: { module: "esnext", target: "ESNext", strict: true, noImplicitReturns: true, noFallthroughCasesInSwitch: true, preserveConstEnums: true, }, }); } async updateVscodeConf() { await this.ensureDirectory(".vscode"); const path = ".vscode/settings.json"; const json_config = await this.readContent(path); const config = JSON.parse(json_config || "{}"); if (!config["deno.enable"]) { config["deno.enable"] = true; await this.writeJsonFile(path, config); } await this.writeJsonFile(".vscode/tasks.json", { version: "2.0.0", tasks: [ { label: "test", type: "shell", group: { kind: "test", isDefault: true, }, command: "./run test", }, ], }); if (await this.isFile("cli.ts")) { await this.writeJsonFile(".vscode/launch.json", { version: "0.2.0", configurations: [ { name: "Deno", type: "node", request: "launch", cwd: "\${workspaceFolder}", program: "cli.ts", console: "externalTerminal", attachSimplePort: 9229, runtimeExecutable: "deno", runtimeArgs: [ "run", "--inspect", ].concat( (await this.readContent("config/run.flags")) .trim() .split(" ") .filter((part) => !!part), ), }, ], }); } } async updateGitIgnore() { await this.writeLines( ".gitignore", [ `deno.d.ts`, `.vscode`, `.local`, `.output`, `web/*.js`, ], ); } async updateGitHooks() { await this.writeLines( ".git/hooks/pre-commit", [ `#!/bin/sh`, `set -e`, `./run fmt --check`, `./run test`, ], ); await this.sys.chmod(".git/hooks/pre-commit", 0o755); } async updateReadme() { const project = this.sys.cwd().split("/").pop(); let sections = ""; if (await this.isDirectory("doc")) { const index = await this.readContent("doc/index"); for (let section of index.split("\n")) { if (section?.trim()) { sections += "\n" + (await this.readContent(`doc/${section.trim()}.md`)); } } } await this.writeLines( "README.md", [ `# typescript/${project}`, `[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/${project}?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=${project})`, sections, ], ); await this.formatPath("README.md"); } async normalize() { if (!await this.isDirectory(".git")) { throw new Error("Not in a git repository"); } await this.updateRunScript(); await this.updateDenoDefs(); await this.updateVscodeConf(); await this.updateTsConfig(); await this.updateEditorConfig(); await this.updateGitIgnore(); await this.updateGitHooks(); await this.updateReadme(); } } export async function normalize(sys = Sys) { const normalizer = new ProjectNormalizer(sys); await normalizer.normalize(); }