256 lines
6.1 KiB
TypeScript
Executable File
256 lines
6.1 KiB
TypeScript
Executable File
import { Sys } from "../deps.ts";
|
|
|
|
export class ProjectNormalizer {
|
|
constructor(readonly sys = Sys) {
|
|
}
|
|
|
|
async isDirectory(path: string): Promise<boolean> {
|
|
try {
|
|
return (await this.sys.stat(path)).isDirectory;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async isFile(path: string): Promise<boolean> {
|
|
try {
|
|
return (await this.sys.stat(path)).isFile;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async readContent(path: string): Promise<string> {
|
|
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<string> {
|
|
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();
|
|
}
|