forked from wangziqi/gongxue-base
199 lines
8.0 KiB
JavaScript
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;
|
|
}
|
|
}
|