From 8df371dee5781d8157f5dcbd3567edc9705966b1 Mon Sep 17 00:00:00 2001 From: Derrick Farris Date: Wed, 3 Jan 2024 08:43:46 -0800 Subject: [PATCH] feat(build): add node build file (#54) * feat(build): add node build file * fix(build): add whitespace between source files in combined * fix(build): get closure compiler step working! * fix(build): fix shebang * fix(build): remove `package-lock.json` --- package.json | 2 +- src/build/build.js | 126 ++++++++++++++++++++++++++++++++++++++++ src/build/buildClass.js | 84 +++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/build/build.js create mode 100644 src/build/buildClass.js diff --git a/package.json b/package.json index bf9bde19..1df3c425 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/KilledByAPixel/LittleJS", "scripts": { - "build": "cd src && engineBuild.bat" + "build": "node src/build/build.js" }, "devDependencies": { "google-closure-compiler": "~20230502.0.0", diff --git a/src/build/build.js b/src/build/build.js new file mode 100644 index 00000000..118bb70c --- /dev/null +++ b/src/build/build.js @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +const start = performance.now(); + +const fs = require("node:fs"); +const child_process = require("node:child_process"); +const { Build } = require("./buildClass"); + +const BUILD_DIR = "build"; +const ENGINE_NAME = "littlejs"; + +// Nuke build dir and recreate +fs.rmSync(BUILD_DIR, { recursive: true, force: true }); +fs.mkdirSync(BUILD_DIR); + +const closureCompilerStep = (filename) => { + fs.copyFileSync(filename, `${filename}.tmp`); + try { + child_process.execSync( + `npx google-closure-compiler --js=${filename}.tmp --js_output_file=${filename} --language_out=ECMASCRIPT_2021 --warning_level=VERBOSE --jscomp_off="*"` + ); + } catch (e) { + console.error(e); + console.error( + "Failed to run Google Closure Compiler step... Make sure the file is valid before running?" + ); + process.exit(1); + } + fs.rmSync(`${filename}.tmp`); +}; + +const uglifyBuildStep = (filename) => { + try { + child_process.execSync(`npx uglifyjs -o ${filename} -- ${filename}`); + } catch (e) { + console.error(e); + process.exit(1); + } +}; + +// Build engine -- all +{ + const build = new Build(); + build + .addSourceFile("src/engineDebug.js") + .addSourceFile("src/engineUtilities.js") + .addSourceFile("src/engineSettings.js") + .addSourceFile("src/engineObject.js") + .addSourceFile("src/engineDraw.js") + .addSourceFile("src/engineInput.js") + .addSourceFile("src/engineAudio.js") + .addSourceFile("src/engineTileLayer.js") + .addSourceFile("src/engineParticles.js") + .addSourceFile("src/engineMedals.js") + .addSourceFile("src/engineWebGL.js") + .addSourceFile("src/engine.js") + .setOutputFile(`build/${ENGINE_NAME}.js`) + .build(); +} + +// Build engine -- release +{ + const build = new Build(); + build + .addSourceFile("src/engineRelease.js") + .addSourceFile("src/engineUtilities.js") + .addSourceFile("src/engineSettings.js") + .addSourceFile("src/engine.js") + .addSourceFile("src/engineObject.js") + .addSourceFile("src/engineDraw.js") + .addSourceFile("src/engineInput.js") + .addSourceFile("src/engineAudio.js") + .addSourceFile("src/engineTileLayer.js") + .addSourceFile("src/engineParticles.js") + .addSourceFile("src/engineMedals.js") + .addSourceFile("src/engineWebGL.js") + .setOutputFile(`build/${ENGINE_NAME}.release.js`) + .build(); +} + +// Build engine -- minified +{ + const build = new Build(); + build + .addSourceFile(`build/${ENGINE_NAME}.release.js`) + .setOutputFile(`build/${ENGINE_NAME}.min.js`) + .addBuildStep(closureCompilerStep) + .addBuildStep(uglifyBuildStep) + .build(); +} + +// Build engine -- ESM +{ + const build = new Build(); + build + .addSourceFile(`build/${ENGINE_NAME}.js`) + .addSourceFile("src/engineExport.js") + .setOutputFile(`build/${ENGINE_NAME}.esm.js`) + .build(); +} + +// Build engine -- ESM minified / release +{ + const build = new Build(); + build + .addSourceFile(`build/${ENGINE_NAME}.min.js`) + .addSourceFile("src/engineExport.js") + .setOutputFile(`build/${ENGINE_NAME}.esm.min.js`) + .addBuildStep(uglifyBuildStep) + .build(); +} + +// Run tsc to create type definitions +try { + child_process.execSync( + `npx tsc build/${ENGINE_NAME}.esm.js --declaration --allowJs --emitDeclarationOnly --outFile build/${ENGINE_NAME}.d.ts` + ); +} catch (e) { + console.error(e); + process.exit(1); +} + +const end = performance.now(); +const duration = end - start; + +console.log(`Build completed in ${duration.toFixed(2)} ms.`); diff --git a/src/build/buildClass.js b/src/build/buildClass.js new file mode 100644 index 00000000..7dccffcc --- /dev/null +++ b/src/build/buildClass.js @@ -0,0 +1,84 @@ +const fs = require("node:fs"); + +/** + A class representing a single build with its own source files, build steps, and output file +*/ +class Build { + #files; + #outputFile; + #buildSteps; + + constructor() { + this.#files = []; + this.#outputFile = "out.js"; + this.#buildSteps = []; + } + + addSourceFile(filename) { + this.#files.push(filename); + return this; + } + + setOutputFile(filename) { + this.#outputFile = filename; + return this; + } + + /** + * + * @param {buildStepCallback} cb - A callback that accepts a `filename` as its only argument representing the file to execute this step on. + * @returns {this} + */ + addBuildStep(cb) { + this.#buildSteps.push(cb); + return this; + } + + /** + * @returns {boolean} Whether build completed successfully or not. + */ + build() { + // read files + const files = this.#files; + if (!files.length) { + console.error( + "No files given to build. Make sure you add files with `addSourceFile` before building." + ); + return false; // no files, this should be an error + } + + // concat files into one buffer + let buf = fs.readFileSync(files[0]); + buf += "\n"; + // Start with i = 1 since we init buffer with 0th index + for (let i = 1; i < files.length; i += 1) { + buf += fs.readFileSync(files[i]); + buf += "\n"; + } + + // output file + fs.writeFileSync(this.#outputFile, buf, { flag: "w+" }); + + // go through build steps in order + const buildSteps = this.#buildSteps; + for (let i = 0; i < buildSteps.length; i += 1) { + buildSteps[i](this.#outputFile); + } + } + + get sourceFiles() { + return this.#files; + } + + get outputFile() { + return this.#outputFile; + } +} + +/** + * Callback provided to run a build step in the Build class. + * @callback buildStepCallback + * @param {string} filename + */ + +module.exports = { Build };