Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 167 additions & 27 deletions build.mjs
Original file line number Diff line number Diff line change
@@ -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[:<moduleFormat>][:<buildType>][:<bundleState>] - Specify the target
Expand All @@ -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();
91 changes: 42 additions & 49 deletions examples/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -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',
Expand Down Expand Up @@ -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
];
};
Loading
Loading