Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
const fs = require('fs');
const path = require('path');
const yauzl = require('yauzl');
const config = require('./resource-bundles.config.cjs');
const root = path.join(__dirname, '..');
const portableBundleRoot = path.join(root, 'dist-release-final', 'win-unpacked', 'resources', 'resources-bundles');
const manifestPath = path.join(root, 'dist-release-final', 'resource-manifest.json');
const retryableFsCodes = new Set(['EBUSY', 'EACCES', 'EPERM', 'ENOTEMPTY']);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function isRetryableFsError(error) {
return error && retryableFsCodes.has(error.code);
}
async function retryFsOperation(label, operation, attempts = 6) {
let lastError = null;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await operation(attempt);
} catch (error) {
lastError = error;
if (!isRetryableFsError(error) || attempt === attempts) {
throw error;
}
const delayMs = 350 * attempt;
console.warn(`[hydrate-oem-portable] ${label} failed with ${error.code}; retry ${attempt}/${attempts - 1} after ${delayMs}ms`);
await sleep(delayMs);
}
}
throw lastError;
}
function removeIfExists(target) {
if (fs.existsSync(target)) {
fs.rmSync(target, { recursive: true, force: true });
}
}
function uniqueTempPath(target) {
return `${target}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
async function replaceWithRetry(source, destination) {
await retryFsOperation(`replace ${destination}`, async () => {
removeIfExists(destination);
fs.renameSync(source, destination);
});
}
function readJson(file, fallback) {
if (!fs.existsSync(file)) return fallback;
try {
return JSON.parse(fs.readFileSync(file, 'utf8'));
} catch {
return fallback;
}
}
function extractZipOnce(zipPath, destination) {
return new Promise((resolve, reject) => {
const finalDestinationRoot = path.resolve(destination);
const stagingDestination = uniqueTempPath(destination);
removeIfExists(stagingDestination);
fs.mkdirSync(stagingDestination, { recursive: true });
const destinationRoot = path.resolve(stagingDestination);
yauzl.open(zipPath, { lazyEntries: true }, (openError, zipFile) => {
if (openError || !zipFile) {
reject(openError || new Error(`Cannot open zip: ${zipPath}`));
return;
}
let settled = false;
const finish = (error) => {
if (settled) return;
settled = true;
try {
zipFile.close();
} catch {
// Ignore close errors after the original extraction failure.
}
if (error) {
removeIfExists(stagingDestination);
reject(error);
return;
}
replaceWithRetry(stagingDestination, finalDestinationRoot)
.then(() => resolve())
.catch(replaceError => {
removeIfExists(stagingDestination);
reject(replaceError);
});
};
zipFile.readEntry();
zipFile.on('entry', (entry) => {
if (settled) return;
const normalizedName = entry.fileName.replace(/\\/g, '/');
const destPath = path.resolve(destinationRoot, normalizedName);
if (!destPath.startsWith(destinationRoot + path.sep) && destPath !== destinationRoot) {
finish(new Error(`Unsafe zip entry: ${entry.fileName}`));
return;
}
try {
if (/\/$/.test(normalizedName)) {
fs.mkdirSync(destPath, { recursive: true });
zipFile.readEntry();
return;
}
fs.mkdirSync(path.dirname(destPath), { recursive: true });
} catch (error) {
finish(error);
return;
}
zipFile.openReadStream(entry, (streamError, readStream) => {
if (streamError || !readStream) {
finish(streamError || new Error(`Cannot read zip entry: ${entry.fileName}`));
return;
}
const writeStream = fs.createWriteStream(destPath);
readStream.pipe(writeStream);
writeStream.on('finish', () => {
if (!settled) zipFile.readEntry();
});
writeStream.on('error', finish);
readStream.on('error', finish);
});
});
zipFile.on('end', () => finish());
zipFile.on('error', finish);
});
});
}
function extractZip(zipPath, destination) {
return retryFsOperation(
`extract ${path.basename(zipPath)} to ${destination}`,
() => extractZipOnce(zipPath, destination)
);
}
async function copyDirectory(source, destination) {
if (!fs.existsSync(source)) return false;
await retryFsOperation(`copy ${source} to ${destination}`, async () => {
fs.mkdirSync(path.dirname(destination), { recursive: true });
removeIfExists(destination);
fs.cpSync(source, destination, { recursive: true, force: true });
});
return true;
}
function pruneWindowsOnlyBundles() {
const chromiumRoot = path.join(portableBundleRoot, 'playwright', 'chromium-1200');
const macChromium = path.join(chromiumRoot, 'chrome-mac');
const winChromium = path.join(chromiumRoot, 'chrome-win64');
if (process.platform === 'win32' && fs.existsSync(winChromium)) {
removeIfExists(macChromium);
}
}
function directoryHasFiles(dir) {
if (!fs.existsSync(dir)) return false;
const walk = (current) => {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const full = path.join(current, entry.name);
if (entry.isFile()) return true;
if (entry.isDirectory() && walk(full)) return true;
}
return false;
};
return walk(dir);
}
async function main() {
const unpackedDir = path.join(root, 'dist-release-final', 'win-unpacked');
if (!fs.existsSync(unpackedDir)) {
throw new Error(`win-unpacked does not exist: ${unpackedDir}`);
}
const manifest = readJson(manifestPath, { bundles: {} });
fs.mkdirSync(portableBundleRoot, { recursive: true });
const hydrated = [];
for (const bundle of config.bundles) {
const destination = path.join(portableBundleRoot, bundle.name);
const archiveName = manifest.bundles?.[bundle.name]?.archive;
const archivePath = archiveName ? path.join(config.outputDir, archiveName) : '';
if (archivePath && fs.existsSync(archivePath)) {
await extractZip(archivePath, destination);
hydrated.push(bundle.name);
} else if (await copyDirectory(bundle.source, destination)) {
hydrated.push(bundle.name);
} else if (bundle.required) {
throw new Error(`Required resource bundle is missing: ${bundle.name}. Expected archive ${archivePath || '(none)'} or source ${bundle.source}`);
}
if (bundle.required && !directoryHasFiles(destination)) {
throw new Error(`Required resource bundle was not hydrated: ${bundle.name} (${destination})`);
}
}
pruneWindowsOnlyBundles();
console.log(`[hydrate-oem-portable] hydrated bundles: ${hydrated.join(', ') || 'none'}`);
}
main().catch((error) => {
console.error('[hydrate-oem-portable] failed:', error);
process.exit(1);
});