| ... | ... | @@ -0,0 +1,593 @@ |
| 1 | #!/usr/bin/env node |
| 2 | // autofmt v1 - https://paperclover.dev/nix/config/src/branch/main/files/autofmt.js |
| 3 | // |
| 4 | // Different codebases use different formatters. Autofmt looks for project |
| 5 | // configuration to pick the correct formatter, allowing an editor to simply |
| 6 | // point to this script and ambiguities resolved. This single file program |
| 7 | // depends only on Node.js v22. |
| 8 | // |
| 9 | // When using this repository's Nix-based Neovim configuration, autofmt is |
| 10 | // automatically configured as the default formatter. To configure manually, |
| 11 | // set the editor's formatter to run `autofmt --stdio=<filename>`. The filename |
| 12 | // is used to determine which formatter to invoke. |
| 13 | // |
| 14 | // With `conform.nvim`: |
| 15 | // formatters = { |
| 16 | // autofmt = { command = "autofmt" } |
| 17 | // } |
| 18 | // |
| 19 | // With Zed: |
| 20 | // "formatter": { |
| 21 | // "external": { |
| 22 | // "command": "autofmt", |
| 23 | // "arguments": ["--stdio", "{buffer_path}"] |
| 24 | // } |
| 25 | // }, |
| 26 | // |
| 27 | // @ts-nocheck |
| 28 | |
| 29 | // -- definitions -- |
| 30 | const extensions = { |
| 31 | c: [".c", ".h"], |
| 32 | cpp: [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".hh"], |
| 33 | css: [".css"], |
| 34 | html: [".html"], |
| 35 | javascript: [".js", ".ts", ".cjs", ".cts", ".mjs", ".mts", ".jsx", ".tsx"], |
| 36 | json: [".json", ".jsonc"], |
| 37 | markdown: [".md", ".markdown"], |
| 38 | mdx: [".mdx"], |
| 39 | nix: [".nix"], |
| 40 | rust: [".rs"], |
| 41 | toml: [".toml"], |
| 42 | yaml: [".yml", ".yaml"], |
| 43 | zig: [".zig"], |
| 44 | }; |
| 45 | const formatters = { |
| 46 | // this object is sorted by priority |
| 47 | // |
| 48 | // `languages: true` are multiplexers that apply to all listed |
| 49 | // languages, as it is assumed a project will setup their multiplexer |
| 50 | // correctly for all tracked files. |
| 51 | // |
| 52 | // if a formatter doesnt match a config file, the first one is picked |
| 53 | // as a default, making things like `deno fmt file.ts` or `clang-format` |
| 54 | // the default when there are no config files. |
| 55 | dprint: { |
| 56 | languages: true, |
| 57 | files: ["dprint.json", "dprint.jsonc"], |
| 58 | cmd: ["dprint", "fmt", "--", "$files"], |
| 59 | stdin: (file) => ["dprint", "fmt", "--stdin", file], |
| 60 | }, |
| 61 | treefmt: { |
| 62 | languages: true, |
| 63 | files: ["treefmt.toml", ".treefmt.toml"], |
| 64 | cmd: ["treefmt", "--", "$files"], |
| 65 | stdin: (file) => ["treefmt", "--stdin", file], |
| 66 | }, |
| 67 | // -- web ecosystem -- |
| 68 | deno: { |
| 69 | languages: ["javascript", "markdown", "json", "yaml", "html", "css"], |
| 70 | files: ["dprint.json", "dprint.jsonc"], |
| 71 | cmd: ["deno", "fmt", "--", "$files"], |
| 72 | stdin: (file) => ["deno", "fmt", "--ext", path.extname(file).slice(1), "-"], |
| 73 | }, |
| 74 | prettier: { |
| 75 | languages: ["javascript", "markdown", "json", "yaml", "mdx", "html", "css"], |
| 76 | files: ["node_modules/.bin/prettier"], |
| 77 | cmd: ["node_modules/.bin/prettier", "--write", "--", "$files"], |
| 78 | stdin: (file) => ["node_modules/.bin/prettier", "--stdin-filepath", file], |
| 79 | }, |
| 80 | biome: { |
| 81 | languages: ["javascript", "json", "css"], |
| 82 | files: ["node_modules/.bin/biome"], |
| 83 | cmd: ["node_modules/.bin/biome", "format", "--", "$files"], |
| 84 | stdin: (file) => [ |
| 85 | "node_modules/bin/biome", |
| 86 | "format", |
| 87 | `--stdin-file-path=${file}`, |
| 88 | ], |
| 89 | }, |
| 90 | "nixfmt-rfc-style": { |
| 91 | languages: ["nix"], |
| 92 | files: { |
| 93 | "flake.nix": (contents) => contents.includes("nixfmt"), |
| 94 | }, |
| 95 | cmd: ["nixfmt", "--", "$files"], |
| 96 | stdin: (file) => ["nixfmt", `--filename=${file}`], |
| 97 | }, |
| 98 | alejandra: { |
| 99 | languages: ["nix"], |
| 100 | files: { |
| 101 | "flake.nix": (contents) => contents.includes("alejandra"), |
| 102 | }, |
| 103 | cmd: ["alejandra", "--", "$files"], |
| 104 | stdin: () => ["alejandra"], |
| 105 | }, |
| 106 | clang: { |
| 107 | languages: ["c", "cpp"], |
| 108 | files: true, |
| 109 | cmd: ["clang-format", "--", "$files"], |
| 110 | stdin: (file) => ["clang-format", `--assume-filename=${file}`], |
| 111 | }, |
| 112 | zig: { |
| 113 | languages: ["zig"], |
| 114 | files: true, |
| 115 | cmd: ["zig", "fmt", "$files"], |
| 116 | stdin: (file) => [ |
| 117 | "zig", |
| 118 | "fmt", |
| 119 | "--stdin", |
| 120 | ...file.endsWith(".zon") ? ["--zon"] : [], |
| 121 | ], |
| 122 | }, |
| 123 | rustfmt: { |
| 124 | languages: ["rust"], |
| 125 | files: true, |
| 126 | cmd: ["rustfmt", "--", "$files"], |
| 127 | stdin: () => ["rustfmt"], |
| 128 | }, |
| 129 | taplo: { |
| 130 | languages: ["toml"], |
| 131 | files: ["taplo.toml"], |
| 132 | cmd: ["taplo", "format", "--", "$files"], |
| 133 | cmd: () => ["taplo", "format", "-"], |
| 134 | }, |
| 135 | }; |
| 136 | |
| 137 | // -- cli -- |
| 138 | if (!fs.globSync) { |
| 139 | console.error(`error: autofmt must be run with Node.js v22 or newer`); |
| 140 | process.exit(1); |
| 141 | } |
| 142 | const [, bin, ...argv] = process.argv; |
| 143 | let inputs = []; |
| 144 | let globalExclude = []; |
| 145 | let gitignore = true; |
| 146 | let dryRun = false; |
| 147 | let stdio = null; |
| 148 | let excludes = [".git"]; |
| 149 | while (argv.length > 0) { |
| 150 | const arg = argv.shift(); |
| 151 | if (arg === "-h" || arg === "--help") usage(); |
| 152 | else if (arg === "--no-gitignore") gitignore = false; |
| 153 | else if (arg === "--dry-run") dryRun = true; |
| 154 | else if (arg.match(/^--stdio=./)) { |
| 155 | if (stdio) { |
| 156 | console.error("error: can only pass --stdio once"); |
| 157 | usage(); |
| 158 | } |
| 159 | stdio = arg.slice("--stdio=".length); |
| 160 | } else if (arg === "--stdio") { |
| 161 | const value = argv.shift(); |
| 162 | if (!value) { |
| 163 | console.error("error: missing value for --stdio"); |
| 164 | usage(); |
| 165 | } |
| 166 | if (stdio) { |
| 167 | console.error("error: can only pass --stdio once"); |
| 168 | usage(); |
| 169 | } |
| 170 | stdio = value; |
| 171 | } else if (arg.match(/^--exclude=./)) { |
| 172 | excludes.push(arg.slice("--exclude=".length)); |
| 173 | } else if (arg === "--") { |
| 174 | inputs.push(...argv); |
| 175 | break; |
| 176 | } else if (arg.startsWith("-")) { |
| 177 | console.error("error: unknown option " + JSON.stringify(arg)); |
| 178 | usage(); |
| 179 | } else inputs.push(arg); |
| 180 | } |
| 181 | function usage() { |
| 182 | const exe = path.basename(bin); |
| 183 | console.error(`usage: ${exe} [...files or directories]`); |
| 184 | console.error(``); |
| 185 | console.error(`uses the right formatter for the job (by scanning config)`); |
| 186 | console.error(`when autofmt reads dirs, it will respect gitignore`); |
| 187 | console.error(``); |
| 188 | console.error(`to format current directory recursively, run '${exe} .'`); |
| 189 | console.error(``); |
| 190 | console.error(`options:`); |
| 191 | console.error(` --dry-run print commands instead of running them`); |
| 192 | console.error(` --no-gitignore do not read '.gitignore'`); |
| 193 | console.error(` --exclude=<glob> add an exclusion glob`); |
| 194 | console.error(` --stdio=<filename> read/write contents via stdin/stdout`); |
| 195 | console.error(``); |
| 196 | process.exit(1); |
| 197 | } |
| 198 | if (inputs.length === 0 && !stdio) usage(); |
| 199 | if (stdio && inputs.length > 0) { |
| 200 | console.error("error: stdio mode only operates on one file"); |
| 201 | process.exit(1); |
| 202 | } |
| 203 | const { sep } = path; |
| 204 | |
| 205 | // -- disable warnings -- |
| 206 | const { emit: originalEmit } = process; |
| 207 | const warnings = ["ExperimentalWarning"]; |
| 208 | process.emit = function (event, error) { |
| 209 | return event === "warning" && warnings.includes(error.name) |
| 210 | ? false |
| 211 | : originalEmit.apply(process, arguments); |
| 212 | }; |
| 213 | |
| 214 | // -- vars -- |
| 215 | const extToLanguage = Object.fromEntries( |
| 216 | Object.entries(extensions).flatMap(([lang, exts]) => |
| 217 | exts.map((ext) => [ext, lang]) |
| 218 | ), |
| 219 | ); |
| 220 | const multis = Object.keys(formatters).filter( |
| 221 | (x) => formatters[x].languages === true, |
| 222 | ); |
| 223 | const files = []; |
| 224 | const gitignores = new Map(); |
| 225 | const dirs = new Map(); |
| 226 | const cached = new Map(); |
| 227 | |
| 228 | // -- stdin mode -- |
| 229 | if (stdio) { |
| 230 | const fmtWithPath = pickFormatter(stdio); |
| 231 | if (!fmtWithPath) { |
| 232 | console.error(`No formatter configured for ${path.relative(".", stdio)}`); |
| 233 | process.exit(1); |
| 234 | } |
| 235 | let [fmt, cwd] = fmtWithPath.split("\0"); |
| 236 | let cmd = formatters[fmt].stdin(stdio); |
| 237 | cwd ??= process.cwd(); |
| 238 | if (dryRun) { |
| 239 | console.info(cmd.join(" ")); |
| 240 | process.exit(0); |
| 241 | } |
| 242 | const proc = child_process.spawn(cmd[0], cmd.slice(1), { |
| 243 | stdio: ["inherit", "inherit", "inherit"], |
| 244 | }); |
| 245 | proc.on("error", (e) => { |
| 246 | let message = ""; |
| 247 | if (e?.code === "ENOENT") { |
| 248 | message = `${cmd[0]} is not installed`; |
| 249 | } else { |
| 250 | message = String(e?.message ?? e); |
| 251 | } |
| 252 | console.error(`error: ${message}`); |
| 253 | process.exit(1); |
| 254 | }); |
| 255 | const [code] = await events.once(proc, "exit"); |
| 256 | process.exit(code ?? 1); |
| 257 | } |
| 258 | |
| 259 | // -- decide what formatters to run |
| 260 | inputs = inputs.map((x) => path.resolve(x)); |
| 261 | inputs.forEach(walk); |
| 262 | const toRun = new Map(); |
| 263 | for (const file of new Set(files)) { |
| 264 | const fmt = pickFormatter(file); |
| 265 | if (!fmt) { |
| 266 | if (inputs.includes(file)) { |
| 267 | console.warn(`No formatter configured for ${path.relative(".", file)}`); |
| 268 | } |
| 269 | continue; |
| 270 | } |
| 271 | let list = toRun.get(fmt); |
| 272 | list ?? toRun.set(fmt, list = []); |
| 273 | list.push(file); |
| 274 | } |
| 275 | |
| 276 | // -- create a list of commands -- |
| 277 | const commands = []; |
| 278 | let totalFiles = 0; |
| 279 | for (const [fmtWithPath, files] of toRun) { |
| 280 | let [fmt, cwd] = fmtWithPath.split("\0"); |
| 281 | let { cmd } = formatters[fmt]; |
| 282 | if (cwd) cmd = [path.join(cwd, cmd[0]), ...cmd.slice(1)]; |
| 283 | cwd ??= process.cwd(); |
| 284 | let i = 0; |
| 285 | totalFiles += files.length; |
| 286 | if ((i = cmd.indexOf("$files")) != -1) { |
| 287 | const c = cmd.slice(); |
| 288 | c.splice(i, 1, ...files); |
| 289 | commands.push({ cmd: c, cwd, files }); |
| 290 | } else if ((i = cmd.indexOf("$file")) != -1) { |
| 291 | for (const file of files) { |
| 292 | const c = cmd.slice(); |
| 293 | c.splice(i, 1, file); |
| 294 | commands.push({ cmd: c, cwd, files: [file] }); |
| 295 | } |
| 296 | } else { |
| 297 | throw new Error(`Formatter ${fmt} has incorrectly configured command.`); |
| 298 | } |
| 299 | } |
| 300 | if (commands.length === 0) { |
| 301 | console.error("No formattable files"); |
| 302 | process.exit(0); |
| 303 | } |
| 304 | |
| 305 | // -- dry run mode -- |
| 306 | if (dryRun) { |
| 307 | for (const { cmd } of commands) { |
| 308 | console.info(cmd); |
| 309 | } |
| 310 | process.exit(0); |
| 311 | } |
| 312 | |
| 313 | // -- user interface -- |
| 314 | let filesComplete = 0; |
| 315 | let lastFile = commands[0].cmd; |
| 316 | const syncStart = "\u001B[?2026h"; |
| 317 | const syncEnd = "\u001B[?2026l"; |
| 318 | let buffer = ""; |
| 319 | let statusVisible = false; |
| 320 | const tty = process.stderr.isTTY; |
| 321 | function writeStatus() { |
| 322 | if (!tty) return; |
| 323 | clearStatus(); |
| 324 | buffer ||= syncStart; |
| 325 | buffer += `${filesComplete}/${totalFiles} - ${lastFile}`; |
| 326 | statusVisible = true; |
| 327 | } |
| 328 | function clearStatus() { |
| 329 | if (!tty) return; |
| 330 | if (!statusVisible) return; |
| 331 | buffer ||= syncStart; |
| 332 | buffer += "\r\x1b[2K\r"; |
| 333 | statusVisible = false; |
| 334 | } |
| 335 | function flush() { |
| 336 | if (!buffer) return; |
| 337 | const width = Math.max(1, process.stderr.columns - 1); |
| 338 | process.stderr.write( |
| 339 | buffer.split("\n").map((x) => x.slice(0, width)).join("\n") + syncEnd, |
| 340 | ); |
| 341 | buffer = ""; |
| 342 | } |
| 343 | |
| 344 | // -- async process queue -- |
| 345 | let running = 0; |
| 346 | /** @param cmd {{ cmd: string, cwd: string, files: string[] }} */ |
| 347 | function run({ cmd, cwd, files }) { |
| 348 | running += 1; |
| 349 | let c = child_process.spawn(cmd[0], cmd.slice(1), { |
| 350 | stdio: ["ignore", "pipe", "pipe"], |
| 351 | cwd, |
| 352 | }); |
| 353 | const relatives = new Set( |
| 354 | files.map((file) => file.startsWith(cwd) ? path.relative(cwd, file) : file), |
| 355 | ); |
| 356 | function onLine(line) { |
| 357 | for (const file of relatives) { |
| 358 | if (line.includes(file)) { |
| 359 | filesComplete += 1; |
| 360 | relatives.delete(file); |
| 361 | lastFile = path.relative(process.cwd(), path.resolve(cwd, file)); |
| 362 | if (tty) { |
| 363 | writeStatus(); |
| 364 | flush(); |
| 365 | } else { |
| 366 | console.info(lastFile); |
| 367 | } |
| 368 | return; |
| 369 | } |
| 370 | } |
| 371 | } |
| 372 | let errBuffer = ""; |
| 373 | let exited = false; |
| 374 | c.on("error", (e) => { |
| 375 | let message = ""; |
| 376 | if (e?.code === "ENOENT") { |
| 377 | if (cmd[0].includes("/")) { |
| 378 | running -= 1; |
| 379 | run({ cmd: [path.basename(cmd[0]), ...cmd.slice(1)], cwd, files }); |
| 380 | return; |
| 381 | } |
| 382 | message = `${cmd[0]} is not installed`; |
| 383 | } else { |
| 384 | message = String(e?.message ?? e); |
| 385 | } |
| 386 | clearStatus(); |
| 387 | const filesConcise = |
| 388 | path.relative(".", path.resolve(cwd, relatives.keys().next().value)) + ( |
| 389 | relatives.size > 1 ? ` and ${relatives.size - 1} more` : "" |
| 390 | ); |
| 391 | buffer += errBuffer + `error: ${message}, cannot format ${filesConcise}.\n`; |
| 392 | flush(); |
| 393 | exited = true; |
| 394 | runNext(); |
| 395 | }); |
| 396 | readline.createInterface(c.stderr).addListener("line", (line) => { |
| 397 | errBuffer += line + "\n"; |
| 398 | onLine(line); |
| 399 | }); |
| 400 | readline.createInterface(c.stdout).addListener("line", onLine); |
| 401 | c.on("exit", (code, signal) => { |
| 402 | if (exited) return; |
| 403 | exited = true; |
| 404 | if (code !== 0) { |
| 405 | clearStatus(); |
| 406 | const exitStatus = code != null ? `code ${code}` : `signal ${signal}`; |
| 407 | buffer += errBuffer + `error: ${cmd[0]} exited with ${exitStatus}\n`; |
| 408 | flush(); |
| 409 | } else { |
| 410 | filesComplete += relatives.size; |
| 411 | if (relatives.size) { |
| 412 | lastFile = path.relative( |
| 413 | ".", |
| 414 | path.resolve(cwd, relatives.keys().next().value), |
| 415 | ); |
| 416 | } |
| 417 | writeStatus(); |
| 418 | flush(); |
| 419 | } |
| 420 | runNext(); |
| 421 | }); |
| 422 | function runNext() { |
| 423 | running -= 1; |
| 424 | const next = commands.pop(); |
| 425 | if (next) run(next); |
| 426 | else if (running == 0) { |
| 427 | clearStatus(); |
| 428 | flush(); |
| 429 | console.info( |
| 430 | `Formatted ${filesComplete} file${filesComplete !== 1 ? "s" : ""}`, |
| 431 | ); |
| 432 | } |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | for (let i = 0; i < navigator.hardwareConcurrency; i++) { |
| 437 | const cmd = commands.pop(); |
| 438 | if (cmd) run(cmd); |
| 439 | else break; |
| 440 | } |
| 441 | |
| 442 | // -- library functions -- |
| 443 | |
| 444 | /** @param file {string} */ |
| 445 | function walk(file) { |
| 446 | file = path.resolve(file); |
| 447 | try { |
| 448 | if (fs.statSync(file).isDirectory()) { |
| 449 | const exclude = getGitIgnores(file); |
| 450 | const read = fs |
| 451 | .globSync(escapeGlob(file) + "/{**,.**}", { |
| 452 | exclude, |
| 453 | withFileTypes: true, |
| 454 | }) |
| 455 | .filter((file) => !file.isDirectory()) |
| 456 | .map((file) => path.join(file.parentPath, file.name)); |
| 457 | files.push(...read); |
| 458 | } else { |
| 459 | files.push(file); |
| 460 | } |
| 461 | } catch (err) { |
| 462 | console.error( |
| 463 | `Failed to stat ${file}: ${err?.code ?? err?.message ?? err}`, |
| 464 | ); |
| 465 | process.exit(1); |
| 466 | } |
| 467 | } |
| 468 | |
| 469 | /** @param dir {string} @returns {Array<string>} */ |
| 470 | function readDir(dir) { |
| 471 | dir = path.resolve(dir); |
| 472 | let contents = dirs.get(dir); |
| 473 | if (!contents) { |
| 474 | try { |
| 475 | contents = fs.readdirSync(dir); |
| 476 | } catch { |
| 477 | contents = []; |
| 478 | } |
| 479 | dirs.set(dir, contents); |
| 480 | } |
| 481 | return contents; |
| 482 | } |
| 483 | |
| 484 | /** @param dir {string} @returns {string} */ |
| 485 | function pickFormatter(file) { |
| 486 | const lang = extToLanguage[path.extname(file)]; |
| 487 | const dir = path.dirname(file); |
| 488 | let c = walkUp(dir, (x) => cached.get(x) ?? cached.get(`${x}:${lang}`)); |
| 489 | if (c) return c; |
| 490 | |
| 491 | const possible = Object.keys(formatters).filter( |
| 492 | (x) => |
| 493 | Array.isArray(formatters[x].languages) && |
| 494 | formatters[x].languages.includes(lang), |
| 495 | ); |
| 496 | const order = [...multis, ...possible]; |
| 497 | return walkUp(dir, (x) => { |
| 498 | const children = readDir(x); |
| 499 | for (const fmt of order) { |
| 500 | let matches = false; |
| 501 | if (formatters[fmt].files === true) { |
| 502 | matches = true; |
| 503 | } |
| 504 | if (!matches) { |
| 505 | const filesToCheck = Array.isArray(formatters[fmt].files) |
| 506 | ? formatters[fmt].files.map((base) => ({ base, contents: true })) |
| 507 | : Object.entries(formatters[fmt].files) |
| 508 | .map(([base, contents]) => ({ base, contents })); |
| 509 | for (const { base, contents } of filesToCheck) { |
| 510 | if (base.includes("/")) { |
| 511 | matches = readDir(path.join(x, path.dirname(base))) // |
| 512 | .includes(path.basename(base)); |
| 513 | } else { |
| 514 | matches = children.includes(base); |
| 515 | } |
| 516 | if (matches && typeof contents === "function") { |
| 517 | matches = !!contents(fs.readFileSync(path.join(x, base), "utf-8")); |
| 518 | } |
| 519 | } |
| 520 | } |
| 521 | if (matches) { |
| 522 | const formatterId = `${fmt}\0${x}`; |
| 523 | const k = multis.includes(fmt) ? x : `${x}:${lang}`; |
| 524 | cached.set(k, formatterId); |
| 525 | return formatterId; |
| 526 | } |
| 527 | } |
| 528 | }) ?? possible[0]; |
| 529 | } |
| 530 | |
| 531 | /** @param dir {string} @param find {(x: string) => any} */ |
| 532 | function walkUp(dir, find) { |
| 533 | do { |
| 534 | const found = find(dir); |
| 535 | if (found != null) return found; |
| 536 | const parent = path.dirname(dir); |
| 537 | if (parent === dir) break; |
| 538 | dir = parent; |
| 539 | } while (true); |
| 540 | return null; |
| 541 | } |
| 542 | |
| 543 | /** @param dir {string} */ |
| 544 | function getGitIgnores(dir) { |
| 545 | if (!gitignore) return []; |
| 546 | if (dir.endsWith(sep)) dir = dir.slice(0, -1); |
| 547 | const files = fs.globSync(`${dir}${sep}{**${sep}*${sep},}.gitignore`, {}); |
| 548 | const referenced = []; |
| 549 | for (const abs of files) { |
| 550 | const dir = path.dirname(abs); |
| 551 | referenced.push(dir); |
| 552 | if (!gitignores.has(dir)) gitignores.set(dir, readGitIgnore(dir)); |
| 553 | } |
| 554 | do { |
| 555 | const parent = path.dirname(dir); |
| 556 | if (parent === dir || fs.existsSync(path.join(dir, ".git"))) break; |
| 557 | referenced.push(parent); |
| 558 | if (!gitignores.has(parent)) gitignores.set(parent, readGitIgnore(parent)); |
| 559 | dir = parent; |
| 560 | } while (true); |
| 561 | return referenced.flatMap((root) => |
| 562 | (gitignores.get(root) ?? []) |
| 563 | .filter((x) => x[0] !== "!") |
| 564 | .map( |
| 565 | (rule) => `${root}${sep}${rule[0] === "/" ? "" : `**${sep}`}${rule}`, |
| 566 | ) |
| 567 | ); |
| 568 | } |
| 569 | |
| 570 | /** @param dir {string} */ |
| 571 | function readGitIgnore(dir) { |
| 572 | try { |
| 573 | return fs |
| 574 | .readFileSync(`${dir}${sep}.gitignore`, "utf-8") |
| 575 | .split("\n") |
| 576 | .map((line) => line.replace(/#.*$/, "").trim()) |
| 577 | .filter(Boolean); |
| 578 | } catch { |
| 579 | return []; |
| 580 | } |
| 581 | } |
| 582 | |
| 583 | function escapeGlob(str) { |
| 584 | return str.replace(/[\\*,{}]/g, "\\$&"); |
| 585 | } |
| 586 | |
| 587 | import * as child_process from "node:child_process"; |
| 588 | import * as fs from "node:fs"; |
| 589 | import * as path from "node:path"; |
| 590 | import * as readline from "node:readline"; |
| 591 | import * as events from "node:events"; |
| 592 | import process from "node:process"; |
| 593 | import assert from "node:assert"; |