Files
packager/scripts/prepare-runtime-sources.mjs
Lu Yifei a7c9e72b1c Merge origin/master and adapt specialized player integration
Integrate Packager 3.14.0, updated audio/render sources, preview isolation and escaping fixes. Adapt continuation temporaries and deserialized layer ordering, preserve frozen extension support, and align CI with master.
2026-11-08 14:28:15 +08:00

429 lines
26 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {createRequire} from 'node:module';
import {rollup} from 'rollup';
import commonjs from '@rollup/plugin-commonjs';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import json from '@rollup/plugin-json';
import {createHash} from 'node:crypto';
import {unbundleTWGL} from './unbundle-twgl.mjs';
const require = createRequire(import.meta.url);
const {parse} = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const vm = path.resolve(root, '../scratch-vm');
const sourceRoot = path.join(root, 'src');
const vmSource = path.join(vm, 'src');
const nativeDependencies = ['scratch-render', 'scratch-audio', '@turbowarp/scratch-storage', '@turbowarp/scratch-svg-renderer']
.map(name => path.join(root, 'node_modules', name, 'src') + path.sep);
const output = path.join(root, 'generated/runtime-sources.json');
const sources = {};
const provenance = {};
const ast = code => parse(code, {sourceType: 'unambiguous'});
const method = (name, body) => ast(`class A { ${name} ${body} }`).program.body[0].body.body[0];
const twglModules = unbundleTWGL(await fs.readFile(require.resolve('twgl.js'), 'utf8'));
const auditedWrappers = new Map();
for (const [file, hash] of [
['utils.js', 'c58704c6a6ef17f3e78a1ef4dd6e7aa20712fe673a929eb4c08fc86fcc9341cd'],
['base64.js', '5df6b73d9ed6c317c67835f3427d860c16fd93acf503eb471fd897167e747fdb']
]) {
const filename = path.join(root, 'node_modules/@turbowarp/jszip/lib', file);
if (createHash('sha256').update(await fs.readFile(filename)).digest('hex') !== hash) {
throw new Error(`JSZip ${file} changed; review circular export initialization before updating the source pack`);
}
auditedWrappers.set(filename, {sha256: hash,
reason: 'JSZip utils -> base64 -> utils captures partially initialized mutable CommonJS exports; preserve circular export identity and first-use ordering',
cycle: ['@turbowarp/jszip/lib/utils.js', '@turbowarp/jszip/lib/base64.js', '@turbowarp/jszip/lib/utils.js']});
}
// Production source preparation happens once, in Node, before publishing the build tool.
// The browser receives resolved ESM, so it needs neither require() nor filesystem plugins.
function nativeModules(code, id) {
const tree = ast(code);
const imports = new Map();
const getImport = name => {
if (!imports.has(name)) imports.set(name, t.identifier(`__dependency${imports.size}`));
return t.cloneNode(imports.get(name));
};
let exported = false;
let namedExports = false;
traverse(tree, {
ReferencedIdentifier(p) {
if (p.node.name === 'exports' && !p.scope.hasBinding('exports')) {
namedExports = true; p.replaceWith(t.identifier('__exports'));
}
},
CallExpression(p) {
if (t.isIdentifier(p.node.callee, {name: 'require'})) {
if (p.node.arguments.length !== 1 || !t.isStringLiteral(p.node.arguments[0])) {
throw new Error(`Non-static first-party require in ${id}`);
}
p.replaceWith(getImport(p.node.arguments[0].value));
}
},
ExpressionStatement(p) {
const expression = p.node.expression;
if (t.isAssignmentExpression(expression) && t.isMemberExpression(expression.left) &&
t.isIdentifier(expression.left.object, {name: 'module'}) &&
t.isIdentifier(expression.left.property, {name: 'exports'})) {
if (exported) throw new Error(`Multiple CommonJS exports in ${id}`);
exported = true;
p.replaceWith(t.exportDefaultDeclaration(expression.right));
}
}
});
if (namedExports) {
tree.program.body.unshift(...ast('const __exports = {};').program.body);
tree.program.body.push(...ast('export default __exports;').program.body);
}
tree.program.body.unshift(...[...imports].map(([name, local]) =>
t.importDeclaration([['@turbowarp/json', 'uuid', '@vernier/godirect', 'transformation-matrix'].includes(name) ?
t.importNamespaceSpecifier(local) : t.importDefaultSpecifier(local)], t.stringLiteral(name))));
return generate(tree, {comments: false}).code;
}
function prepareVM(code, id) {
if (/\/engine\/blocks-(execute|runtime)-cache.js$/.test(id)) code = code.replace("require('./blocks');", '');
const tree = ast(code);
if (id.endsWith('/virtual-machine.js')) {
// The playback facade and in-bundle addons are the only consumers of this class.
const roots = new Set(['constructor', 'start', 'greenFlag', 'stopAll', 'quit', 'clear',
'attachStorage', 'attachRenderer', 'attachAudioEngine', 'attachV2BitmapAdapter',
'setVideoProvider', 'setCloudProvider', 'postIOData', 'setTurboMode', 'setCompatibilityMode',
'setFramerate', 'setInterpolation', 'setRuntimeOptions', 'setCompilerOptions', 'setStageSize',
'setInEditor', 'convertToPackagedRuntime', 'loadProject', 'getTargetIdForDrawableId',
'startDrag', 'stopDrag', 'postSpriteInfo', 'setLocale', 'setTranslate', 'getLocale',
'setVariableValue', 'getVariableValue', 'setListValue', 'getListValue', 'setEditingTarget',
'scanForPeripheral', 'connectPeripheral', 'disconnectPeripheral', 'getPeripheralIsConnected',
'securityManager', 'runtime', 'renderer', 'editingTarget']);
const cls = tree.program.body.find(n => t.isClassDeclaration(n));
traverse(tree, {ExpressionStatement(p) {
const node = p.node.expression;
if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left) &&
t.isThisExpression(node.left.object) && ['exports', 'blockListener', 'flyoutBlockListener',
'monitorBlockListener', 'variableListener'].includes(node.left.property.name)) p.remove();
}});
const load = cls.body.body.find(n => n.key.name === 'loadProject');
load.async = true;
load.body = ast(`async function load(input) {
const project = typeof input === 'string' ? JSON.parse(input) :
input instanceof ArrayBuffer || ArrayBuffer.isView(input) ? JSON.parse(new TextDecoder().decode(input)) : input;
this.clear();
for (const id of project.extensions || []) await this.extensionManager.loadExtensionURL((project.extensionURLs || {})[id] || id);
const {targets, layerOrder} = await require('./serialization/sb3').deserialize(project, this.runtime);
const layerOrders = new Map(targets.map((target, index) => [target, layerOrder[index]]));
for (let index = 0; index < targets.length; index++) {
const target = targets[index];
target.sprite.__spTarget = index;
this.runtime.addTarget(target);
target.updateAllDrawableProperties();
}
this.runtime.executableTargets.sort((a,b) => layerOrders.get(a)-layerOrders.get(b));
this.editingTarget = targets[1] || targets[0];
this.runtime.setEditingTarget(this.editingTarget);
this.runtime.ioDevices.cloud.setStage(this.runtime.getTargetForStage());
this.runtime.handleProjectLoaded();
}`).program.body[0].body;
let changed = true;
while (changed) {
changed = false;
for (const member of cls.body.body) if (roots.has(member.key.name)) {
t.traverseFast(member, node => {
if (t.isMemberExpression(node) && t.isThisExpression(node.object) &&
!node.computed && !roots.has(node.property.name)) {
roots.add(node.property.name); changed = true;
}
});
}
}
cls.body.body = cls.body.body.filter(member => roots.has(member.key.name));
}
if (id.endsWith('/engine/sequencer.js')) {
tree.program.body.unshift(...ast("const SpecializedBlockUtility = require('../compiler/specialized-block-utility'); const Cast = require('../util/cast'); const log = require('../util/log');").program.body);
const cls = tree.program.body.find(n => t.isClassDeclaration(n));
cls.body.body = cls.body.body.filter(n => !['stepThread', 'stepToBranch', 'stepToProcedure'].includes(n.key.name));
cls.body.body.push(method('stepThread(thread)', `{ '__SPECIALIZED_STEP__'; }`));
const retire = cls.body.body.find(n => n.key.name === 'retireThread');
retire.body.body.unshift(...ast('thread.__spLifetime = null; thread.__spFrames = [];').program.body);
}
if (id.endsWith('/engine/runtime.js')) {
const cls = tree.program.body.find(n => t.isClassDeclaration(n));
cls.body.body = cls.body.body.filter(n => !['startHats', 'getOpcodeFunctionById'].includes(n.key.name));
cls.body.body.push(method('startHats(opcode, fields, target)', `{ '__SPECIALIZED_HATS__'; }`));
cls.body.body.push(method('__spPrimitive(opcode, args, utility)', `{ '__SPECIALIZED_PRIMITIVE__'; }`));
for (const name of ['_pushThread', '_restartThread']) {
const m = cls.body.body.find(n => n.key.name === name);
// All threads use the linked sequencer. A new Thread has a new lifetime identity.
t.traverseFast(m, node => {
if (t.isCallExpression(node) && t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.property, {name: 'tryCompile'})) {
node.callee.property.name = '__spInitialize';
}
});
if (name === '_restartThread') {
m.body.body.unshift(...ast('thread.__spLifetime = null;').program.body);
t.traverseFast(m, node => {
if (t.isReturnStatement(node) && t.isIdentifier(node.argument, {name: 'newThread'})) {
node.argument = t.sequenceExpression([
t.assignmentExpression('=', t.memberExpression(t.identifier('newThread'), t.identifier('executableHat')),
t.memberExpression(t.identifier('thread'), t.identifier('executableHat'))), node.argument]);
}
});
}
}
}
if (id.endsWith('/engine/thread.js')) {
const cls = tree.program.body.find(n => t.isClassDeclaration(n, {id: {name: 'Thread'}})) ||
tree.program.body.filter(n => t.isClassDeclaration(n)).at(-1);
cls.body.body = cls.body.body.filter(n => !['tryCompile', 'getCompiledProcedure', 'stopThisScript'].includes(n.key.name));
cls.body.body.push(method('__spInitialize()', `{
this.triedToCompile = true; this.isCompiled = true;
this.__spLifetime = {};
this.__spFrames = [{__spPC: Number(this.topBlock), __spSlots: [], __spArgs: [], __spReturn: -1}];
}`));
}
// Remove now-unused top-level require bindings before module resolution.
traverse(tree, {Program(p) {
p.scope.crawl();
for (const binding of Object.values(p.scope.bindings)) {
if (binding.referenced || ['SpecializedBlockUtility', 'Cast', 'log', 'execute'].includes(binding.identifier.name) || !binding.path.isVariableDeclarator()) continue;
if (binding.path.node.init && t.isCallExpression(binding.path.node.init) &&
t.isIdentifier(binding.path.node.init.callee, {name: 'require'})) binding.path.remove();
}
}});
return generate(tree, {comments: false}).code;
}
const stub = `const unavailable = () => { throw new Error('This player contains no dynamic compiler'); };
unavailable.saveGlobalState = () => {}; unavailable.restoreGlobalState = () => {};
export default unavailable;`;
const aliases = {
'scratch-vm': path.join(vmSource, 'index.js'),
'scratch-render': path.join(root, 'node_modules/scratch-render/src/index.js'),
'scratch-audio': path.join(root, 'node_modules/scratch-audio/src/index.js'),
'@turbowarp/scratch-storage': path.join(root, 'node_modules/@turbowarp/scratch-storage/src/index.js'),
'@turbowarp/jszip': path.join(root, 'node_modules/@turbowarp/jszip/lib/index.js'),
'@turbowarp/scratch-svg-renderer': path.join(root, 'node_modules/@turbowarp/scratch-svg-renderer/src/index.js'),
'transformation-matrix': path.join(root, 'node_modules/transformation-matrix/src/index.js'),
'@vernier/godirect': path.join(vm, 'node_modules/@vernier/godirect/src/godirect.js'),
'scratch-parser': path.join(sourceRoot, 'scaffolding/scratch-parser/index.js'),
'text-encoding': path.join(sourceRoot, 'scaffolding/text-encoding/index.js'),
'htmlparser2': path.join(sourceRoot, 'scaffolding/htmlparser2/index.js'),
'scratch-translate-extension-languages': path.join(sourceRoot, 'scaffolding/scratch-translate-extension-languages/languages.json'),
events: require.resolve('events/'),
buffer: require.resolve('buffer/'),
util: require.resolve('util/'),
stream: require.resolve('stream-browserify'),
crypto: require.resolve('crypto-browserify'),
assert: require.resolve('assert/'),
process: require.resolve('process/browser')
};
const bundle = await rollup({
input: ['\0specialized-entry', '\0specialized-extension-api'],
treeshake: false,
onwarn(warning) {
if (['CIRCULAR_DEPENDENCY', 'THIS_IS_UNDEFINED'].includes(warning.code)) return;
console.warn(warning.message);
},
plugins: [{
name: 'prepared-runtime',
resolveId(source, importer) {
if (source.startsWith('\0specialized')) return source;
if (source === 'twgl.js') return '\0specialized-twgl:9.js';
if (source === 'setimmediate') return '\0specialized-immediate';
if (aliases[source]) return aliases[source];
if (source.startsWith('!ify-loader!')) return require.resolve(source.slice('!ify-loader!'.length));
if (/^!?(worker-loader|raw-loader|arraybuffer-loader|base64-loader)[!?]/.test(source)) {
return `\0asset:${path.resolve(path.dirname(importer), source.slice(source.lastIndexOf('!') + 1))}:${source.includes('raw-loader') ? 'raw' : source.includes('arraybuffer-loader') ? 'bytes' : source.includes('base64-loader') ? 'base64' : 'worker'}`;
}
if (source === 'fs' || source === 'path' || source === 'http' || source === 'https') return '\0specialized-empty';
},
async load(id) {
if (id.startsWith('\0specialized-twgl:')) return nativeModules(twglModules[parseInt(id.split(':')[1], 10)], id);
// Production players run in a browser, including desktop WebViews. ZIP
// resources never expose Node streams and the supported engines have Promise.
if (id.endsWith('/@turbowarp/jszip/lib/external.js')) return 'module.exports = {Promise};';
if (id === '\0specialized-extension-api') return `
import Cast from ${JSON.stringify(path.join(vmSource, 'util/cast.js'))};
${['ArgumentType:argument-type', 'BlockType:block-type', 'BlockShape:tw-block-shape', 'TargetType:target-type'].map(item => {
const [name, file] = item.split(':');
return `import ${name} from ${JSON.stringify(path.join(vmSource, `extension-support/${file}.js`))};`;
}).join('\n')}
export default {Cast, ArgumentType, BlockType, BlockShape, TargetType};`;
if (id.endsWith('/scaffolding/scratch-libraries.js')) return `
export {default as VM} from 'scratch-vm';
export {default as Renderer} from 'scratch-render';
export {default as ScratchStorage} from '@turbowarp/scratch-storage';
export {default as AudioEngine} from 'scratch-audio';
export {default as BitmapAdapter} from ${JSON.stringify(path.join(root, 'node_modules/@turbowarp/scratch-svg-renderer/src/bitmap-adapter.js'))};`;
if (id === '\0specialized-empty') return 'export default {};';
if (id === '\0specialized-immediate') return `if (!globalThis.setImmediate) {
globalThis.setImmediate = (callback, ...args) => {
if (typeof callback !== 'function') throw new TypeError('setImmediate requires a function');
return setTimeout(callback, 0, ...args);
}; globalThis.clearImmediate = clearTimeout;
} export default {};`;
if (id.endsWith('/extension-support/tw-iframe-extension-worker.js')) return `export default class Unavailable { constructor() { throw new Error('Only worker extensions are supported'); } }`;
if (id.endsWith('/extension-support/tw-unsandboxed-extension-runner.js')) return `export default {load() { throw new Error('Unsandboxed extensions are not supported'); }};`;
if (id.endsWith('/addons/unsafe-cloud-behaviors.js')) return `export default function () { throw new Error('Dynamic cloud code is not supported'); }`;
if (id === path.join(vmSource, 'index.js')) return `export {default} from './virtual-machine.js';`;
if (id === '\0specialized-entry') return `import * as Scaffolding from ${JSON.stringify(path.join(sourceRoot, 'scaffolding/scaffolding.js'))};
import * as ScaffoldingAddons from ${JSON.stringify(path.join(sourceRoot, 'addons/index.js'))};
export {Scaffolding, ScaffoldingAddons};`;
if (/\/compiler\/(compile|jsexecute|specialized-project)(\.js)?$/.test(id)) return stub;
if (id.endsWith('/engine/execute.js')) return `export default function execute(sequencer, thread) {
if (!thread.updateMonitor) throw new Error('No interpreter in specialized player');
const runtime = sequencer.runtime;
const block = runtime.monitorBlocks.getBlock(thread.topBlock);
if (!block) { sequencer.retireThread(thread); return; }
const args = {};
for (const [key, field] of Object.entries(block.fields || {})) args[key] = key === 'VARIABLE' || key === 'LIST' ? field : field.value;
const utility = {target: thread.target, runtime, thread,
ioQuery: (device, operation, args = []) => runtime.ioDevices[device][operation](...args)};
const value = runtime.__spPrimitive(block.opcode, args, utility);
if (value !== undefined) runtime.requestUpdateMonitor({id: thread.topBlock, value,
spriteName: block.targetId ? thread.target.getName() : null});
sequencer.retireThread(thread);
}`;
if (id.startsWith('\0asset:')) {
const [, filename, kind] = id.split(':');
if (kind === 'worker') return `export default class WorkerUnavailable { constructor() { throw new Error('Extension worker source was not linked'); } }`;
const bytes = await fs.readFile(filename);
if (kind === 'raw') return `export default ${JSON.stringify(bytes.toString())};`;
if (kind === 'base64') return `export default ${JSON.stringify(bytes.toString('base64'))};`;
return `export default Uint8Array.from(atob(${JSON.stringify(bytes.toString('base64'))}), c => c.charCodeAt(0)).buffer;`;
}
if (/\.(svg|png|jpg|mp3|wav)$/.test(id)) {
const bytes = await fs.readFile(id);
if (id.includes('/scratch-storage/') || id.includes('/@turbowarp/scratch-storage/')) {
return `export default Uint8Array.from(atob(${JSON.stringify(bytes.toString('base64'))}), c => c.charCodeAt(0));`;
}
const ext = path.extname(id).slice(1);
const type = {svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', mp3: 'audio/mpeg', wav: 'audio/wav'}[ext];
return `export default ${JSON.stringify(`data:${type};base64,${bytes.toString('base64')}`)};`;
}
if (id.endsWith('.css')) {
const css = await fs.readFile(id, 'utf8');
const classes = {};
const transformed = css.replace(/\.([a-zA-Z_][\w-]*)/g, (_, name) => {
classes[name.replace(/-([a-z])/g, (_, char) => char.toUpperCase())] = `sc-${name}`;
return `.sc-${name}`;
});
return `const style = document.createElement('style'); style.textContent = ${JSON.stringify(transformed)};
document.head.appendChild(style); export default ${JSON.stringify(classes)};`;
}
},
async transform(code, id) {
if (id.endsWith('/@turbowarp/jszip/lib/support.js')) {
const tree = ast(code);
tree.program.body = tree.program.body.filter(node => !t.isTryStatement(node));
tree.program.body.push(...ast('exports.nodestream = false;').program.body);
code = generate(tree, {comments: false}).code;
}
if (id.endsWith('/@turbowarp/jszip/lib/stream/StreamHelper.js')) {
const tree = ast(code);
tree.program.body = tree.program.body.filter(node => !(t.isIfStatement(node) &&
t.isMemberExpression(node.test) && t.isIdentifier(node.test.object, {name: 'support'}) &&
t.isIdentifier(node.test.property, {name: 'nodestream'})));
code = generate(tree, {comments: false}).code;
}
if (/\.[cm]?js$/.test(id)) {
const globals = new Set();
const tree = ast(code);
traverse(tree, {ReferencedIdentifier(p) {
if (['global', 'process', 'Buffer'].includes(p.node.name) && !p.scope.hasBinding(p.node.name)) globals.add(p.node.name);
}});
code = [...globals].map(name => name === 'global' ? 'const global = globalThis;' :
name === 'Buffer' ? "const Buffer = require('buffer').Buffer;" : "const process = require('process');").join('\n') + '\n' + code;
}
if (/node_modules\/js-md5\/src\/md5.js$/.test(id)) {
code = code.replace(/var NODE_JS = [^;]+;/, 'var NODE_JS = false;');
return {code, map: null};
}
const textUtility = /node_modules\/(grapheme-breaker|linebreak)\/(?!node_modules\/).*\.js$/.test(id);
if (textUtility) {
const matches = [...code.matchAll(/fs\.readFileSync\(__dirname \+ '([^']+)'(?:, '([^']+)')?\)/g)];
for (const match of matches) {
const bytes = await fs.readFile(path.join(path.dirname(id), match[1]));
code = code.replace(match[0], match[2] === 'base64' ? JSON.stringify(bytes.toString('base64')) :
`require('buffer').Buffer.from(${JSON.stringify(bytes.toString('base64'))}, 'base64')`);
}
// CoffeeScript's outer one-shot closure is only a module scope. Unwrap
// it before converting its exports; nested implementation closures stay.
const tree = ast(code);
const statement = tree.program.body.at(-1);
const call = statement?.expression;
if (t.isCallExpression(call) && t.isMemberExpression(call.callee) &&
t.isFunctionExpression(call.callee.object) && t.isIdentifier(call.callee.property, {name: 'call'}) &&
call.arguments.length === 1 && t.isThisExpression(call.arguments[0])) {
tree.program.body.splice(-1, 1, ...call.callee.object.body.body);
}
code = generate(tree, {comments: false}).code;
}
const acyclicUtility = textUtility || /\/node_modules\/(pako|tiny-inflate|base64-js|unicode-trie)\//.test(id);
if (!id.startsWith(vmSource) && !id.startsWith(sourceRoot) && !acyclicUtility && !nativeDependencies.some(prefix => id.startsWith(prefix))) return {code, map: null};
if (!/\.[cm]?js$/.test(id)) return;
if (id.endsWith('/addons/index.js')) code = code.replace('window.ScaffoldingAddons = {\n run\n};', 'export {run};');
if (id.startsWith(vmSource)) code = prepareVM(code, id);
code = code.replace(/process\.env\.NODE_ENV/g, '"production"');
const result = nativeModules(code, id);
return {code: result, map: null};
}
}, nodeResolve({browser: true, preferBuiltins: false, mainFields: ['browser', 'module', 'main']}),
json(), commonjs({strictRequires: [...auditedWrappers.keys()], transformMixedEsModules: true, requireReturnsDefault: 'auto'})]
});
const generated = await bundle.generate({format: 'es', preserveModules: true, entryFileNames: '[name].js',
chunkFileNames: '[name]-[hash].js', generatedCode: 'es2015'});
let entry, extensionApi;
const wrapperAudit = {};
for (const item of generated.output) {
if (item.type !== 'chunk') throw new Error(`Unexpected source-pack asset ${item.fileName}`);
sources[item.fileName] = item.code;
provenance[item.fileName] = Object.keys(item.modules).map(id => id.startsWith('\0specialized-twgl:') ?
`tw-packager/node_modules/twgl.js/dist/4.x/twgl-full.js#module-${parseInt(id.split(':')[1], 10)}` :
id.replace(vm, 'scratch-vm').replace(root, 'tw-packager'));
if (item.exports.includes('__require')) {
const audits = Object.keys(item.modules).map(id => auditedWrappers.get(id)).filter(Boolean);
if (audits.length !== 1) throw new Error(`Unaudited CommonJS wrapper: ${item.fileName}`);
wrapperAudit[item.fileName] = audits[0];
}
if (item.facadeModuleId === '\0specialized-entry') entry = item.fileName;
if (item.facadeModuleId === '\0specialized-extension-api') extensionApi = item.fileName;
}
// Share the existing scalar semantics, while excluding generator/execution helpers.
const helperTree = ast(await fs.readFile(path.join(vmSource, 'compiler/jsexecute.js'), 'utf8'));
const helperParts = [];
const excluded = new Set(['startHats', 'waitThreads', 'executeInCompatibilityLayer', 'retire',
'timer', 'distance', 'yieldThenCall', 'yieldThenCallGenerator', 'createBranchInfo']);
traverse(helperTree, {AssignmentExpression(p) {
const {left, right} = p.node;
if (!t.isTemplateLiteral(right) || right.expressions.length) return;
if (t.isIdentifier(left, {name: 'baseRuntime'}) ||
(t.isMemberExpression(left) && t.isIdentifier(left.object, {name: 'runtimeFunctions'}) && !excluded.has(left.property.name))) {
helperParts.push(right.quasis[0].value.cooked);
}
}});
const helperAST = ast(helperParts.join('\n'));
helperAST.program.body = helperAST.program.body.filter(n => !t.isVariableDeclaration(n) ||
!n.declarations.some(d => ['stuckCounter', 'isStuck', 'colorToList'].includes(d.id.name)));
helperAST.program.body = helperAST.program.body.map(n => t.isVariableDeclaration(n) ? t.exportNamedDeclaration(n) : n);
const helpers = generate(helperAST, {comments: false}).code;
// Use the pinned minifier's platform property inventory in both Node and the
// browser linker. Native browser/JS properties must never be global candidates.
const platformTree = ast(await fs.readFile(path.join(path.dirname(require.resolve('terser/package.json')), 'tools/domprops.js'), 'utf8'));
const platformArray = platformTree.program.body[0]?.declaration?.declarations[0]?.init;
if (!t.isArrayExpression(platformArray) || !platformArray.elements.every(t.isStringLiteral)) {
throw new Error('Terser platform property inventory changed; review aggressive renaming');
}
const platformProperties = platformArray.elements.map(node => node.value);
const fingerprint = createHash('sha256').update(JSON.stringify({sources, helpers, platformProperties})).digest('hex');
await fs.mkdir(path.dirname(output), {recursive: true});
await fs.writeFile(output, JSON.stringify({version: 1, fingerprint, entry, extensionApi, sources, provenance, helpers, wrapperAudit, platformProperties}));
await bundle.close();
console.log(`Prepared ${Object.keys(sources).length} runtime modules (${fingerprint.slice(0, 12)})`);