import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; const scriptRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const postinstallCommand = 'node ../../scripts/taro-components-h5-runtime-patch.js --apply'; const packageContract = Object.freeze({ packageName: '@tarojs/components', packageVersion: '4.2.0', lockIntegrity: 'sha512-SQIK5UxKfmkhV0MdhmC0KV6duSkBtF9C8sX3dF6afKX8DvULXP5dirH8Y4bJEL+Mt+dkhlXiBmuSk/C2KB7sVg==', }); export const taroInputWatcherPatch = Object.freeze({ id: 'inputWatcher', label: 'Input watcher', targetRelativePath: 'dist/components/taro-input-core.js', pristineSha256: '2483ffc5727959174e988c7171d7f7bb0a6300851cae1a13699d62a7d4769f95', patchedSha256: '260bb8a07d66eaf3398904acb94a7c2cacabe4411b70a01fb0d03931fe95c499', before: 'if (this.inputRef.value !== value) {', after: 'if (this.inputRef && this.inputRef.value !== value) {', }); export const taroButtonLoadingPatch = Object.freeze({ id: 'buttonLoading', label: 'Button loading node', targetRelativePath: 'dist/components/taro-button-core.js', pristineSha256: 'de5dfab0fc4c68a388b996b059255c57cc1ec52891238e7aacea588e7a9dd63e', patchedSha256: '428db74e51382c68bc10211ff7815d494b086de465fdef97ca09f5b7ab8368ea', before: 'loading && h("i", { class: \'weui-loading\' })', after: 'h("i", { class: \'weui-loading\', style: { display: loading ? \'inline-block\' : \'none\' } })', }); export const taroH5RuntimePatchDefinition = Object.freeze({ ...packageContract, postinstallCommand, patches: Object.freeze([taroInputWatcherPatch, taroButtonLoadingPatch]), }); function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function assert(condition, message) { if (!condition) throw new Error(message); } function sha256(content) { return crypto.createHash('sha256').update(content).digest('hex'); } function occurrences(source, needle) { return source.split(needle).length - 1; } export function replaceExactlyOnce(source, before, after, label = 'Taro H5 runtime patch') { const count = occurrences(source, before); assert(count === 1, `expected the ${label} target exactly once, found ${count}`); return source.replace(before, after); } function validateRepositoryContract(root, definition) { const taroManifestPath = path.join(root, 'apps', 'taro', 'package.json'); const lockPath = path.join(root, 'package-lock.json'); const taroManifest = readJson(taroManifestPath); const lock = readJson(lockPath); assert( taroManifest.devDependencies?.[definition.packageName] === definition.packageVersion, `apps/taro/package.json must pin ${definition.packageName}@${definition.packageVersion}`, ); assert( taroManifest.scripts?.postinstall === definition.postinstallCommand, 'apps/taro postinstall must apply the reviewed Taro H5 runtime patches', ); assert(lock.packages?.['apps/taro']?.hasInstallScript === true, 'package-lock.json must record the Taro workspace install hook'); assert( lock.packages?.['apps/taro']?.devDependencies?.[definition.packageName] === definition.packageVersion, `package-lock.json must pin the Taro workspace to ${definition.packageName}@${definition.packageVersion}`, ); const lockEntry = lock.packages?.[`node_modules/${definition.packageName}`]; assert(lockEntry?.version === definition.packageVersion, `package-lock.json must resolve ${definition.packageName}@${definition.packageVersion}`); assert(lockEntry?.integrity === definition.lockIntegrity, `package-lock.json has an unexpected ${definition.packageName} integrity`); return taroManifestPath; } function resolveInstalledPackage(root, definition, taroManifestPath) { const requireFromTaro = createRequire(taroManifestPath); const installedManifestPath = requireFromTaro.resolve(`${definition.packageName}/package.json`); const installedManifest = readJson(installedManifestPath); assert( installedManifest.version === definition.packageVersion, `installed ${definition.packageName}@${installedManifest.version} does not match the reviewed ${definition.packageVersion}`, ); return path.dirname(installedManifestPath); } function validatePatchedText(source, patch) { assert(occurrences(source, patch.before) === 0, `patched Taro ${patch.label} still contains the unsafe expression`); assert(occurrences(source, patch.after) === 1, `patched Taro ${patch.label} guard is missing or duplicated`); } function planPatch(root, installedPackageRoot, patch, mode) { const targetPath = path.join(installedPackageRoot, patch.targetRelativePath); assert(fs.existsSync(targetPath), `Taro ${patch.label} target is missing: ${targetPath}`); const source = fs.readFileSync(targetPath, 'utf8'); const installedSha256 = sha256(source); let state = 'patched'; let finalSource = source; if (installedSha256 === patch.pristineSha256) { state = 'pristine'; assert(mode === 'apply', `reviewed Taro ${patch.label} patch is not applied; run npm install or the patch command`); finalSource = replaceExactlyOnce(source, patch.before, patch.after, patch.label); assert(sha256(finalSource) === patch.patchedSha256, `Taro ${patch.label} patch output hash is unexpected`); validatePatchedText(finalSource, patch); state = 'patched-now'; } else if (installedSha256 === patch.patchedSha256) { validatePatchedText(source, patch); } else { throw new Error( `unreviewed ${packageContract.packageName} ${patch.label} target hash ${installedSha256}; do not apply the patch to unknown package contents`, ); } const finalSha256 = sha256(finalSource); assert(finalSha256 === patch.patchedSha256, `installed Taro ${patch.label} patch hash does not match the reviewed result`); return { id: patch.id, state, targetPath, target: path.relative(root, targetPath).replace(/\\/g, '/'), source, finalSource, pristineSha256: patch.pristineSha256, patchedSha256: patch.patchedSha256, installedSha256: finalSha256, }; } export function enforceTaroH5RuntimePatches({ root = scriptRoot, mode = 'check', definition = taroH5RuntimePatchDefinition, } = {}) { assert(mode === 'apply' || mode === 'check', 'mode must be apply or check'); const taroManifestPath = validateRepositoryContract(root, definition); const installedPackageRoot = resolveInstalledPackage(root, definition, taroManifestPath); // Validate every target before writing either file so an unknown package state fails atomically. const plans = definition.patches.map(patch => planPatch(root, installedPackageRoot, patch, mode)); if (mode === 'apply') { for (const plan of plans) { if (plan.finalSource !== plan.source) fs.writeFileSync(plan.targetPath, plan.finalSource, 'utf8'); } } return { schemaVersion: 1, status: 'pass', package: definition.packageName, version: definition.packageVersion, patches: Object.fromEntries(plans.map(({ id, state, target, pristineSha256, patchedSha256, installedSha256 }) => [ id, { state, target, pristineSha256, patchedSha256, installedSha256 }, ])), }; } function main() { const argv = process.argv.slice(2); const apply = argv.includes('--apply'); const check = argv.includes('--check'); assert(apply !== check, 'pass exactly one of --apply or --check'); const result = enforceTaroH5RuntimePatches({ mode: apply ? 'apply' : 'check' }); if (argv.includes('--json')) console.log(JSON.stringify(result, null, 2)); else { const summary = Object.entries(result.patches) .map(([id, patch]) => `${id}=${patch.state}:${patch.installedSha256}`) .join(', '); console.log(`[PASS] ${result.package}@${result.version} H5 runtime patches (${summary})`); } } const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isMain) { try { main(); } catch (error) { console.error(`[FAIL] ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; } }