Files
2026-10-26 12:20:04 +08:00

92 lines
6.9 KiB
JavaScript

'use strict';
const fs = require('fs'), path = require('path'), vm = require('vm'), crypto = require('crypto');
const zip = require('./zip'), names = require('./bundles.json');
const hash = data => crypto.createHash('sha256').update(data).digest('hex');
function files(root, relative = '') {
return fs.readdirSync(path.join(root, relative)).sort().reduce((all, name) => {
const file = path.posix.join(relative, name), stat = fs.lstatSync(path.join(root, file));
if (stat.isSymbolicLink()) throw Error('Unexpected symlink: ' + file);
return all.concat(stat.isDirectory() ? files(root, file) : [file]);
}, []);
}
function pack(destination, bundles = names) {
const root = path.resolve(destination), context = { window: {} };
vm.runInNewContext(fs.readFileSync(path.join(root, 'src/settings.js'), 'utf8'), context);
const settings = context.window._CCSettings, server = String(settings.server || '').replace(/\/+$/, '');
if (!/^https:\/\/[^\s?#]+$/.test(server)) throw Error('Configure the WeChat HTTPS resource server');
const manifest = { schemaVersion: 1, bundles: {} }, archives = [];
for (const name of bundles) {
if (!/^[a-z][a-zA-Z0-9_]*$/.test(name) || manifest.bundles[name]) throw Error('Invalid or duplicate bundle name');
if (!(settings.remoteBundles || []).includes(name)) throw Error('Activity must be a remote bundle: ' + name);
const version = (settings.bundleVers || {})[name] || '', configFile = 'config.' + (version ? version + '.' : '') + 'json';
const folder = path.join(root, 'remote', name), config = JSON.parse(fs.readFileSync(path.join(folder, configFile), 'utf8'));
if (config.name !== name || config.isZip) throw Error('Use Merge All JSON for ' + name);
if (!fs.existsSync(path.join(root, 'src/scripts', name, 'index.js'))) throw Error('Missing local bundle script: ' + name);
const entries = files(folder).filter(file => !/^config(?:\.[^.]+)?\.json$/.test(file) || file === configFile).map(file => {
if (!/^[a-zA-Z0-9_./+@=-]+$/.test(file)) throw Error('Unsupported generated resource path: ' + file);
if (/\.(?:js|jsc)$/i.test(file)) throw Error('Scripts must remain in the client package: ' + file);
if (file === 'bundle-manifest.json') throw Error('Reserved resource path: ' + file);
return { name: file, data: fs.readFileSync(path.join(folder, file)) };
});
const metadata = { schemaVersion: 1, name, config: configFile, version,
files: entries.map(e => ({ path: e.name, size: e.data.length, sha256: hash(e.data) })) };
entries.push({ name: 'bundle-manifest.json', data: Buffer.from(JSON.stringify(metadata)) });
const archive = zip(entries), zipHash = hash(archive), file = name + '/' + name + '.' + zipHash + '.zip';
manifest.bundles[name] = { file, hash: zipHash, size: archive.length };
archives.push({ file, data: archive });
}
// A sibling output cannot accidentally enter the WeChat main code package.
const publishDir = path.join(path.dirname(root), path.basename(root) + '-activities', 'remote');
fs.mkdirSync(publishDir, { recursive: true });
for (const entry of archives) {
const target = path.join(publishDir, entry.file);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target + '.tmp', entry.data); fs.renameSync(target + '.tmp', target);
}
// Never overwrite a working manifest until every archive has been written.
const manifestFile = path.join(publishDir, 'manifest.json');
fs.writeFileSync(manifestFile + '.tmp', JSON.stringify(manifest, null, 2) + '\n'); fs.renameSync(manifestFile + '.tmp', manifestFile);
// Pages depend on art: switch this set together while still downloading pages on demand.
const cloudRise = ['matching', 'stage1', 'stage2', 'stage3', 'art'];
const groups = cloudRise.every(name => bundles.includes(name)) ? { cloudRise } : {};
fs.writeFileSync(path.join(root, 'activity-bundles-config.js'), 'module.exports=' + JSON.stringify({ baseUrl: server + '/remote/', bundles, groups }) + ';\n');
for (const [source, target] of [['runtime.js', 'activity-bundles.js'], ['sha256.js', 'activity-bundles-sha256.js']])
fs.copyFileSync(path.join(__dirname, source), path.join(root, target));
const bootFile = path.join(root, 'game.js'); let boot = fs.readFileSync(bootFile, 'utf8');
// Also repair incremental builds that still contain the former Cloud Rise bootstrap.
boot = boot.replace(/require\(['"]\.\/cloud-rise-package['"]\);\s*/g, '');
const entry = "require('./activity-bundles');";
if (!boot.includes(entry)) {
if (!boot.includes('window.boot();')) throw Error('Missing WeChat boot insertion point');
boot = boot.replace('window.boot();', entry + '\nwindow.boot();');
}
fs.writeFileSync(bootFile, boot);
settings.subpackages = (settings.subpackages || []).filter(name => !bundles.includes(name) && name !== 'cloud_rise');
fs.writeFileSync(path.join(root, 'src/settings.js'), 'window._CCSettings=' + JSON.stringify(settings) + ';\n');
const gameFile = path.join(root, 'game.json');
if (fs.existsSync(gameFile)) {
const game = JSON.parse(fs.readFileSync(gameFile, 'utf8'));
game.subpackages = (game.subpackages || []).filter(item => !bundles.includes(item.name) && item.name !== 'cloud_rise');
fs.writeFileSync(gameFile, JSON.stringify(game, null, 2) + '\n');
}
if (fs.existsSync(path.join(root, 'memory-diagnostics.js'))) {
const game = fs.existsSync(gameFile) ? JSON.parse(fs.readFileSync(gameFile, 'utf8')) : {};
const build = { builtAt: new Date().toISOString(),
id: hash(Buffer.from(files(root, 'src/scripts').filter(name => /\.js$/.test(name))
.map(name => name + ':' + hash(fs.readFileSync(path.join(root, name)))).join('\n') + JSON.stringify(manifest))).slice(0, 20),
flags: { iOSHighPerformance: game.iOSHighPerformance === true, 'iOSHighPerformance+': game['iOSHighPerformance+'] === true } };
fs.writeFileSync(path.join(root, 'memory-diagnostics-build.js'), 'module.exports=' + JSON.stringify(build) + ';\n');
}
const projectFile = path.join(root, 'project.config.json');
if (fs.existsSync(projectFile)) {
const project = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
project.packOptions = project.packOptions || {}; const ignore = project.packOptions.ignore || [];
for (const value of ['remote', ...bundles.map(name => 'subpackages/' + name)])
if (!ignore.some(item => item.type === 'folder' && item.value === value)) ignore.push({ type: 'folder', value });
project.packOptions.ignore = ignore; fs.writeFileSync(projectFile, JSON.stringify(project, null, 2) + '\n');
}
return { publishDir, manifest };
}
module.exports = { pack, names };
if (require.main === module) console.log(JSON.stringify(pack(process.argv[2] || 'build/wechatgame'), null, 2));