diff --git a/build.mjs b/build.mjs index ad18c6ad12a..da5150edfee 100644 --- a/build.mjs +++ b/build.mjs @@ -5,38 +5,56 @@ * Options: * --type - Specify the build type: rel, dbg, prf, min, types. * --format - Specify the module format: esm, umd. - * --watch - Rebuild the Rollup leaf build when inputs change. - * --sourcemaps - Build with source maps using Rollup directly. + * --watch - Rebuild when inputs change. + * --sourcemaps - Build with source maps. * --clean - Remove build output. * * --treemap - Enable treemap build visualization (rel only). * --treenet - Enable treenet build visualization (rel only). * --treesun - Enable treesun build visualization (rel only). - * --treeflame - Enable treeflame build visualization (rel only). */ import { spawn } from 'node:child_process'; -import { rm } from 'node:fs/promises'; +import { rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { parseArgs, stripVTControlCharacters } from 'node:util'; +import { buildTarget, OUT_PREFIX, watchTarget } from './utils/esbuild-build-target.mjs'; +import { buildTypes, TYPES_INPUT, TYPES_OUTPUT, watchTypes } from './utils/types-build-target.mjs'; + const JS_TYPES = /** @type {const} */ (['rel', 'dbg', 'prf', 'min']); const BUILD_TYPES = /** @type {const} */ ([...JS_TYPES, 'types']); const MODULE_FORMATS = /** @type {const} */ (['umd', 'esm']); -const TREE_FLAGS = ['treemap', 'treenet', 'treesun', 'treeflame']; +const INPUT = 'src/index.js'; +const TREE_FLAGS = ['treemap', 'treenet', 'treesun']; +const TREE_TEMPLATES = { + treemap: 'treemap', + treenet: 'network', + treesun: 'sunburst' +}; const BIN_DIR = path.join('node_modules', '.bin'); +const VISUALIZER = process.platform === 'win32' ? 'esbuild-visualizer.cmd' : 'esbuild-visualizer'; +const BOLD = '\x1b[1m'; +const REGULAR = '\x1b[22m'; +const CYAN = '\x1b[36m'; +const GREEN = '\x1b[32m'; +const RED = '\x1b[31m'; +const RESET = '\x1b[39m'; +const COLORS = process.env.FORCE_COLOR !== '0' && !process.env.NO_COLOR; const USAGE = `Usage: node build.mjs [options] Options: - --type (default: rel) - --format (default: esm, tree visualizers default to both) + --type + --format (required for JS builds, tree visualizers default to both) --watch, -w --sourcemaps, -m --clean - --treemap, --treenet, --treesun, --treeflame + --treemap, --treenet, --treesun -Use npm run build or turbo run build:all for aggregate builds.`; +Tree visualizers default to --type=rel and both formats. +Use npm run build or turbo run build:all for aggregate builds. +Use npm run watch or turbo run watch:all for aggregate watches.`; const { values } = parseArgs({ args: process.argv.slice(2), @@ -49,16 +67,15 @@ const { values } = parseArgs({ treemap: { type: 'boolean' }, treenet: { type: 'boolean' }, treesun: { type: 'boolean' }, - treeflame: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } }, allowPositionals: false }); const hasType = values.type !== undefined; -const type = values.type ?? 'rel'; +const type = values.type; const hasFormat = values.format !== undefined; -const format = values.format ?? 'esm'; +const format = values.format; const trees = TREE_FLAGS.filter(flag => values[flag]); const pipe = (input, output) => { @@ -75,7 +92,9 @@ const pipe = (input, output) => { if (out.trim()) { output.write(`${style}${raw}\n`); style = ''; - } else if (raw) { + continue; + } + if (raw) { style += raw; } } @@ -111,72 +130,289 @@ const run = (cmd, args) => { const fail = (msg) => { console.error(msg); - process.exit(1); + return process.exit(1); }; -const bin = name => path.join(BIN_DIR, process.platform === 'win32' ? `${name}.cmd` : name); +const getTreeTargets = () => { + const buildType = type ?? 'rel'; -const getRollupBuild = () => { - if (!BUILD_TYPES.includes(type)) { - fail(`--type must be one of: ${BUILD_TYPES.join(', ')}`); + if (!BUILD_TYPES.includes(buildType)) { + return fail(`--type must be one of: ${BUILD_TYPES.join(', ')}`); + } + + if (buildType !== 'rel') { + return fail('tree visualizers only support --type=rel'); } if (hasFormat && !MODULE_FORMATS.includes(format)) { - fail(`--format must be one of: ${MODULE_FORMATS.join(', ')}`); + return fail(`--format must be one of: ${MODULE_FORMATS.join(', ')}`); + } + + return (hasFormat ? [format] : MODULE_FORMATS).map(moduleFormat => ({ + buildType, + moduleFormat + })); +}; + +const ms = (value) => { + return value >= 1000 ? `${(value / 1000).toFixed(1)}s` : `${Math.round(value)}ms`; +}; + +const bold = (value) => { + return COLORS ? `${BOLD}${value}${REGULAR}` : value; +}; + +const writeLog = (stream, code, value) => { + const text = COLORS ? `${code}${value}${RESET}` : value; + stream.write(`${text}\n`); +}; + +const startLog = (input, output) => writeLog(process.stderr, CYAN, `${bold(input)} → ${bold(output)}...`); + +const createdLog = (output, elapsed) => { + writeLog(process.stderr, GREEN, `created ${bold(output)} in ${bold(ms(elapsed))}`); +}; + +const failedLog = (output, elapsed) => { + writeLog(process.stderr, RED, `failed ${bold(output)} in ${bold(ms(elapsed))}`); +}; + +const runTreeVisualizers = async (moduleFormat, metafile) => { + const suffix = moduleFormat === 'esm' ? 'es' : 'umd'; + const metadata = `build/.esbuild-metafile.${moduleFormat}.json`; + + await writeFile(metadata, JSON.stringify(metafile)); + + const codes = await Promise.all(trees.map(async (flag) => { + const output = `${flag}.${suffix}.html`; + startLog(metadata, output); + const start = performance.now(); + const code = await run(path.join(BIN_DIR, VISUALIZER), [ + '--metadata', metadata, + '--filename', output, + '--template', TREE_TEMPLATES[flag] + ]); + if (code) { + failedLog(output, performance.now() - start); + return code; + } + createdLog(output, performance.now() - start); + + return 0; + })); + const code = codes.find(Boolean) ?? 0; + if (code) { + return code; } - if (trees.length && type !== 'rel') { - fail('tree visualizers only support --type=rel'); + await rm(metadata, { force: true }); + + return 0; +}; + +const targetOutput = (buildType, moduleFormat) => { + const prefix = OUT_PREFIX[buildType]; + const file = `build/${prefix}${moduleFormat === 'umd' ? '.js' : '.mjs'}`; + return moduleFormat === 'esm' && buildType !== 'min' ? `${file}, build/${prefix}/` : file; +}; + +const buildTreeTarget = async (target) => { + const output = targetOutput(target.buildType, target.moduleFormat); + startLog(INPUT, output); + const start = performance.now(); + const result = await buildTarget({ + ...target, + sourcemaps: values.sourcemaps, + metafile: true + }); + createdLog(output, performance.now() - start); + + return runTreeVisualizers(target.moduleFormat, result.metafile); +}; + +const buildTrees = async () => { + const codes = await Promise.all(getTreeTargets().map(target => buildTreeTarget(target))); + + return codes.find(Boolean) ?? 0; +}; + +const watchTrees = async () => { + const watchers = await Promise.all(getTreeTargets().map((target) => { + return watchTarget({ + ...target, + sourcemaps: values.sourcemaps, + metafile: true, + start: startLog, + log(path, elapsed, errors) { + if (errors) { + failedLog(path, elapsed); + return; + } + createdLog(path, elapsed); + }, + end(result) { + return runTreeVisualizers(target.moduleFormat, result.metafile); + } + }); + })); + + return watchers.flat(); +}; + +const getJSTargets = () => { + if (!hasType) { + return fail('--type is required'); } - if (values.watch && !hasType && !hasFormat) { - return null; + if (!BUILD_TYPES.includes(type)) { + return fail(`--type must be one of: ${BUILD_TYPES.join(', ')}`); } - if ((values.watch || trees.length) && !hasFormat) { - return `build:${type}`; + if (!hasFormat) { + return fail('--format is required for JS builds'); } - if (type === 'types') { - if (values.format) { - fail('--type=types cannot be combined with --format'); - } - return 'build:types'; + if (!MODULE_FORMATS.includes(format)) { + return fail(`--format must be one of: ${MODULE_FORMATS.join(', ')}`); } - return `build:${format}:${type}`; + if (type === 'types' && values.format) { + return fail('--type=types cannot be combined with --format'); + } + + /** @type {{ buildType: 'rel'|'dbg'|'prf'|'min', moduleFormat: 'umd'|'esm' }[]} */ + const targets = []; + JS_TYPES.forEach((buildType) => { + MODULE_FORMATS.forEach((moduleFormat) => { + if (type !== 'types' && !trees.length && buildType === type && moduleFormat === format) { + targets.push({ buildType, moduleFormat }); + } + }); + }); + + return targets; }; -const runRollup = () => { - const env = []; - const build = getRollupBuild(); - if (build) { - env.push(build); +const buildJS = async () => { + const targets = getJSTargets(); + if (!targets.length) { + return 0; } - env.push(...trees); - const args = ['-c']; - if (values.sourcemaps) { - args.push('-m'); + await Promise.all(targets.map(async (target) => { + const output = targetOutput(target.buildType, target.moduleFormat); + startLog(INPUT, output); + const start = performance.now(); + await buildTarget({ + ...target, + sourcemaps: values.sourcemaps + }); + createdLog(output, performance.now() - start); + })); + + return 0; +}; + +const watchJS = async () => { + const targets = getJSTargets(); + if (!targets.length) { + return []; } - for (const item of env) { - args.push('--environment', item); + + const watchers = await Promise.all(targets.map((target) => { + return watchTarget({ + ...target, + sourcemaps: values.sourcemaps, + start: startLog, + log(path, elapsed, errors) { + if (errors) { + failedLog(path, elapsed); + return; + } + createdLog(path, elapsed); + } + }); + })); + + return watchers.flat(); +}; + +const buildTypesTarget = async () => { + if (values.format) { + return fail('--type=types cannot be combined with --format'); } - if (values.watch) { - args.push('-w', '--no-watch.clearScreen'); + + startLog(TYPES_INPUT, TYPES_OUTPUT); + const start = performance.now(); + await buildTypes(); + createdLog(TYPES_OUTPUT, performance.now() - start); + + return 0; +}; + +const watchTypesTarget = () => { + if (values.format) { + return fail('--type=types cannot be combined with --format'); } - return run(bin('rollup'), args); + return watchTypes({ + start: startLog, + log(path, elapsed, errors) { + if (errors) { + failedLog(path, elapsed); + return; + } + createdLog(path, elapsed); + } + }); }; -if (values.help) { - console.log(USAGE); - process.exit(0); -} +const main = async () => { + if (values.help) { + console.log(USAGE); + return 0; + } + + if (values.clean) { + await rm('build', { recursive: true, force: true }); + return 0; + } + + if (values.watch && values.sourcemaps && !hasType && !hasFormat && !trees.length) { + return fail('--sourcemaps cannot be combined with aggregate --watch'); + } + + if (trees.length && values.watch) { + await watchTrees(); + await new Promise(() => {}); + return 0; + } + + if (trees.length) { + return buildTrees(); + } + + if (values.watch && !hasType && !hasFormat) { + return fail('aggregate watch must be run with npm run watch or turbo run watch:all'); + } + + if (type === 'types' && values.watch) { + await watchTypesTarget(); + await new Promise(() => {}); + return 0; + } -if (values.clean) { - await rm('build', { recursive: true, force: true }); - process.exit(0); -} + if (type === 'types') { + return buildTypesTarget(); + } + + if (values.watch) { + await watchJS(); + await new Promise(() => {}); + return 0; + } + + return buildJS(); +}; -process.exitCode = await runRollup(); +process.exitCode = await main(); diff --git a/package-lock.json b/package-lock.json index e3a84a40e77..828609dcee5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,15 @@ "@rollup/pluginutils": "5.3.0", "@swc/core": "1.15.30", "@types/node": "24.12.2", + "acorn": "8.15.0", "c8": "10.1.3", "chai": "6.2.2", + "esbuild": "0.27.4", + "esbuild-visualizer": "0.7.0", "eslint": "9.39.4", "fflate": "0.8.2", "globals": "17.5.0", + "jscc": "1.1.1", "jsdom": "28.1.0", "mocha": "11.7.5", "nise": "6.1.5", @@ -33,7 +37,6 @@ "rollup": "4.60.2", "rollup-plugin-dts": "6.4.1", "rollup-plugin-jscc": "2.0.0", - "rollup-plugin-visualizer": "6.0.11", "serve": "14.2.6", "sinon": "21.1.2", "turbo": "2.9.12", @@ -290,6 +293,448 @@ "node": ">=18" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "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", @@ -2802,6 +3247,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -3054,6 +3509,66 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/esbuild-visualizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/esbuild-visualizer/-/esbuild-visualizer-0.7.0.tgz", + "integrity": "sha512-Vz22k+G2WT7GuCo7rbhaQwGbZ26lEhwzsctkEdQlu2SVrshoM4hzQeRpu/3DP596a9+9K2JyYsinuC6AC896Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.4.0", + "picomatch": "^4.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "esbuild-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5400,6 +5915,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "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", @@ -6034,65 +6567,6 @@ "rollup": ">=2" } }, - "node_modules/rollup-plugin-visualizer": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.11.tgz", - "integrity": "sha512-TBwVHVY7buHjIKVLqr9scTVFwqZqMXINcCphPwIWKPDCOBIa+jCQfafvbjRJDZgXdq/A996Dy6yGJ/+/NtAXDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "open": "^8.0.0", - "picomatch": "^4.0.2", - "source-map": "^0.7.4", - "yargs": "^17.5.1" - }, - "bin": { - "rollup-plugin-visualizer": "dist/bin/cli.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "rolldown": "1.x || ^1.0.0-beta", - "rollup": "2.x || 3.x || 4.x" - }, - "peerDependenciesMeta": { - "rolldown": { - "optional": true - }, - "rollup": { - "optional": true - } - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup-plugin-visualizer/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/rollup-pluginutils": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", @@ -6608,16 +7082,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", diff --git a/package.json b/package.json index 91c8d2ca5ef..70b54abbea2 100644 --- a/package.json +++ b/package.json @@ -95,11 +95,15 @@ "@rollup/pluginutils": "5.3.0", "@swc/core": "1.15.30", "@types/node": "24.12.2", + "acorn": "8.15.0", "c8": "10.1.3", "chai": "6.2.2", + "esbuild": "0.27.4", + "esbuild-visualizer": "0.7.0", "eslint": "9.39.4", "fflate": "0.8.2", "globals": "17.5.0", + "jscc": "1.1.1", "jsdom": "28.1.0", "mocha": "11.7.5", "nise": "6.1.5", @@ -107,7 +111,6 @@ "rollup": "4.60.2", "rollup-plugin-dts": "6.4.1", "rollup-plugin-jscc": "2.0.0", - "rollup-plugin-visualizer": "6.0.11", "serve": "14.2.6", "sinon": "21.1.2", "turbo": "2.9.12", @@ -122,24 +125,27 @@ "scripts": { "build": "turbo run build:all", "build:types": "node build.mjs --type=types", - "build:rel:umd": "node build.mjs --format=umd", - "build:rel:esm": "node build.mjs", + "build:rel:umd": "node build.mjs --type=rel --format=umd", + "build:rel:esm": "node build.mjs --type=rel --format=esm", "build:dbg:umd": "node build.mjs --type=dbg --format=umd", - "build:dbg:esm": "node build.mjs --type=dbg", + "build:dbg:esm": "node build.mjs --type=dbg --format=esm", "build:prf:umd": "node build.mjs --type=prf --format=umd", - "build:prf:esm": "node build.mjs --type=prf", + "build:prf:esm": "node build.mjs --type=prf --format=esm", "build:min:umd": "node build.mjs --type=min --format=umd", - "build:min:esm": "node build.mjs --type=min", + "build:min:esm": "node build.mjs --type=min --format=esm", "build:treemap": "node build.mjs --treemap", "build:treenet": "node build.mjs --treenet", "build:treesun": "node build.mjs --treesun", - "build:treeflame": "node build.mjs --treeflame", - "build:sourcemaps": "node build.mjs --sourcemaps", - "watch": "node build.mjs --watch", - "watch:rel": "node build.mjs --type=rel --watch", - "watch:dbg": "node build.mjs --type=dbg --watch", - "watch:prf": "node build.mjs --type=prf --watch", - "watch:min": "node build.mjs --type=min --watch", + "build:sourcemaps": "node build.mjs --type=rel --format=esm --sourcemaps", + "watch": "turbo run watch:all", + "watch:rel:umd": "node build.mjs --type=rel --format=umd --watch", + "watch:rel:esm": "node build.mjs --type=rel --format=esm --watch", + "watch:dbg:umd": "node build.mjs --type=dbg --format=umd --watch", + "watch:dbg:esm": "node build.mjs --type=dbg --format=esm --watch", + "watch:prf:umd": "node build.mjs --type=prf --format=umd --watch", + "watch:prf:esm": "node build.mjs --type=prf --format=esm --watch", + "watch:min:umd": "node build.mjs --type=min --format=umd --watch", + "watch:min:esm": "node build.mjs --type=min --format=esm --watch", "watch:types": "node build.mjs --type=types --watch", "clean": "node build.mjs --clean", "docs": "typedoc", diff --git a/tsconfig.build.json b/tsconfig.build.json index 787b9c8db81..fae47bf5591 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,7 +4,10 @@ "allowJs": true, "declaration": true, "emitDeclarationOnly": true, + "incremental": true, "outDir": "build/playcanvas/src", + "skipLibCheck": true, + "tsBuildInfoFile": "build/.cache/playcanvas-types.tsbuildinfo", "typeRoots": [ "./node_modules/@types" ] } } diff --git a/tsconfig.json b/tsconfig.json index b768dd78187..0881564b830 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,14 +2,14 @@ "compilerOptions": { "allowJs": true, "allowSyntheticDefaultImports": true, - "baseUrl": ".", "checkJs": true, "module": "es6", - "moduleResolution": "node", + "moduleResolution": "bundler", "noImplicitReturns": true, "noImplicitThis": true, "noUnusedLocals": true, "outDir": "types", + "rootDir": ".", "strictNullChecks": true, "strictPropertyInitialization": true, "target": "es6", diff --git a/turbo.json b/turbo.json index 716d82421fd..5678850ffdc 100644 --- a/turbo.json +++ b/turbo.json @@ -8,6 +8,8 @@ "package.json", "rollup.config.mjs", "tsconfig*.json", + "utils/esbuild-*.mjs", + "utils/types-*.mjs", "utils/plugins/**/*.mjs", "utils/rollup-*.mjs" ], @@ -145,8 +147,126 @@ ], "outputs": [ "build/playcanvas.d.ts", + "build/.cache/playcanvas-types.tsbuildinfo", "build/playcanvas/src/**/*.d.ts" ] + }, + "watch:all": { + "cache": false, + "with": [ + "watch:umd", + "watch:esm", + "watch:types" + ] + }, + "watch:rel": { + "cache": false, + "with": [ + "watch:rel:umd", + "watch:rel:esm" + ] + }, + "watch:dbg": { + "cache": false, + "with": [ + "watch:dbg:umd", + "watch:dbg:esm" + ] + }, + "watch:prf": { + "cache": false, + "with": [ + "watch:prf:umd", + "watch:prf:esm" + ] + }, + "watch:min": { + "cache": false, + "with": [ + "watch:min:umd", + "watch:min:esm" + ] + }, + "watch:umd": { + "cache": false, + "with": [ + "watch:rel:umd", + "watch:dbg:umd", + "watch:prf:umd", + "watch:min:umd" + ] + }, + "watch:esm": { + "cache": false, + "with": [ + "watch:rel:esm", + "watch:dbg:esm", + "watch:prf:esm", + "watch:min:esm" + ] + }, + "watch:rel:umd": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:rel:esm": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:dbg:umd": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:dbg:esm": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:prf:umd": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:prf:esm": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:min:umd": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:min:esm": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] + }, + "watch:types": { + "cache": false, + "persistent": true, + "inputs": [ + "src/**/*.js" + ] } } } diff --git a/utils/esbuild-build-target.mjs b/utils/esbuild-build-target.mjs new file mode 100644 index 00000000000..5641d97de49 --- /dev/null +++ b/utils/esbuild-build-target.mjs @@ -0,0 +1,703 @@ +import esbuild from 'esbuild'; +import fs from 'node:fs'; +import path from 'node:path'; +import { parse } from 'acorn'; +import { fileURLToPath } from 'node:url'; + +import { importValidationPlugin } from './plugins/esbuild-import-validation.mjs'; +import { + applyTransforms, + createStripTransform, + transformPipelinePlugin +} from './plugins/esbuild-transform-pipeline.mjs'; +import { getBanner } from './rollup-get-banner.mjs'; +import { revision, version } from './rollup-version-revision.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__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 = { + dbg: ' (DEBUG)', + rel: ' (RELEASE)', + prf: ' (PROFILE)', + min: ' (RELEASE)' +}; + +const OUT_PREFIX = { + dbg: 'playcanvas.dbg', + rel: 'playcanvas', + prf: 'playcanvas.prf', + min: 'playcanvas.min' +}; + +const EXTERNALS = ['node:worker_threads', 'url']; +const FFLATE = 'fflate'; +const TARGET = 'es2020'; +const FFLATE_EXPORTS = ['zipSync', 'strToU8']; +const BUNDLE_SECTION_COMMENT = /^\/\/ (?:\.\.\/)?(?:src|node_modules|modules)\/.*\n/gm; +const CLASS_FIELD_SUPPORT = { + 'class-field': true, + 'class-static-field': true +}; +const PRUNABLE_IMPORTS = new Set([ + 'Debug', + 'DebugGraphics', + 'DebugHelper', + 'WebgpuDebug', + 'WorldClustersDebug', + 'validateUserChunks' +]); + +const compactIndent = code => code.replace(/^ +/gm, spaces => spaces.replace(/ {2}/g, '\t')); + +const shouldCompactIndent = ({ buildType, sourcemaps }) => { + return !sourcemaps && (buildType === 'rel' || buildType === 'prf'); +}; + +const parseModule = source => parse(source, { + ecmaVersion: 'latest', + sourceType: 'module' +}); + +const getJSCCOptions = (type) => { + const base = { + _CURRENT_SDK_VERSION: version, + _CURRENT_SDK_REVISION: revision + }; + + return { + dbg: { + values: { + ...base, + _DEBUG: 1, + _PROFILER: 1 + }, + keepLines: true + }, + rel: { + values: base, + keepLines: false + }, + prf: { + values: { + ...base, + _PROFILER: 1 + }, + keepLines: false + } + }[type]; +}; + +const getImportMetaUrl = (file) => { + return /* js */ `(typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('${file}', document.baseURI).href))`; +}; + +const getUmdBanner = (banner) => { + return /* js */ `${banner} +(function (global, factory) { +\ttypeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : +\ttypeof define === 'function' && define.amd ? define(['exports'], factory) : +\t(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.pc = {})); +})(this, (function (exports) { 'use strict'; +\tvar _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;`; +}; + +const getUmdFooter = () => { + return /* js */ `Object.assign(exports, pc); +}));`; +}; + +const getPlugins = ({ + input, + buildType, + moduleFormat, + file +}) => { + const isDebug = buildType === 'dbg'; + const isUMD = moduleFormat === 'umd'; + const jscc = getJSCCOptions(buildType === 'min' ? 'rel' : buildType); + const plugins = [ + transformPipelinePlugin({ + jsccValues: jscc.values, + jsccKeepLines: jscc.keepLines, + stripFunctions: !isDebug ? STRIP_FUNCTIONS : null, + processShaders: !isDebug, + dynamicImportLegacy: isUMD, + dynamicImportSuppress: !isUMD, + stripComments: !isDebug, + importMetaUrl: isUMD ? getImportMetaUrl(file) : null + }) + ]; + + if (isDebug) { + plugins.push(importValidationPlugin(input)); + } + + return plugins; +}; + +const getBundledOptions = ({ + moduleFormat, + buildType, + input = 'src/index.js', + dir = 'build', + sourcemaps = false, + watch = false, + metafile = false +}) => { + const isUMD = moduleFormat === 'umd'; + const isDebug = buildType === 'dbg'; + const isMin = buildType === 'min'; + const prefix = OUT_PREFIX[buildType]; + const file = `${prefix}${isUMD ? '.js' : '.mjs'}`; + const outfile = `${dir}/${file}`; + const banner = getBanner(BANNER[buildType]); + const preserveClassFields = moduleFormat === 'esm' && (buildType === 'rel' || buildType === 'prf'); + + return { + entryPoints: [input], + bundle: true, + outfile, + format: isUMD ? 'iife' : 'esm', + globalName: isUMD ? 'pc' : undefined, + target: TARGET, + metafile, + ...(preserveClassFields ? { supported: CLASS_FIELD_SUPPORT } : {}), + sourcemap: sourcemaps ? true : isDebug ? 'inline' : false, + minify: isMin, + drop: isMin ? ['console'] : undefined, + legalComments: 'none', + banner: { + js: isUMD ? getUmdBanner(banner) : banner + }, + footer: isUMD ? { + js: getUmdFooter() + } : undefined, + plugins: getPlugins({ + input, + buildType, + moduleFormat, + file + }), + external: EXTERNALS, + logLevel: watch ? 'silent' : 'warning' + }; +}; + +const buildBundled = async (options) => { + const opts = getBundledOptions(options); + const result = await esbuild.build(opts); + const stripSections = !options.sourcemaps && options.moduleFormat === 'esm' && options.buildType !== 'dbg'; + if (stripSections || shouldCompactIndent(options)) { + let code = await fs.promises.readFile(opts.outfile, 'utf8'); + if (stripSections) { + code = code.replace(BUNDLE_SECTION_COMMENT, ''); + } + if (shouldCompactIndent(options)) { + code = compactIndent(code); + } + await fs.promises.writeFile(opts.outfile, code); + } + if (!options.sourcemaps) { + await fs.promises.rm(`${opts.outfile}.map`, { force: true }); + } + + return result; +}; + +const watchBundled = async (options, startLog, log) => { + const opts = getBundledOptions({ + ...options, + watch: true + }); + const input = options.input ?? 'src/index.js'; + const output = opts.outfile; + if (!options.sourcemaps) { + await fs.promises.rm(`${output}.map`, { force: true }); + } + const ctx = await esbuild.context({ + ...opts, + plugins: [ + ...(opts.plugins ?? []), + { + name: 'watch-log', + setup(build) { + let start = 0; + build.onStart(() => { + start = performance.now(); + startLog(input, output); + }); + build.onEnd(async (result) => { + const stripSections = !options.sourcemaps && + options.moduleFormat === 'esm' && + options.buildType !== 'dbg'; + if (!result.errors.length && (stripSections || shouldCompactIndent(options))) { + let code = await fs.promises.readFile(output, 'utf8'); + if (stripSections) { + code = code.replace(BUNDLE_SECTION_COMMENT, ''); + } + if (shouldCompactIndent(options)) { + code = compactIndent(code); + } + await fs.promises.writeFile(output, code); + } + log(output, performance.now() - start, result.errors.length); + if (!result.errors.length) { + await options.end?.(result); + } + }); + } + } + ] + }); + await ctx.watch(); + + return ctx; +}; + +const collectIdentifiers = (node, used) => { + if (!node || typeof node !== 'object' || node.type === 'ImportDeclaration') { + return; + } + if (node.type === 'Identifier') { + used.add(node.name); + } + + for (const key in node) { + if (key === 'start' || key === 'end' || key === 'loc' || key === 'range') { + continue; + } + + const value = node[key]; + if (Array.isArray(value)) { + value.forEach(item => collectIdentifiers(item, used)); + continue; + } + collectIdentifiers(value, used); + } +}; + +const pruneUnusedImports = (source) => { + const ast = parseModule(source); + const used = new Set(); + ast.body.forEach(node => collectIdentifiers(node, used)); + + const ranges = []; + for (const node of ast.body) { + if (node.type !== 'ImportDeclaration' || !node.specifiers.length) { + continue; + } + + const removable = node.specifiers.every((spec) => { + return PRUNABLE_IMPORTS.has(spec.local.name) && !used.has(spec.local.name); + }); + if (removable) { + let end = node.end; + while (source[end] === '\r' || source[end] === '\n') { + end++; + } + ranges.push([node.start, end]); + } + } + + for (const [start, end] of ranges.reverse()) { + source = `${source.slice(0, start)}${source.slice(end)}`; + } + + return source; +}; + +const collectImports = (source, file) => { + const ast = parseModule(source); + const imports = []; + + for (const node of ast.body) { + const name = node.source?.value; + if (!name || name === FFLATE) { + continue; + } + + if (name.startsWith('.')) { + const resolved = path.resolve(path.dirname(file), name); + imports.push(path.extname(resolved) ? resolved : `${resolved}.js`); + } + } + + return imports.sort(); +}; + +const rewriteFflate = (source, file, input) => { + if (!source.includes(`from '${FFLATE}'`) && !source.includes(`from "${FFLATE}"`)) { + return source; + } + + const root = path.dirname(path.resolve(input)); + const rel = path.relative(path.dirname(file), root).split(path.sep).join('/'); + const modulePath = path.posix.join(rel, '..', 'modules', FFLATE, 'esm', 'browser.js'); + + return source.replace(/from ['"]fflate['"]/g, `from '${modulePath}'`); +}; + +const writeFflate = async (outDir) => { + const dest = path.join(outDir, 'modules', FFLATE, 'esm', 'browser.js'); + + await fs.promises.mkdir(path.dirname(dest), { recursive: true }); + await esbuild.build({ + stdin: { + contents: /* js */ `export { ${FFLATE_EXPORTS.join(', ')} } from '${FFLATE}';`, + resolveDir: rootDir, + sourcefile: `${FFLATE}.js` + }, + bundle: true, + write: true, + outfile: dest, + format: 'esm', + target: TARGET, + minify: true, + legalComments: 'none', + logLevel: 'silent' + }); +}; + +const getUnbundledContext = ({ + buildType, + input = 'src/index.js', + dir = 'build', + sourcemaps = false +}) => { + const isDebug = buildType === 'dbg'; + const prefix = OUT_PREFIX[buildType]; + const jscc = getJSCCOptions(buildType); + + return { + input, + output: `${dir}/${prefix}`, + outDir: path.resolve(`${dir}/${prefix}`), + sourcemaps, + compact: shouldCompactIndent({ buildType, sourcemaps }), + preserveClassFields: buildType === 'rel' || buildType === 'prf', + isDebug, + jscc, + strip: !isDebug ? createStripTransform(STRIP_FUNCTIONS) : null + }; +}; + +const transformFile = async (file, ctx) => { + let source = await fs.promises.readFile(file, 'utf8'); + source = applyTransforms(source, { + jsccValues: ctx.jscc.values, + jsccKeepLines: ctx.jscc.keepLines, + strip: ctx.strip, + processShaders: !ctx.isDebug, + dynamicImportLegacy: false, + dynamicImportSuppress: true, + stripComments: !ctx.isDebug, + importMetaUrl: null + }, file); + source = pruneUnusedImports(source); + const imports = collectImports(source, file); + source = rewriteFflate(source, file, ctx.input); + + const result = await esbuild.transform(source, { + loader: 'js', + target: TARGET, + format: 'esm', + ...(ctx.preserveClassFields ? { supported: CLASS_FIELD_SUPPORT } : {}), + sourcemap: ctx.sourcemaps, + sourcefile: path.relative(rootDir, file), + legalComments: 'none' + }); + const rel = path.relative(rootDir, file); + + return { + file, + imports, + code: ctx.compact ? compactIndent(result.code) : result.code, + map: result.map, + dest: path.join(ctx.outDir, rel) + }; +}; + +const buildGraph = async (ctx) => { + const graph = new Map(); + const pending = new Map(); + + const walk = (file) => { + file = path.resolve(file); + if (graph.has(file)) { + return graph.get(file); + } + + const queued = pending.get(file); + if (queued) { + return queued; + } + + const promise = transformFile(file, ctx).then(async (item) => { + graph.set(file, item); + await Promise.all(item.imports.map(walk)); + pending.delete(file); + return item; + }); + pending.set(file, promise); + + return promise; + }; + + await walk(ctx.input); + + return graph; +}; + +const writeItem = async (item, sourcemaps) => { + await fs.promises.writeFile(item.dest, item.code); + if (sourcemaps && item.map) { + await fs.promises.writeFile(`${item.dest}.map`, item.map); + await fs.promises.appendFile(item.dest, `\n//# sourceMappingURL=${path.basename(item.dest)}.map\n`); + } +}; + +const writeGraph = async (graph, sourcemaps) => { + const dirs = new Set([...graph.values()].map(item => path.dirname(item.dest))); + + await Promise.all([...dirs].map(dir => fs.promises.mkdir(dir, { recursive: true }))); + await Promise.all([...graph.values()].map(item => writeItem(item, sourcemaps))); +}; + +const cleanOutDir = async (dir, preserveTypes) => { + if (!preserveTypes) { + await fs.promises.rm(dir, { recursive: true, force: true }); + return; + } + + const entries = await fs.promises.readdir(dir, { withFileTypes: true }).then(value => value, () => []); + await Promise.all(entries.map(async (entry) => { + const file = path.join(dir, entry.name); + if (entry.isDirectory()) { + await cleanOutDir(file, true); + const left = await fs.promises.readdir(file).then(value => value, () => []); + if (!left.length) { + await fs.promises.rm(file, { recursive: true, force: true }); + } + return; + } + if (!file.endsWith('.d.ts')) { + await fs.promises.rm(file, { force: true }); + } + })); +}; + +const buildUnbundled = async ({ + buildType, + input = 'src/index.js', + dir = 'build', + sourcemaps = false +}) => { + const ctx = getUnbundledContext({ + buildType, + input, + dir, + sourcemaps + }); + const graph = await buildGraph(ctx); + + await cleanOutDir(ctx.outDir, buildType === 'rel'); + await writeGraph(graph, sourcemaps); + await writeFflate(ctx.outDir); +}; + +const watchUnbundled = async (options, startLog, log) => { + const input = options.input ?? 'src/index.js'; + const ctx = getUnbundledContext({ + ...options, + input + }); + let graph = null; + let active = false; + let pending = false; + let timer = null; + + const fullBuild = async () => { + graph = await buildGraph(ctx); + await cleanOutDir(ctx.outDir, options.buildType === 'rel'); + await writeGraph(graph, ctx.sourcemaps); + await writeFflate(ctx.outDir); + }; + + const update = async (file) => { + if (!file || !graph?.has(file)) { + await fullBuild(); + return; + } + + const stat = await fs.promises.stat(file).then(value => value, () => null); + if (!stat?.isFile()) { + await fullBuild(); + return; + } + + const next = await transformFile(file, ctx); + const prev = graph.get(file); + if ( + prev.imports.length !== next.imports.length || + !prev.imports.every((value, i) => value === next.imports[i]) + ) { + await fullBuild(); + return; + } + + graph.set(file, next); + await fs.promises.mkdir(path.dirname(next.dest), { recursive: true }); + await writeItem(next, ctx.sourcemaps); + }; + + const queue = (file) => { + pending = !file || pending === true || (pending && pending !== file) ? true : file; + }; + + const run = (file) => { + startLog(input, ctx.output); + const start = performance.now(); + return update(file).then(() => { + log(ctx.output, performance.now() - start, 0); + }, (err) => { + console.error(err.message); + log(ctx.output, performance.now() - start, 1); + }); + }; + + const rebuild = (file) => { + if (active) { + queue(file); + return; + } + + active = true; + run(file).then(() => { + active = false; + if (pending) { + const file = pending === true ? null : pending; + pending = false; + rebuild(file); + } + }); + }; + + await run(null); + + const watcher = fs.watch(path.dirname(options.input ?? 'src/index.js'), { recursive: true }, (event, file) => { + if (!file?.endsWith('.js')) { + return; + } + const source = path.resolve(path.dirname(input), file); + clearTimeout(timer); + timer = setTimeout(() => rebuild(source), 100); + }); + + return { + dispose() { + clearTimeout(timer); + watcher.close(); + } + }; +}; + +const buildTarget = async ({ + moduleFormat, + buildType, + input = 'src/index.js', + dir = 'build', + preserveModules = moduleFormat === 'esm' && buildType !== 'min', + sourcemaps = false, + metafile = false +}) => { + const tasks = [buildBundled({ + moduleFormat, + buildType, + input, + dir, + sourcemaps, + metafile + })]; + + if (preserveModules) { + tasks.push(buildUnbundled({ + buildType, + input, + dir, + sourcemaps + })); + } + + const [result] = await Promise.all(tasks); + + return result; +}; + +const watchTarget = async ({ + moduleFormat, + buildType, + input = 'src/index.js', + dir = 'build', + preserveModules = moduleFormat === 'esm' && buildType !== 'min', + sourcemaps = false, + metafile = false, + start, + log, + end +}) => { + const watchers = [ + await watchBundled({ + moduleFormat, + buildType, + input, + dir, + sourcemaps, + metafile, + end + }, start, log) + ]; + + if (preserveModules) { + watchers.push(await watchUnbundled({ + buildType, + input, + dir, + sourcemaps + }, start, log)); + } + + return watchers; +}; + +export { buildTarget, OUT_PREFIX, watchTarget }; diff --git a/utils/plugins/esbuild-import-validation.mjs b/utils/plugins/esbuild-import-validation.mjs new file mode 100644 index 00000000000..b9eb96d1ce8 --- /dev/null +++ b/utils/plugins/esbuild-import-validation.mjs @@ -0,0 +1,44 @@ +import path from 'node:path'; + +const LEVELS = { + core: 0, + platform: 1, + scene: 2, + framework: 3, + extras: 4 +}; + +/** + * @param {string} rootFile - The root file. + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +const importValidationPlugin = (rootFile) => { + const root = 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(root, args.importer)); + const levelImporter = LEVELS[relImporter.split(path.sep)[0]]; + + const imported = path.resolve(path.join(importerDir, args.path)); + const relImported = path.dirname(path.relative(root, imported)); + const levelImported = LEVELS[relImported.split(path.sep)[0]]; + + if (levelImporter !== undefined && levelImported !== undefined && levelImporter < levelImported) { + console.log(`(!) Incorrect import: [${path.relative(root, args.importer)}] -> [${args.path}]`); + } + + return undefined; + }); + } + }; +}; + +export { importValidationPlugin }; diff --git a/utils/plugins/esbuild-strip.mjs b/utils/plugins/esbuild-strip.mjs new file mode 100644 index 00000000000..1b703422c38 --- /dev/null +++ b/utils/plugins/esbuild-strip.mjs @@ -0,0 +1,30 @@ +import { parse } from 'acorn'; +import strip from '@rollup/plugin-strip'; + +/** + * @param {string[]} functions - Function names to strip. + * @returns {(source: string, file?: string) => string} The strip transform. + */ +const createStripTransform = (functions) => { + const plugin = strip({ + functions, + debugger: false, + sourceMap: false + }); + + const context = { + parse(code) { + return parse(code, { + ecmaVersion: 'latest', + sourceType: 'module' + }); + } + }; + + return (source, file = 'source.js') => { + const result = plugin.transform.call(context, source, file); + return result ? result.code : source; + }; +}; + +export { createStripTransform }; diff --git a/utils/plugins/esbuild-transform-pipeline.mjs b/utils/plugins/esbuild-transform-pipeline.mjs new file mode 100644 index 00000000000..72fc23c60a6 --- /dev/null +++ b/utils/plugins/esbuild-transform-pipeline.mjs @@ -0,0 +1,123 @@ +import fs from 'node:fs'; +import jscc from 'jscc'; + +import { createStripTransform } from './esbuild-strip.mjs'; + +const processJSCC = (source, values, keepLines) => { + const result = jscc(source, null, { + values, + keepLines, + sourceMap: false, + prefixes: ['// '] + }); + + return result.code; +}; + +const 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'); + }); +}; + +/** + * @param {string} source - The source code. + * @param {object} options - The transform options. + * @param {Record} options.jsccValues - JSCC values. + * @param {boolean} options.jsccKeepLines - Whether to preserve line count. + * @param {((source: string, file?: string) => string)|null} options.strip - Strip transform. + * @param {boolean} options.processShaders - Whether to process shader chunks. + * @param {boolean} options.dynamicImportLegacy - Whether to wrap dynamic imports. + * @param {boolean} options.dynamicImportSuppress - Whether to add bundler ignore comments. + * @param {boolean} options.stripComments - Whether to strip JSDoc comments. + * @param {string|null} options.importMetaUrl - UMD import.meta.url replacement. + * @param {string} [file] - The file path. + * @returns {string} The processed source code. + */ +const applyTransforms = (source, { + jsccValues, + jsccKeepLines, + strip, + processShaders, + dynamicImportLegacy, + dynamicImportSuppress, + stripComments, + importMetaUrl +}, file) => { + source = processJSCC(source, jsccValues, jsccKeepLines); + if (processShaders) { + source = processShaderChunks(source); + } + if (strip) { + source = strip(source, file); + } + if (stripComments) { + source = source.replace(/\/\*\*[\s\S]*?\*\//g, ''); + } + if (importMetaUrl) { + source = source.replace(/import\.meta\.url/g, importMetaUrl); + } + if (dynamicImportLegacy) { + source = source.replace(/(\W)import\(/g, '$1new Function("modulePath", "return import(modulePath)")('); + } + if (dynamicImportSuppress) { + source = source.replace(/import\(([^'])/g, 'import(/* @vite-ignore */ /* webpackIgnore: true */ $1'); + } + + return source; +}; + +/** + * @param {object} options - The transform plugin options. + * @param {Record} [options.jsccValues] - JSCC values. + * @param {boolean} [options.jsccKeepLines] - Whether to preserve line count. + * @param {string[]|null} [options.stripFunctions] - Functions to strip. + * @param {boolean} [options.processShaders] - Whether to process shader chunks. + * @param {boolean} [options.dynamicImportLegacy] - Whether to wrap dynamic imports. + * @param {boolean} [options.dynamicImportSuppress] - Whether to add bundler ignore comments. + * @param {boolean} [options.stripComments] - Whether to strip JSDoc comments. + * @param {string|null} [options.importMetaUrl] - UMD import.meta.url replacement. + * @returns {import('esbuild').Plugin} The esbuild plugin. + */ +const transformPipelinePlugin = ({ + jsccValues = {}, + jsccKeepLines = false, + stripFunctions = null, + processShaders = false, + dynamicImportLegacy = false, + dynamicImportSuppress = false, + stripComments = false, + importMetaUrl = null +} = {}) => { + const strip = stripFunctions ? createStripTransform(stripFunctions) : null; + + return { + name: 'transform-pipeline', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + const source = await fs.promises.readFile(args.path, 'utf8'); + const contents = applyTransforms(source, { + jsccValues, + jsccKeepLines, + strip, + processShaders, + dynamicImportLegacy, + dynamicImportSuppress, + stripComments, + importMetaUrl + }, args.path); + + return { contents, loader: 'js' }; + }); + } + }; +}; + +export { applyTransforms, createStripTransform, transformPipelinePlugin }; diff --git a/utils/plugins/rollup-run-tsc.mjs b/utils/plugins/rollup-run-tsc.mjs index eb9970c3485..53aa3f5cbab 100644 --- a/utils/plugins/rollup-run-tsc.mjs +++ b/utils/plugins/rollup-run-tsc.mjs @@ -22,7 +22,9 @@ const addWatch = (context, src) => { const stats = fs.statSync(fullPath); if (stats.isFile()) { context.addWatchFile(path.resolve('.', fullPath)); - } else if (stats.isDirectory()) { + continue; + } + if (stats.isDirectory()) { addWatch(context, fullPath); } } diff --git a/utils/plugins/rollup-types-fixup.mjs b/utils/plugins/rollup-types-fixup.mjs index 2607ad19cff..d4d90fc26cd 100644 --- a/utils/plugins/rollup-types-fixup.mjs +++ b/utils/plugins/rollup-types-fixup.mjs @@ -198,6 +198,7 @@ const STANDARD_MAT_PROPS = [ const REPLACEMENTS = [{ path: `${TYPES_PATH}/scene/materials/standard-material.d.ts`, replacement: { + guard: 'set alphaFade(arg: boolean);', transformer: (contents) => { // Find the jsdoc block description using eg "@property {Type} {name}" @@ -230,6 +231,7 @@ import { Texture } from '../../platform/graphics/texture.js'; }, { path: `${TYPES_PATH}/framework/script/script-type.d.ts`, replacement: { + guard: 'initialize?(): void;', from: 'get enabled(): boolean;', to: `get enabled(): boolean; /** @@ -261,22 +263,26 @@ import { Texture } from '../../platform/graphics/texture.js'; } }]; +export function fixTypes(root = '.') { + REPLACEMENTS.forEach((item) => { + const { from, to, footer, guard, transformer } = item.replacement; + let contents = fs.readFileSync(path.resolve(root, item.path), 'utf-8'); + if (!guard || !contents.includes(guard)) { + contents = transformer ? transformer(contents) : contents.replace(from, to); + } + if (footer && !contents.includes(footer.trim())) { + contents += footer; + } + fs.writeFileSync(path.resolve(root, item.path), contents, 'utf-8'); + console.log(`${GREEN_OUT}type fixed ${BOLD_OUT}${item.path}${REGULAR_OUT}`); + }); +} + export function typesFixup(root = '.') { return { name: 'types-fixup', buildStart() { - REPLACEMENTS.forEach((item) => { - const { from, to, footer, transformer } = item.replacement; - let contents = fs.readFileSync(path.resolve(root, item.path), 'utf-8'); - if (transformer) { - contents = transformer(contents); - } else { - contents = contents.replace(from, to); - } - contents += footer ?? ''; - fs.writeFileSync(path.resolve(root, item.path), contents, 'utf-8'); - console.log(`${GREEN_OUT}type fixed ${BOLD_OUT}${item.path}${REGULAR_OUT}`); - }); + fixTypes(root); } }; } diff --git a/utils/rollup-build-target.mjs b/utils/rollup-build-target.mjs index 0260a3ee7b5..46d2cc20327 100644 --- a/utils/rollup-build-target.mjs +++ b/utils/rollup-build-target.mjs @@ -7,7 +7,6 @@ import { minify } from '@swc/core'; // unofficial package plugins import dts from 'rollup-plugin-dts'; import jscc from 'rollup-plugin-jscc'; -import { visualizer } from 'rollup-plugin-visualizer'; // custom plugins import { shaderChunks } from './plugins/rollup-shader-chunks.mjs'; @@ -111,45 +110,6 @@ function getJSCCOptions(buildType) { 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; -} - /** * Create a Rollup plugin to minify the output using SWC. * @@ -213,7 +173,6 @@ function buildJSOptions({ /** @type {OutputOptions[]} */ const output = [{ banner: isMin ? undefined : banner, - plugins: buildType === 'rel' ? getOutPlugins(isUMD ? 'umd' : 'es') : undefined, format: isUMD ? 'umd' : 'es', indent: '\t', sourcemap: isDebug && 'inline', diff --git a/utils/types-build-target.mjs b/utils/types-build-target.mjs new file mode 100644 index 00000000000..0491824c35f --- /dev/null +++ b/utils/types-build-target.mjs @@ -0,0 +1,176 @@ +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { rollup } from 'rollup'; +import dts from 'rollup-plugin-dts'; + +import { fixTypes } from './plugins/rollup-types-fixup.mjs'; + +const BIN_DIR = path.join('node_modules', '.bin'); +const TSC_CONFIG = 'tsconfig.build.json'; +const TSC_INFO = 'build/.cache/playcanvas-types.tsbuildinfo'; +const TYPES_ENTRY = 'build/playcanvas/src/index.d.ts'; +const TYPES_DIR = 'build/playcanvas/src'; +const TYPES_INPUT = 'src/index.js'; +const TYPES_OUTPUT = 'build/playcanvas.d.ts'; +const TYPES_FOOTER = 'export as namespace pc;\nexport as namespace pcx;'; +const REQUIRED_TYPES = [ + TYPES_ENTRY, + 'build/playcanvas/src/scene/materials/standard-material.d.ts', + 'build/playcanvas/src/framework/script/script-type.d.ts' +]; + +const exists = (file) => { + return fs.promises.stat(file).then(() => true, () => false); +}; + +const latestTypesMtime = async (dir) => { + const stat = await fs.promises.stat(dir).then(value => value, () => null); + if (!stat) { + return 0; + } + if (stat.isFile()) { + return dir.endsWith('.d.ts') ? stat.mtimeMs : 0; + } + + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + const times = await Promise.all(entries.map((entry) => { + return latestTypesMtime(path.join(dir, entry.name)); + })); + + return Math.max(stat.mtimeMs, ...times); +}; + +const runTsc = (root) => { + return new Promise((resolve, reject) => { + const cmd = path.join(BIN_DIR, process.platform === 'win32' ? 'tsc.cmd' : 'tsc'); + const child = spawn(cmd, ['--project', path.join(root, TSC_CONFIG)], { + shell: process.platform === 'win32', + stdio: 'inherit' + }); + child.on('error', reject); + child.on('close', (code) => { + if (code) { + reject(new Error(`tsc failed with code ${code}`)); + return; + } + resolve(); + }); + }); +}; + +const emitTypes = async (root) => { + const found = await Promise.all(REQUIRED_TYPES.map((file) => { + return exists(path.join(root, file)); + })); + if (found.some(value => !value)) { + await fs.promises.rm(path.join(root, TSC_INFO), { force: true }); + } + await runTsc(root); +}; + +const bundleTypes = async (root, dir) => { + await fs.promises.mkdir(dir, { recursive: true }); + const opts = { + input: path.join(root, TYPES_ENTRY), + output: [{ + file: path.join(dir, 'playcanvas.d.ts'), + footer: TYPES_FOOTER, + format: 'es' + }], + plugins: [ + dts() + ] + }; + const bundle = await rollup(opts); + await bundle.write(opts.output[0]); + await bundle.close(); +}; + +const buildTypes = async ({ + root = '.', + dir = 'build', + skipUnchanged = false +} = {}) => { + const src = path.join(root, TYPES_DIR); + const output = path.join(dir, 'playcanvas.d.ts'); + const before = skipUnchanged ? await latestTypesMtime(src) : 0; + + await emitTypes(root); + + const after = skipUnchanged ? await latestTypesMtime(src) : 1; + const dirty = !skipUnchanged || !await exists(output) || after > before; + if (dirty) { + fixTypes(root); + await bundleTypes(root, dir); + } + + return { + bundled: dirty + }; +}; + +const watchTypes = async ({ + root = '.', + dir = 'build', + start, + log +}) => { + const output = path.join(dir, 'playcanvas.d.ts'); + let active = false; + let pending = false; + let initial = true; + let timer = null; + + const run = () => { + start(TYPES_INPUT, output); + const time = performance.now(); + const skipUnchanged = !initial; + initial = false; + return buildTypes({ + root, + dir, + skipUnchanged + }).then(() => { + log(output, performance.now() - time, 0); + }, (err) => { + console.error(err.message); + log(output, performance.now() - time, 1); + }); + }; + + const rebuild = () => { + if (active) { + pending = true; + return; + } + + active = true; + run().then(() => { + active = false; + if (pending) { + pending = false; + rebuild(); + } + }); + }; + + await run(); + + const watcher = fs.watch(path.join(root, 'src'), { recursive: true }, (event, file) => { + if (!file?.endsWith('.js')) { + return; + } + clearTimeout(timer); + timer = setTimeout(rebuild, 100); + }); + + return { + dispose() { + clearTimeout(timer); + watcher.close(); + } + }; +}; + +export { buildTypes, TYPES_INPUT, TYPES_OUTPUT, watchTypes };