hyapp-h5/scripts/check-activity-invite-huwaa-layer-assets.mjs
2026-07-17 11:54:43 +08:00

225 lines
7.1 KiB
JavaScript

import { createHash } from 'node:crypto';
import { readFile, readdir, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(scriptDirectory, '..');
const inviteDirectory = path.join(root, 'activity', 'invite');
const pageDirectory = path.join(inviteDirectory, 'huwaa');
const assetDirectory = path.join(pageDirectory, 'assets', 'layers');
const manifestPath = path.join(assetDirectory, 'manifest.json');
const forbiddenAggregateNodes = new Set([
'543:1998',
'850:232',
'850:431',
'850:638',
'850:237',
'850:255',
'850:262',
'850:291',
'850:314',
'850:425',
'850:628',
'850:835',
]);
function invariant(condition, message) {
if (!condition) throw new Error(message);
}
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
invariant(manifest.schemaVersion === 1, 'Unsupported manifest schema');
invariant(
manifest.figma?.fileKey === 'OTOg5cwbhVNWxkImGmzTJl',
'Unexpected Figma file key'
);
invariant(manifest.figma?.canvas?.id === '543:1998', 'Unexpected Figma canvas');
invariant(
JSON.stringify(manifest.figma?.entryFrames) ===
JSON.stringify({
default: '850:232',
win: '850:431',
estimatedRewards: '850:638',
}),
'Entry-frame mapping changed'
);
invariant(
manifest.figma?.designSize?.width === 375 &&
manifest.figma?.designSize?.height === 1114,
'Unexpected design size'
);
invariant(Array.isArray(manifest.assets), 'Manifest assets must be an array');
invariant(manifest.assets.length === 26, 'Manifest must describe 26 layers');
const listedFiles = new Set();
for (const asset of manifest.assets) {
invariant(
asset.file && !listedFiles.has(asset.file),
`Duplicate asset ${asset.file}`
);
listedFiles.add(asset.file);
invariant(asset.sourceNode, `${asset.file}: sourceNode is required`);
invariant(
!forbiddenAggregateNodes.has(asset.sourceNode),
`${asset.file}: aggregate Figma export is forbidden`
);
invariant(
asset.sourceName && asset.sourceType,
`${asset.file}: source name/type is required`
);
invariant(asset.purpose, `${asset.file}: purpose is required`);
invariant(
asset.bounds?.width > 0 && asset.bounds?.height > 0,
`${asset.file}: design bounds are required`
);
invariant(
asset.original?.width > 0 && asset.original?.height > 0,
`${asset.file}: original dimensions are required`
);
invariant(asset.exportFormat, `${asset.file}: export format is required`);
invariant(
Number.isInteger(asset.bytes) && asset.bytes > 0,
`${asset.file}: byte size is required`
);
invariant(
/^[a-f0-9]{64}$/.test(asset.sha256 || ''),
`${asset.file}: SHA-256 is required`
);
for (const flag of [
'containsText',
'containsControls',
'containsDynamicMedia',
]) {
invariant(
typeof asset[flag] === 'boolean',
`${asset.file}: ${flag} must be boolean`
);
}
invariant(!asset.containsText, `${asset.file}: rendered text is forbidden`);
invariant(
!asset.containsControls,
`${asset.file}: rendered controls are forbidden`
);
invariant(
!asset.containsDynamicMedia,
`${asset.file}: dynamic business media is forbidden`
);
const assetPath = path.join(assetDirectory, asset.file);
const [bytes, info] = await Promise.all([
readFile(assetPath),
stat(assetPath),
]);
const digest = createHash('sha256').update(bytes).digest('hex');
invariant(info.size === asset.bytes, `${asset.file}: byte size mismatch`);
invariant(digest === asset.sha256, `${asset.file}: SHA-256 mismatch`);
}
const diskAssets = (await readdir(assetDirectory))
.filter((file) => file !== 'manifest.json')
.sort();
invariant(
JSON.stringify(diskAssets) === JSON.stringify([...listedFiles].sort()),
'Every visual asset must have exactly one manifest entry'
);
const [html, css, js, theme, api, locales] = await Promise.all([
readFile(path.join(inviteDirectory, 'huwaa.html'), 'utf8'),
readFile(path.join(pageDirectory, 'huwaa.css'), 'utf8'),
readFile(path.join(pageDirectory, 'huwaa.js'), 'utf8'),
readFile(path.join(root, 'common', 'theme.css'), 'utf8'),
readFile(path.join(root, 'common', 'api.js'), 'utf8'),
Promise.all(
['en', 'ar', 'tr', 'es'].map((locale) =>
readFile(
path.join(root, 'common', 'locales', `${locale}.json`),
'utf8'
)
)
),
]);
const runtimeSource = [html, css, js].join('\n');
for (const nodeID of forbiddenAggregateNodes) {
invariant(
!runtimeSource.includes(nodeID),
`Runtime must not reference aggregate Figma node ${nodeID}`
);
}
for (const forbidden of [
'whole-page',
'page-screenshot',
'figma-frame.png',
'850-232.png',
'850-431.png',
'850-638.png',
]) {
invariant(
!runtimeSource.toLowerCase().includes(forbidden),
`Forbidden aggregate screenshot reference: ${forbidden}`
);
}
for (const asset of manifest.assets) {
invariant(
runtimeSource.includes(asset.file),
`${asset.file}: traced asset is not consumed by the page`
);
}
invariant(theme.includes('--hy-theme-primary'), 'Common theme is invalid');
invariant(html.includes('../../common/theme.css'), 'Common theme is required');
invariant(html.includes('../../common/i18n.js'), 'Common i18n is required');
invariant(html.includes('../../common/api.js'), 'Common API is required');
invariant(
html.includes('data-hy-app-code="huwaa"'),
'Huwaa app identity is required'
);
invariant(
html.indexOf("window.HyAppDefaultAppCode = 'huwaa'") <
html.indexOf('../../common/api.js'),
'Huwaa app code must be configured before common API initialization'
);
invariant(api.includes('activityWheelAPI'), 'Wheel API is required');
invariant(js.includes('HyAppAPI.activityWheel'), 'Wheel API must be consumed');
invariant(
/\.balances\(\s*['"]COIN['"]\s*\)/.test(js),
'Wallet balance API must be consumed'
);
invariant(
js.includes('/api/v1/users/me/invite-overview'),
'Invite overview API must be consumed'
);
invariant(
js.includes("params.get('mock') === '1'"),
'Visual fixtures must require an explicit mock query'
);
invariant(
!html.includes('DDDDD'),
'Figma placeholder reward names must not be baked into production HTML'
);
invariant(
html.includes('<span class="huwaa-task-status"'),
'Completed task status must be a label, not a button'
);
invariant(
html.includes('aria-live="polite"'),
'Win result must announce dynamic content'
);
invariant(
css.includes('prefers-reduced-motion'),
'Wheel animation must respect reduced motion'
);
for (const locale of locales) {
invariant(
locale.includes('"huwaa_invite.page_title"'),
'Required Huwaa invite translations are missing'
);
}
console.log(
`activity/invite/huwaa.html asset audit passed (${manifest.assets.length} traced layers)`
);