Files
twenty/packages/twenty-website-new/scripts/check-lottie-frames.mjs
T

120 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
import { execFile } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const LOTTIE_PATH = path.join(
ROOT,
'public',
'lottie',
'stepper',
'stepper.lottie',
);
const ANIMATION_ENTRY = 'animations/main.json';
const FRAME_MAP_PATH = path.join(
ROOT,
'src',
'sections',
'HomeStepper',
'components',
'Visual',
'use-dot-lottie-scroll-sync.ts',
);
const EXPECTED_CONSTANT_REGEX =
/(?:export\s+)?const\s+HOME_STEPPER_LOTTIE_EXPECTED_TOTAL_FRAMES\s*=\s*(\d+)\s*;/;
function fail(message) {
// eslint-disable-next-line no-console
console.error(`\n check-lottie-frames: ${message}\n`);
process.exitCode = 1;
}
async function readExpectedTotalFrames() {
const source = await readFile(FRAME_MAP_PATH, 'utf8');
const match = source.match(EXPECTED_CONSTANT_REGEX);
if (match === null) {
throw new Error(
`couldn't find HOME_STEPPER_LOTTIE_EXPECTED_TOTAL_FRAMES in ${path.relative(ROOT, FRAME_MAP_PATH)}. ` +
'Has the constant been renamed? Update both the TS file and the regex in this script.',
);
}
return Number.parseInt(match[1], 10);
}
async function readActualTotalFrames() {
let stdout;
try {
const result = await execFileAsync(
'unzip',
['-p', LOTTIE_PATH, ANIMATION_ENTRY],
{ maxBuffer: 32 * 1024 * 1024, encoding: 'buffer' },
);
stdout = result.stdout;
} catch (error) {
if (error?.code === 'ENOENT') {
throw new Error(
'`unzip` binary not found on PATH. Install it (Debian/Ubuntu: `apt install unzip`, macOS ships with it) — ' +
'this script needs it to read the Lottie animation JSON.',
);
}
throw new Error(
`failed to extract ${ANIMATION_ENTRY} from ${path.relative(ROOT, LOTTIE_PATH)}: ${error?.message ?? error}`,
);
}
let animation;
try {
animation = JSON.parse(stdout.toString('utf8'));
} catch (error) {
throw new Error(
`${ANIMATION_ENTRY} inside the .lottie file is not valid JSON: ${error?.message ?? error}`,
);
}
const ip = animation?.ip;
const op = animation?.op;
if (typeof ip !== 'number' || typeof op !== 'number') {
throw new Error(
`${ANIMATION_ENTRY} is missing numeric \`ip\` / \`op\` fields ` +
`(got ip=${JSON.stringify(ip)}, op=${JSON.stringify(op)}). ` +
'Has the Lottie schema changed?',
);
}
return Math.floor(op - ip);
}
async function main() {
const [expected, actual] = await Promise.all([
readExpectedTotalFrames(),
readActualTotalFrames(),
]);
if (expected !== actual) {
fail(
`Lottie totalFrames mismatch — expected ${expected} (per HOME_STEPPER_LOTTIE_EXPECTED_TOTAL_FRAMES), ` +
`got ${actual} from ${path.relative(ROOT, LOTTIE_PATH)}.\n ` +
' The home-stepper scroll → frame map is keyed to the authored timeline; ' +
'either:\n 1. revert the Lottie re-export, or\n 2. update HOME_STEPPER_LOTTIE_EXPECTED_TOTAL_FRAMES ' +
'and the STEP_*_END anchors in home-stepper-lottie-frame-map.ts together.',
);
return;
}
// eslint-disable-next-line no-console
console.log(
`check-lottie-frames: OK (stepper.lottie totalFrames = ${actual}).`,
);
}
main().catch((error) => {
fail(error?.message ?? String(error));
});