'use strict'; const fs = require('fs'), path = require('path'), vm = require('vm'); const names = ['matching', 'stage1', 'stage2', 'stage3', 'art']; function files(folder) { if (!fs.existsSync(folder)) return []; return fs.readdirSync(folder).sort().reduce((out, name) => { const file = path.join(folder, name), stat = fs.lstatSync(file); if (stat.isSymbolicLink()) throw Error('Unexpected symlink in build output: ' + file); return out.concat(stat.isDirectory() ? files(file) : [file]); }, []); } function pack(destination, versionFile = path.join(__dirname, '../../assets/cloud_rise/version.json')) { const release = JSON.parse(fs.readFileSync(versionFile, 'utf8')); if (typeof release.version !== 'string' || release.version.length > 32 || !/^\d+\.\d+\.\d+$/.test(release.version)) { throw Error('Cloud Rise version must have three numeric parts, e.g. 1.0.0'); } const root = path.resolve(destination), remote = path.join(root, 'remote'); const gameFile = path.join(root, 'game.json'), settingsFile = path.join(root, 'src/settings.js'); const game = JSON.parse(fs.readFileSync(gameFile, 'utf8')); const context = { window: {} }; vm.runInNewContext(fs.readFileSync(settingsFile, 'utf8'), context); const settings = context.window._CCSettings; if (!settings || !settings.bundleVers) throw Error('Missing Creator bundle settings'); const server = String(settings.server || '').replace(/\/+$/, ''); if (!/^https:\/\/[^\s?#]+$/.test(server)) throw Error('Configure the WeChat HTTPS resource server before building Cloud Rise'); const entries = []; for (const name of names) { const folder = path.join(remote, name), version = settings.bundleVers[name]; if (!version || !fs.existsSync(path.join(folder, 'config.' + version + '.json'))) { throw Error('Missing remote Cloud Rise bundle (enable MD5 Cache): ' + name); } if (!(settings.remoteBundles || []).includes(name)) throw Error('Cloud Rise must be a remote bundle: ' + name); const config = JSON.parse(fs.readFileSync(path.join(folder, 'config.' + version + '.json'), 'utf8')); if (config.name !== name || config.isZip) throw Error('Use Merge All JSON for independent remote bundles: ' + name); if (!fs.existsSync(path.join(root, 'src/scripts', name, 'index.js'))) { throw Error('Missing locally shipped Creator bundle script: ' + name); } for (const file of files(folder)) { if (path.dirname(file) === folder && /^config\..*\.json$/.test(path.basename(file)) && path.basename(file) !== 'config.' + version + '.json') continue; if (/\.js$/i.test(file)) throw Error('Executable scripts must remain in the client package: ' + file); const relative = path.relative(folder, file).replace(/\\/g, '/'); const isConfig = relative === 'config.' + version + '.json'; // A fixed entry point avoids a separate manifest of Creator's config hashes. const target = isConfig ? 'config.json' : relative; const data = isConfig ? Buffer.from(JSON.stringify(Object.assign({}, config, { cloudRiseVersion: release.version }))) : fs.readFileSync(file); entries.push({ name: name + '/' + target, data }); } } const config = { baseUrl: server + '/remote/cloud_rise/' }; const bootFile = path.join(root, 'game.js'); let boot = fs.readFileSync(bootFile, 'utf8'); const entry = "require('./cloud-rise-package');"; if (!boot.includes(entry)) { if (!boot.includes('window.boot();')) throw Error('Missing WeChat boot insertion point'); boot = boot.replace('window.boot();', entry + '\nwindow.boot();'); } // Publish loose remote bundles outside the WeChat code upload directory. const publishDir = path.join(path.dirname(root), path.basename(root) + '-cloud-rise', 'remote', 'cloud_rise'); fs.mkdirSync(publishDir, { recursive: true }); const oldZip = path.join(publishDir, 'cloud_rise.zip'); if (fs.existsSync(oldZip)) fs.unlinkSync(oldZip); for (const name of names) { const target = path.resolve(publishDir, name); if (path.dirname(target) !== path.resolve(publishDir) || !names.includes(path.basename(target))) throw Error('Unsafe bundle output'); if (fs.existsSync(target)) fs.rmdirSync(target, { recursive: true }); } for (const file of entries) { const target = path.join(publishDir, file.name); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, file.data); } fs.writeFileSync(path.join(publishDir, 'version.json'), JSON.stringify({ version: release.version }, null, 2) + '\n'); fs.writeFileSync(path.join(root, 'cloud-rise-package-config.js'), 'module.exports=' + JSON.stringify(config) + ';\n'); fs.copyFileSync(path.join(__dirname, 'runtime.js'), path.join(root, 'cloud-rise-package.js')); game.subpackages = (game.subpackages || []).filter(item => !names.includes(item.name) && item.name !== 'cloud_rise'); settings.subpackages = (settings.subpackages || []).filter(name => !names.includes(name) && name !== 'cloud_rise'); fs.writeFileSync(gameFile, JSON.stringify(game, null, 2) + '\n'); fs.writeFileSync(settingsFile, 'window._CCSettings=' + JSON.stringify(settings) + ';\n'); fs.writeFileSync(bootFile, boot); // Exclude remote output and stale subpackages from the WeChat code upload. 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', 'subpackages/cloud_rise', ...names.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, version: release.version, size: entries.reduce((sum, file) => sum + file.data.length, 0) }; } module.exports = { pack, names };