diff --git a/.editorconfig b/.editorconfig index 6e00fdb6..b6fda276 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,9 @@ root = true -[*] -end_of_line = lf +[**] +indent_style = space +indent_size = 4 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true - +end_of_line = lf diff --git a/assets.build.ts b/assets.build.ts index ff290322..32cfd413 100644 --- a/assets.build.ts +++ b/assets.build.ts @@ -8,38 +8,38 @@ import {deploy} from './tools/deploy/api.ts'; // Smogdex minisprites (webp), shipped under a whole-set content hash with // a pointer in __meta/ for the dex to read. -const minispriteInputs = spriteglob(["src/minisprites/pokemon/gen6/*", "src/minisprites/items/*"], {a: false}); +const minispriteInputs = spriteglob(['src/minisprites/pokemon/gen6/*', 'src/minisprites/items/*'], {a: false}); const webpMinisprites = forEachRule(minispriteInputs, { - display: "webp minisprite %f", - cmds: ["cwebp -z 9 %f -o %o"], -}, "%B.webp"); + display: 'webp minisprite %f', + cmds: ['cwebp -z 9 %f -o %o'], +}, '%B.webp'); deploy(ctx => { const h = ctx.hash(...webpMinisprites); for (const f of webpMinisprites) { - newspritecopy(ctx, f, {dir: "minisprites/" + h}); + newspritecopy(ctx, f, {dir: 'minisprites/' + h}); } - ctx.write("__meta/minisprites-hash.txt", h); + ctx.write('__meta/minisprites-hash.txt', h); }); // Smogdex spritesheet. The sheet tool bakes sprite ids parsed from the %f // filenames into the css, hence nameSensitive. const [sheetPng, sheetCss] = rule(minispriteInputs, { - display: "smogdex sheet", + display: 'smogdex sheet', nameSensitive: true, deps: [ - "data/species.json", - "data/items.json", - "data/lib/index.ts", - "lib/root/index.ts", - "tools/smogdexspritesheet/index.ts", + 'data/species.json', + 'data/items.json', + 'data/lib/index.ts', + 'lib/root/index.ts', + 'tools/smogdexspritesheet/index.ts', ], - cmds: ["node tools/smogdexspritesheet/index.ts --image %o1 --stylesheet %o2 -- %f"], -}, ["spritesheet.png", "spritesheet.css"]); + cmds: ['node tools/smogdexspritesheet/index.ts --image %o1 --stylesheet %o2 -- %f'], +}, ['spritesheet.png', 'spritesheet.css']); -const sheetWebp = rule(sheetPng, ["cwebp -z 9 %f -o %o"], "spritesheet.webp"); +const sheetWebp = rule(sheetPng, ['cwebp -z 9 %f -o %o'], 'spritesheet.webp'); // Hash-stamped css + webp. The css suffix pointer rides in __meta/ for the // dex to read. @@ -49,14 +49,14 @@ deploy(ctx => { const src = ctx.read(sheetCss); const css = src.replaceAll('url("./spritesheet.webp")', `url("./spritesheet-${wh}.webp")`); if (css === src) { - throw new Error("spritesheet.css: no webp urls rewritten"); + throw new Error('spritesheet.css: no webp urls rewritten'); } // Suffix from source content: the rewritten css is a pure function // of (css, webp), so this changes exactly when the served bytes // change. const ch = ctx.hash(sheetCss, sheetWebp); ctx.write(`spritesheet-${ch}.css`, css); - ctx.write("__meta/spritesheet_css_suffix.txt", `-${ch}\n`); + ctx.write('__meta/spritesheet_css_suffix.txt', `-${ch}\n`); }); // Forumsprites: padded minisprites under stamped names, with the @@ -68,20 +68,20 @@ const forumG6 = gen6Padded(); deploy(ctx => { const manifest = new Manifest(ctx); for (const f of forumItems) { - itemspritecopy(manifest, f, {dir: "forumsprites"}); + itemspritecopy(manifest, f, {dir: 'forumsprites'}); } for (const f of forumG6) { - spritecopy(manifest, f, {dir: "forumsprites"}, true); + spritecopy(manifest, f, {dir: 'forumsprites'}, true); } - manifest.write("__meta/forumsprites/manifest.json"); + manifest.write('__meta/forumsprites/manifest.json'); }); // PMD sprites ship as-is, stamped. deploy(ctx => { const manifest = new Manifest(ctx); - for (const f of ctx.list("src/pmd")) { - spritecopy(manifest, f, {dir: "pmd"}); + for (const f of ctx.list('src/pmd')) { + spritecopy(manifest, f, {dir: 'pmd'}); } - manifest.write("__meta/pmd/manifest.json"); + manifest.write('__meta/pmd/manifest.json'); }); diff --git a/data/lib/index.ts b/data/lib/index.ts index d90d5e87..75061d70 100644 --- a/data/lib/index.ts +++ b/data/lib/index.ts @@ -3,12 +3,12 @@ import path from 'path'; import fs from 'fs'; import root from '@smogon/sprite-root/index.ts'; -const libdir = path.join(root, "data"); +const libdir = path.join(root, 'data'); export type Id = string; export type SpecieEntry = { - type : 'specie', + type: 'specie', num: number, formeNum: number, base: string, @@ -17,56 +17,56 @@ export type SpecieEntry = { }; export type ItemEntry = { - type : 'item', - sid : string, - names : string[] + type: 'item', + sid: string, + names: string[] }; export type Entry = SpecieEntry | ItemEntry; -const objects : Record = {}; -Object.assign(objects, JSON.parse(fs.readFileSync(path.join(libdir, "species.json"), 'utf8'))); -Object.assign(objects, JSON.parse(fs.readFileSync(path.join(libdir, "items.json"), 'utf8'))); +const objects: Record = {}; +Object.assign(objects, JSON.parse(fs.readFileSync(path.join(libdir, 'species.json'), 'utf8'))); +Object.assign(objects, JSON.parse(fs.readFileSync(path.join(libdir, 'items.json'), 'utf8'))); const map = new Map(); for (const entry of Object.values(objects)) { map.set(entry.sid, entry); } -export function get(id : Id) : Entry { +export function get(id: Id): Entry { const entry = map.get(id); if (entry === undefined) throw new Error(`No id for ${id}`); return entry; } -export function entries() : Entry[] { +export function entries(): Entry[] { return Array.from(map.values()); } // TODO Moved here from deploy/spritename.ts, better place to put these?? export type SpriteFilename = ({ - extension : true, - name : string + extension: true, + name: string } | { - extension : false, - id : Id + extension: false, + id: Id }) & { - extra : Map + extra: Map }; export type InputSpriteFilename = ({ - extension : true, - name : string + extension: true, + name: string } | { - extension? : false, - id : Id + extension?: false, + id: Id }) & { - extra? : Map + extra?: Map }; -export function parseFilename(s : string) : SpriteFilename { +export function parseFilename(s: string): SpriteFilename { if (s.length < 2) throw new Error(`Filename ${s} needs to be at least 2 characters'`); @@ -74,7 +74,7 @@ export function parseFilename(s : string) : SpriteFilename { if (!prefix.match(/[a-z]/)) throw new Error(`Filename ${s} must start with alpha character`); - const parts = s.split("-"); + const parts = s.split('-'); const extra = new Map(); for (const part of parts.slice(1)) { if (part.length === 0) @@ -91,8 +91,8 @@ export function parseFilename(s : string) : SpriteFilename { } } -export function formatFilename(si : InputSpriteFilename) { - let s : string; +export function formatFilename(si: InputSpriteFilename) { + let s: string; if (si.extension) { s = `x${si.name}`; } else { diff --git a/lib/root/index.ts b/lib/root/index.ts index fb2d045b..dc15c5ca 100644 --- a/lib/root/index.ts +++ b/lib/root/index.ts @@ -1,4 +1,4 @@ import path from 'path'; -export default path.resolve(import.meta.dirname, "../../"); +export default path.resolve(import.meta.dirname, '../../'); diff --git a/ps.build.ts b/ps.build.ts index a591e3a6..e741a250 100644 --- a/ps.build.ts +++ b/ps.build.ts @@ -12,19 +12,19 @@ import {type DeployCtx, deploy} from './tools/deploy/api.ts'; // hence nameSensitive. const sheetDeps = [ - "data/species.json", - "data/items.json", - "data/lib/index.ts", - "lib/root/index.ts", - "tools/sheet/index.ts", + 'data/species.json', + 'data/items.json', + 'data/lib/index.ts', + 'lib/root/index.ts', + 'tools/sheet/index.ts', ]; -rule("ps-pokemon.sheet.mjs", { - display: "ps pokemon sheet", +rule('ps-pokemon.sheet.mjs', { + display: 'ps pokemon sheet', nameSensitive: true, - deps: ["src/minisprites/pokemon/gen6/*", ...sheetDeps], - cmds: ["node tools/sheet/index.ts %f %o", compresspng({config: "SPRITESHEET"})], -}, "pokemonicons-sheet.png"); + deps: ['src/minisprites/pokemon/gen6/*', ...sheetDeps], + cmds: ['node tools/sheet/index.ts %f %o', compresspng({config: 'SPRITESHEET'})], +}, 'pokemonicons-sheet.png'); // TODO: reenable when trainers are moved // rule("ps-trainers.sheet.mjs", { @@ -33,37 +33,37 @@ rule("ps-pokemon.sheet.mjs", { // cmds: ["node tools/sheet/index.ts %f %o", compresspng({config: "SPRITESHEET"})], // }, "trainers-sheet.png"); -rule("ps-items.sheet.mjs", { - display: "ps items sheet", +rule('ps-items.sheet.mjs', { + display: 'ps items sheet', nameSensitive: true, - deps: ["src/minisprites/items/*", ...sheetDeps], - cmds: ["node tools/sheet/index.ts %f %o", compresspng({config: "SPRITESHEET"})], -}, "itemicons-sheet.png"); + deps: ['src/minisprites/items/*', ...sheetDeps], + cmds: ['node tools/sheet/index.ts %f %o', compresspng({config: 'SPRITESHEET'})], +}, 'itemicons-sheet.png'); // PS pokeball icons; input order is the sheet order. rule([ - "src/_uncategorized/noncanonical/ui/battle/Ball-Normal.png", - "src/_uncategorized/noncanonical/ui/battle/Ball-Sick.png", - "src/_uncategorized/noncanonical/ui/battle/Ball-Null.png", + 'src/_uncategorized/noncanonical/ui/battle/Ball-Normal.png', + 'src/_uncategorized/noncanonical/ui/battle/Ball-Sick.png', + 'src/_uncategorized/noncanonical/ui/battle/Ball-Null.png', ], { - display: "pokemonicons-pokeball-sheet", + display: 'pokemonicons-pokeball-sheet', cmds: [ `magick convert ${PNG_DETERMINISTIC} -background transparent -gravity center -extent 40x30 %f +append %o`, - compresspng({config: "SPRITESHEET"}), + compresspng({config: 'SPRITESHEET'}), ], -}, "pokemonicons-pokeball-sheet.png"); +}, 'pokemonicons-pokeball-sheet.png'); // Padded Dex, plus missing CAPs backfilled from the gen5/model gifs. -const dex = forEachRule("src/dex/*", { - display: "pad dex %f", - cmds: [pad({w: 120, h: 120}), compresspng({config: "DEX"})], -}, "%b"); +const dex = forEachRule('src/dex/*', { + display: 'pad dex %f', + cmds: [pad({w: 120, h: 120}), compresspng({config: 'DEX'})], +}, '%b'); const dexSet = new Set(dex.map(base)); const dexMissing = []; -for (const file of spriteglob(["src/sprites/gen5/*.gif", "src/models/*.gif"], {b: false, s: false})) { +for (const file of spriteglob(['src/sprites/gen5/*.gif', 'src/models/*.gif'], {b: false, s: false})) { if (!dexSet.has(base(file))) { dexMissing.push(file); dexSet.add(base(file)); @@ -71,13 +71,13 @@ for (const file of spriteglob(["src/sprites/gen5/*.gif", "src/models/*.gif"], {b } forEachRule(dexMissing, { - display: "missing dex %B", + display: 'missing dex %B', cmds: [ `magick convert "%f[0]" ${PNG_DETERMINISTIC} -trim %o`, `magick mogrify ${PNG_DETERMINISTIC} -background transparent -gravity center -resize "120x120>" -extent 120x120 %o`, - compresspng({config: "DEX"}), + compresspng({config: 'DEX'}), ], -}, "%B.png"); +}, '%B.png'); // ani/: the models plus champions backfill, under PS ids. @@ -86,9 +86,9 @@ const aniChampions = gen10Modelslike(); deploy(ctx => { const seenModels = new Set(); - for (const f of ctx.list("src/models")) { + for (const f of ctx.list('src/models')) { seenModels.add(f.name); - psSpritecopy(ctx, f, "ani"); + psSpritecopy(ctx, f, 'ani'); } for (const f of aniChampions) { @@ -96,7 +96,7 @@ deploy(ctx => { continue; } seenModels.add(f.name); - psSpritecopy(ctx, f, "ani"); + psSpritecopy(ctx, f, 'ani'); } // TODO: ship the padded dex, sheets, trainers, types/categories when @@ -104,12 +104,12 @@ deploy(ctx => { }); // PS ids keep the forme dash, unlike the smogon aliases. -function psSpritecopy(ctx : DeployCtx, f : Sprite, dir : string) : void { +function psSpritecopy(ctx: DeployCtx, f: Sprite, dir: string): void { const sn = spritedata.parseFilename(f.name); - let name : string; + let name: string; // Skip asymmetrical for now - if (sn.extra.has("a") || sn.extra.has("b") || sn.extra.has("s")) { + if (sn.extra.has('a') || sn.extra.has('b') || sn.extra.has('s')) { return; } @@ -125,11 +125,11 @@ function psSpritecopy(ctx : DeployCtx, f : Sprite, dir : string) : void { if (sd.forme) { name += `-${toPSID(sd.forme)}`; } - if (sn.extra.has("f")) { - name += "-f"; + if (sn.extra.has('f')) { + name += '-f'; } - if (sn.extra.has("g")) { - name += "-gmax"; + if (sn.extra.has('g')) { + name += '-gmax'; } if (f.ext === null) { diff --git a/rules/minisprites.ts b/rules/minisprites.ts index 14a7bb36..df91186d 100644 --- a/rules/minisprites.ts +++ b/rules/minisprites.ts @@ -4,30 +4,30 @@ import {compresspng, pad, trimimg} from '../tools/build/helpers.ts'; // Uniform size minisprites -export function gen6Padded() : Artifact[] { - return forEachRule("src/minisprites/pokemon/gen6/*.png", { - display: "pad g6 minisprite %f", - cmds: [pad({w: 40, h: 30}), compresspng({config: "MINISPRITE"})], - }, "%b"); +export function gen6Padded(): Artifact[] { + return forEachRule('src/minisprites/pokemon/gen6/*.png', { + display: 'pad g6 minisprite %f', + cmds: [pad({w: 40, h: 30}), compresspng({config: 'MINISPRITE'})], + }, '%b'); } -export function itemPadded() : Artifact[] { - return forEachRule("src/minisprites/items/*.png", { - display: "pad item minisprite %f", - cmds: [pad({w: 24, h: 24}), compresspng({config: "MINISPRITE"})], - }, "%b"); +export function itemPadded(): Artifact[] { + return forEachRule('src/minisprites/items/*.png', { + display: 'pad item minisprite %f', + cmds: [pad({w: 24, h: 24}), compresspng({config: 'MINISPRITE'})], + }, '%b'); } -export function gen6Trimmed() : Artifact[] { - return forEachRule("src/minisprites/pokemon/gen6/*.png", { - display: "trim g6 minisprite %f", - cmds: [trimimg(), compresspng({config: "MINISPRITE"})], - }, "%b"); +export function gen6Trimmed(): Artifact[] { + return forEachRule('src/minisprites/pokemon/gen6/*.png', { + display: 'trim g6 minisprite %f', + cmds: [trimimg(), compresspng({config: 'MINISPRITE'})], + }, '%b'); } -export function itemTrimmed() : Artifact[] { - return forEachRule("src/minisprites/items/*.png", { - display: "trim item minisprite %f", - cmds: [trimimg(), compresspng({config: "MINISPRITE"})], - }, "%b"); +export function itemTrimmed(): Artifact[] { + return forEachRule('src/minisprites/items/*.png', { + display: 'trim item minisprite %f', + cmds: [trimimg(), compresspng({config: 'MINISPRITE'})], + }, '%b'); } diff --git a/rules/modelslike.ts b/rules/modelslike.ts index d2a0ddc8..a0dde6d5 100644 --- a/rules/modelslike.ts +++ b/rules/modelslike.ts @@ -3,39 +3,39 @@ import {type Artifact, forEachRule} from '../tools/build/artifact.ts'; // Gen 9 -export function gen9Modelslike() : Artifact[] { - return forEachRule("src/gen9species/*.png", { - display: "96x96 %f", +export function gen9Modelslike(): Artifact[] { + return forEachRule('src/gen9species/*.png', { + display: '96x96 %f', // TODO, add customizable compression for gif // ... or investigate using webp instead of both png/gif here cmds: [ - "magick convert %f -trim +repage -resize 90x90 %o", - "gifsicle -O3 -b %o", + 'magick convert %f -trim +repage -resize 90x90 %o', + 'gifsicle -O3 -b %o', ], - }, "%B.gif"); + }, '%B.gif'); } // Gen 10 -export function gen10Modelslike() : Artifact[] { - return forEachRule("src/champions/*.png", { - display: "96x96 %f", +export function gen10Modelslike(): Artifact[] { + return forEachRule('src/champions/*.png', { + display: '96x96 %f', // TODO, add customizable compression for gif // ... or investigate using webp instead of both png/gif here cmds: [ - "magick convert %f -trim +repage -resize 90x90 %o", - "gifsicle -O3 -b %o", + 'magick convert %f -trim +repage -resize 90x90 %o', + 'gifsicle -O3 -b %o', ], - }, "%B.gif"); + }, '%B.gif'); } // Gen 5 CAPs... -export function gen5Gifs() : Artifact[] { - return forEachRule("src/sprites/gen5/*.png", [ +export function gen5Gifs(): Artifact[] { + return forEachRule('src/sprites/gen5/*.png', [ // TODO, add customizable compression for gif // ... or investigate using webp instead of both png/gif here - "magick convert %f %o", - "gifsicle -O3 -b %o", - ], "%B.gif"); + 'magick convert %f %o', + 'gifsicle -O3 -b %o', + ], '%B.gif'); } diff --git a/rules/publish.ts b/rules/publish.ts index 9f9bedab..411370a2 100644 --- a/rules/publish.ts +++ b/rules/publish.ts @@ -10,14 +10,14 @@ export type Sprite = Artifact | SrcFile; // The unhashed -> hashed name mapping published beside a stamped set. export class Manifest { - readonly ctx : DeployCtx; + readonly ctx: DeployCtx; private entries = new Map(); - constructor(ctx : DeployCtx) { + constructor(ctx: DeployCtx) { this.ctx = ctx; } - set(key : string, value : string) : void { + set(key: string, value: string): void { // ActionQueue only dedups final dsts; hashed dsts differ even when // unhashed names collide, so check the key explicitly. if (this.entries.has(key)) { @@ -26,8 +26,8 @@ export class Manifest { this.entries.set(key, value); } - write(dst : string) : void { - const sorted : Record = {}; + write(dst: string): void { + const sorted: Record = {}; for (const k of [...this.entries.keys()].sort()) { sorted[k] = this.entries.get(k)!; } @@ -36,11 +36,11 @@ export class Manifest { } export interface Dest { - dir : string; - ext? : string; + dir: string; + ext?: string; } -function extOf(f : Sprite, ext? : string) : string { +function extOf(f: Sprite, ext?: string): string { const result = ext ?? f.ext; if (result === null) { throw new Error(`Sprite ${f.name} has no extension`); @@ -48,37 +48,37 @@ function extOf(f : Sprite, ext? : string) : string { return result; } -export function toSmogonAlias(name : string) : string { +export function toSmogonAlias(name: string): string { return name.toLowerCase(). - replace(/[ _]+/, "-"). + replace(/[ _]+/, '-'). replace(/[^a-z0-9-]+/g, ''); } -export function toPSID(name : string) : string { +export function toPSID(name: string): string { return name.toLowerCase().replace(/[^a-z0-9]+/g, ''); } // Copy with a content-hash-stamped name and record the unhashed -> hashed // mapping in `manifest`. -export function stampcopy(manifest : Manifest, f : Sprite, {dir, ext}: Dest, name : string) : void { +export function stampcopy(manifest: Manifest, f: Sprite, {dir, ext}: Dest, name: string): void { const h = manifest.ctx.hash(f); manifest.set(`${name}.${extOf(f, ext)}`, `${name}-${h}.${extOf(f, ext)}`); manifest.ctx.copy(f, `${dir}/${name}-${h}.${extOf(f, ext)}`); } -export function spritecopy(manifest : Manifest, f : Sprite, dest : Dest, - allowUnknown = false) : void { +export function spritecopy(manifest: Manifest, f: Sprite, dest: Dest, + allowUnknown = false): void { const sn = spritedata.parseFilename(f.name); - let name : string; + let name: string; // Skip asymmetrical for now - if (sn.extra.has("a") || sn.extra.has("b") || sn.extra.has("s")) { + if (sn.extra.has('a') || sn.extra.has('b') || sn.extra.has('s')) { return; } if (sn.extension) { - if (allowUnknown && sn.name === "Unknown") { - name = "unknown"; + if (allowUnknown && sn.name === 'Unknown') { + name = 'unknown'; } else { // Skip this, we don't use Unknown/Substitute return; @@ -93,18 +93,18 @@ export function spritecopy(manifest : Manifest, f : Sprite, dest : Dest, name += `-${toSmogonAlias(sd.forme)}`; } } - if (sn.extra.has("f")) { - name += "-f"; + if (sn.extra.has('f')) { + name += '-f'; } - if (sn.extra.has("g")) { - name += "-gmax"; + if (sn.extra.has('g')) { + name += '-gmax'; } stampcopy(manifest, f, dest, name); } // TODO: merge with above -export function itemspritecopy(manifest : Manifest, f : Sprite, dest : Dest) : void { +export function itemspritecopy(manifest: Manifest, f: Sprite, dest: Dest): void { const sn = spritedata.parseFilename(f.name); if (sn.extension) { throw new Error(`Not an item sprite: ${f.name}`); @@ -118,7 +118,7 @@ export function itemspritecopy(manifest : Manifest, f : Sprite, dest : Dest) : v } } -export function newspritecopy(ctx : DeployCtx, f : Sprite, dest : Dest) : void { +export function newspritecopy(ctx: DeployCtx, f: Sprite, dest: Dest): void { const sn = spritedata.parseFilename(f.name); if (sn.extension) { return; @@ -126,11 +126,11 @@ export function newspritecopy(ctx : DeployCtx, f : Sprite, dest : Dest) : void { const sd = spritedata.get(sn.id); for (const n of sd.type === 'item' ? sd.names : [sd.base + sd.forme]) { let name = toPSID(n); - if (sn.extra.has("f")) { - name += "f"; + if (sn.extra.has('f')) { + name += 'f'; } - if (sn.extra.has("g")) { - name += "gmax"; + if (sn.extra.has('g')) { + name += 'gmax'; } ctx.copy(f, `${dest.dir}/${name}.${extOf(f, dest.ext)}`); } diff --git a/rules/social.ts b/rules/social.ts index eefc1d8e..ce8d9d72 100644 --- a/rules/social.ts +++ b/rules/social.ts @@ -5,10 +5,10 @@ import {PNG_DETERMINISTIC, base, compresspng, spriteglob} from '../tools/build/h // Smogdex social images: models, backfilled with gen9 species not yet in // models (first source wins). -function socialInputs() : string[] { - const social = spriteglob(["src/models/*"], {b: false, s: false}); +function socialInputs(): string[] { + const social = spriteglob(['src/models/*'], {b: false, s: false}); const socialSeen = new Set(social.map(base)); - for (const file of spriteglob(["src/gen9species/*"], {b: false, s: false})) { + for (const file of spriteglob(['src/gen9species/*'], {b: false, s: false})) { if (!socialSeen.has(base(file))) { social.push(file); socialSeen.add(base(file)); @@ -17,22 +17,22 @@ function socialInputs() : string[] { return social; } -export function fbSprites() : Artifact[] { +export function fbSprites(): Artifact[] { return forEachRule(socialInputs(), { - display: "fbsprite %f", + display: 'fbsprite %f', cmds: [ `magick convert "%f[0]" ${PNG_DETERMINISTIC} -trim -resize 150x150 -background white -gravity center -extent 198x198 -bordercolor black -border 1 %o`, - compresspng({config: "MODELS"}), + compresspng({config: 'MODELS'}), ], - }, "%B.png"); + }, '%B.png'); } -export function twitterSprites() : Artifact[] { +export function twitterSprites(): Artifact[] { return forEachRule(socialInputs(), { - display: "twittersprite %f", + display: 'twittersprite %f', cmds: [ `magick convert "%f[0]" ${PNG_DETERMINISTIC} -trim -resize 115x115 -background white -gravity center -extent 120x120 %o`, - compresspng({config: "MODELS"}), + compresspng({config: 'MODELS'}), ], - }, "%B.png"); + }, '%B.png'); } diff --git a/smogon.build.ts b/smogon.build.ts index 5927270b..5833fd1c 100644 --- a/smogon.build.ts +++ b/smogon.build.ts @@ -13,15 +13,15 @@ const xyGen5 = gen5Gifs(); deploy(ctx => { const seenModels = new Set(); const manifest = new Manifest(ctx); - const xycopy = (f : Sprite) => { + const xycopy = (f: Sprite) => { if (seenModels.has(f.name)) { return; } seenModels.add(f.name); - spritecopy(manifest, f, {dir: "xy"}); + spritecopy(manifest, f, {dir: 'xy'}); }; - for (const f of ctx.list("src/models")) { + for (const f of ctx.list('src/models')) { xycopy(f); } for (const f of xyModels) { @@ -31,7 +31,7 @@ deploy(ctx => { xycopy(f); } // Non-model CAPs - for (const f of ctx.list("src/sprites/gen5")) { + for (const f of ctx.list('src/sprites/gen5')) { if (f.ext === 'gif') { xycopy(f); } @@ -39,7 +39,7 @@ deploy(ctx => { for (const f of xyGen5) { xycopy(f); } - manifest.write("xy/manifest.json"); + manifest.write('xy/manifest.json'); }); // xyicons/: trimmed gen6 minisprites. @@ -49,9 +49,9 @@ const xyIcons = gen6Trimmed(); deploy(ctx => { const manifest = new Manifest(ctx); for (const f of xyIcons) { - spritecopy(manifest, f, {dir: "xyicons"}); + spritecopy(manifest, f, {dir: 'xyicons'}); } - manifest.write("xyicons/manifest.json"); + manifest.write('xyicons/manifest.json'); }); // Deprecated, unstamped sets: diff --git a/tools/build/artifact.ts b/tools/build/artifact.ts index 01ae0e6f..ca3f8491 100644 --- a/tools/build/artifact.ts +++ b/tools/build/artifact.ts @@ -11,36 +11,36 @@ import {type Cmd, basenameNoExt, flattenCmds, substitute, substituteNames} from // rebuilds anything. The digest is resolved by the executor (from the store // on a clean hit, or by running the rule). export class Artifact { - readonly name : string; // nominal basename sans ext ("abra", "spritesheet") - readonly ext : string; // "png", "gif", ... (no dot) - readonly decl : RuleDecl; - readonly index : number; // position among decl.outputs - private digest : string | null = null; + readonly name: string; // nominal basename sans ext ("abra", "spritesheet") + readonly ext: string; // "png", "gif", ... (no dot) + readonly decl: RuleDecl; + readonly index: number; // position among decl.outputs + private digest: string | null = null; - constructor(name : string, ext : string, decl : RuleDecl, index : number) { + constructor(name: string, ext: string, decl: RuleDecl, index: number) { this.name = name; this.ext = ext; this.decl = decl; this.index = index; } - get filename() : string { + get filename(): string { return `${this.name}.${this.ext}`; } // The producing rule's source-path inputs (provenance). - get sources() : string[] { + get sources(): string[] { return this.decl.inputs.filter(i => typeof i === 'string'); } - get hash() : string { + get hash(): string { if (this.digest === null) { throw new Error(`Artifact ${this.filename} has not been built yet`); } return this.digest; } - resolve(digest : string) : void { + resolve(digest: string): void { if (this.digest !== null && this.digest !== digest) { throw new Error(`Artifact ${this.filename} resolved twice with different digests`); } @@ -51,54 +51,54 @@ export class Artifact { export type Input = string | Artifact; // string = source path relative to repo root export interface CmdSpec { - display? : string; + display?: string; // Tracked-but-not-substituted inputs: part of rule identity, but never // expanded into %f. Use for files a tool reads on its own (e.g. // tools/sheet readdirs the minisprite directories). - deps? : Input | Input[]; + deps?: Input | Input[]; // IMPORTANT: set this on any rule whose output BYTES depend on input // NAMES (a tool that readdirs, or parses ids out of %f filenames, e.g. // the spritesheet builders). Identity is normally content-only, so // without this flag a same-bytes rename would leave the output stale. - nameSensitive? : boolean; - cmds : Cmd[]; + nameSensitive?: boolean; + cmds: Cmd[]; } export interface RuleDecl { - id : number; // registration order; identity for artifact inputs - inputs : Input[]; // ordered (%f order; some rules are order-sensitive) - deps : Input[]; - outputs : Artifact[]; - cmds : string[]; // flattened PRE-substitution templates - display : string | null; // nominally substituted; cosmetic - displayTemplate : string | null; // pre-substitution; groups forEach rules - nameSensitive : boolean; + id: number; // registration order; identity for artifact inputs + inputs: Input[]; // ordered (%f order; some rules are order-sensitive) + deps: Input[]; + outputs: Artifact[]; + cmds: string[]; // flattened PRE-substitution templates + display: string | null; // nominally substituted; cosmetic + displayTemplate: string | null; // pre-substitution; groups forEach rules + nameSensitive: boolean; } -let decls : RuleDecl[] = []; +let decls: RuleDecl[] = []; // Declaring an identical rule twice returns the existing artifacts, so // shared rule sets are plain functions that any number of deploys may call. let declIndex = new Map(); -export function getDecls() : RuleDecl[] { +export function getDecls(): RuleDecl[] { return decls; } -export function resetDecls() : void { +export function resetDecls(): void { decls = []; declIndex = new Map(); } // Nominal path of an input, for %b/%B and displays. -function nominal(i : Input) : string { +function nominal(i: Input): string { return typeof i === 'string' ? i : i.filename; } -function extOf(i : Input) : string { +function extOf(i: Input): string { return typeof i === 'string' ? pathlib.extname(i) : `.${i.ext}`; } -function pathOrThrow(i : Input, what : string) : string { +function pathOrThrow(i: Input, what: string): string { if (typeof i !== 'string') { throw new Error(`nameSensitive rules with artifact ${what} are not yet supported`); } @@ -110,7 +110,7 @@ function pathOrThrow(i : Input, what : string) : string { // source renames and byte-identical duplicates dedupe for free. Output exts // are included because tools pick output formats from extensions; nominal // names are not. -export function computeKey(decl : RuleDecl, digestOf : (i : Input) => string) : string { +export function computeKey(decl: RuleDecl, digestOf: (i: Input) => string): string { const h = createHash('sha256'); h.update([ 'v2', @@ -128,17 +128,17 @@ export function computeKey(decl : RuleDecl, digestOf : (i : Input) => string) : return h.digest('hex'); } -function normalizeSpec(spec : CmdSpec | Cmd[]) : CmdSpec { +function normalizeSpec(spec: CmdSpec | Cmd[]): CmdSpec { return Array.isArray(spec) ? {cmds: spec} : spec; } // Glob string patterns; artifacts pass through. Order is preserved. -function resolveInputs(input : Input | Input[] | undefined) : Input[] { +function resolveInputs(input: Input | Input[] | undefined): Input[] { const list = input === undefined ? [] : Array.isArray(input) ? input : [input]; - return list.flatMap((i) : Input[] => typeof i === 'string' ? glob(i) : [i]); + return list.flatMap((i): Input[] => typeof i === 'string' ? glob(i) : [i]); } -function makeDecl(inputs : Input[], deps : Input[], spec : CmdSpec, outputs : string[]) : RuleDecl { +function makeDecl(inputs: Input[], deps: Input[], spec: CmdSpec, outputs: string[]): RuleDecl { const nominalInputs = inputs.map(nominal); // %b/%B expand to nominal input names, never CAS paths, so they are // resolved at declaration. This also lands them in the identity key: a @@ -153,7 +153,7 @@ function makeDecl(inputs : Input[], deps : Input[], spec : CmdSpec, outputs : st // An identical declaration returns the already-registered rule. Artifact // inputs are keyed by their producing rule's id, so chains dedupe too. - const token = (i : Input) => typeof i === 'string' ? `s:${i}` : `a:${i.decl.id}:${i.index}`; + const token = (i: Input) => typeof i === 'string' ? `s:${i}` : `a:${i.decl.id}:${i.index}`; const identity = [ cmds.join('\0'), inputs.map(token).join('\0'), @@ -167,7 +167,7 @@ function makeDecl(inputs : Input[], deps : Input[], spec : CmdSpec, outputs : st return existing; } - const decl : RuleDecl = { + const decl: RuleDecl = { id: decls.length, inputs, deps, @@ -216,20 +216,20 @@ function makeDecl(inputs : Input[], deps : Input[], spec : CmdSpec, outputs : st // One Artifact per declared output, as a tuple when the output list is a // literal, so `const [png, css] = rule(...)` needs no undefined checks. A // single string output returns its Artifact directly. -export function rule(input : Input | Input[], spec : CmdSpec | Cmd[], - output : string) : Artifact; +export function rule(input: Input | Input[], spec: CmdSpec | Cmd[], + output: string): Artifact; export function rule( - input : Input | Input[], spec : CmdSpec | Cmd[], - output : T) : {[K in keyof T] : Artifact}; -export function rule(input : Input | Input[], spec : CmdSpec | Cmd[], - output : string | readonly string[]) : Artifact | Artifact[] { + input: Input | Input[], spec: CmdSpec | Cmd[], + output: T): {[K in keyof T]: Artifact}; +export function rule(input: Input | Input[], spec: CmdSpec | Cmd[], + output: string | readonly string[]): Artifact | Artifact[] { const s = normalizeSpec(spec); const decl = makeDecl(resolveInputs(input), resolveInputs(s.deps), s, astable(output)); return typeof output === 'string' ? decl.outputs[0]! : decl.outputs; } -export function forEachRule(input : Input | Input[], spec : CmdSpec | Cmd[], - output : string) : Artifact[] { +export function forEachRule(input: Input | Input[], spec: CmdSpec | Cmd[], + output: string): Artifact[] { const s = normalizeSpec(spec); if (/%[fo]/.test(output)) { throw new Error(`forEachRule output template may only use %b/%B: ${output}`); diff --git a/tools/build/cas.ts b/tools/build/cas.ts index 16e71705..67ceb881 100644 --- a/tools/build/cas.ts +++ b/tools/build/cas.ts @@ -11,18 +11,18 @@ import {hashFileSync} from './hash.ts'; // store. Writers stage the file elsewhere on the same filesystem and insert // it with an atomic rename. -export function casPath(casDir : string, digest : string, ext : string) : string { +export function casPath(casDir: string, digest: string, ext: string): string { return pathlib.join(casDir, digest.slice(0, 2), `${digest}.${ext}`); } -export function casExists(casDir : string, digest : string, ext : string) : boolean { +export function casExists(casDir: string, digest: string, ext: string): boolean { return casStat(casDir, digest, ext) !== null; } // Size of an object, or null if absent. Callers verify it against the // recorded size: a crash between rename and data flush can leave a // truncated object, which must read as dirty, not clean. -export function casStat(casDir : string, digest : string, ext : string) : bigint | null { +export function casStat(casDir: string, digest: string, ext: string): bigint | null { try { const st = fs.statSync(casPath(casDir, digest, ext), {bigint: true}); return st.isFile() ? st.size : null; @@ -32,14 +32,14 @@ export function casStat(casDir : string, digest : string, ext : string) : bigint } export interface CasObject { - digest : string; // sha256 hex of the bytes - size : bigint; + digest: string; // sha256 hex of the bytes + size: bigint; } // Move tmpPath into the store, returning the content digest and size. An // existing object is trusted only if its bytes actually hash to the digest; // otherwise (crash-truncated object) the fresh bytes replace it. -export function casInsert(casDir : string, tmpPath : string, ext : string) : CasObject { +export function casInsert(casDir: string, tmpPath: string, ext: string): CasObject { const digest = hashFileSync(tmpPath).toString('hex'); const size = fs.statSync(tmpPath, {bigint: true}).size; const target = casPath(casDir, digest, ext); @@ -63,13 +63,13 @@ export function casInsert(casDir : string, tmpPath : string, ext : string) : Cas // Remove every object not in `live` (keys are ".", the object // basename) and prune emptied fanout directories. Returns the removal count. -export function casSweep(casDir : string, live : Set) : number { +export function casSweep(casDir: string, live: Set): number { let removed = 0; - let fanout : fs.Dirent[]; + let fanout: fs.Dirent[]; try { fanout = fs.readdirSync(casDir, {withFileTypes: true}); } catch (err) { - if ((err as {code? : string}).code === 'ENOENT') { + if ((err as {code?: string}).code === 'ENOENT') { return 0; } throw err; diff --git a/tools/build/config.ts b/tools/build/config.ts index 86adc1df..4a745720 100644 --- a/tools/build/config.ts +++ b/tools/build/config.ts @@ -1,7 +1,7 @@ import fs from 'fs'; -export function parseConfig(text : string) : Map { +export function parseConfig(text: string): Map { const result = new Map(); for (let line of text.split('\n')) { line = line.trim(); @@ -17,7 +17,7 @@ export function parseConfig(text : string) : Map { return result; } -export function loadConfig(path : string) : Map { +export function loadConfig(path: string): Map { if (!fs.existsSync(path)) { return new Map(); } diff --git a/tools/build/driver.ts b/tools/build/driver.ts index a1b27377..d74958f4 100644 --- a/tools/build/driver.ts +++ b/tools/build/driver.ts @@ -7,29 +7,29 @@ import {reconcileHashes} from './hash.ts'; import {type Store} from './store.ts'; export interface BuildOpts { - root : string; - store : Store; - casDir : string; // relative to root; substituted into commands - tmpDir : string; - jobs : number; - dryRun : boolean; - failFast : boolean; - verbose : boolean; + root: string; + store: Store; + casDir: string; // relative to root; substituted into commands + tmpDir: string; + jobs: number; + dryRun: boolean; + failFast: boolean; + verbose: boolean; // GC after a fully successful build: drop rules whose key is no longer // declared, sweep unreferenced CAS objects, prune the file cache. Only // safe when `decls` is the full rule universe (a partial build would GC // the other deploys' state), so the CLI sets it for union builds only. - gc : boolean; - signal : AbortSignal; - log? : (line : string) => void; - logError? : (line : string) => void; + gc: boolean; + signal: AbortSignal; + log?: (line: string) => void; + logError?: (line: string) => void; } export interface DriveResult extends BuildResult { - interrupted : boolean; + interrupted: boolean; } -export async function build(decls : readonly RuleDecl[], opts : BuildOpts) : Promise { +export async function build(decls: readonly RuleDecl[], opts: BuildOpts): Promise { const log = opts.log ?? console.log; const logError = opts.logError ?? console.error; const {store} = opts; @@ -38,7 +38,7 @@ export async function build(decls : readonly RuleDecl[], opts : BuildOpts) : Pro // producers of its artifact inputs, even ones outside `decls`. const closure = new Set(); const sources = new Set(); - const add = (decl : RuleDecl) => { + const add = (decl: RuleDecl) => { if (closure.has(decl)) { return; } diff --git a/tools/build/exec.ts b/tools/build/exec.ts index e8a35f80..1b066616 100644 --- a/tools/build/exec.ts +++ b/tools/build/exec.ts @@ -2,16 +2,16 @@ import {spawn} from 'child_process'; export interface ExecResult { - code : number | null; - signal : NodeJS.Signals | null; - output : string; - durationMs : number; + code: number | null; + signal: NodeJS.Signals | null; + output: string; + durationMs: number; } const livePids = new Set(); // Emergency stop (e.g. second Ctrl-C): SIGKILL every live process group. -export function killAllProcessGroups() : void { +export function killAllProcessGroups(): void { for (const pid of livePids) { try { process.kill(-pid, 'SIGKILL'); @@ -22,7 +22,7 @@ export function killAllProcessGroups() : void { // The command script is fed to sh via stdin rather than -c: a single argv // entry is capped by the kernel (MAX_ARG_STRLEN, ~128KB) and the largest %f // expansion is already 80KB+. -export function runShell(command : string, opts : {cwd : string, signal : AbortSignal}) : Promise { +export function runShell(command: string, opts: {cwd: string, signal: AbortSignal}): Promise { return new Promise((resolve, reject) => { const start = performance.now(); // detached: own process group, so an abort kills grandchildren @@ -31,14 +31,14 @@ export function runShell(command : string, opts : {cwd : string, signal : AbortS if (child.pid !== undefined) { livePids.add(child.pid); } - const chunks : Buffer[] = []; + const chunks: Buffer[] = []; child.stdout.on('data', c => chunks.push(c)); child.stderr.on('data', c => chunks.push(c)); child.stdin.on('error', () => {}); // EPIPE if the shell exits early child.stdin.end(command + '\n'); - let killTimer : NodeJS.Timeout | undefined; - const kill = (sig : NodeJS.Signals) => { + let killTimer: NodeJS.Timeout | undefined; + const kill = (sig: NodeJS.Signals) => { try { process.kill(-child.pid!, sig); } catch {} diff --git a/tools/build/executor.ts b/tools/build/executor.ts index 2d47d525..b0346b25 100644 --- a/tools/build/executor.ts +++ b/tools/build/executor.ts @@ -12,38 +12,38 @@ import {substitute} from './subst.ts'; export type DirtyReason = 'new' | 'cas-missing'; export type RuleOutcome = - | {status : 'clean'} // key hit, CAS objects present - | {status : 'ran', reason : DirtyReason} - | {status : 'would-run', reason : DirtyReason | 'blocked'} // dry run - | {status : 'failed', message : string} - | {status : 'blocked'}; // a producer failed + | {status: 'clean'} // key hit, CAS objects present + | {status: 'ran', reason: DirtyReason} + | {status: 'would-run', reason: DirtyReason | 'blocked'} // dry run + | {status: 'failed', message: string} + | {status: 'blocked'}; // a producer failed export interface BuildResult { - outcomes : Map; // no entry = not attempted (aborted) - keys : Map; // only decls whose key resolved - ok : boolean; // every decl clean or ran + outcomes: Map; // no entry = not attempted (aborted) + keys: Map; // only decls whose key resolved + ok: boolean; // every decl clean or ran } export interface ExecutorOpts { - root : string; // cwd for commands - store : Store; - casDir : string; // relative to root (substituted into commands) - tmpDir : string; - jobs : number; - dryRun : boolean; - failFast : boolean; - verbose? : boolean; // annotate run lines with the dirty reason - sourceHashes : Map; // every source input/dep, pre-reconciled - signal : AbortSignal; - log? : (line : string) => void; - logError? : (line : string) => void; + root: string; // cwd for commands + store: Store; + casDir: string; // relative to root (substituted into commands) + tmpDir: string; + jobs: number; + dryRun: boolean; + failFast: boolean; + verbose?: boolean; // annotate run lines with the dirty reason + sourceHashes: Map; // every source input/dep, pre-reconciled + signal: AbortSignal; + log?: (line: string) => void; + logError?: (line: string) => void; } -export function label(decl : RuleDecl) : string { +export function label(decl: RuleDecl): string { return decl.display ?? decl.cmds[0]!; } -function indent(text : string) : string { +function indent(text: string): string { return text.replace(/\n$/, '').split('\n').map(l => ' ' + l).join('\n'); } @@ -54,14 +54,14 @@ class DryDirty extends Error {} class Aborted extends Error {} class Semaphore { - private available : number; - private waiters : (() => void)[] = []; + private available: number; + private waiters: (() => void)[] = []; - constructor(n : number) { + constructor(n: number) { this.available = n; } - async acquire() : Promise { + async acquire(): Promise { if (this.available > 0) { this.available--; return; @@ -69,7 +69,7 @@ class Semaphore { await new Promise(resolve => this.waiters.push(resolve)); } - release() : void { + release(): void { const waiter = this.waiters.shift(); if (waiter !== undefined) { waiter(); @@ -85,20 +85,20 @@ class Semaphore { // The artifact graph is a DAG by construction (a rule can only reference // artifacts that already exist as values), so there is no cycle check. export class Executor { - private opts : ExecutorOpts; + private opts: ExecutorOpts; private memo = new Map>(); private inflightByKey = new Map>(); private outcomes = new Map(); private keys = new Map(); - private semaphore : Semaphore; + private semaphore: Semaphore; private failAc = new AbortController(); - private runSignal : AbortSignal; + private runSignal: AbortSignal; private counter = 0; private tmpSeq = 0; - private log : (line : string) => void; - private logError : (line : string) => void; + private log: (line: string) => void; + private logError: (line: string) => void; - constructor(opts : ExecutorOpts) { + constructor(opts: ExecutorOpts) { this.opts = opts; this.semaphore = new Semaphore(opts.jobs); this.runSignal = AbortSignal.any([opts.signal, this.failAc.signal]); @@ -106,7 +106,7 @@ export class Executor { this.logError = opts.logError ?? console.error; } - async build(decls : readonly RuleDecl[]) : Promise { + async build(decls: readonly RuleDecl[]): Promise { await Promise.allSettled(decls.map(d => this.demand(d))); const ok = decls.every(d => { const status = this.outcomes.get(d)?.status; @@ -115,7 +115,7 @@ export class Executor { return {outcomes: this.outcomes, keys: this.keys, ok}; } - private demand(decl : RuleDecl) : Promise { + private demand(decl: RuleDecl): Promise { let p = this.memo.get(decl); if (p === undefined) { // Any non-sentinel escape (a store error, a resolve conflict, a @@ -136,7 +136,7 @@ export class Executor { return p; } - private async digestOf(i : Input) : Promise { + private async digestOf(i: Input): Promise { if (typeof i !== 'string') { await this.demand(i.decl); return i.hash; @@ -148,8 +148,8 @@ export class Executor { return hash.toString('hex'); } - private async demandInner(decl : RuleDecl) : Promise { - let digests : Map; + private async demandInner(decl: RuleDecl): Promise { + let digests: Map; try { const inputs = [...decl.inputs, ...decl.deps]; const resolved = await Promise.all(inputs.map(i => this.digestOf(i))); @@ -191,7 +191,7 @@ export class Executor { return result; } - private async perform(decl : RuleDecl, key : string) : Promise { + private async perform(decl: RuleDecl, key: string): Promise { const {store, casDir} = this.opts; const stored = store.lookupRule(key); if (stored !== null @@ -201,7 +201,7 @@ export class Executor { this.outcomes.set(decl, {status: 'clean'}); return stored.map(o => o.digest); } - const reason : DirtyReason = stored === null ? 'new' : 'cas-missing'; + const reason: DirtyReason = stored === null ? 'new' : 'cas-missing'; if (this.opts.dryRun) { this.outcomes.set(decl, {status: 'would-run', reason}); @@ -220,7 +220,7 @@ export class Executor { } } - private async execute(decl : RuleDecl, key : string, reason : DirtyReason) : Promise { + private async execute(decl: RuleDecl, key: string, reason: DirtyReason): Promise { const {store, casDir, tmpDir, root} = this.opts; const ruleTmp = pathlib.join(tmpDir, String(this.tmpSeq++)); fs.mkdirSync(ruleTmp, {recursive: true}); @@ -261,7 +261,7 @@ export class Executor { // Nothing is recorded for a failure: failed and never-ran are the same // state, so the rule stays dirty. - private fail(decl : RuleDecl, command : string, output : string, message : string) : never { + private fail(decl: RuleDecl, command: string, output: string, message: string): never { this.outcomes.set(decl, {status: 'failed', message}); this.logError(`FAILED: ${label(decl)} (${message})`); this.logError(` command: ${command}`); diff --git a/tools/build/hash.ts b/tools/build/hash.ts index 34669d5b..80ba324e 100644 --- a/tools/build/hash.ts +++ b/tools/build/hash.ts @@ -3,25 +3,25 @@ import fs from 'fs'; import {createHash} from 'crypto'; export interface FileStat { - size : bigint; - mtimeNs : bigint; - hash : Buffer; + size: bigint; + mtimeNs: bigint; + hash: Buffer; } -export function hashFileSync(path : string) : Buffer { +export function hashFileSync(path: string): Buffer { return createHash('sha256').update(fs.readFileSync(path)).digest(); } export interface ReconcileResult { - hashes : Map; // current content hash for every extant path - updated : Map; // cache entries that changed (to persist) - missing : string[]; // paths that don't exist or aren't files + hashes: Map; // current content hash for every extant path + updated: Map; // cache entries that changed (to persist) + missing: string[]; // paths that don't exist or aren't files } // Content hashes with a stat cache: only files whose (size, mtime_ns) changed // since the recorded cache entry are rehashed. mtime_ns exceeds 2^53, hence // bigint stats throughout. -export function reconcileHashes(paths : Iterable, cache : Map) : ReconcileResult { +export function reconcileHashes(paths: Iterable, cache: Map): ReconcileResult { const hashes = new Map(); const updated = new Map(); const missing = []; diff --git a/tools/build/helpers.ts b/tools/build/helpers.ts index a5b9094c..d0d5933d 100644 --- a/tools/build/helpers.ts +++ b/tools/build/helpers.ts @@ -7,16 +7,16 @@ import {type Cmd, basenameNoExt} from './subst.ts'; let config = new Map(); -export function setConfig(cfg : Map) : void { +export function setConfig(cfg: Map): void { config = cfg; } -export function getconfig(name : string) : string | undefined { +export function getconfig(name: string): string | undefined { const value = config.get(name); return value === '' ? undefined : value; } -export function astable(x : string | readonly string[] | undefined) : string[] { +export function astable(x: string | readonly string[] | undefined): string[] { if (x === undefined) { return []; } @@ -27,7 +27,7 @@ export function astable(x : string | readonly string[] | undefined) : string[] { // Non-glob strings pass through literally; existence is checked at hash time. // Results are sorted within a pattern; declared order is preserved across // patterns (some rules, e.g. the pokeball sheet, are input-order-sensitive). -function globOne(pat : string) : string[] { +function globOne(pat: string): string[] { if (!pat.includes('*')) { return [pat]; } @@ -55,26 +55,26 @@ function globOne(pat : string) : string[] { return results; } -export function glob(pats : string | string[]) : string[] { +export function glob(pats: string | string[]): string[] { return astable(pats).flatMap(globOne); } // tup.base: basename without directory or final extension. For an artifact, // its nominal name. -export function base(x : string | Artifact) : string { +export function base(x: string | Artifact): string { return typeof x === 'string' ? basenameNoExt(x) : x.name; } export interface SpriteData { - id : string; - data : Record; + id: string; + data: Record; } // Port of util/sprites.lua spritedata. Lua used gmatch("[^-]+"), which skips // empty segments, hence the filter. -export function spritedata(basename : string) : SpriteData { +export function spritedata(basename: string): SpriteData { const parts = basename.split('-').filter(p => p !== ''); - const data : Record = {}; + const data: Record = {}; for (const part of parts.slice(1)) { if (part.length === 1) { data[part] = true; @@ -85,7 +85,7 @@ export function spritedata(basename : string) : SpriteData { return {id: parts[0] ?? '', data}; } -export function spriteglob(pats : string | string[], flagspec? : Record) : string[] { +export function spriteglob(pats: string | string[], flagspec?: Record): string[] { return glob(pats).filter(filename => { const sd = spritedata(base(filename)); for (const [k, v] of Object.entries(flagspec ?? {})) { @@ -101,31 +101,31 @@ export function spriteglob(pats : string | string[], flagspec? : Record. - ext : string; - size : bigint; // verified against the object on every clean check + digest: string; // sha256 hex of the bytes; the CAS object is . + ext: string; + size: bigint; // verified against the object on every clean check } const DDL = ` @@ -39,15 +39,15 @@ CREATE TABLE IF NOT EXISTS rule_outputs ( ); `; -function userVersion(db : DatabaseSync) : number { - const row = db.prepare('PRAGMA user_version').get() as {user_version : bigint | number}; +function userVersion(db: DatabaseSync): number { + const row = db.prepare('PRAGMA user_version').get() as {user_version: bigint | number}; return Number(row.user_version); } export class Store { - private db : DatabaseSync; + private db: DatabaseSync; - constructor(dbPath : string) { + constructor(dbPath: string) { fs.mkdirSync(pathlib.dirname(dbPath), {recursive: true}); this.db = new DatabaseSync(dbPath, {readBigInts: true}); this.db.exec('PRAGMA journal_mode = WAL'); @@ -56,7 +56,7 @@ export class Store { this.migrate(); } - private transaction(fn : () => T) : T { + private transaction(fn: () => T): T { this.db.exec('BEGIN'); try { const result = fn(); @@ -68,7 +68,7 @@ export class Store { } } - private migrate() : void { + private migrate(): void { const version = userVersion(this.db); if (version === 0) { this.db.exec('BEGIN;' + DDL + 'PRAGMA user_version = 2; COMMIT;'); @@ -88,10 +88,10 @@ export class Store { } } - loadFileCache() : Map { + loadFileCache(): Map { const result = new Map(); const rows = this.db.prepare('SELECT path, size, mtime_ns, hash FROM file_cache').all() as - unknown as {path : string, size : bigint, mtime_ns : bigint, hash : Uint8Array}[]; + unknown as {path: string, size: bigint, mtime_ns: bigint, hash: Uint8Array}[]; for (const row of rows) { const h = row.hash; result.set(row.path, {size: row.size, mtimeNs: row.mtime_ns, @@ -100,7 +100,7 @@ export class Store { return result; } - saveFileCache(entries : Map) : void { + saveFileCache(entries: Map): void { const upsert = this.db.prepare(` INSERT INTO file_cache (path, size, mtime_ns, hash) VALUES (?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET @@ -112,9 +112,9 @@ export class Store { }); } - pruneFileCache(live : Set) : void { + pruneFileCache(live: Set): void { const paths = this.db.prepare('SELECT path FROM file_cache').all() as - unknown as {path : string}[]; + unknown as {path: string}[]; const del = this.db.prepare('DELETE FROM file_cache WHERE path = ?'); this.transaction(() => { for (const {path} of paths) { @@ -125,10 +125,10 @@ export class Store { }); } - lookupRule(key : string) : StoredOutput[] | null { + lookupRule(key: string): StoredOutput[] | null { return this.transaction(() => { const rule = this.db.prepare('SELECT id FROM rules WHERE key = ?').get(key) as - {id : bigint} | undefined; + {id: bigint} | undefined; if (rule === undefined) { return null; } @@ -142,7 +142,7 @@ export class Store { // One transaction per completed rule: an interrupted build only ever // contains fully-recorded rules. - recordRule(key : string, cmds : string[], outputs : StoredOutput[]) : void { + recordRule(key: string, cmds: string[], outputs: StoredOutput[]): void { const upsert = this.db.prepare(` INSERT INTO rules (key, cmds) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET cmds = excluded.cmds @@ -151,16 +151,16 @@ export class Store { const insOutput = this.db.prepare( 'INSERT INTO rule_outputs (rule_id, ord, digest, ext, size) VALUES (?, ?, ?, ?, ?)'); this.transaction(() => { - const {id} = upsert.get(key, cmds.join('\n')) as {id : bigint}; + const {id} = upsert.get(key, cmds.join('\n')) as {id: bigint}; delOutputs.run(id); outputs.forEach((o, i) => insOutput.run(id, i, o.digest, o.ext, o.size)); }); } // GC: drop every rule whose key is not live. Returns the removal count. - deleteKeysNotIn(live : Set) : number { + deleteKeysNotIn(live: Set): number { const keys = this.db.prepare('SELECT id, key FROM rules').all() as - unknown as {id : bigint, key : string}[]; + unknown as {id: bigint, key: string}[]; const del = this.db.prepare('DELETE FROM rules WHERE id = ?'); let removed = 0; this.transaction(() => { @@ -175,13 +175,13 @@ export class Store { } // Every CAS object referenced by some rule, as "." basenames. - liveObjects() : Set { + liveObjects(): Set { const rows = this.db.prepare('SELECT digest, ext FROM rule_outputs').all() as - unknown as {digest : string, ext : string}[]; + unknown as {digest: string, ext: string}[]; return new Set(rows.map(r => `${r.digest}.${r.ext}`)); } - close() : void { + close(): void { this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); this.db.close(); } @@ -189,7 +189,7 @@ export class Store { // Schema version of an existing db, without opening it for writing (the // Store constructor migrates); null if there is no db. -export function dbVersion(dbPath : string) : number | null { +export function dbVersion(dbPath: string): number | null { if (!fs.existsSync(dbPath)) { return null; } @@ -203,14 +203,14 @@ export function dbVersion(dbPath : string) : number | null { // Guards against two concurrent builds; the exclusive transaction is released // by the OS on any crash, so there are no stale lock files. -export function acquireLock(lockPath : string) : () => void { +export function acquireLock(lockPath: string): () => void { fs.mkdirSync(pathlib.dirname(lockPath), {recursive: true}); const lock = new DatabaseSync(lockPath, {timeout: 0}); try { lock.exec('BEGIN EXCLUSIVE'); } catch (err) { lock.close(); - if ((err as {errcode? : number}).errcode === 5) { // SQLITE_BUSY + if ((err as {errcode?: number}).errcode === 5) { // SQLITE_BUSY throw new BuildError('Another build is already running.'); } throw err; diff --git a/tools/build/subst.ts b/tools/build/subst.ts index 59a0bec5..da4caaa0 100644 --- a/tools/build/subst.ts +++ b/tools/build/subst.ts @@ -5,7 +5,7 @@ import pathlib from 'path'; // of them (flattened, like the Lua flatten()). export type Cmd = string | Cmd[]; -export function flattenCmds(cmds : Cmd) : string[] { +export function flattenCmds(cmds: Cmd): string[] { if (typeof cmds === 'string') { const trimmed = cmds.trim(); return trimmed === '' ? [] : [trimmed]; @@ -13,7 +13,7 @@ export function flattenCmds(cmds : Cmd) : string[] { return cmds.flatMap(flattenCmds); } -export function basenameNoExt(path : string) : string { +export function basenameNoExt(path: string): string { const base = pathlib.basename(path); const dot = base.lastIndexOf('.'); return dot > 0 ? base.slice(0, dot) : base; @@ -22,8 +22,8 @@ export function basenameNoExt(path : string) : string { // Expand only the name substitutions (%b/%B). They are static per rule, so // declarations expand them eagerly; %f/%o/%oN wait for execution, when // concrete paths exist. -export function substituteNames(s : string, inputs : string[]) : string { - return s.replace(/%([bB])/g, (_, c : string) => +export function substituteNames(s: string, inputs: string[]): string { + return s.replace(/%([bB])/g, (_, c: string) => c === 'b' ? inputs.map(p => pathlib.basename(p)).join(' ') : inputs.map(basenameNoExt).join(' ')); } @@ -32,8 +32,8 @@ export function substituteNames(s : string, inputs : string[]) : string { // %f inputs, space-joined %b input basenames // %o outputs, space-joined %B input basenames without extension // %oN (1-based) a single output, for rules with several -export function substitute(s : string, inputs : string[], outputs : string[]) : string { - return s.replace(/%o(\d+)|%([a-zA-Z])/g, (match, n : string | undefined, c : string | undefined) => { +export function substitute(s: string, inputs: string[], outputs: string[]): string { + return s.replace(/%o(\d+)|%([a-zA-Z])/g, (match, n: string | undefined, c: string | undefined) => { if (n !== undefined) { const i = Number(n); if (i < 1 || i > outputs.length) { diff --git a/tools/build/test/artifact.test.ts b/tools/build/test/artifact.test.ts index 45f99dc0..31c273f0 100644 --- a/tools/build/test/artifact.test.ts +++ b/tools/build/test/artifact.test.ts @@ -6,7 +6,7 @@ import {type Input, computeKey, forEachRule, getDecls, resetDecls, rule} from '. beforeEach(resetDecls); -function digests(map : Record) : (i : Input) => string { +function digests(map: Record): (i: Input) => string { return i => { if (typeof i !== 'string') { return i.hash; diff --git a/tools/build/test/cas.test.ts b/tools/build/test/cas.test.ts index 9051b5f8..8c0d08d0 100644 --- a/tools/build/test/cas.test.ts +++ b/tools/build/test/cas.test.ts @@ -8,11 +8,11 @@ import {test} from 'node:test'; import {casExists, casInsert, casPath, casStat, casSweep} from '../cas.ts'; -function makeTmpRoot() : string { +function makeTmpRoot(): string { return fs.mkdtempSync(pathlib.join(os.tmpdir(), 'cas-test-')); } -function stage(root : string, data : string) : string { +function stage(root: string, data: string): string { const p = pathlib.join(root, `stage-${Math.random().toString(36).slice(2)}`); fs.writeFileSync(p, data); return p; diff --git a/tools/build/test/executor.test.ts b/tools/build/test/executor.test.ts index 09dbf31d..0d62d7ae 100644 --- a/tools/build/test/executor.test.ts +++ b/tools/build/test/executor.test.ts @@ -14,14 +14,14 @@ import {Store} from '../store.ts'; beforeEach(resetDecls); interface Env { - root : string; - dbPath : string; - casDir : string; - tmpDir : string; - execLog : string; + root: string; + dbPath: string; + casDir: string; + tmpDir: string; + execLog: string; } -function setup() : Env { +function setup(): Env { const root = fs.mkdtempSync(pathlib.join(os.tmpdir(), 'executor-test-')); fs.mkdirSync(pathlib.join(root, 'src')); return { @@ -33,19 +33,19 @@ function setup() : Env { }; } -function src(env : Env, name : string, content : string) : string { +function src(env: Env, name: string, content: string): string { const p = pathlib.join(env.root, 'src', name); fs.writeFileSync(p, content); return p; } // A copy rule that also counts its executions in env.execLog. -function copyRule(env : Env, input : string | Artifact, out : string, - extra : Partial = {}) : Artifact { +function copyRule(env: Env, input: string | Artifact, out: string, + extra: Partial = {}): Artifact { return rule(input, {cmds: [`cat %f > %o && echo x >> ${env.execLog}`], ...extra}, out); } -function execCount(env : Env) : number { +function execCount(env: Env): number { try { return fs.readFileSync(env.execLog, 'utf8').split('\n').filter(l => l !== '').length; } catch { @@ -53,8 +53,8 @@ function execCount(env : Env) : number { } } -async function runBuild(env : Env, opts : {dryRun? : boolean, gc? : boolean} = {}, - decls = getDecls()) : Promise { +async function runBuild(env: Env, opts: {dryRun?: boolean, gc?: boolean} = {}, + decls = getDecls()): Promise { const store = new Store(env.dbPath); try { return await build(decls, { @@ -76,7 +76,7 @@ async function runBuild(env : Env, opts : {dryRun? : boolean, gc? : boolean} = { } } -function statuses(result : DriveResult) : RuleOutcome['status'][] { +function statuses(result: DriveResult): RuleOutcome['status'][] { return getDecls().map(d => result.outcomes.get(d)!.status); } @@ -147,7 +147,7 @@ test('byte-identical inputs share one execution across two declarations', async // The consumer must be a *different* computation: an identical command over // identical bytes would share the producer's key (by design). -function upcaseRule(env : Env, input : Artifact, out : string) : Artifact { +function upcaseRule(env: Env, input: Artifact, out: string): Artifact { return rule(input, [`tr a-z A-Z < %f > %o && echo x >> ${env.execLog}`], out); } @@ -213,7 +213,7 @@ test('a missing declared output fails the rule', async () => { assert.ok(!result.ok); const outcome = result.outcomes.get(getDecls()[0]!)!; assert.equal(outcome.status, 'failed'); - assert.match((outcome as {message : string}).message, /did not produce: missing.css/); + assert.match((outcome as {message: string}).message, /did not produce: missing.css/); const store = new Store(env.dbPath); assert.equal(store.liveObjects().size, 0); store.close(); diff --git a/tools/build/test/store.test.ts b/tools/build/test/store.test.ts index d9203abd..4cd6f738 100644 --- a/tools/build/test/store.test.ts +++ b/tools/build/test/store.test.ts @@ -9,7 +9,7 @@ import {DatabaseSync} from 'node:sqlite'; import {Store} from '../store.ts'; -function makeDbPath() : string { +function makeDbPath(): string { return pathlib.join(fs.mkdtempSync(pathlib.join(os.tmpdir(), 'store-test-')), 'db.sqlite'); } @@ -82,7 +82,7 @@ test('migrates a v1 db: drops rule tables, keeps file_cache', () => { store.close(); const check = new DatabaseSync(dbPath); - const {user_version} = check.prepare('PRAGMA user_version').get() as {user_version : number}; + const {user_version} = check.prepare('PRAGMA user_version').get() as {user_version: number}; assert.equal(Number(user_version), 2); check.close(); }); diff --git a/tools/deploy/api.ts b/tools/deploy/api.ts index fb064574..fed6ff1c 100644 --- a/tools/deploy/api.ts +++ b/tools/deploy/api.ts @@ -12,7 +12,7 @@ import * as pathlib from './path.ts'; // A source-tree file from ctx.list(); shaped like a deploy Path plus the // repo-relative path. export interface SrcFile extends pathlib.Path { - path : string; + path: string; } export type CopySource = Artifact | SrcFile | string; // string = repo-relative path @@ -20,64 +20,64 @@ export type CopySource = Artifact | SrcFile | string; // string = repo-relativ // The finish API: nice naming over built artifacts and raw sources. Ops are // queued in call order, which is the tar entry order. export interface DeployCtx { - copy(src : CopySource, dst : string) : void; - write(dst : string, data : string) : void; - read(src : CopySource) : string; - list(dir : string) : SrcFile[]; + copy(src: CopySource, dst: string): void; + write(dst: string, data: string): void; + read(src: CopySource): string; + list(dir: string): SrcFile[]; // 8-char base32 content stamp. One source: the digest of its bytes, // byte-compatible with artifact hashes. Several: a digest of the sorted // per-source digests (order-insensitive). - hash(...srcs : CopySource[]) : string; + hash(...srcs: CopySource[]): string; } -export type DeployFn = (ctx : DeployCtx) => void | Promise; +export type DeployFn = (ctx: DeployCtx) => void | Promise; // Like rule(): a buildFile registers free-floating deploy blocks next to the // rules they ship. They run in registration order after the build, sharing // one ctx (and so one output tree) per buildFile. -const deploys : DeployFn[] = []; +const deploys: DeployFn[] = []; -export function deploy(fn : DeployFn) : void { +export function deploy(fn: DeployFn): void { deploys.push(fn); } -export function getDeploys() : readonly DeployFn[] { +export function getDeploys(): readonly DeployFn[] { return deploys; } -export function resetDeploys() : void { +export function resetDeploys(): void { deploys.length = 0; } -function shortHash(digest : Buffer) : string { +function shortHash(digest: Buffer): string { return b32encode(digest, 'RFC4648').slice(0, 8); } -export function makeCtx(casDir : string, queue : ActionQueue) : DeployCtx { +export function makeCtx(casDir: string, queue: ActionQueue): DeployCtx { // Every path here is repo-root-relative; the CLI chdirs to the root. - const srcPath = (src : CopySource) : string => { + const srcPath = (src: CopySource): string => { if (src instanceof Artifact) { return casPath(casDir, src.hash, src.ext); } return typeof src === 'string' ? src : src.path; }; - const digestOf = (src : CopySource) : Buffer => { + const digestOf = (src: CopySource): Buffer => { if (src instanceof Artifact) { return Buffer.from(src.hash, 'hex'); } return crypto.createHash('sha256').update(fs.readFileSync(srcPath(src))).digest(); }; return { - copy(src : CopySource, dst : string) : void { + copy(src: CopySource, dst: string): void { queue.copy(srcPath(src), dst); }, - write(dst : string, data : string) : void { + write(dst: string, data: string): void { queue.write(data, dst); }, - read(src : CopySource) : string { + read(src: CopySource): string { return fs.readFileSync(srcPath(src), 'utf8'); }, - list(dir : string) : SrcFile[] { + list(dir: string): SrcFile[] { const result = []; // Files only, no dotfiles: the same filtering the build-side // glob applies to rule inputs. @@ -90,7 +90,7 @@ export function makeCtx(casDir : string, queue : ActionQueue) : DeployCtx { } return result; }, - hash(...srcs : CopySource[]) : string { + hash(...srcs: CopySource[]): string { if (srcs.length === 1) { return shortHash(digestOf(srcs[0]!)); } diff --git a/tools/deploy/config.ts b/tools/deploy/config.ts index ace6347a..caaef37c 100644 --- a/tools/deploy/config.ts +++ b/tools/deploy/config.ts @@ -7,32 +7,32 @@ import JSON5 from 'json5'; import {BuildError} from '../build/errors.ts'; export interface DeployEntry { - subset : string[]; - cmd : string; + subset: string[]; + cmd: string; // dir entries get their subset materialized into a temp directory whose // path replaces %d in cmd; tar entries get the subset tarred on stdin. - dir? : boolean; + dir?: boolean; } export interface DeployTarget { - buildFile : string; - deploy : DeployEntry[]; + buildFile: string; + deploy: DeployEntry[]; } export type DeployConfig = Map; -function isStringArray(v : unknown) : v is string[] { +function isStringArray(v: unknown): v is string[] { return Array.isArray(v) && v.every(x => typeof x === 'string'); } -export function loadDeployConfig(path : string) : DeployConfig { - let text : string; +export function loadDeployConfig(path: string): DeployConfig { + let text: string; try { text = fs.readFileSync(path, 'utf8'); } catch { throw new BuildError(`missing ${path}; see README ("Deploying") for the schema`); } - let raw : unknown; + let raw: unknown; try { raw = JSON5.parse(text); } catch (err) { @@ -42,7 +42,7 @@ export function loadDeployConfig(path : string) : DeployConfig { throw new BuildError(`${path}: top level must be an object of deploy names`); } - const config : DeployConfig = new Map(); + const config: DeployConfig = new Map(); for (const [name, t] of Object.entries(raw)) { const target = t as Partial; if (typeof t !== 'object' || t === null || typeof target.buildFile !== 'string' @@ -70,8 +70,8 @@ export function loadDeployConfig(path : string) : DeployConfig { // Route finish outputs to deploy entries: per entry, the set of dsts its // subset globs match. Every glob must match something and every dst must be // covered by some entry; to unship an output, don't emit it in finish. -export function matchSubsets(dsts : readonly string[], - entries : readonly DeployEntry[]) : Set[] { +export function matchSubsets(dsts: readonly string[], + entries: readonly DeployEntry[]): Set[] { const covered = new Set(); const perEntry = entries.map(entry => { const matched = new Set(); diff --git a/tools/deploy/index.ts b/tools/deploy/index.ts index 7aeb97d5..b20acf48 100644 --- a/tools/deploy/index.ts +++ b/tools/deploy/index.ts @@ -28,17 +28,17 @@ const CAS_DIR = '.build/cas'; const TMP_DIR = '.build/tmp'; interface CommonOpts { - jobs : string; - dryRun? : boolean; - failFast? : boolean; - config : string; - verbose? : boolean; + jobs: string; + dryRun?: boolean; + failFast?: boolean; + config: string; + verbose?: boolean; } interface VerbOpts extends CommonOpts { - output? : string; - link? : boolean; - tar? : boolean; + output?: string; + link?: boolean; + tar?: boolean; } const USAGE = `usage: node tools/deploy/index.ts [options] @@ -88,14 +88,14 @@ const VERB_OPTIONS = { }, } as const; -function requireOutput(opts : VerbOpts) : string { +function requireOutput(opts: VerbOpts): string { if (opts.output === undefined) { throw new BuildError('missing -o/--output'); } return opts.output; } -function discoverDeployFiles() : string[] { +function discoverDeployFiles(): string[] { const files = fs.readdirSync('.').filter(f => f.endsWith('.build.ts')).sort(); if (files.length === 0) { throw new BuildError('No *.build.ts files at the repo root'); @@ -106,7 +106,7 @@ function discoverDeployFiles() : string[] { // Importing a deploy module declares its rules and registers its deploy // blocks; the registry delta over each sequential import is that file's // blocks. -async function importDeploys(files : string[]) : Promise> { +async function importDeploys(files: string[]): Promise> { const specs = new Map(); for (const file of files) { const before = getDeploys().length; @@ -119,8 +119,8 @@ async function importDeploys(files : string[]) : Promise Promise) : Promise { +async function buildThen(decls: readonly RuleDecl[], opts: CommonOpts, gc: boolean, + then?: () => Promise): Promise { const jobs = Number(opts.jobs); if (!Number.isInteger(jobs) || jobs < 1) { throw new BuildError(`Invalid --jobs value: ${opts.jobs}`); @@ -149,7 +149,7 @@ async function buildThen(decls : readonly RuleDecl[], opts : CommonOpts, gc : bo }; process.on('SIGINT', onSignal); process.on('SIGTERM', onSignal); - let ok : boolean; + let ok: boolean; try { const result = await build(decls, { root, @@ -184,7 +184,7 @@ async function buildThen(decls : readonly RuleDecl[], opts : CommonOpts, gc : bo } } -function finishOf(specs : Map, file : string) : readonly DeployFn[] { +function finishOf(specs: Map, file: string): readonly DeployFn[] { const fns = specs.get(file); if (fns === undefined || fns.length === 0) { throw new BuildError(`${file} registers no deploy blocks (use deploy())`); @@ -192,7 +192,7 @@ function finishOf(specs : Map, file : string) : rea return fns; } -async function runFinish(fns : readonly DeployFn[], verbose : boolean) : Promise { +async function runFinish(fns: readonly DeployFn[], verbose: boolean): Promise { const aq = new ActionQueue(); const ctx = makeCtx(CAS_DIR, aq); for (const fn of fns) { @@ -205,14 +205,14 @@ async function runFinish(fns : readonly DeployFn[], verbose : boolean) : Promise return aq; } -function waitExit(child : ChildProcess) : Promise { +function waitExit(child: ChildProcess): Promise { return new Promise((resolve, reject) => { child.on('error', reject); child.on('close', code => resolve(code)); }); } -async function cmdBuild(files : string[], opts : CommonOpts) : Promise { +async function cmdBuild(files: string[], opts: CommonOpts): Promise { setConfig(loadConfig(opts.config)); // GC needs the full rule universe: only an unfiltered union build // can know which keys are no longer declared anywhere. @@ -221,7 +221,7 @@ async function cmdBuild(files : string[], opts : CommonOpts) : Promise { process.exitCode = await buildThen(getDecls(), opts, gc); } -async function cmdDeploy(names : string[], opts : VerbOpts) : Promise { +async function cmdDeploy(names: string[], opts: VerbOpts): Promise { const config = loadDeployConfig('deploy.json5'); if (names.length === 0) { for (const [name, target] of config) { @@ -292,7 +292,7 @@ async function cmdDeploy(names : string[], opts : VerbOpts) : Promise { }); } -async function cmdRun(file : string, opts : VerbOpts) : Promise { +async function cmdRun(file: string, opts: VerbOpts): Promise { const output = requireOutput(opts); setConfig(loadConfig(opts.config)); const specs = await importDeploys([file]); @@ -306,14 +306,14 @@ async function cmdRun(file : string, opts : VerbOpts) : Promise { }); } -function slugOf(decl : RuleDecl) : string { +function slugOf(decl: RuleDecl): string { const template = decl.displayTemplate ?? decl.cmds[0]!; const slug = template.replace(/%[a-zA-Z0-9]+/g, ' ') .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); return slug === '' ? 'rule' : slug; } -async function cmdInspect(paths : string[], opts : VerbOpts) : Promise { +async function cmdInspect(paths: string[], opts: VerbOpts): Promise { const output = requireOutput(opts); setConfig(loadConfig(opts.config)); await importDeploys(discoverDeployFiles()); @@ -373,7 +373,7 @@ async function cmdInspect(paths : string[], opts : VerbOpts) : Promise { }); } -async function main(argv : string[]) : Promise { +async function main(argv: string[]): Promise { const [verb, ...rest] = argv; if (verb === undefined || verb === '-h' || verb === '--help') { console.log(USAGE); @@ -392,13 +392,13 @@ async function main(argv : string[]) : Promise { } catch (err) { throw new BuildError(`${(err as Error).message.split('\n')[0]} (-h for usage)`); } - const v = parsed.values as {[k : string] : string | boolean | undefined}; + const v = parsed.values as {[k: string]: string | boolean | undefined}; const positionals = parsed.positionals; if (v.help) { console.log(USAGE); return; } - const opts : VerbOpts = { + const opts: VerbOpts = { jobs: v.jobs as string, dryRun: Boolean(v['dry-run']), failFast: Boolean(v['fail-fast']), diff --git a/tools/deploy/path.ts b/tools/deploy/path.ts index efdbe0ba..d1b4e123 100644 --- a/tools/deploy/path.ts +++ b/tools/deploy/path.ts @@ -2,12 +2,12 @@ import pathlib from 'path'; // Slight variation of pathlib parse, less fields, different ext handling -export type Path = {dir : string, name : string, ext : string | null}; +export type Path = {dir: string, name: string, ext: string | null}; -export function parse(s : string) : Path { +export function parse(s: string): Path { let {dir, name, ext: dotext} = pathlib.parse(s); let ext; - if (dotext === "") { + if (dotext === '') { ext = null; } else { ext = dotext.slice(1); @@ -15,18 +15,18 @@ export function parse(s : string) : Path { return {dir, name, ext}; } -export function format({dir, name, ext} : Path) { - const dotext = ext === null ? "" : `.${ext}`; - return pathlib.format({dir, name, ext : dotext}); +export function format({dir, name, ext}: Path) { + const dotext = ext === null ? '' : `.${ext}`; + return pathlib.format({dir, name, ext: dotext}); } -export function join(s : string, {dir, name, ext} : Path) : Path { +export function join(s: string, {dir, name, ext}: Path): Path { return {dir: pathlib.join(s, dir), name, ext}; } export type Delta = Partial; -export function update({dir, name, ext} : Path, delta : Delta) : Path { +export function update({dir, name, ext}: Path, delta: Delta): Path { return { dir: delta.dir ?? dir, name: delta.name ?? name, @@ -38,7 +38,7 @@ export function update({dir, name, ext} : Path, delta : Delta) : Path { export type PathLike = Path | string; -export function path(p : PathLike, delta? : Delta) : Path { +export function path(p: PathLike, delta?: Delta): Path { let parsed = typeof p === 'string' ? parse(p) : p; if (delta !== undefined) parsed = update(parsed, delta); diff --git a/tools/deploy/queue.ts b/tools/deploy/queue.ts index 7aa511c4..5f7bda1f 100644 --- a/tools/deploy/queue.ts +++ b/tools/deploy/queue.ts @@ -4,35 +4,35 @@ import nodePath from 'path'; import tar from 'tar-stream'; type Op = { - type : 'Write', - data : string, + type: 'Write', + data: string, } | { - type : 'Copy', - src : string, + type: 'Copy', + src: string, }; type OpEntry = { - type : 'Op', - op : Op, - dst : string, - valid : 'Success' | 'Absolute' | 'Multiple', - debugObjs : unknown[] + type: 'Op', + op: Op, + dst: string, + valid: 'Success' | 'Absolute' | 'Multiple', + debugObjs: unknown[] }; type DebugEntry = { - type : 'Debug', - obj : unknown, - stray : boolean + type: 'Debug', + obj: unknown, + stray: boolean }; export type LogEntry = OpEntry | DebugEntry; export class ActionQueue { - private seen : Map; + private seen: Map; // Have an accessor for this in the future? idk - public log : LogEntry[]; - public valid : boolean; - private debugBuffer : unknown[]; + public log: LogEntry[]; + public valid: boolean; + private debugBuffer: unknown[]; constructor() { this.seen = new Map; @@ -41,27 +41,27 @@ export class ActionQueue { this.debugBuffer = []; } - throw(obj : unknown) { + throw(obj: unknown) { this.gdebug(obj, false); this.valid = false; } - debug(obj : unknown) { + debug(obj: unknown) { this.debugBuffer.push(obj); } - gdebug(obj : unknown, stray : boolean) { + gdebug(obj: unknown, stray: boolean) { this.log.push({type: 'Debug', obj, stray}); } - private pushOp(op: Op, dst : string) { + private pushOp(op: Op, dst: string) { dst = nodePath.normalize(dst); - const entry : OpEntry = { - type : 'Op', + const entry: OpEntry = { + type: 'Op', op, dst, - valid : 'Success', - debugObjs : this.debugBuffer + valid: 'Success', + debugObjs: this.debugBuffer }; this.log.push(entry); this.debugBuffer = []; @@ -82,11 +82,11 @@ export class ActionQueue { } } - copy(src : string, dst : string) { + copy(src: string, dst: string) { this.pushOp({type: 'Copy', src}, dst); } - write(data : string, dst : string) { + write(data: string, dst: string) { this.pushOp({type: 'Write', data}, dst); } @@ -97,7 +97,7 @@ export class ActionQueue { this.debugBuffer = []; } - print(level : 'errors' | 'all') { + print(level: 'errors' | 'all') { for (const entry of this.log) { if (entry.type === 'Op') { const op = entry.op; @@ -108,7 +108,7 @@ export class ActionQueue { addendum = ` (${entry.valid})`; } for (const obj of entry.debugObjs) { - console.error("DEBUG:", obj); + console.error('DEBUG:', obj); } if (op.type === 'Copy') { console.error(`COPY${addendum}: ${op.src} ==> ${entry.dst}`); @@ -125,7 +125,7 @@ export class ActionQueue { } } - async run(dir : string, mode : 'link' | 'copy' | 'tar', filter? : (dst : string) => boolean) { + async run(dir: string, mode: 'link' | 'copy' | 'tar', filter?: (dst: string) => boolean) { if (!this.valid) throw new Error(`Invalid ActionQueue`); if (mode !== 'tar') { @@ -160,7 +160,7 @@ export class ActionQueue { } } - pack(filter? : (dst : string) => boolean) : NodeJS.ReadableStream { + pack(filter?: (dst: string) => boolean): NodeJS.ReadableStream { if (!this.valid) throw new Error(`Invalid ActionQueue`); let t = tar.pack(); diff --git a/tools/deploy/test/api.test.ts b/tools/deploy/test/api.test.ts index 174ec0e6..0f6efdac 100644 --- a/tools/deploy/test/api.test.ts +++ b/tools/deploy/test/api.test.ts @@ -16,16 +16,16 @@ import {ActionQueue} from '../queue.ts'; beforeEach(resetDecls); -function tmpdir() : string { +function tmpdir(): string { return fs.mkdtempSync(pathlib.join(os.tmpdir(), 'deploy-api-test-')); } -function shortHash(data : Buffer | string) : string { +function shortHash(data: Buffer | string): string { return b32encode(createHash('sha256').update(data).digest(), 'RFC4648').slice(0, 8); } // Stage `content` as a built artifact in a scratch CAS. -function makeArtifact(casDir : string, content : string, ext : string) { +function makeArtifact(casDir: string, content: string, ext: string) { const digest = createHash('sha256').update(content).digest('hex'); const artifact = rule('in.png', ['t %f %o'], `art.${ext}`); artifact.resolve(digest); @@ -70,7 +70,7 @@ test('ctx queues artifact copies from the CAS, writes and reads', () => { assert.equal(ctx.read(artifact), 'bytes'); const ops = aq.log.filter(e => e.type === 'Op'); assert.deepEqual(ops.map(e => e.dst), ['m.json', 'sprites/x.webp']); - assert.equal((ops[1] as {op : {src : string}}).op.src, casPath(casDir, artifact.hash, 'webp')); + assert.equal((ops[1] as {op: {src: string}}).op.src, casPath(casDir, artifact.hash, 'webp')); }); test('ctx.list sorts, parses extensions, skips dotfiles and directories', () => { @@ -88,13 +88,13 @@ test('ctx.list sorts, parses extensions, skips dotfiles and directories', () => ]); }); -function packedEntries(aq : ActionQueue, filter? : (dst : string) => boolean) - : Promise<{name : string, data : string}[]> { +function packedEntries(aq: ActionQueue, filter?: (dst: string) => boolean) + : Promise<{name: string, data: string}[]> { return new Promise((resolve, reject) => { const extract = tar.extract(); - const entries : {name : string, data : string}[] = []; + const entries: {name: string, data: string}[] = []; extract.on('entry', (header, stream, next) => { - const chunks : Buffer[] = []; + const chunks: Buffer[] = []; stream.on('data', c => chunks.push(c)); stream.on('end', () => { entries.push({name: header.name, data: Buffer.concat(chunks).toString()}); diff --git a/tools/deploy/test/config.test.ts b/tools/deploy/test/config.test.ts index 6b0de96e..9e8469fd 100644 --- a/tools/deploy/test/config.test.ts +++ b/tools/deploy/test/config.test.ts @@ -7,7 +7,7 @@ import {test} from 'node:test'; import {loadDeployConfig, matchSubsets} from '../config.ts'; -function configFile(text : string) : string { +function configFile(text: string): string { const dir = fs.mkdtempSync(pathlib.join(os.tmpdir(), 'deploy-config-test-')); const p = pathlib.join(dir, 'deploy.json5'); fs.writeFileSync(p, text); diff --git a/tools/sheet/index.ts b/tools/sheet/index.ts index b98256cd..ad06b033 100644 --- a/tools/sheet/index.ts +++ b/tools/sheet/index.ts @@ -10,30 +10,30 @@ if (!sheetjs || !dest) { } // Must have file:/// for Windows -const {default: sheet} = await import(path.join("file:///", process.cwd(), sheetjs)); +const {default: sheet} = await import(path.join('file:///', process.cwd(), sheetjs)); for (let i = 0; i < sheet.entries.length; i++) { if (sheet.entries[i] === undefined) throw new Error(`gap: ${i}`); if (sheet.entries[i] === null) { // ImageMagick blank entry - sheet.entries[i] = "xc:transparent"; + sheet.entries[i] = 'xc:transparent'; } } // Write list of filenames to stdin, Windows can't handle large cli arg lists. // Before we wrote a tmp file and deleted it afterwards, but apparently tup // can't track unlinkSync on Windows? -const proc = cp.spawn("magick", [ - "montage", - "@-", +const proc = cp.spawn('magick', [ + 'montage', + '@-', // Keep sheet bytes deterministic for the content-addressed build. - "-define", "png:exclude-chunks=date,time", - "-background", "transparent", - "-geometry", `${sheet.width}x${sheet.height}>`, - "-gravity", "center", - "-tile", `${sheet.tile}x`, - "-depth", "8", + '-define', 'png:exclude-chunks=date,time', + '-background', 'transparent', + '-geometry', `${sheet.width}x${sheet.height}>`, + '-gravity', 'center', + '-tile', `${sheet.tile}x`, + '-depth', '8', dest ], { stdio: ['pipe', 'inherit', 'inherit'] diff --git a/tools/smogdexspritesheet/index.ts b/tools/smogdexspritesheet/index.ts index 6bc24e4e..aea579b6 100755 --- a/tools/smogdexspritesheet/index.ts +++ b/tools/smogdexspritesheet/index.ts @@ -27,8 +27,8 @@ const removeRe = /[^a-z0-9-]/g export function toAlias(s: string) { s = s.toLowerCase() - s = s.replace(spaceRe, "-") - s = s.replace(removeRe, "") + s = s.replace(spaceRe, '-') + s = s.replace(removeRe, '') return s } @@ -42,11 +42,11 @@ for (let [filename, sprite] of Object.entries(result.coordinates)) { let data = spritedata.get(parsed.id); if (data.type === 'specie') { // TODO would like to use toPSID here, mess with it later. - let name = toAlias(data.base + (data.forme ? "-" + data.forme : "")); - if (parsed.extra.has("g")) { - name += "-gmax"; - } else if (parsed.extra.has("f")) { - name += "-f"; + let name = toAlias(data.base + (data.forme ? '-' + data.forme : '')); + if (parsed.extra.has('g')) { + name += '-gmax'; + } else if (parsed.extra.has('f')) { + name += '-f'; } sprites.set(name, sprite); } else { @@ -56,7 +56,7 @@ for (let [filename, sprite] of Object.entries(result.coordinates)) { } } -let stylesheet = ""; +let stylesheet = ''; for (let [id, sprite] of sprites) { // webp reference depends on optimization in Tupfile, fix it later, just need to ship stylesheet += `.sprite-${id} { diff --git a/tools/trim/image.ts b/tools/trim/image.ts index 3ab2cbfc..45279ee0 100644 --- a/tools/trim/image.ts +++ b/tools/trim/image.ts @@ -1,8 +1,8 @@ import cp from 'child_process'; -export function getDims(input : string) { - const info = cp.execFileSync('magick', ['convert', input, '-format', "%w+%h+%@", 'info:'], +export function getDims(input: string) { + const info = cp.execFileSync('magick', ['convert', input, '-format', '%w+%h+%@', 'info:'], {encoding:'utf8'}); const [imageWidth, imageHeight, width, height, left, top] = @@ -21,13 +21,13 @@ export function getDims(input : string) { } } -export function crop(input : string, {width, height, left, top} : {width : number, height: number, left: number, top: number}, output : string) { +export function crop(input: string, {width, height, left, top}: {width: number, height: number, left: number, top: number}, output: string) { cp.execFileSync('magick', ['convert', input, '+repage', '-crop', `${width}x${height}+${left}+${top}`, output]); } // Trim, preserving displacement from center // Returns crop coords -export function losslessTrim(dims : {width : number, height: number, left: number, top: number, bottom: number, right: number}) { +export function losslessTrim(dims: {width: number, height: number, left: number, top: number, bottom: number, right: number}) { return { left: Math.min(dims.left, dims.right), width: dims.width + Math.abs(dims.left - dims.right),