From 9de8fda8c07665a6ec7745c11664d5beaf2e7b65 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 23 Feb 2026 13:40:04 +0000 Subject: [PATCH 1/4] feat: migrated build to esbuild --- build.mjs | 194 ++++++-- package-lock.json | 467 +++++++++++++++++++ package.json | 2 +- rollup.config.mjs | 83 +--- utils/esbuild-build-target.mjs | 304 ++++++++++++ utils/plugins/esbuild-dynamic.mjs | 65 +++ utils/plugins/esbuild-import-validation.mjs | 49 ++ utils/plugins/esbuild-jscc.mjs | 90 ++++ utils/plugins/esbuild-shader-chunks.mjs | 42 ++ utils/plugins/esbuild-strip.mjs | 159 +++++++ utils/plugins/esbuild-transform-pipeline.mjs | 60 +++ 11 files changed, 1411 insertions(+), 104 deletions(-) create mode 100644 utils/esbuild-build-target.mjs create mode 100644 utils/plugins/esbuild-dynamic.mjs create mode 100644 utils/plugins/esbuild-import-validation.mjs create mode 100644 utils/plugins/esbuild-jscc.mjs create mode 100644 utils/plugins/esbuild-shader-chunks.mjs create mode 100644 utils/plugins/esbuild-strip.mjs create mode 100644 utils/plugins/esbuild-transform-pipeline.mjs diff --git a/build.mjs b/build.mjs index e7e88ad56d6..7e76e821c56 100644 --- a/build.mjs +++ b/build.mjs @@ -1,6 +1,6 @@ /** - * Build helper scripts - * Usage: node build.mjs [options] -- [rollup options] + * Build helper script using esbuild for JS targets and Rollup for types. + * Usage: node build.mjs [options] * * Options: * target[:][:][:] - Specify the target @@ -9,37 +9,177 @@ * - bundleState (unbundled, bundled) * Example: target:esm:release:bundled * - * treemap - Enable treemap build visualization (release only). - * treenet - Enable treenet build visualization (release only). - * treesun - Enable treesun build visualization (release only). - * treeflame - Enable treeflame build visualization (release only). + * -w / --watch - Enable watch mode (rebuilds on file changes). */ -import { execSync } from 'child_process'; +import fs from 'fs'; +import { buildTarget, OUT_PREFIX } from './utils/esbuild-build-target.mjs'; +import { version, revision } from './utils/rollup-version-revision.mjs'; +import { buildTypesOption } from './utils/rollup-build-target.mjs'; + +const CYAN_OUT = '\x1b[36m'; +const BLUE_OUT = '\x1b[34m'; +const GREEN_OUT = '\x1b[32m'; +const RED_OUT = '\x1b[31m'; +const BOLD_OUT = '\x1b[1m'; +const REGULAR_OUT = '\x1b[22m'; +const RESET_OUT = '\x1b[0m'; + +const BUILD_TYPES = ['release', 'debug', 'profiler', 'min']; +const MODULE_FORMAT = ['umd', 'esm']; +const BUNDLE_STATES = ['unbundled', 'bundled']; const args = process.argv.slice(2); -const ENV_START_MATCHES = [ - 'target', - 'treemap', - 'treenet', - 'treesun', - 'treeflame' -]; - -const env = []; -for (let i = 0; i < args.length; i++) { - if (ENV_START_MATCHES.some(match => args[i].startsWith(match)) && args[i - 1] !== '--environment') { - env.push(`--environment ${args[i]}`); - args.splice(i, 1); - i--; - continue; +// Extract target and flags +let envTarget = null; +let watchMode = false; +for (const arg of args) { + if (arg.startsWith('target')) { + const parts = arg.split(':'); + envTarget = parts.slice(1).join(':').toLowerCase() || null; + } + if (arg === '-w' || arg === '--watch') { + watchMode = true; + } +} + +const title = [ + 'Building PlayCanvas Engine', + `version ${BOLD_OUT}v${version}${REGULAR_OUT}`, + `revision ${BOLD_OUT}${revision}${REGULAR_OUT}`, + `target ${BOLD_OUT}${envTarget ?? 'all'}${REGULAR_OUT}` +].join('\n'); +console.log(`${BLUE_OUT}${title}${RESET_OUT}`); + +if (envTarget === null && fs.existsSync('build')) { + fs.rmSync('build', { recursive: true }); +} + +function includeBuild(buildType, moduleFormat, bundleState) { + return envTarget === null || + envTarget === buildType || + envTarget === moduleFormat || + envTarget === bundleState || + envTarget === `${moduleFormat}:${buildType}` || + envTarget === `${moduleFormat}:${bundleState}` || + envTarget === `${buildType}:${bundleState}` || + envTarget === `${moduleFormat}:${buildType}:${bundleState}`; +} + +// Collect JS build targets +const jsTargets = []; +BUILD_TYPES.forEach((buildType) => { + MODULE_FORMAT.forEach((moduleFormat) => { + BUNDLE_STATES.forEach((bundleState) => { + if (bundleState === 'unbundled' && moduleFormat === 'umd') return; + if (bundleState === 'unbundled' && buildType === 'min') return; + if (!includeBuild(buildType, moduleFormat, bundleState)) return; + + jsTargets.push({ moduleFormat, buildType, bundleState }); + }); + }); +}); + +const buildTypes = envTarget === null || envTarget === 'types'; + +if (!jsTargets.length && !buildTypes) { + console.error(`${RED_OUT}${BOLD_OUT}No targets found${RESET_OUT}`); + process.exit(1); +} + +/** + * Get the output path description for a build target (matches Rollup's display). + * + * @param {object} target - The build target. + * @returns {string} The output path. + */ +function getOutputPath(target) { + const prefix = OUT_PREFIX[target.buildType]; + const isUMD = target.moduleFormat === 'umd'; + const bundled = isUMD || target.buildType === 'min' || target.bundleState === 'bundled'; + if (bundled) { + return `build/${prefix}${isUMD ? '.js' : '.mjs'}`; } + return `build/${prefix}/`; +} + +/** + * Build all JS targets using esbuild. + */ +async function buildAllJS() { + await Promise.all(jsTargets.map(async (target) => { + const output = getOutputPath(target); + console.log(`${CYAN_OUT}${BOLD_OUT}src/index.js${REGULAR_OUT} \u2192 ${BOLD_OUT}${output}${REGULAR_OUT}...${RESET_OUT}`); + const buildStart = performance.now(); + try { + await buildTarget(target); + const elapsed = ((performance.now() - buildStart) / 1000).toFixed(1); + console.log(`${GREEN_OUT}created ${BOLD_OUT}${output}${REGULAR_OUT} in ${BOLD_OUT}${elapsed}s${REGULAR_OUT}${RESET_OUT}`); + } catch (err) { + console.error(`${RED_OUT}${BOLD_OUT}error building ${output}${REGULAR_OUT}: ${err.message}${RESET_OUT}`); + throw err; + } + })); +} + +/** + * Build TypeScript definitions using Rollup + rollup-plugin-dts. + */ +async function buildAllTypes() { + const typesOutput = 'build/playcanvas.d.ts'; + console.log(`${CYAN_OUT}${BOLD_OUT}src/index.js${REGULAR_OUT} \u2192 ${BOLD_OUT}${typesOutput}${REGULAR_OUT}...${RESET_OUT}`); + const startTime = performance.now(); + + const { rollup } = await import('rollup'); + const typesConfig = buildTypesOption(); + + const bundle = await rollup({ + input: typesConfig.input, + plugins: typesConfig.plugins + }); + + const outputOptions = Array.isArray(typesConfig.output) ? typesConfig.output : [typesConfig.output]; + await Promise.all(outputOptions.map(output => bundle.write(output))); + await bundle.close(); + + const elapsed = ((performance.now() - startTime) / 1000).toFixed(1); + console.log(`${GREEN_OUT}created ${BOLD_OUT}${typesOutput}${REGULAR_OUT} in ${BOLD_OUT}${elapsed}s${REGULAR_OUT}${RESET_OUT}`); } -const cmd = `rollup -c ${args.join(' ')} ${env.join(' ')}`; -try { - execSync(cmd, { stdio: 'inherit' }); -} catch (e) { - console.error(e.message); +// Main execution +async function main() { + try { + if (jsTargets.length) { + await buildAllJS(); + } + + if (buildTypes) { + await buildAllTypes(); + } + + if (watchMode) { + console.log(`${BLUE_OUT}Watching for changes...${RESET_OUT}`); + let debounceTimer = null; + + fs.watch('src', { recursive: true }, (eventType, filename) => { + if (!filename?.endsWith('.js')) return; + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(async () => { + console.log(`\n${BLUE_OUT}Change detected: ${filename}${RESET_OUT}`); + try { + if (jsTargets.length) await buildAllJS(); + if (buildTypes) await buildAllTypes(); + } catch (e) { + console.error(e.message); + } + }, 100); + }); + } + } catch (e) { + console.error(e.message); + if (!watchMode) process.exit(1); + } } + +main(); diff --git a/package-lock.json b/package-lock.json index 7a22188b4a2..981a06d1b92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/node": "24.10.13", "c8": "10.1.3", "chai": "6.2.2", + "esbuild": "^0.25.2", "eslint": "9.39.3", "fflate": "0.8.2", "globals": "17.3.0", @@ -289,6 +290,431 @@ "node": ">=18" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -2962,6 +3388,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/package.json b/package.json index 20e69631c2f..b75f0dcb392 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@types/node": "24.10.13", "c8": "10.1.3", "chai": "6.2.2", + "esbuild": "^0.25.2", "eslint": "9.39.3", "fflate": "0.8.2", "globals": "17.3.0", @@ -127,7 +128,6 @@ "build:treenet": "npm run build target:release treenet", "build:treesun": "npm run build target:release treesun", "build:treeflame": "npm run build target:release treeflame", - "build:sourcemaps": "npm run build -- -m", "watch": "npm run build -- -w", "watch:release": "npm run build target:release -- -w", "watch:debug": "npm run build target:debug -- -w", diff --git a/rollup.config.mjs b/rollup.config.mjs index 98aa339171e..c48807b9db6 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,79 +1,10 @@ -import fs from 'fs'; -import { version, revision } from './utils/rollup-version-revision.mjs'; -import { buildJSOptions, buildTypesOption } from './utils/rollup-build-target.mjs'; - -/** @import { RollupOptions } from 'rollup' */ - -const BLUE_OUT = '\x1b[34m'; -const RED_OUT = '\x1b[31m'; -const BOLD_OUT = '\x1b[1m'; -const REGULAR_OUT = '\x1b[22m'; -const RESET_OUT = '\x1b[0m'; - -const BUILD_TYPES = /** @type {const} */ (['release', 'debug', 'profiler', 'min']); -const MODULE_FORMAT = /** @type {const} */ (['umd', 'esm']); -const BUNDLE_STATES = /** @type {const} */ (['unbundled', 'bundled']); - -const envTarget = process.env.target ? process.env.target.toLowerCase() : null; - -const title = [ - 'Building PlayCanvas Engine', - `version ${BOLD_OUT}v${version}${REGULAR_OUT}`, - `revision ${BOLD_OUT}${revision}${REGULAR_OUT}`, - `target ${BOLD_OUT}${envTarget ?? 'all'}${REGULAR_OUT}` -].join('\n'); -console.log(`${BLUE_OUT}${title}${RESET_OUT}`); - -if (envTarget === null && fs.existsSync('build')) { - // no targets specified, clean build directory - fs.rmSync('build', { recursive: true }); -} - -function includeBuild(buildType, moduleFormat, bundleState) { - return envTarget === null || - envTarget === buildType || - envTarget === moduleFormat || - envTarget === bundleState || - envTarget === `${moduleFormat}:${buildType}` || - envTarget === `${moduleFormat}:${bundleState}` || - envTarget === `${buildType}:${bundleState}` || - envTarget === `${moduleFormat}:${buildType}:${bundleState}`; -} - /** - * @type {RollupOptions[]} + * Rollup configuration — retained for types build only. + * JS builds are now handled by esbuild via build.mjs. + * + * This config is used by the examples build which imports buildJSOptions/buildTypesOption + * from utils/rollup-build-target.mjs. */ -const targets = []; -BUILD_TYPES.forEach((buildType) => { - MODULE_FORMAT.forEach((moduleFormat) => { - BUNDLE_STATES.forEach((bundleState) => { - if (bundleState === 'unbundled' && moduleFormat === 'umd') { - return; - } - if (bundleState === 'unbundled' && buildType === 'min') { - return; - } - - if (!includeBuild(buildType, moduleFormat, bundleState)) { - return; - } - - targets.push(...buildJSOptions({ - moduleFormat, - buildType, - bundleState - })); - }); - }); -}); - -if (envTarget === null || envTarget === 'types') { - targets.push(buildTypesOption()); -} - -if (!targets.length) { - console.error(`${RED_OUT}${BOLD_OUT}No targets found${RESET_OUT}`); - process.exit(1); -} +import { buildTypesOption } from './utils/rollup-build-target.mjs'; -export default targets; +export default [buildTypesOption()]; diff --git a/utils/esbuild-build-target.mjs b/utils/esbuild-build-target.mjs new file mode 100644 index 00000000000..52e3c9d1857 --- /dev/null +++ b/utils/esbuild-build-target.mjs @@ -0,0 +1,304 @@ +import esbuild from 'esbuild'; +import fs from 'fs'; +import { dirname, resolve as pathResolve, join as pathJoin, relative as pathRelative, posix, sep } from 'path'; +import { fileURLToPath } from 'url'; + +import { processJSCC } from './plugins/esbuild-jscc.mjs'; +import { buildStripPattern, applyStrip } from './plugins/esbuild-strip.mjs'; +import { processShaderChunks } from './plugins/esbuild-shader-chunks.mjs'; +import { applyDynamicImportSuppress } from './plugins/esbuild-dynamic.mjs'; +import { importValidationPlugin } from './plugins/esbuild-import-validation.mjs'; +import { transformPipelinePlugin } from './plugins/esbuild-transform-pipeline.mjs'; +import { version, revision } from './rollup-version-revision.mjs'; +import { getBanner } from './rollup-get-banner.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = pathResolve(__dirname, '..'); + +const STRIP_FUNCTIONS = [ + 'Debug.assert', + 'Debug.assertDeprecated', + 'Debug.assertDestroyed', + 'Debug.call', + 'Debug.deprecated', + 'Debug.warn', + 'Debug.warnOnce', + 'Debug.error', + 'Debug.errorOnce', + 'Debug.log', + 'Debug.logOnce', + 'Debug.removed', + 'Debug.trace', + 'DebugHelper.setName', + 'DebugHelper.setLabel', + 'DebugHelper.setDestroyed', + 'DebugGraphics.toString', + 'DebugGraphics.clearGpuMarkers', + 'DebugGraphics.pushGpuMarker', + 'DebugGraphics.popGpuMarker', + 'WebgpuDebug.validate', + 'WebgpuDebug.memory', + 'WebgpuDebug.internal', + 'WebgpuDebug.end', + 'WebgpuDebug.endShader', + 'WorldClustersDebug.render' +]; + +const BANNER = { + debug: ' (DEBUG)', + release: ' (RELEASE)', + profiler: ' (PROFILE)', + min: ' (RELEASE)' +}; + +const OUT_PREFIX = { + debug: 'playcanvas.dbg', + release: 'playcanvas', + profiler: 'playcanvas.prf', + min: 'playcanvas.min' +}; + +/** + * Get JSCC values for a given build type. + * + * @param {'debug'|'release'|'profiler'} buildType - The build type. + * @returns {{ values: Object, keepLines: boolean }} JSCC config. + */ +function getJSCCConfig(buildType) { + const base = { + _CURRENT_SDK_VERSION: version, + _CURRENT_SDK_REVISION: revision + }; + + switch (buildType) { + case 'debug': + return { values: { ...base, _DEBUG: 1, _PROFILER: 1 }, keepLines: true }; + case 'profiler': + return { values: { ...base, _PROFILER: 1 }, keepLines: false }; + case 'release': + default: + return { values: base, keepLines: false }; + } +} + +/** + * Build esbuild plugins list for a given build configuration. + * Uses a single combined pipeline plugin for all source transforms (since esbuild + * only runs the first matching onLoad handler per file). + * + * @param {object} opts - Build options. + * @param {'debug'|'release'|'profiler'|'min'} opts.buildType - The build type. + * @param {boolean} opts.isUMD - Whether this is a UMD (IIFE) build. + * @param {string} opts.input - Entry point path. + * @returns {import('esbuild').Plugin[]} Plugins array. + */ +function getPlugins({ buildType, isUMD, input }) { + const isDebug = buildType === 'debug'; + const effectiveBuildType = buildType === 'min' ? 'release' : buildType; + const jsccConfig = getJSCCConfig(effectiveBuildType); + + const plugins = [ + transformPipelinePlugin({ + jsccValues: jsccConfig.values, + jsccKeepLines: jsccConfig.keepLines, + stripFunctions: !isDebug ? STRIP_FUNCTIONS : null, + processShaders: !isDebug, + dynamicImportLegacy: isUMD, + dynamicImportSuppress: !isUMD + }) + ]; + + if (isDebug) { + plugins.push(importValidationPlugin(input)); + } + + return plugins; +} + +/** + * Build a bundled JS target (single output file). + * + * @param {object} options - Build options. + * @param {'umd'|'esm'} options.moduleFormat - The module format. + * @param {'debug'|'release'|'profiler'|'min'} options.buildType - The build type. + * @param {string} [options.input] - Entry point (default: 'src/index.js'). + * @param {string} [options.dir] - Output directory (default: 'build'). + * @returns {Promise} + */ +async function buildBundled({ + moduleFormat, + buildType, + input = 'src/index.js', + dir = 'build' +}) { + const isUMD = moduleFormat === 'umd'; + const isDebug = buildType === 'debug'; + const isMin = buildType === 'min'; + const prefix = OUT_PREFIX[buildType]; + const outfile = `${dir}/${prefix}${isUMD ? '.js' : '.mjs'}`; + + const banner = getBanner(BANNER[buildType]); + + /** @type {import('esbuild').BuildOptions} */ + const opts = { + entryPoints: [input], + bundle: true, + outfile, + format: isUMD ? 'iife' : 'esm', + globalName: isUMD ? 'pc' : undefined, + target: 'es2020', + sourcemap: isDebug ? 'inline' : false, + minify: isMin, + legalComments: 'none', + banner: { js: banner }, + plugins: getPlugins({ buildType, isUMD, input }), + external: ['node:worker_threads'], + logLevel: 'warning' + }; + + if (isMin) { + opts.drop = ['console']; + } + + if (isUMD) { + // Wrap IIFE output with a UMD detection header + opts.banner = { + js: `${banner}\n(function (root, factory) {\n\tif (typeof module !== 'undefined' && module.exports) {\n\t\tmodule.exports = factory();\n\t} else {\n\t\troot.pc = factory();\n\t}\n}(typeof self !== 'undefined' ? self : this, function () {` + }; + opts.footer = { + js: 'return pc;\n}));' + }; + // Use 'esm' internally so esbuild doesn't double-wrap, we handle the UMD wrapper ourselves + opts.format = 'esm'; + } + + await esbuild.build(opts); +} + +/** + * Collect all .js files under a directory recursively. + * + * @param {string} dirPath - Directory to scan. + * @returns {string[]} List of file paths. + */ +function collectJSFiles(dirPath) { + const files = []; + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const full = pathJoin(dirPath, entry.name); + if (entry.isDirectory()) { + files.push(...collectJSFiles(full)); + } else if (entry.isFile() && entry.name.endsWith('.js')) { + files.push(full); + } + } + return files; +} + +/** + * Resolve and copy the fflate module into the unbundled output directory, + * replicating Rollup's node_modules → modules renaming. + * + * @param {string} outDir - Output directory (e.g. 'build/playcanvas'). + */ +function copyFflateModule(outDir) { + const fflateEntry = pathJoin(rootDir, 'node_modules', 'fflate', 'esm', 'browser.js'); + const destDir = pathJoin(outDir, 'modules', 'fflate', 'esm'); + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(fflateEntry, pathJoin(destDir, 'browser.js')); +} + +/** + * Build an unbundled JS target (per-file transform, preserving module structure). + * + * @param {object} options - Build options. + * @param {'debug'|'release'|'profiler'} options.buildType - The build type. + * @param {string} [options.input] - Entry point directory root (default: 'src'). + * @param {string} [options.dir] - Output base directory (default: 'build'). + * @returns {Promise} + */ +async function buildUnbundled({ + buildType, + input = 'src', + dir = 'build' +}) { + const isDebug = buildType === 'debug'; + const prefix = OUT_PREFIX[buildType]; + const outDir = `${dir}/${prefix}`; + const effectiveBuildType = buildType === 'min' ? 'release' : buildType; + const jsccConfig = getJSCCConfig(effectiveBuildType); + const stripPattern = !isDebug ? buildStripPattern(STRIP_FUNCTIONS) : null; + + const srcFiles = collectJSFiles(input); + + const transformPromises = srcFiles.map(async (srcFile) => { + let source = await fs.promises.readFile(srcFile, 'utf8'); + + // Apply JSCC + source = processJSCC(source, jsccConfig.values, jsccConfig.keepLines); + + // Apply shader chunks (non-debug) + if (!isDebug) { + source = processShaderChunks(source); + } + + // Apply strip (non-debug) + if (stripPattern) { + source = applyStrip(source, stripPattern); + } + + // Apply dynamic import suppress (always for ESM unbundled) + source = applyDynamicImportSuppress(source); + + // Rewrite bare 'fflate' import to relative modules path + if (source.includes('from \'fflate\'') || source.includes('from "fflate"')) { + const relFromSrc = pathRelative(dirname(srcFile), input); + const modulePath = posix.join( + relFromSrc.split(sep).join('/'), + '..', 'modules', 'fflate', 'esm', 'browser.js' + ); + source = source.replace(/from ['"]fflate['"]/g, `from '${modulePath}'`); + } + + const destFile = pathJoin(outDir, srcFile); + await fs.promises.mkdir(dirname(destFile), { recursive: true }); + await fs.promises.writeFile(destFile, source); + }); + + await Promise.all(transformPromises); + + // Copy fflate module + copyFflateModule(outDir); +} + +/** + * Build a single JS target (bundled or unbundled). + * + * @param {object} options - Build options. + * @param {'umd'|'esm'} options.moduleFormat - The module format. + * @param {'debug'|'release'|'profiler'|'min'} options.buildType - The build type. + * @param {'unbundled'|'bundled'} [options.bundleState] - The bundle state. + * @param {string} [options.input] - Entry point. + * @param {string} [options.dir] - Output directory. + * @returns {Promise} + */ +async function buildTarget({ + moduleFormat, + buildType, + bundleState, + input = 'src/index.js', + dir = 'build' +}) { + const isUMD = moduleFormat === 'umd'; + const isMin = buildType === 'min'; + const bundled = isUMD || isMin || bundleState === 'bundled'; + + if (bundled) { + await buildBundled({ moduleFormat, buildType, input, dir }); + } else { + await buildUnbundled({ buildType, input: dirname(input), dir }); + } +} + +export { buildTarget, buildBundled, buildUnbundled, OUT_PREFIX, BANNER }; diff --git a/utils/plugins/esbuild-dynamic.mjs b/utils/plugins/esbuild-dynamic.mjs new file mode 100644 index 00000000000..29ff15c97f3 --- /dev/null +++ b/utils/plugins/esbuild-dynamic.mjs @@ -0,0 +1,65 @@ +import fs from 'fs'; + +/** + * esbuild plugin that wraps dynamic `import()` calls in `new Function(...)` for + * legacy browser support (UMD builds). + * + * Port of dynamicImportLegacyBrowserSupport from rollup-dynamic.mjs. + * + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +export function dynamicImportLegacyPlugin() { + return { + name: 'dynamic-import-legacy', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + const source = await fs.promises.readFile(args.path, 'utf8'); + const transformed = applyDynamicImportLegacy(source); + if (transformed === source) return undefined; + return { contents: transformed, loader: 'js' }; + }); + } + }; +} + +/** + * Apply legacy dynamic import transform — callable without esbuild. + * + * @param {string} source - Source code. + * @returns {string} Transformed source. + */ +export function applyDynamicImportLegacy(source) { + return source.replace(/(\W)import\(/g, '$1new Function("modulePath", "return import(modulePath)")('); +} + +/** + * esbuild plugin that adds bundler-suppress comments before dynamic `import()` calls + * to quiet Vite/webpack warnings (ESM builds). + * + * Port of dynamicImportBundlerSuppress from rollup-dynamic.mjs. + * + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +export function dynamicImportSuppressPlugin() { + return { + name: 'dynamic-import-suppress', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + const source = await fs.promises.readFile(args.path, 'utf8'); + const transformed = applyDynamicImportSuppress(source); + if (transformed === source) return undefined; + return { contents: transformed, loader: 'js' }; + }); + } + }; +} + +/** + * Apply bundler suppress comments — callable without esbuild. + * + * @param {string} source - Source code. + * @returns {string} Transformed source. + */ +export function applyDynamicImportSuppress(source) { + return source.replace(/import\(([^'])/g, 'import(/* @vite-ignore */ /* webpackIgnore: true */ $1'); +} diff --git a/utils/plugins/esbuild-import-validation.mjs b/utils/plugins/esbuild-import-validation.mjs new file mode 100644 index 00000000000..2023ba6a4c7 --- /dev/null +++ b/utils/plugins/esbuild-import-validation.mjs @@ -0,0 +1,49 @@ +import path from 'node:path'; + +/** + * esbuild plugin that validates engine layer imports. + * Warns when a lower-level module imports from a higher-level module. + * + * Hierarchy: core (0) → platform (1) → scene (2) → framework (3) → extras (4) + * + * Port of engineLayerImportValidation from rollup-import-validation.mjs. + * + * @param {string} rootFile - The root file, typically `src/index.js`. + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +export function importValidationPlugin(rootFile) { + const folderLevels = { + 'core': 0, + 'platform': 1, + 'scene': 2, + 'framework': 3, + 'extras': 4 + }; + + const rootPath = path.parse(path.resolve(rootFile)).dir; + + return { + name: 'import-validation', + setup(build) { + build.onResolve({ filter: /^\./ }, (args) => { + if (!args.importer) return undefined; + + const importerDir = path.parse(args.importer).dir; + const relImporter = path.dirname(path.relative(rootPath, args.importer)); + const folderImporter = relImporter.split(path.sep)[0]; + const levelImporter = folderLevels[folderImporter]; + + const absImported = path.resolve(path.join(importerDir, args.path)); + const relImported = path.dirname(path.relative(rootPath, absImported)); + const folderImported = relImported.split(path.sep)[0]; + const levelImported = folderLevels[folderImported]; + + if (levelImporter !== undefined && levelImported !== undefined && levelImporter < levelImported) { + console.log(`(!) Incorrect import: [${path.relative(rootPath, args.importer)}] -> [${args.path}]`); + } + + return undefined; + }); + } + }; +} diff --git a/utils/plugins/esbuild-jscc.mjs b/utils/plugins/esbuild-jscc.mjs new file mode 100644 index 00000000000..8b2e6e83309 --- /dev/null +++ b/utils/plugins/esbuild-jscc.mjs @@ -0,0 +1,90 @@ +import fs from 'fs'; + +/** + * esbuild plugin that implements JSCC-style conditional compilation. + * + * Processes `// #if _VAR` / `// #else` / `// #endif` comment-based directives + * and replaces `$_VAR` value tokens in source code. + * + * @param {Object} options - Plugin options. + * @param {Object} options.values - Map of variable names to values. + * @param {boolean} [options.keepLines] - If true, replace stripped lines with blanks + * to preserve line numbers (useful for debug sourcemaps). + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +export function jsccPlugin({ values = {}, keepLines = false } = {}) { + return { + name: 'jscc', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + let source = await fs.promises.readFile(args.path, 'utf8'); + source = processJSCC(source, values, keepLines); + return { contents: source, loader: 'js' }; + }); + } + }; +} + +/** + * Apply JSCC processing to source text — callable without esbuild for unbundled transforms. + * + * @param {string} source - Source code. + * @param {Object} values - Map of variable names to values. + * @param {boolean} keepLines - Preserve line count by replacing removed lines with blanks. + * @returns {string} Processed source. + */ +export function processJSCC(source, values, keepLines) { + // Replace value tokens: $_VAR_NAME → value + for (const [key, val] of Object.entries(values)) { + const token = `$${key}`; + if (source.includes(token)) { + source = source.replaceAll(token, String(val)); + } + } + + // Process conditional directives: // #if, // #else, // #endif + const lines = source.split('\n'); + const output = []; + const stack = []; // stack of { active, parentActive, elseSeen } + + for (const line of lines) { + const trimmed = line.trim(); + + const ifMatch = trimmed.match(/^\/\/\s*#if\s+(\w+)/); + if (ifMatch) { + const varName = ifMatch[1]; + const parentActive = stack.length === 0 || stack[stack.length - 1].active; + const active = parentActive && !!values[varName]; + stack.push({ active, parentActive, elseSeen: false }); + if (keepLines) output.push(''); + else output.push(''); + continue; + } + + if (trimmed.match(/^\/\/\s*#else\s*$/)) { + if (stack.length > 0) { + const top = stack[stack.length - 1]; + top.elseSeen = true; + top.active = top.parentActive && !top.active; + } + if (keepLines) output.push(''); + else output.push(''); + continue; + } + + if (trimmed.match(/^\/\/\s*#endif\s*$/)) { + stack.pop(); + if (keepLines) output.push(''); + else output.push(''); + continue; + } + + if (stack.length === 0 || stack[stack.length - 1].active) { + output.push(line); + } else if (keepLines) { + output.push(''); + } + } + + return output.join('\n'); +} diff --git a/utils/plugins/esbuild-shader-chunks.mjs b/utils/plugins/esbuild-shader-chunks.mjs new file mode 100644 index 00000000000..c0ee3c80ba7 --- /dev/null +++ b/utils/plugins/esbuild-shader-chunks.mjs @@ -0,0 +1,42 @@ +import fs from 'fs'; + +/** + * esbuild plugin that minifies shader code inside template literals marked with + * `/* glsl *\/` or `/* wgsl *\/` comments. + * + * Port of utils/plugins/rollup-shader-chunks.mjs. + * + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +export function shaderChunksPlugin() { + return { + name: 'shader-chunks', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + const raw = await fs.promises.readFile(args.path, 'utf8'); + const transformed = processShaderChunks(raw); + if (transformed === raw) return undefined; // no change, skip + return { contents: transformed, loader: 'js' }; + }); + } + }; +} + +/** + * Process shader chunks in source text — callable without esbuild for unbundled transforms. + * + * @param {string} source - Source code. + * @returns {string} Processed source. + */ +export function processShaderChunks(source) { + return source.replace(/\/\* *(glsl|wgsl) *\*\/\s*(`.*?`)/gs, (match, type, code) => { + return code + .trim() + .replace(/\r/g, '') + .replace(/ {4}/g, '\t') + .replace(/[ \t]*\/\/.*/g, '') + .replace(/[ \t]*\/\*[\s\S]*?\*\//g, '') + .concat('\n') + .replace(/\n{2,}/g, '\n'); + }); +} diff --git a/utils/plugins/esbuild-strip.mjs b/utils/plugins/esbuild-strip.mjs new file mode 100644 index 00000000000..2a91c089ccd --- /dev/null +++ b/utils/plugins/esbuild-strip.mjs @@ -0,0 +1,159 @@ +import fs from 'fs'; + +/** + * esbuild plugin that strips specified function calls from source code. + * Equivalent to @rollup/plugin-strip. + * + * Removes entire expression statements that are calls to the listed functions, + * e.g. `Debug.assert(condition, msg);` is removed entirely. + * + * @param {Object} options - Plugin options. + * @param {string[]} options.functions - List of function names to strip (e.g. 'Debug.assert'). + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +export function stripPlugin({ functions = [] } = {}) { + const pattern = buildStripPattern(functions); + + return { + name: 'strip', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + let source = await fs.promises.readFile(args.path, 'utf8'); + source = applyStrip(source, pattern); + return { contents: source, loader: 'js' }; + }); + } + }; +} + +/** + * Build a regex that matches the start of a strip-target call. + * Matches at line start with optional whitespace and optional label prefix (e.g. `default:`). + * The actual parenthesis matching is done procedurally to avoid catastrophic backtracking. + * + * @param {string[]} functions - Function names to strip. + * @returns {RegExp} Pattern that matches `FuncName(` in strippable positions. + */ +export function buildStripPattern(functions) { + const escaped = functions.map(f => f.replace(/\./g, '\\.')).join('|'); + // Match at line start: optional whitespace, optional label (e.g. `default:`), then function call + return new RegExp(`^([ \\t]*(?:\\w+:[ \\t]*)?)(${escaped})\\(`, 'gm'); +} + +/** + * Apply strip processing to source text — callable without esbuild for unbundled transforms. + * Uses parenthesis counting to find the end of the call, avoiding regex backtracking. + * + * @param {string} source - Source code. + * @param {RegExp} pattern - Compiled strip pattern from buildStripPattern. + * @returns {string} Processed source. + */ +export function applyStrip(source, pattern) { + pattern.lastIndex = 0; + + const removals = []; // [startIndex, endIndex] pairs to remove + let match; + + while ((match = pattern.exec(source)) !== null) { + const linePrefix = match[1]; // leading whitespace + optional label + const funcNameStart = match.index + linePrefix.length; + const parenStart = match.index + match[0].length - 1; + let depth = 1; + let i = parenStart + 1; + let inString = false; + let stringChar = ''; + let inTemplate = false; + let templateDepth = 0; + + while (i < source.length && depth > 0) { + const ch = source[i]; + + // Skip single-line comments + if (ch === '/' && i + 1 < source.length && source[i + 1] === '/') { + while (i < source.length && source[i] !== '\n') i++; + continue; + } + + // Skip multi-line comments + if (ch === '/' && i + 1 < source.length && source[i + 1] === '*') { + i += 2; + while (i < source.length - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++; + i += 2; + continue; + } + + if (inString) { + if (ch === stringChar && source[i - 1] !== '\\') { + inString = false; + } + i++; + continue; + } + + if (inTemplate) { + if (ch === '`' && source[i - 1] !== '\\') { + inTemplate = false; + } else if (ch === '$' && i + 1 < source.length && source[i + 1] === '{') { + templateDepth++; + i += 2; + continue; + } else if (ch === '}' && templateDepth > 0) { + templateDepth--; + } + i++; + continue; + } + + if (ch === '\'' || ch === '"') { + inString = true; + stringChar = ch; + } else if (ch === '`') { + inTemplate = true; + templateDepth = 0; + } else if (ch === '(') { + depth++; + } else if (ch === ')') { + depth--; + } + + i++; + } + + if (depth === 0) { + // i is now right after the closing paren + let end = i; + // Skip optional semicolon and trailing whitespace/newline + while (end < source.length && (source[end] === ' ' || source[end] === '\t')) end++; + if (end < source.length && source[end] === ';') end++; + while (end < source.length && (source[end] === ' ' || source[end] === '\t')) end++; + if (end < source.length && source[end] === '\n') end++; + else if (end < source.length && source[end] === '\r') { + end++; + if (end < source.length && source[end] === '\n') end++; + } + + // The match starts at the beginning of the line (anchored with ^). + // If the prefix is only whitespace, remove the entire line. + // If the prefix includes a label (e.g. "default: "), remove only the call. + const start = match.index; + if (/^[ \t]*$/.test(linePrefix)) { + removals.push([start, end]); + } else { + removals.push([funcNameStart, end]); + } + } + } + + if (removals.length === 0) return source; + + // Build result by skipping removed ranges, handling overlapping/nested ranges + let result = ''; + let pos = 0; + for (const [start, end] of removals) { + if (start < pos) continue; // skip ranges nested inside an already-removed range + result += source.slice(pos, start); + pos = end; + } + result += source.slice(pos); + return result; +} diff --git a/utils/plugins/esbuild-transform-pipeline.mjs b/utils/plugins/esbuild-transform-pipeline.mjs new file mode 100644 index 00000000000..ca31e744396 --- /dev/null +++ b/utils/plugins/esbuild-transform-pipeline.mjs @@ -0,0 +1,60 @@ +import fs from 'fs'; +import { processJSCC } from './esbuild-jscc.mjs'; +import { buildStripPattern, applyStrip } from './esbuild-strip.mjs'; +import { processShaderChunks } from './esbuild-shader-chunks.mjs'; +import { applyDynamicImportLegacy, applyDynamicImportSuppress } from './esbuild-dynamic.mjs'; + +/** + * Combined esbuild plugin that applies all source transforms in a single onLoad handler. + * + * esbuild only runs the FIRST matching onLoad handler per file, so all transforms must + * be in one plugin to chain correctly. + * + * @param {Object} options - Pipeline options. + * @param {Object} options.jsccValues - JSCC variable values. + * @param {boolean} [options.jsccKeepLines] - Preserve line count for JSCC. + * @param {string[]} [options.stripFunctions] - Functions to strip (null = don't strip). + * @param {boolean} [options.processShaders] - Whether to minify shader chunks. + * @param {boolean} [options.dynamicImportLegacy] - Wrap imports for legacy browsers. + * @param {boolean} [options.dynamicImportSuppress] - Add bundler-suppress comments. + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +export function transformPipelinePlugin({ + jsccValues = {}, + jsccKeepLines = false, + stripFunctions = null, + processShaders = false, + dynamicImportLegacy = false, + dynamicImportSuppress = false +} = {}) { + const stripPattern = stripFunctions ? buildStripPattern(stripFunctions) : null; + + return { + name: 'transform-pipeline', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + let source = await fs.promises.readFile(args.path, 'utf8'); + + source = processJSCC(source, jsccValues, jsccKeepLines); + + if (processShaders) { + source = processShaderChunks(source); + } + + if (stripPattern) { + source = applyStrip(source, stripPattern); + } + + if (dynamicImportLegacy) { + source = applyDynamicImportLegacy(source); + } + + if (dynamicImportSuppress) { + source = applyDynamicImportSuppress(source); + } + + return { contents: source, loader: 'js' }; + }); + } + }; +} From 1854ebeec2df83c88874205002570d3807560e10 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 23 Feb 2026 14:33:20 +0000 Subject: [PATCH 2/4] feat: add jscc support and clean up esbuild plugins --- package-lock.json | 1 + package.json | 1 + utils/plugins/esbuild-dynamic.mjs | 49 +-------------- utils/plugins/esbuild-jscc.mjs | 81 ++----------------------- utils/plugins/esbuild-shader-chunks.mjs | 25 +------- utils/plugins/esbuild-strip.mjs | 31 +--------- 6 files changed, 10 insertions(+), 178 deletions(-) diff --git a/package-lock.json b/package-lock.json index 981a06d1b92..68e7d234756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "eslint": "9.39.3", "fflate": "0.8.2", "globals": "17.3.0", + "jscc": "^1.1.1", "jsdom": "28.1.0", "mocha": "11.7.5", "nise": "6.1.1", diff --git a/package.json b/package.json index b75f0dcb392..3ba393c8697 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "eslint": "9.39.3", "fflate": "0.8.2", "globals": "17.3.0", + "jscc": "^1.1.1", "jsdom": "28.1.0", "mocha": "11.7.5", "nise": "6.1.1", diff --git a/utils/plugins/esbuild-dynamic.mjs b/utils/plugins/esbuild-dynamic.mjs index 29ff15c97f3..bd40ab0be36 100644 --- a/utils/plugins/esbuild-dynamic.mjs +++ b/utils/plugins/esbuild-dynamic.mjs @@ -1,29 +1,5 @@ -import fs from 'fs'; - -/** - * esbuild plugin that wraps dynamic `import()` calls in `new Function(...)` for - * legacy browser support (UMD builds). - * - * Port of dynamicImportLegacyBrowserSupport from rollup-dynamic.mjs. - * - * @returns {import('esbuild').Plugin} The esbuild plugin. - */ -export function dynamicImportLegacyPlugin() { - return { - name: 'dynamic-import-legacy', - setup(build) { - build.onLoad({ filter: /\.js$/ }, async (args) => { - const source = await fs.promises.readFile(args.path, 'utf8'); - const transformed = applyDynamicImportLegacy(source); - if (transformed === source) return undefined; - return { contents: transformed, loader: 'js' }; - }); - } - }; -} - /** - * Apply legacy dynamic import transform — callable without esbuild. + * Wrap dynamic `import()` calls in `new Function(...)` for legacy browser support (UMD builds). * * @param {string} source - Source code. * @returns {string} Transformed source. @@ -33,30 +9,9 @@ export function applyDynamicImportLegacy(source) { } /** - * esbuild plugin that adds bundler-suppress comments before dynamic `import()` calls + * Add bundler-suppress comments before dynamic `import()` calls * to quiet Vite/webpack warnings (ESM builds). * - * Port of dynamicImportBundlerSuppress from rollup-dynamic.mjs. - * - * @returns {import('esbuild').Plugin} The esbuild plugin. - */ -export function dynamicImportSuppressPlugin() { - return { - name: 'dynamic-import-suppress', - setup(build) { - build.onLoad({ filter: /\.js$/ }, async (args) => { - const source = await fs.promises.readFile(args.path, 'utf8'); - const transformed = applyDynamicImportSuppress(source); - if (transformed === source) return undefined; - return { contents: transformed, loader: 'js' }; - }); - } - }; -} - -/** - * Apply bundler suppress comments — callable without esbuild. - * * @param {string} source - Source code. * @returns {string} Transformed source. */ diff --git a/utils/plugins/esbuild-jscc.mjs b/utils/plugins/esbuild-jscc.mjs index 8b2e6e83309..e1d3407008a 100644 --- a/utils/plugins/esbuild-jscc.mjs +++ b/utils/plugins/esbuild-jscc.mjs @@ -1,90 +1,17 @@ -import fs from 'fs'; +import jscc from 'jscc'; /** - * esbuild plugin that implements JSCC-style conditional compilation. + * Apply JSCC processing to source text. * * Processes `// #if _VAR` / `// #else` / `// #endif` comment-based directives * and replaces `$_VAR` value tokens in source code. * - * @param {Object} options - Plugin options. - * @param {Object} options.values - Map of variable names to values. - * @param {boolean} [options.keepLines] - If true, replace stripped lines with blanks - * to preserve line numbers (useful for debug sourcemaps). - * @returns {import('esbuild').Plugin} The esbuild plugin. - */ -export function jsccPlugin({ values = {}, keepLines = false } = {}) { - return { - name: 'jscc', - setup(build) { - build.onLoad({ filter: /\.js$/ }, async (args) => { - let source = await fs.promises.readFile(args.path, 'utf8'); - source = processJSCC(source, values, keepLines); - return { contents: source, loader: 'js' }; - }); - } - }; -} - -/** - * Apply JSCC processing to source text — callable without esbuild for unbundled transforms. - * * @param {string} source - Source code. * @param {Object} values - Map of variable names to values. * @param {boolean} keepLines - Preserve line count by replacing removed lines with blanks. * @returns {string} Processed source. */ export function processJSCC(source, values, keepLines) { - // Replace value tokens: $_VAR_NAME → value - for (const [key, val] of Object.entries(values)) { - const token = `$${key}`; - if (source.includes(token)) { - source = source.replaceAll(token, String(val)); - } - } - - // Process conditional directives: // #if, // #else, // #endif - const lines = source.split('\n'); - const output = []; - const stack = []; // stack of { active, parentActive, elseSeen } - - for (const line of lines) { - const trimmed = line.trim(); - - const ifMatch = trimmed.match(/^\/\/\s*#if\s+(\w+)/); - if (ifMatch) { - const varName = ifMatch[1]; - const parentActive = stack.length === 0 || stack[stack.length - 1].active; - const active = parentActive && !!values[varName]; - stack.push({ active, parentActive, elseSeen: false }); - if (keepLines) output.push(''); - else output.push(''); - continue; - } - - if (trimmed.match(/^\/\/\s*#else\s*$/)) { - if (stack.length > 0) { - const top = stack[stack.length - 1]; - top.elseSeen = true; - top.active = top.parentActive && !top.active; - } - if (keepLines) output.push(''); - else output.push(''); - continue; - } - - if (trimmed.match(/^\/\/\s*#endif\s*$/)) { - stack.pop(); - if (keepLines) output.push(''); - else output.push(''); - continue; - } - - if (stack.length === 0 || stack[stack.length - 1].active) { - output.push(line); - } else if (keepLines) { - output.push(''); - } - } - - return output.join('\n'); + const result = jscc(source, null, { values, keepLines, sourceMap: false }); + return result.code; } diff --git a/utils/plugins/esbuild-shader-chunks.mjs b/utils/plugins/esbuild-shader-chunks.mjs index c0ee3c80ba7..cda46ddd5a3 100644 --- a/utils/plugins/esbuild-shader-chunks.mjs +++ b/utils/plugins/esbuild-shader-chunks.mjs @@ -1,30 +1,7 @@ -import fs from 'fs'; - /** - * esbuild plugin that minifies shader code inside template literals marked with + * Minify shader code inside template literals marked with * `/* glsl *\/` or `/* wgsl *\/` comments. * - * Port of utils/plugins/rollup-shader-chunks.mjs. - * - * @returns {import('esbuild').Plugin} The esbuild plugin. - */ -export function shaderChunksPlugin() { - return { - name: 'shader-chunks', - setup(build) { - build.onLoad({ filter: /\.js$/ }, async (args) => { - const raw = await fs.promises.readFile(args.path, 'utf8'); - const transformed = processShaderChunks(raw); - if (transformed === raw) return undefined; // no change, skip - return { contents: transformed, loader: 'js' }; - }); - } - }; -} - -/** - * Process shader chunks in source text — callable without esbuild for unbundled transforms. - * * @param {string} source - Source code. * @returns {string} Processed source. */ diff --git a/utils/plugins/esbuild-strip.mjs b/utils/plugins/esbuild-strip.mjs index 2a91c089ccd..b15ea167368 100644 --- a/utils/plugins/esbuild-strip.mjs +++ b/utils/plugins/esbuild-strip.mjs @@ -1,31 +1,3 @@ -import fs from 'fs'; - -/** - * esbuild plugin that strips specified function calls from source code. - * Equivalent to @rollup/plugin-strip. - * - * Removes entire expression statements that are calls to the listed functions, - * e.g. `Debug.assert(condition, msg);` is removed entirely. - * - * @param {Object} options - Plugin options. - * @param {string[]} options.functions - List of function names to strip (e.g. 'Debug.assert'). - * @returns {import('esbuild').Plugin} The esbuild plugin. - */ -export function stripPlugin({ functions = [] } = {}) { - const pattern = buildStripPattern(functions); - - return { - name: 'strip', - setup(build) { - build.onLoad({ filter: /\.js$/ }, async (args) => { - let source = await fs.promises.readFile(args.path, 'utf8'); - source = applyStrip(source, pattern); - return { contents: source, loader: 'js' }; - }); - } - }; -} - /** * Build a regex that matches the start of a strip-target call. * Matches at line start with optional whitespace and optional label prefix (e.g. `default:`). @@ -36,12 +8,11 @@ export function stripPlugin({ functions = [] } = {}) { */ export function buildStripPattern(functions) { const escaped = functions.map(f => f.replace(/\./g, '\\.')).join('|'); - // Match at line start: optional whitespace, optional label (e.g. `default:`), then function call return new RegExp(`^([ \\t]*(?:\\w+:[ \\t]*)?)(${escaped})\\(`, 'gm'); } /** - * Apply strip processing to source text — callable without esbuild for unbundled transforms. + * Strip specified function calls from source text. * Uses parenthesis counting to find the end of the call, avoiding regex backtracking. * * @param {string} source - Source code. From 723e1ca4f3733c59f26f6868b529ffd7267b52aa Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 23 Feb 2026 15:01:23 +0000 Subject: [PATCH 3/4] feat: removed unused rollup plugins and updated examples build --- examples/rollup.config.mjs | 91 ++- package-lock.json | 624 ------------------- package.json | 6 - utils/esbuild-build-target.mjs | 179 +++--- utils/plugins/esbuild-transform-pipeline.mjs | 69 +- utils/plugins/rollup-dynamic.mjs | 39 -- utils/plugins/rollup-import-validation.mjs | 52 -- utils/plugins/rollup-shader-chunks.mjs | 49 -- utils/plugins/rollup-spaces-to-tabs.mjs | 34 - utils/rollup-build-target.mjs | 272 +------- utils/rollup-swc-options.mjs | 33 - 11 files changed, 172 insertions(+), 1276 deletions(-) delete mode 100644 utils/plugins/rollup-dynamic.mjs delete mode 100644 utils/plugins/rollup-import-validation.mjs delete mode 100644 utils/plugins/rollup-shader-chunks.mjs delete mode 100644 utils/plugins/rollup-spaces-to-tabs.mjs delete mode 100644 utils/rollup-swc-options.mjs diff --git a/examples/rollup.config.mjs b/examples/rollup.config.mjs index 5c961b0324a..d06d51c36ba 100644 --- a/examples/rollup.config.mjs +++ b/examples/rollup.config.mjs @@ -7,7 +7,8 @@ import replace from '@rollup/plugin-replace'; import terser from '@rollup/plugin-terser'; import { exampleMetaData } from './cache/metadata.mjs'; -import { buildJSOptions, buildTypesOption } from '../utils/rollup-build-target.mjs'; +import { buildTarget } from '../utils/esbuild-build-target.mjs'; +import { buildTypesOption } from '../utils/rollup-build-target.mjs'; import { version } from '../utils/rollup-version-revision.mjs'; import { buildHtml } from './utils/plugins/rollup-build-html.mjs'; import { buildShare } from './utils/plugins/rollup-build-share.mjs'; @@ -182,18 +183,41 @@ const EXAMPLE_TARGETS = exampleMetaData.flatMap(({ categoryKebab, exampleNameKeb return options; }); -const ENGINE_TARGETS = (() => { +/** + * Build engine JS targets with esbuild before Rollup runs. + * Replaces the previous Rollup-based buildJSOptions calls. + */ +async function buildEngineJS() { + const builds = []; + const opts = { + moduleFormat: /** @type {'esm'} */ ('esm'), + bundleState: /** @type {'bundled'} */ ('bundled'), + input: '../src/index.js', + dir: 'dist/iframe' + }; + + if (NODE_ENV === 'production') { + builds.push(buildTarget({ ...opts, buildType: 'release' })); + } + if (NODE_ENV === 'production' || NODE_ENV === 'development') { + builds.push(buildTarget({ ...opts, buildType: 'debug' })); + } + if (NODE_ENV === 'production' || NODE_ENV === 'profiler') { + builds.push(buildTarget({ ...opts, buildType: 'profiler' })); + } + + await Promise.all(builds); +} + +const ENGINE_TYPES_TARGETS = (() => { /** @type {RollupOptions[]} */ const options = []; - // Types - // Outputs: dist/iframe/playcanvas.d.ts options.push(buildTypesOption({ root: '..', dir: 'dist/iframe' })); - // Sources if (ENGINE_PATH) { const src = path.resolve(ENGINE_PATH); const content = fs.readFileSync(src, 'utf8'); @@ -210,49 +234,12 @@ const ENGINE_TARGETS = (() => { dest: 'dist/iframe/ENGINE_PATH/index.js' })); } - return options; } - // Builds - if (NODE_ENV === 'production') { - // Outputs: dist/iframe/playcanvas.mjs - options.push( - ...buildJSOptions({ - moduleFormat: 'esm', - buildType: 'release', - bundleState: 'bundled', - input: '../src/index.js', - dir: 'dist/iframe' - }) - ); - } - if (NODE_ENV === 'production' || NODE_ENV === 'development') { - // Outputs: dist/iframe/playcanvas.dbg.mjs - options.push( - ...buildJSOptions({ - moduleFormat: 'esm', - buildType: 'debug', - bundleState: 'bundled', - input: '../src/index.js', - dir: 'dist/iframe' - }) - ); - } - if (NODE_ENV === 'production' || NODE_ENV === 'profiler') { - // Outputs: dist/iframe/playcanvas.prf.mjs - options.push( - ...buildJSOptions({ - moduleFormat: 'esm', - buildType: 'profiler', - bundleState: 'bundled', - input: '../src/index.js', - dir: 'dist/iframe' - }) - ); - } return options; })(); +/** @type {RollupOptions[]} */ const APP_TARGETS = [{ // A debug build is ~2.3MB and a release build ~0.6MB input: 'src/app/index.mjs', @@ -288,9 +275,15 @@ const APP_TARGETS = [{ ] }]; -export default [ - ...STATIC_TARGETS, - ...EXAMPLE_TARGETS, - ...ENGINE_TARGETS, - ...APP_TARGETS -]; +export default async () => { + if (!ENGINE_PATH) { + await buildEngineJS(); + } + + return [ + ...STATIC_TARGETS, + ...EXAMPLE_TARGETS, + ...ENGINE_TYPES_TARGETS, + ...APP_TARGETS + ]; +}; diff --git a/package-lock.json b/package-lock.json index 68e7d234756..b2f31ee6821 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,7 @@ "devDependencies": { "@playcanvas/eslint-config": "2.1.0", "@rollup/plugin-node-resolve": "16.0.3", - "@rollup/plugin-strip": "3.0.4", - "@rollup/plugin-swc": "0.4.0", "@rollup/plugin-terser": "0.4.4", - "@rollup/pluginutils": "5.3.0", - "@swc/core": "1.15.11", "@types/node": "24.10.13", "c8": "10.1.3", "chai": "6.2.2", @@ -34,8 +30,6 @@ "publint": "0.3.17", "rollup": "4.58.0", "rollup-plugin-dts": "6.3.0", - "rollup-plugin-jscc": "2.0.0", - "rollup-plugin-visualizer": "6.0.8", "serve": "14.2.5", "sinon": "21.0.1", "typedoc": "0.28.17", @@ -1119,52 +1113,6 @@ } } }, - "node_modules/@rollup/plugin-strip": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-strip/-/plugin-strip-3.0.4.tgz", - "integrity": "sha512-LDRV49ZaavxUo2YoKKMQjCxzCxugu1rCPQa0lDYBOWLj6vtzBMr8DcoJjsmg+s450RbKbe3qI9ZLaSO+O1oNbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-swc": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-swc/-/plugin-swc-0.4.0.tgz", - "integrity": "sha512-oAtqXa8rOl7BOK1Rz3rRxI+LIL53S9SqO2KSq2UUUzWgOgXg6492Jh5mL2mv/f9cpit8zFWdwILuVeozZ0C8mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "smob": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@swc/core": "^1.3.0", - "rollup": "^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", @@ -1665,232 +1613,6 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/@swc/core": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.11.tgz", - "integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.11", - "@swc/core-darwin-x64": "1.15.11", - "@swc/core-linux-arm-gnueabihf": "1.15.11", - "@swc/core-linux-arm64-gnu": "1.15.11", - "@swc/core-linux-arm64-musl": "1.15.11", - "@swc/core-linux-x64-gnu": "1.15.11", - "@swc/core-linux-x64-musl": "1.15.11", - "@swc/core-win32-arm64-msvc": "1.15.11", - "@swc/core-win32-ia32-msvc": "1.15.11", - "@swc/core-win32-x64-msvc": "1.15.11" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz", - "integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.11.tgz", - "integrity": "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz", - "integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz", - "integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz", - "integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz", - "integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz", - "integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz", - "integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz", - "integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz", - "integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", - "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2449,22 +2171,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3076,36 +2782,6 @@ "node": ">=0.10.0" } }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3124,19 +2800,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -4014,19 +3677,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4676,41 +4326,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -5795,25 +5410,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6427,184 +6023,6 @@ "typescript": "^4.5 || ^5.0" } }, - "node_modules/rollup-plugin-jscc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-jscc/-/rollup-plugin-jscc-2.0.0.tgz", - "integrity": "sha512-5jG9q79K2u5uRBTKA+GA4gqt1zA7qHQRpcabZMoVs913gr75s428O7K3r58n2vADDzwIhiOKMo7rCMhOyks6dw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsbits/get-package-version": "^1.0.3", - "jscc": "^1.1.1", - "rollup-pluginutils": "^2.8.2" - }, - "engines": { - "node": ">=10.12.0" - }, - "peerDependencies": { - "rollup": ">=2" - } - }, - "node_modules/rollup-plugin-visualizer": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.8.tgz", - "integrity": "sha512-MmLbgYWDiDu8XKoePA1GtmRejl+4GWJTx156zjvycoxCbOq0PkNNwbepyB5tHCfDyRc8PKDLh2f/GLVGKNeV7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "open": "^10.0.0", - "picomatch": "^4.0.2", - "source-map": "^0.7.4", - "yargs": "^18.0.0" - }, - "bin": { - "rollup-plugin-visualizer": "dist/bin/cli.js" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", - "rollup": "2.x || 3.x || 4.x" - }, - "peerDependenciesMeta": { - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - } - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup-plugin-visualizer/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/rollup-pluginutils/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -7101,16 +6519,6 @@ "dev": true, "license": "MIT" }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8172,38 +7580,6 @@ "license": "ISC", "optional": true }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wsl-utils/node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index 3ba393c8697..e177f8b4d43 100644 --- a/package.json +++ b/package.json @@ -84,11 +84,7 @@ "devDependencies": { "@playcanvas/eslint-config": "2.1.0", "@rollup/plugin-node-resolve": "16.0.3", - "@rollup/plugin-strip": "3.0.4", - "@rollup/plugin-swc": "0.4.0", "@rollup/plugin-terser": "0.4.4", - "@rollup/pluginutils": "5.3.0", - "@swc/core": "1.15.11", "@types/node": "24.10.13", "c8": "10.1.3", "chai": "6.2.2", @@ -103,8 +99,6 @@ "publint": "0.3.17", "rollup": "4.58.0", "rollup-plugin-dts": "6.3.0", - "rollup-plugin-jscc": "2.0.0", - "rollup-plugin-visualizer": "6.0.8", "serve": "14.2.5", "sinon": "21.0.1", "typedoc": "0.28.17", diff --git a/utils/esbuild-build-target.mjs b/utils/esbuild-build-target.mjs index 52e3c9d1857..77906756c58 100644 --- a/utils/esbuild-build-target.mjs +++ b/utils/esbuild-build-target.mjs @@ -1,14 +1,15 @@ import esbuild from 'esbuild'; import fs from 'fs'; -import { dirname, resolve as pathResolve, join as pathJoin, relative as pathRelative, posix, sep } from 'path'; +import { + dirname, join as pathJoin, relative as pathRelative, + resolve as pathResolve, posix, sep +} from 'path'; import { fileURLToPath } from 'url'; -import { processJSCC } from './plugins/esbuild-jscc.mjs'; -import { buildStripPattern, applyStrip } from './plugins/esbuild-strip.mjs'; -import { processShaderChunks } from './plugins/esbuild-shader-chunks.mjs'; -import { applyDynamicImportSuppress } from './plugins/esbuild-dynamic.mjs'; import { importValidationPlugin } from './plugins/esbuild-import-validation.mjs'; -import { transformPipelinePlugin } from './plugins/esbuild-transform-pipeline.mjs'; +import { + applyTransforms, buildStripPattern, transformPipelinePlugin +} from './plugins/esbuild-transform-pipeline.mjs'; import { version, revision } from './rollup-version-revision.mjs'; import { getBanner } from './rollup-get-banner.mjs'; @@ -73,9 +74,15 @@ function getJSCCConfig(buildType) { switch (buildType) { case 'debug': - return { values: { ...base, _DEBUG: 1, _PROFILER: 1 }, keepLines: true }; + return { + values: { ...base, _DEBUG: 1, _PROFILER: 1 }, + keepLines: true + }; case 'profiler': - return { values: { ...base, _PROFILER: 1 }, keepLines: false }; + return { + values: { ...base, _PROFILER: 1 }, + keepLines: false + }; case 'release': default: return { values: base, keepLines: false }; @@ -83,37 +90,23 @@ function getJSCCConfig(buildType) { } /** - * Build esbuild plugins list for a given build configuration. - * Uses a single combined pipeline plugin for all source transforms (since esbuild - * only runs the first matching onLoad handler per file). + * Collect all .js files under a directory recursively. * - * @param {object} opts - Build options. - * @param {'debug'|'release'|'profiler'|'min'} opts.buildType - The build type. - * @param {boolean} opts.isUMD - Whether this is a UMD (IIFE) build. - * @param {string} opts.input - Entry point path. - * @returns {import('esbuild').Plugin[]} Plugins array. + * @param {string} dirPath - Directory to scan. + * @returns {string[]} List of file paths. */ -function getPlugins({ buildType, isUMD, input }) { - const isDebug = buildType === 'debug'; - const effectiveBuildType = buildType === 'min' ? 'release' : buildType; - const jsccConfig = getJSCCConfig(effectiveBuildType); - - const plugins = [ - transformPipelinePlugin({ - jsccValues: jsccConfig.values, - jsccKeepLines: jsccConfig.keepLines, - stripFunctions: !isDebug ? STRIP_FUNCTIONS : null, - processShaders: !isDebug, - dynamicImportLegacy: isUMD, - dynamicImportSuppress: !isUMD - }) - ]; - - if (isDebug) { - plugins.push(importValidationPlugin(input)); +function collectJSFiles(dirPath) { + const files = []; + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const full = pathJoin(dirPath, entry.name); + if (entry.isDirectory()) { + files.push(...collectJSFiles(full)); + } else if (entry.isFile() && entry.name.endsWith('.js')) { + files.push(full); + } } - - return plugins; + return files; } /** @@ -137,9 +130,26 @@ async function buildBundled({ const isMin = buildType === 'min'; const prefix = OUT_PREFIX[buildType]; const outfile = `${dir}/${prefix}${isUMD ? '.js' : '.mjs'}`; + const effectiveBuildType = buildType === 'min' ? 'release' : buildType; + const jsccConfig = getJSCCConfig(effectiveBuildType); const banner = getBanner(BANNER[buildType]); + const plugins = [ + transformPipelinePlugin({ + jsccValues: jsccConfig.values, + jsccKeepLines: jsccConfig.keepLines, + stripFunctions: !isDebug ? STRIP_FUNCTIONS : null, + processShaders: !isDebug, + dynamicImportLegacy: isUMD, + dynamicImportSuppress: !isUMD + }) + ]; + + if (isDebug) { + plugins.push(importValidationPlugin(input)); + } + /** @type {import('esbuild').BuildOptions} */ const opts = { entryPoints: [input], @@ -152,7 +162,7 @@ async function buildBundled({ minify: isMin, legalComments: 'none', banner: { js: banner }, - plugins: getPlugins({ buildType, isUMD, input }), + plugins, external: ['node:worker_threads'], logLevel: 'warning' }; @@ -162,53 +172,27 @@ async function buildBundled({ } if (isUMD) { - // Wrap IIFE output with a UMD detection header opts.banner = { - js: `${banner}\n(function (root, factory) {\n\tif (typeof module !== 'undefined' && module.exports) {\n\t\tmodule.exports = factory();\n\t} else {\n\t\troot.pc = factory();\n\t}\n}(typeof self !== 'undefined' ? self : this, function () {` + js: [ + banner, + '(function (root, factory) {', + '\tif (typeof module !== \'undefined\' && module.exports) {', + '\t\tmodule.exports = factory();', + '\t} else {', + '\t\troot.pc = factory();', + '\t}', + '}(typeof self !== \'undefined\' ? self : this, function () {' + ].join('\n') }; opts.footer = { js: 'return pc;\n}));' }; - // Use 'esm' internally so esbuild doesn't double-wrap, we handle the UMD wrapper ourselves opts.format = 'esm'; } await esbuild.build(opts); } -/** - * Collect all .js files under a directory recursively. - * - * @param {string} dirPath - Directory to scan. - * @returns {string[]} List of file paths. - */ -function collectJSFiles(dirPath) { - const files = []; - const entries = fs.readdirSync(dirPath, { withFileTypes: true }); - for (const entry of entries) { - const full = pathJoin(dirPath, entry.name); - if (entry.isDirectory()) { - files.push(...collectJSFiles(full)); - } else if (entry.isFile() && entry.name.endsWith('.js')) { - files.push(full); - } - } - return files; -} - -/** - * Resolve and copy the fflate module into the unbundled output directory, - * replicating Rollup's node_modules → modules renaming. - * - * @param {string} outDir - Output directory (e.g. 'build/playcanvas'). - */ -function copyFflateModule(outDir) { - const fflateEntry = pathJoin(rootDir, 'node_modules', 'fflate', 'esm', 'browser.js'); - const destDir = pathJoin(outDir, 'modules', 'fflate', 'esm'); - fs.mkdirSync(destDir, { recursive: true }); - fs.copyFileSync(fflateEntry, pathJoin(destDir, 'browser.js')); -} - /** * Build an unbundled JS target (per-file transform, preserving module structure). * @@ -228,37 +212,35 @@ async function buildUnbundled({ const outDir = `${dir}/${prefix}`; const effectiveBuildType = buildType === 'min' ? 'release' : buildType; const jsccConfig = getJSCCConfig(effectiveBuildType); - const stripPattern = !isDebug ? buildStripPattern(STRIP_FUNCTIONS) : null; + const stripPattern = + !isDebug ? buildStripPattern(STRIP_FUNCTIONS) : null; const srcFiles = collectJSFiles(input); const transformPromises = srcFiles.map(async (srcFile) => { let source = await fs.promises.readFile(srcFile, 'utf8'); - // Apply JSCC - source = processJSCC(source, jsccConfig.values, jsccConfig.keepLines); - - // Apply shader chunks (non-debug) - if (!isDebug) { - source = processShaderChunks(source); - } - - // Apply strip (non-debug) - if (stripPattern) { - source = applyStrip(source, stripPattern); - } - - // Apply dynamic import suppress (always for ESM unbundled) - source = applyDynamicImportSuppress(source); + source = applyTransforms(source, { + jsccValues: jsccConfig.values, + jsccKeepLines: jsccConfig.keepLines, + stripPattern, + processShaders: !isDebug, + dynamicImportLegacy: false, + dynamicImportSuppress: true + }); // Rewrite bare 'fflate' import to relative modules path - if (source.includes('from \'fflate\'') || source.includes('from "fflate"')) { + if (source.includes('from \'fflate\'') || + source.includes('from "fflate"')) { const relFromSrc = pathRelative(dirname(srcFile), input); const modulePath = posix.join( relFromSrc.split(sep).join('/'), '..', 'modules', 'fflate', 'esm', 'browser.js' ); - source = source.replace(/from ['"]fflate['"]/g, `from '${modulePath}'`); + source = source.replace( + /from ['"]fflate['"]/g, + `from '${modulePath}'` + ); } const destFile = pathJoin(outDir, srcFile); @@ -268,8 +250,13 @@ async function buildUnbundled({ await Promise.all(transformPromises); - // Copy fflate module - copyFflateModule(outDir); + // Copy fflate module into unbundled output + const fflateEntry = pathJoin( + rootDir, 'node_modules', 'fflate', 'esm', 'browser.js' + ); + const destDir = pathJoin(outDir, 'modules', 'fflate', 'esm'); + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(fflateEntry, pathJoin(destDir, 'browser.js')); } /** @@ -297,8 +284,10 @@ async function buildTarget({ if (bundled) { await buildBundled({ moduleFormat, buildType, input, dir }); } else { - await buildUnbundled({ buildType, input: dirname(input), dir }); + await buildUnbundled({ + buildType, input: dirname(input), dir + }); } } -export { buildTarget, buildBundled, buildUnbundled, OUT_PREFIX, BANNER }; +export { buildTarget, OUT_PREFIX }; diff --git a/utils/plugins/esbuild-transform-pipeline.mjs b/utils/plugins/esbuild-transform-pipeline.mjs index ca31e744396..02df6596bd0 100644 --- a/utils/plugins/esbuild-transform-pipeline.mjs +++ b/utils/plugins/esbuild-transform-pipeline.mjs @@ -5,10 +5,39 @@ import { processShaderChunks } from './esbuild-shader-chunks.mjs'; import { applyDynamicImportLegacy, applyDynamicImportSuppress } from './esbuild-dynamic.mjs'; /** - * Combined esbuild plugin that applies all source transforms in a single onLoad handler. + * Apply all source transforms to a string of source code. + * Shared by the esbuild pipeline plugin (bundled builds) and the + * unbundled build path which transforms files directly. * - * esbuild only runs the FIRST matching onLoad handler per file, so all transforms must - * be in one plugin to chain correctly. + * @param {string} source - Source code to transform. + * @param {Object} options - Transform options. + * @param {Object} options.jsccValues - JSCC variable values. + * @param {boolean} options.jsccKeepLines - Preserve line count for JSCC. + * @param {RegExp|null} options.stripPattern - Compiled strip pattern (null = skip). + * @param {boolean} options.processShaders - Whether to minify shader chunks. + * @param {boolean} options.dynamicImportLegacy - Wrap imports for legacy browsers. + * @param {boolean} options.dynamicImportSuppress - Add bundler-suppress comments. + * @returns {string} Transformed source. + */ +export function applyTransforms(source, { + jsccValues, jsccKeepLines, stripPattern, + processShaders: doShaders, dynamicImportLegacy, + dynamicImportSuppress +}) { + source = processJSCC(source, jsccValues, jsccKeepLines); + if (doShaders) source = processShaderChunks(source); + if (stripPattern) source = applyStrip(source, stripPattern); + if (dynamicImportLegacy) source = applyDynamicImportLegacy(source); + if (dynamicImportSuppress) source = applyDynamicImportSuppress(source); + return source; +} + +export { buildStripPattern }; + +/** + * Combined esbuild plugin that applies all source transforms in a single + * onLoad handler. esbuild only runs the FIRST matching onLoad handler per + * file, so all transforms must be in one plugin to chain correctly. * * @param {Object} options - Pipeline options. * @param {Object} options.jsccValues - JSCC variable values. @@ -27,33 +56,23 @@ export function transformPipelinePlugin({ dynamicImportLegacy = false, dynamicImportSuppress = false } = {}) { - const stripPattern = stripFunctions ? buildStripPattern(stripFunctions) : null; + const stripPattern = + stripFunctions ? buildStripPattern(stripFunctions) : null; return { name: 'transform-pipeline', setup(build) { build.onLoad({ filter: /\.js$/ }, async (args) => { - let source = await fs.promises.readFile(args.path, 'utf8'); - - source = processJSCC(source, jsccValues, jsccKeepLines); - - if (processShaders) { - source = processShaderChunks(source); - } - - if (stripPattern) { - source = applyStrip(source, stripPattern); - } - - if (dynamicImportLegacy) { - source = applyDynamicImportLegacy(source); - } - - if (dynamicImportSuppress) { - source = applyDynamicImportSuppress(source); - } - - return { contents: source, loader: 'js' }; + const source = await fs.promises.readFile(args.path, 'utf8'); + const result = applyTransforms(source, { + jsccValues, + jsccKeepLines, + stripPattern, + processShaders, + dynamicImportLegacy, + dynamicImportSuppress + }); + return { contents: result, loader: 'js' }; }); } }; diff --git a/utils/plugins/rollup-dynamic.mjs b/utils/plugins/rollup-dynamic.mjs deleted file mode 100644 index e7271b6ad95..00000000000 --- a/utils/plugins/rollup-dynamic.mjs +++ /dev/null @@ -1,39 +0,0 @@ -/** - * This rollup plugin transform code with dynamic import statements and wraps them - * in a `new Function('import("modulePath")')` statement, in order to avoid parsing errors in older browsers - * without support for dynamic imports. - * - * Note that whilst this will prevent parsing errors, it can trigger CSP errors. - * - * @returns {import('rollup').Plugin} The rollup plugin - */ -export function dynamicImportLegacyBrowserSupport() { - return { - name: 'dynamic-import-old-browsers', - transform(code, id) { - return { - code: code.replace(/(\W)import\(/g, '$1new Function("modulePath", "return import(modulePath)")('), - map: null - }; - } - }; -} - -/** - * This rollup plugin transform code with import statements and adds a \/* vite-ignore *\/ comment to suppress bundler warnings - * generated from dynamic-import-vars {@link https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations} - * {@link https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import} - * - * @returns {import('rollup').Plugin} The rollup plugin - */ -export function dynamicImportBundlerSuppress() { - return { - name: 'dynamic-import-bundler-suppress', - transform(code, id) { - return { - code: code.replace(/import\(([^'])/g, 'import(/* @vite-ignore */ /* webpackIgnore: true */ $1'), - map: null - }; - } - }; -} diff --git a/utils/plugins/rollup-import-validation.mjs b/utils/plugins/rollup-import-validation.mjs deleted file mode 100644 index 7d4100deb42..00000000000 --- a/utils/plugins/rollup-import-validation.mjs +++ /dev/null @@ -1,52 +0,0 @@ -import path from 'node:path'; - -/** @typedef {import('rollup').Plugin} Plugin */ - -/** - * Validate and print warning if an engine module on a lower level imports module on a higher level - * - * @param {string} rootFile - The root file, typically `src/index.js`. - * @returns {Plugin} The plugin. - */ -export function engineLayerImportValidation(rootFile) { - - const folderLevels = { - 'core': 0, - 'platform': 1, - 'scene': 2, - 'framework': 3, - 'extras': 4 - }; - - let rootPath; - - return { - name: 'engineLayerImportValidation', - - buildStart() { - rootPath = path.parse(path.resolve(rootFile)).dir; - }, - - resolveId(imported, importer) { - // skip non-relative paths, those are not our imports, for example 'rollupPluginBabelHelpers.js' - if (importer && imported && imported.includes('./')) { - - // convert importer path - const importerDir = path.parse(importer).dir; - const relImporter = path.dirname(path.relative(rootPath, importer)); - const folderImporter = relImporter.split(path.sep)[0]; - const levelImporter = folderLevels[folderImporter]; - - // convert imported path - const absImported = path.resolve(path.join(importerDir, imported)); - const relImported = path.dirname(path.relative(rootPath, absImported)); - const folderImported = relImported.split(path.sep)[0]; - const levelImported = folderLevels[folderImported]; - - if (levelImporter < levelImported) { - console.log(`(!) Incorrect import: [${path.relative(rootPath, importer)}] -> [${imported}]`); - } - } - } - }; -} diff --git a/utils/plugins/rollup-shader-chunks.mjs b/utils/plugins/rollup-shader-chunks.mjs deleted file mode 100644 index 0710b0c7d02..00000000000 --- a/utils/plugins/rollup-shader-chunks.mjs +++ /dev/null @@ -1,49 +0,0 @@ -import { createFilter } from '@rollup/pluginutils'; - -/** @typedef {import('rollup').Plugin} Plugin */ -/** @typedef {string | string[]} GlobPattern */ -/** - * @typedef {Object | null} PluginOptions - * @property {GlobPattern?} include - pattern(s array) to import - * @property {GlobPattern?} exclude - pattern(s array) to ignore - * @property {boolean?} enabled - enable the plugin - */ - -/** - * @type {readonly string[]} - */ -const DEFAULT_SHADERS = Object.freeze(['**/*.js']); - -/** - * @param {PluginOptions} options - Plugin config object - * @returns {Plugin} The plugin that converts shader code. - */ -export function shaderChunks({ - include = DEFAULT_SHADERS, - exclude = undefined -} = {}) { - const filter = createFilter(include, exclude); - - return { - name: 'shaderChunks', - transform(source, shader) { - if (!filter(shader)) return; - - source = source.replace(/\/\* *(glsl|wgsl) *\*\/\s*(`.*?`)/gs, (match, type, code) => { - return code - .trim() // trim whitespace - .replace(/\r/g, '') // Remove carriage returns - .replace(/ {4}/g, '\t') // 4 spaces to tabs - .replace(/[ \t]*\/\/.*/g, '') // remove single line comments - .replace(/[ \t]*\/\*[\s\S]*?\*\//g, '') // remove multi line comments - .concat('\n') // ensure final new line - .replace(/\n{2,}/g, '\n'); // condense 2 or more empty lines to 1 - }); - - return { - code: source, - map: null - }; - } - }; -} diff --git a/utils/plugins/rollup-spaces-to-tabs.mjs b/utils/plugins/rollup-spaces-to-tabs.mjs deleted file mode 100644 index 3a0534e852c..00000000000 --- a/utils/plugins/rollup-spaces-to-tabs.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { createFilter } from '@rollup/pluginutils'; - -/** @typedef {import('rollup').Plugin} Plugin */ - -/** - * This plugin converts every two spaces into one tab. Two spaces is the default the rollup plugin - * outputs, which is independent of the four spaces of the code base. - * - * @returns {Plugin} The plugin. - */ -export function spacesToTabs() { - const filter = createFilter([ - '**/*.js' - ], []); - - return { - name: 'spacesToTabs', - transform(code, id) { - if (!filter(id)) return undefined; - // ^ = start of line - // " +" = one or more spaces - // gm = find all + multiline - const regex = /^ +/gm; - code = code.replace( - regex, - startSpaces => startSpaces.replace(/ {2}/g, '\t') - ); - return { - code, - map: null - }; - } - }; -} diff --git a/utils/rollup-build-target.mjs b/utils/rollup-build-target.mjs index 06c3dbe76b2..738e05ea7c4 100644 --- a/utils/rollup-build-target.mjs +++ b/utils/rollup-build-target.mjs @@ -1,283 +1,15 @@ -// official package plugins -import resolve from '@rollup/plugin-node-resolve'; -import strip from '@rollup/plugin-strip'; -import swcPlugin from '@rollup/plugin-swc'; - -// unofficial package plugins import dts from 'rollup-plugin-dts'; -import jscc from 'rollup-plugin-jscc'; -import { visualizer } from 'rollup-plugin-visualizer'; // eslint-disable-line import/no-unresolved -// custom plugins -import { shaderChunks } from './plugins/rollup-shader-chunks.mjs'; -import { engineLayerImportValidation } from './plugins/rollup-import-validation.mjs'; -import { spacesToTabs } from './plugins/rollup-spaces-to-tabs.mjs'; -import { dynamicImportLegacyBrowserSupport, dynamicImportBundlerSuppress } from './plugins/rollup-dynamic.mjs'; import { runTsc } from './plugins/rollup-run-tsc.mjs'; import { typesFixup } from './plugins/rollup-types-fixup.mjs'; -import { version, revision } from './rollup-version-revision.mjs'; -import { getBanner } from './rollup-get-banner.mjs'; -import { swcOptions } from './rollup-swc-options.mjs'; - -import { dirname, resolve as pathResolve } from 'path'; -import { fileURLToPath } from 'url'; - -/** @import { RollupOptions, OutputOptions } from 'rollup' */ - -// Find path to the repo root -// @ts-ignore import.meta not allowed by tsconfig module:es6, but it works -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const rootDir = pathResolve(__dirname, '..'); - - -const STRIP_FUNCTIONS = [ - 'Debug.assert', - 'Debug.assertDeprecated', - 'Debug.assertDestroyed', - 'Debug.call', - 'Debug.deprecated', - 'Debug.warn', - 'Debug.warnOnce', - 'Debug.error', - 'Debug.errorOnce', - 'Debug.log', - 'Debug.logOnce', - 'Debug.removed', - 'Debug.trace', - 'DebugHelper.setName', - 'DebugHelper.setLabel', - 'DebugHelper.setDestroyed', - 'DebugGraphics.toString', - 'DebugGraphics.clearGpuMarkers', - 'DebugGraphics.pushGpuMarker', - 'DebugGraphics.popGpuMarker', - 'WebgpuDebug.validate', - 'WebgpuDebug.memory', - 'WebgpuDebug.internal', - 'WebgpuDebug.end', - 'WebgpuDebug.endShader', - 'WorldClustersDebug.render' -]; - -const BANNER = { - debug: ' (DEBUG)', - release: ' (RELEASE)', - profiler: ' (PROFILE)', - min: ' (RELEASE)' -}; - -const OUT_PREFIX = { - debug: 'playcanvas.dbg', - release: 'playcanvas', - profiler: 'playcanvas.prf', - min: 'playcanvas.min' -}; - -const HISTORY = new Map(); - -/** - * @param {'debug'|'release'|'profiler'} buildType - The build type. - * @returns {object} - The JSCC options. - */ -function getJSCCOptions(buildType) { - const options = { - debug: { - values: { - _CURRENT_SDK_VERSION: version, - _CURRENT_SDK_REVISION: revision, - _DEBUG: 1, - _PROFILER: 1 - }, - asloader: false, - keepLines: true - }, - release: { - values: { - _CURRENT_SDK_VERSION: version, - _CURRENT_SDK_REVISION: revision - }, - asloader: false - }, - profiler: { - values: { - _CURRENT_SDK_VERSION: version, - _CURRENT_SDK_REVISION: revision, - _PROFILER: 1 - }, - asloader: false - } - }; - return options[buildType]; -} - -/** - * @param {string} type - The type of the output (e.g., 'umd', 'es'). - * @returns {OutputOptions['plugins']} - The output plugins. - */ -function getOutPlugins(type) { - const plugins = []; - - if (process.env.treemap) { - plugins.push(visualizer({ - filename: `treemap.${type}.html`, - brotliSize: true, - gzipSize: true - })); - } - - if (process.env.treenet) { - plugins.push(visualizer({ - filename: `treenet.${type}.html`, - template: 'network' - })); - } - - if (process.env.treesun) { - plugins.push(visualizer({ - filename: `treesun.${type}.html`, - template: 'sunburst' - })); - } - - if (process.env.treeflame) { - plugins.push(visualizer({ - filename: `treeflame.${type}.html`, - template: 'flamegraph' - })); - } - - return plugins; -} - -/** - * Build rollup options for JS (bundled and unbundled). - * - * For faster subsequent builds, the unbundled and release builds are cached in the HISTORY map to - * be used for bundled and minified builds. They are stored in the HISTORY map with the key: - * `--`. - * - * @param {object} options - The build target options. - * @param {'umd'|'esm'} options.moduleFormat - The module format. - * @param {'debug'|'release'|'profiler'|'min'} options.buildType - The build type. - * @param {'unbundled'|'bundled'} [options.bundleState] - The bundle state. - * @param {string} [options.input] - Only used for examples to change it to `../src/index.js`. - * @param {string} [options.dir] - Only used for examples to change the output location. - * @returns {RollupOptions[]} Rollup targets. - */ -function buildJSOptions({ - moduleFormat, - buildType, - bundleState, - input = 'src/index.js', - dir = 'build' -}) { - const isUMD = moduleFormat === 'umd'; - const isDebug = buildType === 'debug'; - const isMin = buildType === 'min'; - const bundled = isUMD || isMin || bundleState === 'bundled'; - - const prefix = `${OUT_PREFIX[buildType]}`; - const file = `${prefix}${isUMD ? '.js' : '.mjs'}`; - - const targets = []; - - // bundle from unbundled - if (bundled && HISTORY.has(`${buildType}-${moduleFormat}-false`)) { - const unbundled = HISTORY.get(`${buildType}-${moduleFormat}-false`); - - /** - * @type {RollupOptions} - */ - const target = { - input: `${unbundled.output.dir}/src/index.js`, - output: { - banner: getBanner(BANNER[buildType]), - format: 'es', - indent: '\t', - sourcemap: isDebug && 'inline', - name: 'pc', - preserveModules: false, - file: `${dir}/${prefix}.mjs` - } - }; - - HISTORY.set(`${buildType}-${moduleFormat}-true`, target); - targets.push(target); - - return targets; - } - - // minify from release build - if (isMin && HISTORY.has(`release-${moduleFormat}-true`)) { - const release = HISTORY.get(`release-${moduleFormat}-true`); - - /** - * @type {RollupOptions} - */ - const target = { - input: release.output.file, - plugins: [ - swcPlugin({ swc: swcOptions(isDebug, isMin) }) - ], - output: { - banner: isUMD ? getBanner(BANNER[buildType]) : undefined, - file: `${dir}/${file}` - }, - context: isUMD ? 'this' : undefined - }; - - HISTORY.set(`${buildType}-${moduleFormat}-${bundled}`, target); - targets.push(target); - - return targets; - } - - /** - * @type {RollupOptions} - */ - const target = { - input, - output: { - banner: bundled ? getBanner(BANNER[buildType]) : undefined, - plugins: buildType === 'release' ? getOutPlugins(isUMD ? 'umd' : 'es') : undefined, - format: isUMD ? 'umd' : 'es', - indent: '\t', - sourcemap: bundled && isDebug && 'inline', - name: 'pc', - preserveModules: !bundled, - preserveModulesRoot: !bundled ? rootDir : undefined, - file: bundled ? `${dir}/${file}` : undefined, - dir: !bundled ? `${dir}/${prefix}` : undefined, - entryFileNames: chunkInfo => `${chunkInfo.name.replace(/node_modules/g, 'modules')}.js` - }, - plugins: [ - resolve(), - jscc(getJSCCOptions(isMin ? 'release' : buildType)), - isUMD ? dynamicImportLegacyBrowserSupport() : undefined, - !isDebug ? shaderChunks() : undefined, - isDebug ? engineLayerImportValidation(input) : undefined, - !isDebug ? strip({ functions: STRIP_FUNCTIONS }) : undefined, - swcPlugin({ swc: swcOptions(isDebug, isMin) }), - !isUMD ? dynamicImportBundlerSuppress() : undefined, - !isDebug ? spacesToTabs() : undefined - ] - }; - - HISTORY.set(`${buildType}-${moduleFormat}-${bundled}`, target); - targets.push(target); - - return targets; -} - /** * Build rollup options for TypeScript definitions. * * @param {object} options - The build target options. * @param {string} [options.root] - The root directory for finding the TypeScript definitions. * @param {string} [options.dir] - The output directory for the TypeScript definitions. - * @returns {RollupOptions} Rollup targets. + * @returns {import('rollup').RollupOptions} Rollup targets. */ function buildTypesOption({ root = '.', @@ -301,4 +33,4 @@ function buildTypesOption({ }; } -export { buildJSOptions, buildTypesOption }; +export { buildTypesOption }; diff --git a/utils/rollup-swc-options.mjs b/utils/rollup-swc-options.mjs deleted file mode 100644 index 5aa24acef0f..00000000000 --- a/utils/rollup-swc-options.mjs +++ /dev/null @@ -1,33 +0,0 @@ -/** @typedef {import('@swc/core').Config} SWCOptions */ - -/** - * The options for swc(...) plugin. - * - * @param {boolean} isDebug - Whether the build is for debug. - * @param {boolean} minify - Whether to minify. - * @returns {SWCOptions} The swc options. - */ -function swcOptions(isDebug, minify) { - - return { - minify, - jsc: { - target: 'es2020', - minify: { - format: { - comments: !isDebug || minify ? 'some' : 'all' - }, - mangle: minify, - compress: (!isDebug && minify) ? { - drop_console: true, - pure_funcs: [] - } : undefined - }, - externalHelpers: false, - loose: true - } - }; - -} - -export { swcOptions }; From da035fa3fe3af35658965c5d008579a109a46b36 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 23 Feb 2026 16:18:12 +0000 Subject: [PATCH 4/4] feat: integrate unplugin-strip for improved function call stripping and update related build processes --- package-lock.json | 75 +++++++++- package.json | 3 +- utils/esbuild-build-target.mjs | 29 +++- utils/plugins/esbuild-jscc.mjs | 2 +- utils/plugins/esbuild-strip.mjs | 149 ++++--------------- utils/plugins/esbuild-transform-pipeline.mjs | 27 ++-- 6 files changed, 142 insertions(+), 143 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2f31ee6821..12aff8137aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,8 @@ "typedoc": "0.28.17", "typedoc-plugin-mdn-links": "5.1.1", "typedoc-plugin-missing-exports": "4.1.2", - "typescript": "5.9.3" + "typescript": "5.9.3", + "unplugin-strip": "^0.2.1" }, "engines": { "node": ">=18.0.0" @@ -7239,6 +7240,71 @@ "dev": true, "license": "MIT" }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-strip": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/unplugin-strip/-/unplugin-strip-0.2.1.tgz", + "integrity": "sha512-Z5DgWyVhDVBs8zwyzo27g0biiu36nGJsj/xtyeUywdbB1XTHrlBudRYmoKx+a28uK18A5Mg1+tWsB7yHv8l8dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11", + "unplugin": "^1.12.0" + }, + "peerDependencies": { + "@nuxt/kit": "^3", + "@nuxt/schema": "^3", + "esbuild": "*", + "rollup": "^3", + "vite": ">=3", + "webpack": "^4 || ^5" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@nuxt/schema": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-strip/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/update-check": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", @@ -7315,6 +7381,13 @@ "node": ">=20" } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", diff --git a/package.json b/package.json index e177f8b4d43..a6df438bd8c 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,8 @@ "typedoc": "0.28.17", "typedoc-plugin-mdn-links": "5.1.1", "typedoc-plugin-missing-exports": "4.1.2", - "typescript": "5.9.3" + "typescript": "5.9.3", + "unplugin-strip": "^0.2.1" }, "optionalDependencies": { "canvas": "3.2.1" diff --git a/utils/esbuild-build-target.mjs b/utils/esbuild-build-target.mjs index 77906756c58..6eb6350ef8d 100644 --- a/utils/esbuild-build-target.mjs +++ b/utils/esbuild-build-target.mjs @@ -8,7 +8,7 @@ import { fileURLToPath } from 'url'; import { importValidationPlugin } from './plugins/esbuild-import-validation.mjs'; import { - applyTransforms, buildStripPattern, transformPipelinePlugin + applyTransforms, createStripTransform, transformPipelinePlugin } from './plugins/esbuild-transform-pipeline.mjs'; import { version, revision } from './rollup-version-revision.mjs'; import { getBanner } from './rollup-get-banner.mjs'; @@ -142,7 +142,8 @@ async function buildBundled({ stripFunctions: !isDebug ? STRIP_FUNCTIONS : null, processShaders: !isDebug, dynamicImportLegacy: isUMD, - dynamicImportSuppress: !isUMD + dynamicImportSuppress: !isUMD, + stripComments: !isDebug }) ]; @@ -178,6 +179,8 @@ async function buildBundled({ '(function (root, factory) {', '\tif (typeof module !== \'undefined\' && module.exports) {', '\t\tmodule.exports = factory();', + '\t} else if (typeof define === \'function\' && define.amd) {', + '\t\tdefine(factory);', '\t} else {', '\t\troot.pc = factory();', '\t}', @@ -212,8 +215,8 @@ async function buildUnbundled({ const outDir = `${dir}/${prefix}`; const effectiveBuildType = buildType === 'min' ? 'release' : buildType; const jsccConfig = getJSCCConfig(effectiveBuildType); - const stripPattern = - !isDebug ? buildStripPattern(STRIP_FUNCTIONS) : null; + const strip = + !isDebug ? createStripTransform(STRIP_FUNCTIONS) : null; const srcFiles = collectJSFiles(input); @@ -223,10 +226,11 @@ async function buildUnbundled({ source = applyTransforms(source, { jsccValues: jsccConfig.values, jsccKeepLines: jsccConfig.keepLines, - stripPattern, + strip, processShaders: !isDebug, dynamicImportLegacy: false, - dynamicImportSuppress: true + dynamicImportSuppress: true, + stripComments: !isDebug }); // Rewrite bare 'fflate' import to relative modules path @@ -243,6 +247,19 @@ async function buildUnbundled({ ); } + // Skip files that have no meaningful content after transforms + if (!isDebug) { + const meaningful = source + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/.*/g, '') + .replace(/^\s*import\s.*$/gm, '') + .replace(/^\s*export\s.*$/gm, '') + .trim(); + if (meaningful.length === 0) { + return; + } + } + const destFile = pathJoin(outDir, srcFile); await fs.promises.mkdir(dirname(destFile), { recursive: true }); await fs.promises.writeFile(destFile, source); diff --git a/utils/plugins/esbuild-jscc.mjs b/utils/plugins/esbuild-jscc.mjs index e1d3407008a..5d38e328fd6 100644 --- a/utils/plugins/esbuild-jscc.mjs +++ b/utils/plugins/esbuild-jscc.mjs @@ -12,6 +12,6 @@ import jscc from 'jscc'; * @returns {string} Processed source. */ export function processJSCC(source, values, keepLines) { - const result = jscc(source, null, { values, keepLines, sourceMap: false }); + const result = jscc(source, null, { values, keepLines, sourceMap: false, prefixes: ['// '] }); return result.code; } diff --git a/utils/plugins/esbuild-strip.mjs b/utils/plugins/esbuild-strip.mjs index b15ea167368..8156e14a103 100644 --- a/utils/plugins/esbuild-strip.mjs +++ b/utils/plugins/esbuild-strip.mjs @@ -1,130 +1,33 @@ -/** - * Build a regex that matches the start of a strip-target call. - * Matches at line start with optional whitespace and optional label prefix (e.g. `default:`). - * The actual parenthesis matching is done procedurally to avoid catastrophic backtracking. - * - * @param {string[]} functions - Function names to strip. - * @returns {RegExp} Pattern that matches `FuncName(` in strippable positions. - */ -export function buildStripPattern(functions) { - const escaped = functions.map(f => f.replace(/\./g, '\\.')).join('|'); - return new RegExp(`^([ \\t]*(?:\\w+:[ \\t]*)?)(${escaped})\\(`, 'gm'); -} +import { parse } from 'acorn'; +import { unpluginFactory } from 'unplugin-strip'; /** - * Strip specified function calls from source text. - * Uses parenthesis counting to find the end of the call, avoiding regex backtracking. + * Create a strip transform function for the given function names. + * Uses unplugin-strip (AST-based) to reliably remove function calls + * in all positions — statement-level, inline, inside template literals, etc. * - * @param {string} source - Source code. - * @param {RegExp} pattern - Compiled strip pattern from buildStripPattern. - * @returns {string} Processed source. + * @param {string[]} functions - Function names to strip (e.g. 'Debug.assert'). + * @returns {(source: string) => string} Transform function. */ -export function applyStrip(source, pattern) { - pattern.lastIndex = 0; - - const removals = []; // [startIndex, endIndex] pairs to remove - let match; - - while ((match = pattern.exec(source)) !== null) { - const linePrefix = match[1]; // leading whitespace + optional label - const funcNameStart = match.index + linePrefix.length; - const parenStart = match.index + match[0].length - 1; - let depth = 1; - let i = parenStart + 1; - let inString = false; - let stringChar = ''; - let inTemplate = false; - let templateDepth = 0; - - while (i < source.length && depth > 0) { - const ch = source[i]; - - // Skip single-line comments - if (ch === '/' && i + 1 < source.length && source[i + 1] === '/') { - while (i < source.length && source[i] !== '\n') i++; - continue; - } - - // Skip multi-line comments - if (ch === '/' && i + 1 < source.length && source[i + 1] === '*') { - i += 2; - while (i < source.length - 1 && !(source[i] === '*' && source[i + 1] === '/')) i++; - i += 2; - continue; - } - - if (inString) { - if (ch === stringChar && source[i - 1] !== '\\') { - inString = false; - } - i++; - continue; - } - - if (inTemplate) { - if (ch === '`' && source[i - 1] !== '\\') { - inTemplate = false; - } else if (ch === '$' && i + 1 < source.length && source[i + 1] === '{') { - templateDepth++; - i += 2; - continue; - } else if (ch === '}' && templateDepth > 0) { - templateDepth--; - } - i++; - continue; - } - - if (ch === '\'' || ch === '"') { - inString = true; - stringChar = ch; - } else if (ch === '`') { - inTemplate = true; - templateDepth = 0; - } else if (ch === '(') { - depth++; - } else if (ch === ')') { - depth--; - } - - i++; +export function createStripTransform(functions) { + const plugin = unpluginFactory({ + functions, + sourceMap: false, + debugger: false, + include: '**/*.js' + }); + + const ctx = { + parse(code) { + return parse(code, { + ecmaVersion: 'latest', + sourceType: 'module' + }); } + }; - if (depth === 0) { - // i is now right after the closing paren - let end = i; - // Skip optional semicolon and trailing whitespace/newline - while (end < source.length && (source[end] === ' ' || source[end] === '\t')) end++; - if (end < source.length && source[end] === ';') end++; - while (end < source.length && (source[end] === ' ' || source[end] === '\t')) end++; - if (end < source.length && source[end] === '\n') end++; - else if (end < source.length && source[end] === '\r') { - end++; - if (end < source.length && source[end] === '\n') end++; - } - - // The match starts at the beginning of the line (anchored with ^). - // If the prefix is only whitespace, remove the entire line. - // If the prefix includes a label (e.g. "default: "), remove only the call. - const start = match.index; - if (/^[ \t]*$/.test(linePrefix)) { - removals.push([start, end]); - } else { - removals.push([funcNameStart, end]); - } - } - } - - if (removals.length === 0) return source; - - // Build result by skipping removed ranges, handling overlapping/nested ranges - let result = ''; - let pos = 0; - for (const [start, end] of removals) { - if (start < pos) continue; // skip ranges nested inside an already-removed range - result += source.slice(pos, start); - pos = end; - } - result += source.slice(pos); - return result; + return function applyStrip(source) { + const result = plugin.transform.call(ctx, source, 'file.js'); + return result ? result.code : source; + }; } diff --git a/utils/plugins/esbuild-transform-pipeline.mjs b/utils/plugins/esbuild-transform-pipeline.mjs index 02df6596bd0..3782e2c612f 100644 --- a/utils/plugins/esbuild-transform-pipeline.mjs +++ b/utils/plugins/esbuild-transform-pipeline.mjs @@ -1,6 +1,6 @@ import fs from 'fs'; import { processJSCC } from './esbuild-jscc.mjs'; -import { buildStripPattern, applyStrip } from './esbuild-strip.mjs'; +import { createStripTransform } from './esbuild-strip.mjs'; import { processShaderChunks } from './esbuild-shader-chunks.mjs'; import { applyDynamicImportLegacy, applyDynamicImportSuppress } from './esbuild-dynamic.mjs'; @@ -13,26 +13,28 @@ import { applyDynamicImportLegacy, applyDynamicImportSuppress } from './esbuild- * @param {Object} options - Transform options. * @param {Object} options.jsccValues - JSCC variable values. * @param {boolean} options.jsccKeepLines - Preserve line count for JSCC. - * @param {RegExp|null} options.stripPattern - Compiled strip pattern (null = skip). + * @param {((source: string) => string)|null} options.strip - Strip transform (null = skip). * @param {boolean} options.processShaders - Whether to minify shader chunks. * @param {boolean} options.dynamicImportLegacy - Wrap imports for legacy browsers. * @param {boolean} options.dynamicImportSuppress - Add bundler-suppress comments. + * @param {boolean} options.stripComments - Strip JSDoc comments. * @returns {string} Transformed source. */ export function applyTransforms(source, { - jsccValues, jsccKeepLines, stripPattern, + jsccValues, jsccKeepLines, strip, processShaders: doShaders, dynamicImportLegacy, - dynamicImportSuppress + dynamicImportSuppress, stripComments }) { source = processJSCC(source, jsccValues, jsccKeepLines); if (doShaders) source = processShaderChunks(source); - if (stripPattern) source = applyStrip(source, stripPattern); + if (strip) source = strip(source); + if (stripComments) source = source.replace(/\/\*\*[\s\S]*?\*\//g, ''); if (dynamicImportLegacy) source = applyDynamicImportLegacy(source); if (dynamicImportSuppress) source = applyDynamicImportSuppress(source); return source; } -export { buildStripPattern }; +export { createStripTransform }; /** * Combined esbuild plugin that applies all source transforms in a single @@ -46,6 +48,7 @@ export { buildStripPattern }; * @param {boolean} [options.processShaders] - Whether to minify shader chunks. * @param {boolean} [options.dynamicImportLegacy] - Wrap imports for legacy browsers. * @param {boolean} [options.dynamicImportSuppress] - Add bundler-suppress comments. + * @param {boolean} [options.stripComments] - Strip JSDoc comments. * @returns {import('esbuild').Plugin} The esbuild plugin. */ export function transformPipelinePlugin({ @@ -54,10 +57,11 @@ export function transformPipelinePlugin({ stripFunctions = null, processShaders = false, dynamicImportLegacy = false, - dynamicImportSuppress = false + dynamicImportSuppress = false, + stripComments = false } = {}) { - const stripPattern = - stripFunctions ? buildStripPattern(stripFunctions) : null; + const strip = + stripFunctions ? createStripTransform(stripFunctions) : null; return { name: 'transform-pipeline', @@ -67,10 +71,11 @@ export function transformPipelinePlugin({ const result = applyTransforms(source, { jsccValues, jsccKeepLines, - stripPattern, + strip, processShaders, dynamicImportLegacy, - dynamicImportSuppress + dynamicImportSuppress, + stripComments }); return { contents: result, loader: 'js' }; });