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

361 lines
12 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 cpDirectory = path.join(root, 'activity', 'cp');
const pageDirectory = path.join(cpDirectory, 'huwaa');
const assetDirectory = path.join(pageDirectory, 'assets', 'layers');
const manifestPath = path.join(assetDirectory, 'manifest.json');
const expectedEntryFrames = {
plaza: '831:3320',
rank: '831:4048',
rewards: '831:3913',
};
const expectedDesignSizes = {
plaza: { width: 1080, height: 4283 },
rank: { width: 1080, height: 3733 },
rewards: { width: 1080, height: 4782 },
};
const forbiddenAggregateNodes = new Set([
'543:1998',
'823:3005',
'831:3320',
'831:4048',
'831:3913',
'831:3323',
'831:3326',
'831:3339',
'831:3349',
'831:3642',
'831:4381',
'831:3942',
]);
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(expectedEntryFrames),
'Huwaa CP entry-frame mapping changed'
);
invariant(
JSON.stringify(manifest.figma?.designSizes) ===
JSON.stringify(expectedDesignSizes),
'Huwaa CP design sizes changed'
);
invariant(Array.isArray(manifest.assets), 'Manifest assets must be an array');
invariant(manifest.assets.length === 17, 'Manifest must describe 17 layers');
function webPDimensions(bytes) {
invariant(
bytes.subarray(0, 4).toString('ascii') === 'RIFF' &&
bytes.subarray(8, 12).toString('ascii') === 'WEBP',
'Asset is not a WebP file'
);
const chunk = bytes.subarray(12, 16).toString('ascii');
if (chunk === 'VP8X') {
return {
width: bytes.readUIntLE(24, 3) + 1,
height: bytes.readUIntLE(27, 3) + 1,
};
}
if (chunk === 'VP8 ') {
return {
width: bytes.readUInt16LE(26) & 0x3fff,
height: bytes.readUInt16LE(28) & 0x3fff,
};
}
if (chunk === 'VP8L') {
invariant(bytes[20] === 0x2f, 'Invalid lossless WebP signature');
const packed = bytes.readUInt32LE(21);
return {
width: (packed & 0x3fff) + 1,
height: ((packed >>> 14) & 0x3fff) + 1,
};
}
throw new Error(`Unsupported WebP chunk ${chunk}`);
}
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}: export 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',
'containsDynamicText',
'containsControls',
'containsDynamicMedia',
]) {
invariant(
typeof asset[flag] === 'boolean',
`${asset.file}: ${flag} must be boolean`
);
}
invariant(
!asset.containsDynamicText,
`${asset.file}: rendered dynamic text is forbidden`
);
invariant(
!asset.containsControls,
`${asset.file}: rendered controls are forbidden`
);
invariant(
!asset.containsDynamicMedia,
`${asset.file}: dynamic business media is forbidden`
);
invariant(!asset.containsText, `${asset.file}: embedded text 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');
const dimensions = webPDimensions(bytes);
invariant(info.size === asset.bytes, `${asset.file}: byte size mismatch`);
invariant(digest === asset.sha256, `${asset.file}: SHA-256 mismatch`);
invariant(
dimensions.width === asset.original.width &&
dimensions.height === asset.original.height,
`${asset.file}: image dimensions do not match the manifest`
);
}
const diskAssets = (await readdir(assetDirectory))
.filter((file) => file !== 'manifest.json')
.sort();
invariant(
JSON.stringify(diskAssets) === JSON.stringify([...listedFiles].sort()),
'Every Huwaa CP visual asset must have exactly one manifest entry'
);
const [html, css, js, theme, api, locales] = await Promise.all([
readFile(path.join(cpDirectory, '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',
'cp-plaza.png',
'cp-week.png',
'cp-reward.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 Huwaa CP 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('../../common/params.js'),
'Common params are 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(
html.includes("window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es']"),
'Huwaa CP must restrict its language surface to en/ar/tr/es'
);
invariant(api.includes('cpWeeklyRankAPI'), 'CP weekly-rank API is required');
invariant(
js.includes('HyAppAPI.cpWeeklyRank.status'),
'Huwaa CP must consume the weekly-rank API'
);
invariant(
!js.includes('HyAppAPI.cpSpace.intimacyLeaderboard') &&
js.includes("activityData.plazaState = 'unavailable'"),
'The cumulative intimacy leaderboard must not be presented as the Plaza gift-event feed'
);
invariant(
js.includes("get('mock') === '1'") && js.includes('isLocalRuntime()'),
'Visual fixtures must require explicit local mock mode'
);
invariant(
js.includes('Promise.allSettled'),
'Independent CP API failures must be isolated'
);
invariant(
js.includes('requestGeneration'),
'Stale asynchronous CP responses must be rejected'
);
invariant(
js.includes('resolveExplicitEndAt') && !js.includes('localStorage'),
'Countdown must not fabricate and persist a production activity period'
);
invariant(
js.includes('serverTimeOffsetMS') && js.includes('server_time_ms'),
'Countdown must use the backend server clock'
);
invariant(
js.includes('source.enabled !== false'),
'The weekly-rank enabled switch must control the page state'
);
invariant(
js.includes('source.rank_no') && js.includes('section.rankNo === rankNo'),
'Reward tiers must stay aligned to backend rank_no'
);
invariant(
html.includes('hero-art-text-free.webp') &&
html.includes('class="huwaa-cp-wordmark"') &&
html.includes('data-i18n="cp.pageTitle"'),
'The visible event title must be localized DOM text over text-free art'
);
invariant(
html.includes("var aliases = ['app', 'app_code', 'appCode']"),
'All app-code aliases must resolve to Huwaa before common scripts load'
);
invariant(
css.includes('background: #e98c9b') &&
css.includes('left: -487px') &&
css.includes('mix-blend-mode: screen') &&
css.includes('opacity: 0.5'),
'The Figma frame fill and petal texture blend must be reconstructed exactly'
);
invariant(
css.includes('#000 88.93%') && css.includes('transparent 100%'),
'The hero must preserve the Figma bottom fade mask'
);
invariant(
js.includes('stageWrap.style.minHeight = stageWrap.style.height'),
'Responsive stage height must override the initial minimum height'
);
invariant(
js.includes("activeTab === 'rewards' && !activityData.rewards.length"),
'Empty and failed Rewards states must not retain the full three-panel stage height'
);
invariant(
!runtimeSource.includes('lalu-coin'),
'Huwaa CP must not display a Lalu coin asset'
);
invariant(
!runtimeSource.includes('Namename') &&
!runtimeSource.includes('1234567') &&
!runtimeSource.includes('DDDDD'),
'Figma placeholder business data must not be baked into runtime code'
);
invariant(
html.includes('role="tablist"') && html.includes('role="tabpanel"'),
'CP tabs must expose accessible tab semantics'
);
invariant(
html.includes('aria-live="polite"'),
'Dynamic CP state must be announced'
);
invariant(
css.includes('prefers-reduced-motion'),
'Loading motion must respect reduced motion'
);
for (const locale of locales) {
for (const key of [
'"cp.countdownLabel"',
'"cp.huwaaPlazaGiftTitle"',
'"cp.previousWeekTopTitle"',
'"cp.previousWeekTopSubtitle"',
'"cp.rankPodiumLabel"',
'"cp.rankPlace"',
'"cp.loading"',
'"cp.empty"',
'"cp.error"',
'"cp.disabled"',
'"cp.unavailable"',
'"cp.retry"',
'"cp.reward"',
'"cp.rewardCoins"',
'"cp.rewardTierGold"',
'"cp.rewardTierSilver"',
'"cp.rewardTierBronze"',
'"cp.daysLong"',
]) {
invariant(
locale.includes(key),
`Required Huwaa CP translation is missing: ${key}`
);
}
}
console.log(
`activity/cp/huwaa.html asset audit passed (${manifest.assets.length} traced layers)`
);