feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms Directus image transform cache in-process with an explicit acceptFormat, so the generated file matches the browser's cache key byte-for-byte. - resumable, window-aware backfill with progress + cancel - system + custom presets, effective-format reporting, shadow warnings - storage breakdown (previews vs originals), human-readable API fields - diagnostics: resolve-plan prediction, per-file generate, ops ring buffer - isolated dev/staging compose stack + deploy/rollback scripts Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed, idempotent backfill, custom fixed-format presets, error surfacing.
This commit is contained in:
commit
c8f8bbc5ac
42 changed files with 10005 additions and 0 deletions
77
src/lib/diagnostics.test.ts
Normal file
77
src/lib/diagnostics.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { computePlan, getOps, recordOp, type PlanFile } from './diagnostics';
|
||||
import type { Preset } from './types';
|
||||
|
||||
const file: PlanFile = {
|
||||
filename_disk: 'abc123.jpg',
|
||||
type: 'image/jpeg',
|
||||
width: 1200,
|
||||
height: 800,
|
||||
focal_point_x: null,
|
||||
focal_point_y: null,
|
||||
storage: 'local',
|
||||
};
|
||||
|
||||
const preset: Preset = {
|
||||
key: 'system-small-cover',
|
||||
format: 'auto',
|
||||
transforms: [ [ 'resize', { width: 64, height: 64, fit: 'cover' } ] ],
|
||||
};
|
||||
|
||||
function utilsWith(format: string | undefined) {
|
||||
return {
|
||||
resolvePreset: () =>
|
||||
format ? [ [ 'toFormat', format, { quality: undefined } ], [ 'resize', { width: 64 } ] ] : [ [ 'resize', { width: 64 } ] ],
|
||||
maybeExtractFormat: () => format,
|
||||
};
|
||||
}
|
||||
|
||||
test('computePlan predicts a hashed filename with the resolved extension', async () => {
|
||||
const plan = await computePlan(file, preset, 'avif', {
|
||||
utils: utilsWith('avif'),
|
||||
hashFn: () => 'deadbeef',
|
||||
});
|
||||
|
||||
assert.equal(plan.filename, 'abc123__deadbeef.avif');
|
||||
assert.equal(plan.format, 'avif');
|
||||
assert.equal(plan.transforms.length, 2);
|
||||
});
|
||||
|
||||
test('computePlan without a toFormat keeps the original extension but still hashes the resize', async () => {
|
||||
const plan = await computePlan(file, { key: 'resize-only', width: 100 }, null, {
|
||||
utils: utilsWith(undefined),
|
||||
hashFn: () => 'unused',
|
||||
});
|
||||
|
||||
assert.equal(plan.filename, 'abc123__unused.jpg');
|
||||
assert.equal(plan.format, null);
|
||||
});
|
||||
|
||||
test('computePlan passes acceptFormat into the transformation set', async () => {
|
||||
let seen: Record<string, unknown> | null = null;
|
||||
|
||||
await computePlan(file, preset, 'webp', {
|
||||
utils: {
|
||||
resolvePreset: (transformation) => {
|
||||
seen = transformation;
|
||||
return [ [ 'toFormat', 'webp', {} ] ];
|
||||
},
|
||||
maybeExtractFormat: () => 'webp',
|
||||
},
|
||||
hashFn: () => 'x',
|
||||
});
|
||||
|
||||
assert.equal(seen?.['acceptFormat'], 'webp');
|
||||
assert.ok(seen?.['transformationParams']);
|
||||
});
|
||||
|
||||
test('ops ring buffer is newest-first and bounded', () => {
|
||||
recordOp({ ts: 1, fileId: 'a', key: 'k', format: 'avif', ok: true, ms: 1 });
|
||||
recordOp({ ts: 2, fileId: 'b', key: 'k', format: 'avif', ok: false, ms: 2, error: 'x' });
|
||||
|
||||
const ops = getOps(1);
|
||||
assert.equal(ops.length, 1);
|
||||
assert.equal(ops[0]?.fileId, 'b');
|
||||
assert.equal(getOps(0).length, 0);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue