mirror of
https://github.com/smogon/sprites.git
synced 2026-08-25 11:17:41 -05:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
@@ -83,10 +83,10 @@ forEachRule(dexMissing, {
|
||||
|
||||
let aniChampions = gen10Modelslike();
|
||||
|
||||
deploy(ctx => {
|
||||
deploy(async ctx => {
|
||||
let seenModels = new Set<string>();
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,34 +10,34 @@ let xyModels = gen9Modelslike();
|
||||
let xyChampions = gen10Modelslike();
|
||||
let xyGen5 = gen5Gifs();
|
||||
|
||||
deploy(ctx => {
|
||||
deploy(async ctx => {
|
||||
let seenModels = new Set<string>();
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as fs from 'node:fs/promises';
|
||||
|
||||
export function parseConfig(text: string): Map<string, string> {
|
||||
let result = new Map<string, string>();
|
||||
@@ -17,9 +17,15 @@ export function parseConfig(text: string): Map<string, string> {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function loadConfig(path: string): Map<string, string> {
|
||||
if (!fs.existsSync(path)) {
|
||||
return new Map();
|
||||
export async function loadConfig(path: string): Promise<Map<string, string>> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<string>,
|
||||
list(dir: string): Promise<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[]): Promise<string>,
|
||||
};
|
||||
|
||||
export type DeployFn = (ctx: DeployCtx) => void | Promise<void>;
|
||||
@@ -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<Buffer> => {
|
||||
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<string> {
|
||||
return await fs.readFile(srcPath(src), 'utf8');
|
||||
},
|
||||
list(dir: string): SrcFile[] {
|
||||
async list(dir: string): Promise<SrcFile[]> {
|
||||
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<string> {
|
||||
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);
|
||||
|
||||
@@ -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<DeployConfig> {
|
||||
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`);
|
||||
}
|
||||
|
||||
@@ -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<string[]> {
|
||||
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<number | null> {
|
||||
}
|
||||
|
||||
async function cmdBuild(files: string[], opts: CommonOpts): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
}
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
|
||||
async function cmdRun(file: string, opts: VerbOpts): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
||||
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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void>((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<NodeJS.ReadableStream> {
|
||||
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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user