From 40bd3052f5d03f61640a452f889bc2379568c970 Mon Sep 17 00:00:00 2001 From: Christopher Monsanto Date: Mon, 17 Aug 2026 19:10:49 -0400 Subject: [PATCH] Make the deploy layer and tool I/O async DeployCtx read/list/hash return promises (copy and write stay sync: they only queue ops), the stamped copy helpers and buildfile deploy blocks await them, ActionQueue materialization and packing use fs/promises, and the CLI, config loaders, spritesheet writer, and trim tool follow. Declaration-time globbing and the module-init data reads stay sync: rules exist by the end of the import. Co-Authored-By: Claude Fable 5 --- assets.build.ts | 24 +++++++++---------- ps.build.ts | 4 ++-- rules/publish.ts | 14 +++++------ smogon.build.ts | 24 +++++++++---------- tools/build/config.ts | 16 +++++++++---- tools/deploy/api.ts | 29 ++++++++++++----------- tools/deploy/config.ts | 6 ++--- tools/deploy/index.ts | 39 ++++++++++++++++--------------- tools/deploy/queue.ts | 23 +++++++++--------- tools/deploy/test/api.test.ts | 33 +++++++++++++------------- tools/deploy/test/config.test.ts | 24 +++++++++---------- tools/smogdexspritesheet/index.ts | 6 ++--- tools/trim/image.ts | 13 +++++++---- tools/trim/index.ts | 4 ++-- 14 files changed, 137 insertions(+), 122 deletions(-) diff --git a/assets.build.ts b/assets.build.ts index faa72588..cbaca497 100644 --- a/assets.build.ts +++ b/assets.build.ts @@ -15,8 +15,8 @@ let webpMinisprites = forEachRule(minispriteInputs, { cmds: ['cwebp -z 9 %f -o %o'], }, '%B.webp'); -deploy(ctx => { - let h = ctx.hash(...webpMinisprites); +deploy(async ctx => { + let h = await ctx.hash(...webpMinisprites); for (let f of webpMinisprites) { newspritecopy(ctx, f, {dir: 'minisprites/' + h}); } @@ -43,10 +43,10 @@ let 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. -deploy(ctx => { - let wh = ctx.hash(sheetWebp); +deploy(async ctx => { + let wh = await ctx.hash(sheetWebp); ctx.copy(sheetWebp, `spritesheet-${wh}.webp`); - let src = ctx.read(sheetCss); + let src = await ctx.read(sheetCss); let css = src.replaceAll('url("./spritesheet.webp")', `url("./spritesheet-${wh}.webp")`); if (css === src) { throw new Error('spritesheet.css: no webp urls rewritten'); @@ -54,7 +54,7 @@ deploy(ctx => { // Suffix from source content: the rewritten css is a pure function // of (css, webp), so this changes exactly when the served bytes // change. - let ch = ctx.hash(sheetCss, sheetWebp); + let ch = await ctx.hash(sheetCss, sheetWebp); ctx.write(`spritesheet-${ch}.css`, css); ctx.write('__meta/spritesheet_css_suffix.txt', `-${ch}\n`); }); @@ -65,23 +65,23 @@ deploy(ctx => { let forumItems = itemPadded(); let forumG6 = gen6Padded(); -deploy(ctx => { +deploy(async ctx => { let manifest = new Manifest(ctx); for (let f of forumItems) { - itemspritecopy(manifest, f, {dir: 'forumsprites'}); + await itemspritecopy(manifest, f, {dir: 'forumsprites'}); } for (let f of forumG6) { - spritecopy(manifest, f, {dir: 'forumsprites'}, true); + await spritecopy(manifest, f, {dir: 'forumsprites'}, true); } manifest.write('__meta/forumsprites/manifest.json'); }); // PMD sprites ship as-is, stamped. -deploy(ctx => { +deploy(async ctx => { let manifest = new Manifest(ctx); - for (let f of ctx.list('src/pmd')) { - spritecopy(manifest, f, {dir: 'pmd'}); + for (let f of await ctx.list('src/pmd')) { + await spritecopy(manifest, f, {dir: 'pmd'}); } manifest.write('__meta/pmd/manifest.json'); }); diff --git a/ps.build.ts b/ps.build.ts index f86b5e4e..d35a04df 100644 --- a/ps.build.ts +++ b/ps.build.ts @@ -83,10 +83,10 @@ forEachRule(dexMissing, { let aniChampions = gen10Modelslike(); -deploy(ctx => { +deploy(async ctx => { let seenModels = new Set(); - for (let f of ctx.list('src/models')) { + for (let f of await ctx.list('src/models')) { seenModels.add(f.name); psSpritecopy(ctx, f, 'ani'); } diff --git a/rules/publish.ts b/rules/publish.ts index 74242ffb..df17cba9 100644 --- a/rules/publish.ts +++ b/rules/publish.ts @@ -60,14 +60,14 @@ export function toPSID(name: string): string { // 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 { - let h = manifest.ctx.hash(f); +export async function stampcopy(manifest: Manifest, f: Sprite, {dir, ext}: Dest, name: string): Promise { + let h = await 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 async function spritecopy(manifest: Manifest, f: Sprite, dest: Dest, + allowUnknown = false): Promise { let sn = spritedata.parseFilename(f.name); let name: string; @@ -100,11 +100,11 @@ export function spritecopy(manifest: Manifest, f: Sprite, dest: Dest, name += '-gmax'; } - stampcopy(manifest, f, dest, name); + await stampcopy(manifest, f, dest, name); } // TODO: merge with above -export function itemspritecopy(manifest: Manifest, f: Sprite, dest: Dest): void { +export async function itemspritecopy(manifest: Manifest, f: Sprite, dest: Dest): Promise { let sn = spritedata.parseFilename(f.name); if (sn.extension) { throw new Error(`Not an item sprite: ${f.name}`); @@ -114,7 +114,7 @@ export function itemspritecopy(manifest: Manifest, f: Sprite, dest: Dest): void throw new Error(`Not an item sprite: ${f.name}`); } for (let n of sd.names) { - stampcopy(manifest, f, dest, toSmogonAlias(n)); + await stampcopy(manifest, f, dest, toSmogonAlias(n)); } } diff --git a/smogon.build.ts b/smogon.build.ts index a4ad9a5e..52cba797 100644 --- a/smogon.build.ts +++ b/smogon.build.ts @@ -10,34 +10,34 @@ let xyModels = gen9Modelslike(); let xyChampions = gen10Modelslike(); let xyGen5 = gen5Gifs(); -deploy(ctx => { +deploy(async ctx => { let seenModels = new Set(); let manifest = new Manifest(ctx); - let xycopy = (f: Sprite) => { + let xycopy = async (f: Sprite) => { if (seenModels.has(f.name)) { return; } seenModels.add(f.name); - spritecopy(manifest, f, {dir: 'xy'}); + await spritecopy(manifest, f, {dir: 'xy'}); }; - for (let f of ctx.list('src/models')) { - xycopy(f); + for (let f of await ctx.list('src/models')) { + await xycopy(f); } for (let f of xyModels) { - xycopy(f); + await xycopy(f); } for (let f of xyChampions) { - xycopy(f); + await xycopy(f); } // Non-model CAPs - for (let f of ctx.list('src/sprites/gen5')) { + for (let f of await ctx.list('src/sprites/gen5')) { if (f.ext === 'gif') { - xycopy(f); + await xycopy(f); } } for (let f of xyGen5) { - xycopy(f); + await xycopy(f); } manifest.write('xy/manifest.json'); }); @@ -46,10 +46,10 @@ deploy(ctx => { let xyIcons = gen6Trimmed(); -deploy(ctx => { +deploy(async ctx => { let manifest = new Manifest(ctx); for (let f of xyIcons) { - spritecopy(manifest, f, {dir: 'xyicons'}); + await spritecopy(manifest, f, {dir: 'xyicons'}); } manifest.write('xyicons/manifest.json'); }); diff --git a/tools/build/config.ts b/tools/build/config.ts index bc65e14c..2ddf4746 100644 --- a/tools/build/config.ts +++ b/tools/build/config.ts @@ -1,5 +1,5 @@ -import * as fs from 'node:fs'; +import * as fs from 'node:fs/promises'; export function parseConfig(text: string): Map { let result = new Map(); @@ -17,9 +17,15 @@ export function parseConfig(text: string): Map { return result; } -export function loadConfig(path: string): Map { - if (!fs.existsSync(path)) { - return new Map(); +export async function loadConfig(path: string): Promise> { + let text; + try { + text = await fs.readFile(path, 'utf8'); + } catch (err) { + if ((err as {code?: string}).code === 'ENOENT') { + return new Map(); + } + throw err; } - return parseConfig(fs.readFileSync(path, 'utf8')); + return parseConfig(text); } diff --git a/tools/deploy/api.ts b/tools/deploy/api.ts index 43ad1c8e..416344e7 100644 --- a/tools/deploy/api.ts +++ b/tools/deploy/api.ts @@ -1,6 +1,6 @@ import * as crypto from 'node:crypto'; -import * as fs from 'node:fs'; +import * as fs from 'node:fs/promises'; import b32encode from 'base32-encode'; @@ -20,14 +20,16 @@ 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 type DeployCtx = { + // copy and write queue ops without touching the disk, so they are sync; + // read, list, and hash do file I/O. copy(src: CopySource, dst: string): void, write(dst: string, data: string): void, - read(src: CopySource): string, - list(dir: string): SrcFile[], + read(src: CopySource): Promise, + list(dir: string): Promise, // 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[]): Promise, }; export type DeployFn = (ctx: DeployCtx) => void | Promise; @@ -61,11 +63,11 @@ export function makeCtx(casDir: string, queue: ActionQueue): DeployCtx { } return typeof src === 'string' ? src : src.path; }; - let digestOf = (src: CopySource): Buffer => { + let digestOf = async (src: CopySource): Promise => { if (src instanceof Artifact) { return Buffer.from(src.hash, 'hex'); } - return crypto.createHash('sha256').update(fs.readFileSync(srcPath(src))).digest(); + return crypto.createHash('sha256').update(await fs.readFile(srcPath(src))).digest(); }; return { copy(src: CopySource, dst: string): void { @@ -74,14 +76,15 @@ export function makeCtx(casDir: string, queue: ActionQueue): DeployCtx { write(dst: string, data: string): void { queue.write(data, dst); }, - read(src: CopySource): string { - return fs.readFileSync(srcPath(src), 'utf8'); + async read(src: CopySource): Promise { + return await fs.readFile(srcPath(src), 'utf8'); }, - list(dir: string): SrcFile[] { + async list(dir: string): Promise { let result = []; // Files only, no dotfiles: the same filtering the build-side // glob applies to rule inputs. - for (let ent of fs.readdirSync(dir, {withFileTypes: true}).sort((a, b) => a.name < b.name ? -1 : 1)) { + let ents = await fs.readdir(dir, {withFileTypes: true}); + for (let ent of ents.sort((a, b) => a.name < b.name ? -1 : 1)) { if (ent.name.startsWith('.') || (!ent.isFile() && !ent.isSymbolicLink())) { continue; } @@ -90,12 +93,12 @@ export function makeCtx(casDir: string, queue: ActionQueue): DeployCtx { } return result; }, - hash(...srcs: CopySource[]): string { + async hash(...srcs: CopySource[]): Promise { let [only] = srcs; if (only !== undefined && srcs.length === 1) { - return shortHash(digestOf(only)); + return shortHash(await digestOf(only)); } - let digests = srcs.map(digestOf).sort(Buffer.compare); + let digests = (await Promise.all(srcs.map(digestOf))).sort(Buffer.compare); let h = crypto.createHash('sha256'); for (let d of digests) { h.update(d); diff --git a/tools/deploy/config.ts b/tools/deploy/config.ts index fd99400e..323dd38b 100644 --- a/tools/deploy/config.ts +++ b/tools/deploy/config.ts @@ -1,5 +1,5 @@ -import * as fs from 'node:fs'; +import * as fs from 'node:fs/promises'; import * as nodePath from 'node:path'; import JSON5 from 'json5'; @@ -25,10 +25,10 @@ function isStringArray(v: unknown): v is string[] { return Array.isArray(v) && v.every(x => typeof x === 'string'); } -export function loadDeployConfig(path: string): DeployConfig { +export async function loadDeployConfig(path: string): Promise { let text: string; try { - text = fs.readFileSync(path, 'utf8'); + text = await fs.readFile(path, 'utf8'); } catch { throw new BuildError(`missing ${path}; see README ("Deploying") for the schema`); } diff --git a/tools/deploy/index.ts b/tools/deploy/index.ts index d559fd0c..8ac99e9d 100644 --- a/tools/deploy/index.ts +++ b/tools/deploy/index.ts @@ -1,6 +1,6 @@ import {spawn, type ChildProcess} from 'node:child_process'; -import * as fs from 'node:fs'; +import * as fs from 'node:fs/promises'; import * as os from 'node:os'; import * as nodePath from 'node:path'; import {fileURLToPath, pathToFileURL} from 'node:url'; @@ -95,8 +95,8 @@ function requireOutput(opts: VerbOpts): string { return opts.output; } -function discoverDeployFiles(): string[] { - let files = fs.readdirSync('.').filter(f => f.endsWith('.build.ts')).sort(); +async function discoverDeployFiles(): Promise { + let files = (await fs.readdir('.')).filter(f => f.endsWith('.build.ts')).sort(); if (files.length === 0) { throw new BuildError('No *.build.ts files at the repo root'); } @@ -133,7 +133,7 @@ async function buildThen(decls: readonly artifact.RuleDecl[], opts: CommonOpts, let dbPath = dryRun && db.dbVersion(DB_PATH) !== 2 ? ':memory:' : DB_PATH; let store = new db.Store(dbPath); if (!dryRun) { - fs.rmSync(TMP_DIR, {recursive: true, force: true}); + await fs.rm(TMP_DIR, {recursive: true, force: true}); } let ac = new AbortController(); @@ -213,16 +213,16 @@ function waitExit(child: ChildProcess): Promise { } async function cmdBuild(files: string[], opts: CommonOpts): Promise { - setConfig(loadConfig(opts.config)); + setConfig(await loadConfig(opts.config)); // GC needs the full rule universe: only an unfiltered union build // can know which keys are no longer declared anywhere. let gc = files.length === 0 && !Boolean(opts.dryRun); - await importDeploys(files.length > 0 ? files : discoverDeployFiles()); + await importDeploys(files.length > 0 ? files : await discoverDeployFiles()); process.exitCode = await buildThen(artifact.getDecls(), opts, gc); } async function cmdDeploy(names: string[], opts: VerbOpts): Promise { - let config = loadDeployConfig('deploy.json5'); + let config = await loadDeployConfig('deploy.json5'); if (names.length === 0) { for (let [name, target] of config) { console.log(`${name} (${target.buildFile})`); @@ -239,7 +239,7 @@ async function cmdDeploy(names: string[], opts: VerbOpts): Promise { } return {name, target}; }); - setConfig(loadConfig(opts.config)); + setConfig(await loadConfig(opts.config)); let files = [...new Set(targets.map(t => t.target.buildFile))]; let specs = await importDeploys(files); process.exitCode = await buildThen(artifact.getDecls(), opts, false, async () => { @@ -260,8 +260,8 @@ async function cmdDeploy(names: string[], opts: VerbOpts): Promise { continue; } if (entry.dir) { - fs.mkdirSync(TMP_DIR, {recursive: true}); - let tmp = fs.mkdtempSync(nodePath.join(TMP_DIR, 'deploy-')); + await fs.mkdir(TMP_DIR, {recursive: true}); + let tmp = await fs.mkdtemp(nodePath.join(TMP_DIR, 'deploy-')); try { await aq.run(tmp, 'copy', dst => matched.has(dst)); let cmd = spawn(entry.cmd.replaceAll('%d', tmp), @@ -270,7 +270,7 @@ async function cmdDeploy(names: string[], opts: VerbOpts): Promise { return 1; } } finally { - fs.rmSync(tmp, {recursive: true, force: true}); + await fs.rm(tmp, {recursive: true, force: true}); } continue; } @@ -283,7 +283,7 @@ async function cmdDeploy(names: string[], opts: VerbOpts): Promise { // also crash on the resulting EPIPE, which reaches both // stdin and (via streamx's destroy propagation) the pack. stdin.on('error', () => {}); - let pack = aq.pack(dst => matched.has(dst)); + let pack = await aq.pack(dst => matched.has(dst)); pack.on('error', () => {}); pack.pipe(stdin); if (await waitExit(upload) !== 0) { @@ -297,7 +297,7 @@ async function cmdDeploy(names: string[], opts: VerbOpts): Promise { async function cmdRun(file: string, opts: VerbOpts): Promise { let output = requireOutput(opts); - setConfig(loadConfig(opts.config)); + setConfig(await loadConfig(opts.config)); let specs = await importDeploys([file]); process.exitCode = await buildThen(artifact.getDecls(), opts, false, async () => { let aq = await runFinish(finishOf(specs, file), Boolean(opts.verbose)); @@ -318,8 +318,8 @@ function slugOf(decl: artifact.RuleDecl): string { async function cmdInspect(paths: string[], opts: VerbOpts): Promise { let output = requireOutput(opts); - setConfig(loadConfig(opts.config)); - await importDeploys(discoverDeployFiles()); + setConfig(await loadConfig(opts.config)); + await importDeploys(await discoverDeployFiles()); // Accept absolute paths and paths relative to where the user ran the // command; rules declare repo-root-relative paths. let targets = paths.map(p => { @@ -358,17 +358,18 @@ async function cmdInspect(paths: string[], opts: VerbOpts): Promise { let decls = artifact.getDecls().filter(d => closure.has(d)); console.log(`inspect: ${decls.length} rules`); + let exists = (p: string) => fs.access(p).then(() => true, () => false); process.exitCode = await buildThen(decls, opts, false, async () => { for (let decl of decls) { let dir = nodePath.join(output, slugOf(decl)); - fs.mkdirSync(dir, {recursive: true}); + await fs.mkdir(dir, {recursive: true}); for (let artifact of decl.outputs) { let dst = nodePath.join(dir, artifact.filename); - for (let n = 2; fs.existsSync(dst); n++) { + for (let n = 2; await exists(dst); n++) { dst = nodePath.join(dir, `${artifact.name}-${n}.${artifact.ext}`); } - fs.copyFileSync(casPath(CAS_DIR, artifact.hash, artifact.ext), dst); - fs.chmodSync(dst, 0o644); + await fs.copyFile(casPath(CAS_DIR, artifact.hash, artifact.ext), dst); + await fs.chmod(dst, 0o644); console.log(`${nodePath.relative(output, dst)}`); } } diff --git a/tools/deploy/queue.ts b/tools/deploy/queue.ts index 62c475cd..e82df206 100644 --- a/tools/deploy/queue.ts +++ b/tools/deploy/queue.ts @@ -1,5 +1,6 @@ -import * as fs from 'node:fs'; +import * as fs from 'node:fs/promises'; +import {createWriteStream} from 'node:fs'; import * as nodePath from 'node:path'; import tar from 'tar-stream'; @@ -134,25 +135,25 @@ export class ActionQueue { continue; let op = entry.op; let dst = nodePath.join(dir, entry.dst); - fs.mkdirSync(nodePath.dirname(dst), {recursive: true}); + await fs.mkdir(nodePath.dirname(dst), {recursive: true}); if (op.type === 'Copy'){ // Read-only sources are CAS objects; their mode must not // leak into deploy trees (rsync -a would ship it), and a // hardlink cannot carry its own mode, so copy those. - if (mode === 'link' && (fs.statSync(op.src).mode & 0o200) !== 0) { - fs.linkSync(op.src, dst); + if (mode === 'link' && ((await fs.stat(op.src)).mode & 0o200) !== 0) { + await fs.link(op.src, dst); } else { - fs.copyFileSync(op.src, dst); - fs.chmodSync(dst, 0o644); + await fs.copyFile(op.src, dst); + await fs.chmod(dst, 0o644); } } else if (op.type === 'Write') { - fs.writeFileSync(dst, op.data); + await fs.writeFile(dst, op.data); } } } else { // In this case, I guess its a file rather than a dir. - let out = fs.createWriteStream(dir); - this.pack(filter).pipe(out); + let out = createWriteStream(dir); + (await this.pack(filter)).pipe(out); return new Promise((resolve, reject) => { out.on('error', reject); out.on('finish', () => resolve()); @@ -160,7 +161,7 @@ export class ActionQueue { } } - pack(filter?: (dst: string) => boolean): NodeJS.ReadableStream { + async pack(filter?: (dst: string) => boolean): Promise { if (!this.valid) throw new Error(`Invalid ActionQueue`); let t = tar.pack(); @@ -168,7 +169,7 @@ export class ActionQueue { if (entry.type !== 'Op' || (filter !== undefined && !filter(entry.dst))) continue; let op = entry.op; - let data = op.type === 'Copy' ? fs.readFileSync(op.src) : op.data; + let data = op.type === 'Copy' ? await fs.readFile(op.src) : op.data; // A dying consumer destroys the pack and every pending entry // sink, and each sink emits the error; the consumer is the one // reporting the failure, so keep the sinks quiet. diff --git a/tools/deploy/test/api.test.ts b/tools/deploy/test/api.test.ts index 7ed9cdf9..ef794fd3 100644 --- a/tools/deploy/test/api.test.ts +++ b/tools/deploy/test/api.test.ts @@ -35,31 +35,31 @@ function makeArtifact(casDir: string, content: string, ext: string) { return artifact; } -test('ctx.hash matches the historical single-file stamp for artifacts and files', () => { +test('ctx.hash matches the historical single-file stamp for artifacts and files', async () => { let dir = tmpdir(); let file = pathlib.join(dir, 'f.png'); fs.writeFileSync(file, 'stamp-me'); let artifact = makeArtifact(pathlib.join(dir, 'cas'), 'stamp-me', 'png'); let ctx = makeCtx(pathlib.join(dir, 'cas'), new ActionQueue()); - assert.equal(ctx.hash(file), shortHash('stamp-me')); - assert.equal(ctx.hash(artifact), shortHash('stamp-me')); + assert.equal(await ctx.hash(file), shortHash('stamp-me')); + assert.equal(await ctx.hash(artifact), shortHash('stamp-me')); }); -test('multi-source ctx.hash is order-insensitive and content-sensitive', () => { +test('multi-source ctx.hash is order-insensitive and content-sensitive', async () => { let dir = tmpdir(); let a = pathlib.join(dir, 'a.png'); let b = pathlib.join(dir, 'b.png'); fs.writeFileSync(a, 'aaa'); fs.writeFileSync(b, 'bbb'); let ctx = makeCtx(pathlib.join(dir, 'cas'), new ActionQueue()); - let before = ctx.hash(a, b); - assert.equal(before, ctx.hash(b, a)); - assert.notEqual(before, ctx.hash(a)); + let before = await ctx.hash(a, b); + assert.equal(before, await ctx.hash(b, a)); + assert.notEqual(before, await ctx.hash(a)); fs.writeFileSync(b, 'changed'); - assert.notEqual(ctx.hash(a, b), before); + assert.notEqual(await ctx.hash(a, b), before); }); -test('ctx queues artifact copies from the CAS, writes and reads', () => { +test('ctx queues artifact copies from the CAS, writes and reads', async () => { let dir = tmpdir(); let casDir = pathlib.join(dir, 'cas'); let artifact = makeArtifact(casDir, 'bytes', 'webp'); @@ -67,13 +67,13 @@ test('ctx queues artifact copies from the CAS, writes and reads', () => { let ctx = makeCtx(casDir, aq); ctx.write('m.json', '{}'); ctx.copy(artifact, 'sprites/x.webp'); - assert.equal(ctx.read(artifact), 'bytes'); + assert.equal(await ctx.read(artifact), 'bytes'); let 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')); }); -test('ctx.list sorts, parses extensions, skips dotfiles and directories', () => { +test('ctx.list sorts, parses extensions, skips dotfiles and directories', async () => { let dir = tmpdir(); fs.writeFileSync(pathlib.join(dir, 'b.png'), ''); fs.writeFileSync(pathlib.join(dir, 'a.gif'), ''); @@ -81,15 +81,16 @@ test('ctx.list sorts, parses extensions, skips dotfiles and directories', () => fs.writeFileSync(pathlib.join(dir, '.hidden'), ''); fs.mkdirSync(pathlib.join(dir, 'subdir')); let ctx = makeCtx('cas', new ActionQueue()); - assert.deepEqual(ctx.list(dir), [ + assert.deepEqual(await ctx.list(dir), [ {dir, name: 'a', ext: 'gif', path: pathlib.join(dir, 'a.gif')}, {dir, name: 'b', ext: 'png', path: pathlib.join(dir, 'b.png')}, {dir, name: 'noext', ext: null, path: pathlib.join(dir, 'noext')}, ]); }); -function packedEntries(aq: ActionQueue, filter?: (dst: string) => boolean) +async function packedEntries(aq: ActionQueue, filter?: (dst: string) => boolean) : Promise<{name: string, data: string}[]> { + let packed = await aq.pack(filter); return new Promise((resolve, reject) => { let extract = tar.extract(); let entries: {name: string, data: string}[] = []; @@ -103,7 +104,7 @@ function packedEntries(aq: ActionQueue, filter?: (dst: string) => boolean) }); extract.on('finish', () => resolve(entries)); extract.on('error', reject); - aq.pack(filter).pipe(extract); + packed.pipe(extract); }); } @@ -129,12 +130,12 @@ test('pack with a filter packs only matching entries in order', async () => { ]); }); -test('duplicate and absolute destinations invalidate the queue', () => { +test('duplicate and absolute destinations invalidate the queue', async () => { let dup = new ActionQueue(); dup.write('a', 'x.txt'); dup.write('b', 'x.txt'); assert.ok(!dup.valid); - assert.throws(() => dup.pack(), /Invalid ActionQueue/); + await assert.rejects(() => dup.pack(), /Invalid ActionQueue/); let abs = new ActionQueue(); abs.write('a', '/etc/passwd'); diff --git a/tools/deploy/test/config.test.ts b/tools/deploy/test/config.test.ts index 86a6a0e8..fe882a0f 100644 --- a/tools/deploy/test/config.test.ts +++ b/tools/deploy/test/config.test.ts @@ -14,8 +14,8 @@ function configFile(text: string): string { return p; } -test('loadDeployConfig parses json5 with comments and trailing commas', () => { - let config = loadDeployConfig(configFile(`{ +test('loadDeployConfig parses json5 with comments and trailing commas', async () => { + let config = await loadDeployConfig(configFile(`{ // dex assets assets: { buildFile: "assets.build.ts", @@ -31,26 +31,26 @@ test('loadDeployConfig parses json5 with comments and trailing commas', () => { }); }); -test('loadDeployConfig ties the dir flag to %d in the cmd', () => { - let dir = loadDeployConfig(configFile( +test('loadDeployConfig ties the dir flag to %d in the cmd', async () => { + let dir = await loadDeployConfig(configFile( '{ps: {buildFile: "ps.build.ts", deploy: [{subset: ["ani/**"], dir: true, cmd: "rsync -a %d/ani/ h:a/"}]}}')); assert.equal(dir.get('ps')!.deploy[0]!.dir, true); - assert.throws(() => loadDeployConfig(configFile( + await assert.rejects(() => loadDeployConfig(configFile( '{a: {buildFile: "x.build.ts", deploy: [{subset: ["**"], dir: true, cmd: "rsync -a h:a/"}]}}')), /dir entry's cmd must use %d/); - assert.throws(() => loadDeployConfig(configFile( + await assert.rejects(() => loadDeployConfig(configFile( '{a: {buildFile: "x.build.ts", deploy: [{subset: ["**"], cmd: "rsync -a %d/ h:a/"}]}}')), /must not use %d \(missing dir: true\?\)/); }); -test('loadDeployConfig rejects missing files and malformed shapes', () => { - assert.throws(() => loadDeployConfig('/nonexistent/deploy.json5'), /missing/); - assert.throws(() => loadDeployConfig(configFile('[1]')), /must be an object/); - assert.throws(() => loadDeployConfig(configFile('{a: {deploy: []}}')), /expected \{buildFile/); - assert.throws(() => loadDeployConfig(configFile( +test('loadDeployConfig rejects missing files and malformed shapes', async () => { + await assert.rejects(() => loadDeployConfig('/nonexistent/deploy.json5'), /missing/); + await assert.rejects(() => loadDeployConfig(configFile('[1]')), /must be an object/); + await assert.rejects(() => loadDeployConfig(configFile('{a: {deploy: []}}')), /expected \{buildFile/); + await assert.rejects(() => loadDeployConfig(configFile( '{a: {buildFile: "x.build.ts", deploy: [{subset: "**", cmd: "c"}]}}')), /needs \{subset/); - assert.throws(() => loadDeployConfig(configFile('{a: {buildFile:')), /deploy.json5:/); + await assert.rejects(() => loadDeployConfig(configFile('{a: {buildFile:')), /deploy.json5:/); }); test('matchSubsets routes dsts to entries, overlap allowed', () => { diff --git a/tools/smogdexspritesheet/index.ts b/tools/smogdexspritesheet/index.ts index ea4959e9..14638ae9 100755 --- a/tools/smogdexspritesheet/index.ts +++ b/tools/smogdexspritesheet/index.ts @@ -1,7 +1,7 @@ import spritesmith from 'spritesmith' import * as path from 'node:path'; -import * as fs from 'node:fs'; +import * as fs from 'node:fs/promises'; import * as util from 'node:util'; import * as spritedata from '@smogon/sprite-data/index.ts'; @@ -68,5 +68,5 @@ for (let [id, sprite] of sprites) { }`; } -fs.writeFileSync(opts.image, result.image, 'binary'); -fs.writeFileSync(opts.stylesheet, stylesheet); +await fs.writeFile(opts.image, result.image, 'binary'); +await fs.writeFile(opts.stylesheet, stylesheet); diff --git a/tools/trim/image.ts b/tools/trim/image.ts index 059e3cff..f4868715 100644 --- a/tools/trim/image.ts +++ b/tools/trim/image.ts @@ -1,9 +1,12 @@ import * as cp from 'node:child_process'; +import * as util from 'node:util'; -export function getDims(input: string) { - let info = cp.execFileSync('magick', ['convert', input, '-format', '%w+%h+%@', 'info:'], - {encoding:'utf8'}); +let execFile = util.promisify(cp.execFile); + +export async function getDims(input: string) { + let {stdout: info} = await execFile('magick', + ['convert', input, '-format', '%w+%h+%@', 'info:'], {encoding: 'utf8'}); let [imageWidth, imageHeight, width, height, left, top] = info.split(/x|\+/g).map(dim => parseInt(dim)) as [number, number, number, number, number, number]; @@ -21,8 +24,8 @@ export function getDims(input: 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]); +export async function crop(input: string, {width, height, left, top}: {width: number, height: number, left: number, top: number}, output: string) { + await execFile('magick', ['convert', input, '+repage', '-crop', `${width}x${height}+${left}+${top}`, output]); } // Trim, preserving displacement from center diff --git a/tools/trim/index.ts b/tools/trim/index.ts index 40b005ea..1aa5850e 100644 --- a/tools/trim/index.ts +++ b/tools/trim/index.ts @@ -15,7 +15,7 @@ let {values: opts, positionals: files} = parseArgs({ let retVal = 0; for (let file of files) { - let dims = image.getDims(file); + let dims = await image.getDims(file); let alreadyCropped = (dims.left === 0 || dims.right === 0) && (dims.top === 0 || dims.bottom === 0); @@ -38,7 +38,7 @@ for (let file of files) { if (!opts.check && (!alreadyCropped || opts.force)) { let trimDims = image.losslessTrim(dims); - image.crop(file, trimDims, file); + await image.crop(file, trimDims, file); } }