Files
gongxue-base/scripts/taro-components-h5-runtime-patch.js
2026-07-12 19:26:57 +08:00

199 lines
8.0 KiB
JavaScript

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;
}
}