diff --git a/resources/[system]/[builders]/esbuild/.gitignore b/resources/[system]/[builders]/esbuild/.gitignore new file mode 100644 index 000000000..096746c14 --- /dev/null +++ b/resources/[system]/[builders]/esbuild/.gitignore @@ -0,0 +1 @@ +/node_modules/ \ No newline at end of file diff --git a/resources/[system]/[builders]/esbuild/README.md b/resources/[system]/[builders]/esbuild/README.md new file mode 100644 index 000000000..467af9fe4 --- /dev/null +++ b/resources/[system]/[builders]/esbuild/README.md @@ -0,0 +1,60 @@ +## Esbuilder + +What is [esbuild](https://esbuild.github.io/)? +---- +"esbuild - An extremely fast JavaScript bundler" +esbuild claims that the current build tools are 10-100x slower than what they really could be. They want to create a simple build API and lead the way for the new build tool era + +Why [esbuild](https://esbuild.github.io/)? +---- +- Because its extremly fast. +- Easy to use api +- Builtin TypeScript support (no need for an extra plugin) + +Why not to use esbuild? +---- +- In early development (some features may be buggy) +- Has a small community, therefore it is more difficult to find helpful resources if you ever get stuck + +How-to use this resource +==== +Using external .js files as configs +---- +#### **fxmanifest.lua** +```lua +fx_version 'cerulean' +game 'common' + + +client_script 'dist/index.js' + +esbuild_config 'build.js' +``` +#### **build.js** +```js +module.exports = { + entryPoints: [ 'src/main.ts' ], + bundle: true, + minify: true, + outputFile: 'dist/index.js', +} +``` + +Embedding the config inside the fxmanifest +---- +```lua +fx_version 'cerulean' +game 'common' + + +client_script 'dist/index.js' + +esbuild 'label here' { + entryPoints = { 'src/main.ts' }, + bundle = true, + minify = true, + outputFile = 'dist/index.js', +} +``` + +### For more information visit the [esbuild website](https://esbuild.github.io/) and check out the [documentation](https://esbuild.github.io/api/) \ No newline at end of file diff --git a/resources/[system]/[builders]/esbuild/esbuilder.js b/resources/[system]/[builders]/esbuild/esbuilder.js new file mode 100644 index 000000000..667e29f00 --- /dev/null +++ b/resources/[system]/[builders]/esbuild/esbuilder.js @@ -0,0 +1,63 @@ +const fs = require('fs'); +const path = require('path'); +const workerFarm = require('worker-farm'); +const { getResourceConfigs, getFileStat } = require('./util'); + +let esbuild_configs = {}; + +const esbuildTask = { + shouldBuild(resourceName) { + let resourceConfigs = getResourceConfigs(resourceName); + // Filter out all the configs that doesn't need to be built, based on the cache + resourceConfigs = resourceConfigs.filter(config => { + let cacheFile = path.resolve('cache/esbuild', resourceName, `cache_${config.label.toLowerCase().replace(/\//g, '-')}_config.json`); + if (!fs.existsSync(cacheFile)) return true; + let cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); + + // Check the cache for file changes + if(Array.isArray(cache)) { + return cache.some(file => { + let fileStat = getFileStat(file.path); + return fileStat.mtime != file.stat.mtime || + fileStat.inode != file.stat.inode || + fileStat.size != file.stat.size; + }); + } + return false; + }); + + if (!resourceConfigs.length) return false; + esbuild_configs[resourceName] = resourceConfigs; + return true; + }, + + build(resourceName, cb) { + (async () => { + const promises = []; + + esbuild_configs[resourceName].forEach(config => { + let promise = new Promise((resolve, reject) => { + const worker = workerFarm(require.resolve('./esbuilder_runner')); + worker({ + config, + resourcePath: path.resolve(GetResourcePath(resourceName)), + cachePath: path.resolve('cache/esbuild', resourceName, `cache_${config.label.toLowerCase().replace(/\//g, '-')}_config.json`), + }, function(error, result) { + workerFarm.end(worker); + if (error) reject(error); + if (result) resolve(result); + return; + }); + }); + promises.push(promise); + }); + + await Promise.all(promises); + + })() + .then(()=>cb(true)) + .catch(()=>cb(false)); + } +} + +RegisterResourceBuildTaskFactory('esbuilder', ()=>esbuildTask); \ No newline at end of file diff --git a/resources/[system]/[builders]/esbuild/esbuilder_runner.js b/resources/[system]/[builders]/esbuild/esbuilder_runner.js new file mode 100644 index 000000000..15d58fa8b --- /dev/null +++ b/resources/[system]/[builders]/esbuild/esbuilder_runner.js @@ -0,0 +1,66 @@ +const esbuild = require('esbuild'); +const fs = require('fs-extra'); + +function getFileStat(path) { + try { + const stat = fs.statSync(path); + return stat ? { + mtime: stat.mtimeMs, + size: stat.size, + inode: stat.ino, + } : null; + } catch { + return null; + } +} + + +let cache = []; + +class EsbuilderCache { + constructor(inp) { + this.name = "cfx-esbuilder-cache"; + // Update the cache + cache.push({ + path:inp.config.path, + stat:getFileStat(inp.config.path), + }); + } + + setup(build) { + build.onLoad({filter:/.*/s}, args => { + cache.push({ + path:args.path, + stat:getFileStat(args.path), + }); + }); + } +} + + + +module.exports = async (input, cb) => { + + const {config, resourcePath, cachePath} = input; + const {value:options} = config; + + // Disable file watching + options.watch = false; + // Set the working directory to the resourcePath + options.absWorkingDir = resourcePath; + + // Add the cache plugin + if (!Array.isArray(options.plugins)) options.plugins = []; + const plugin = new EsbuilderCache(input); + options.plugins.push(plugin); + + // Run the build configuration + try { + let result = await esbuild.build(options); + fs.outputFileSync(cachePath, JSON.stringify(cache)); + cb(null, result); + } catch(e) { + console.log(e); + cb(e); + } +} \ No newline at end of file diff --git a/resources/[system]/[builders]/esbuild/fxmanifest.lua b/resources/[system]/[builders]/esbuild/fxmanifest.lua new file mode 100644 index 000000000..b01fedcc3 --- /dev/null +++ b/resources/[system]/[builders]/esbuild/fxmanifest.lua @@ -0,0 +1,9 @@ +fx_version 'cerulean' +game 'common' + +version '1.0.0' +author 'Aleksander Evensen | github.com/AleksanderEvensen' +description 'Builds resources using esbuild: https://esbuild.github.io/' + +server_script 'esbuilder.js' +dependency 'yarn' diff --git a/resources/[system]/[builders]/esbuild/package.json b/resources/[system]/[builders]/esbuild/package.json new file mode 100644 index 000000000..f3f4f443f --- /dev/null +++ b/resources/[system]/[builders]/esbuild/package.json @@ -0,0 +1,11 @@ +{ + "name": "esbuild_builder", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "esbuild": "^0.12.28", + "fs-extra": "^10.0.0", + "worker-farm": "^1.7.0" + } +} diff --git a/resources/[system]/[builders]/esbuild/util.js b/resources/[system]/[builders]/esbuild/util.js new file mode 100644 index 000000000..1fc71840e --- /dev/null +++ b/resources/[system]/[builders]/esbuild/util.js @@ -0,0 +1,71 @@ +function DebugPrint(label, message, resource) { + switch(label.toLowerCase()) { + case 'error': + label = `^1${label}^7`; + break; + case 'warning': + label = `^3${label}^7`; + break; + case 'info': + label = `^4${label}^7`; + break; + } + console.log(`[^5esbuilder^7] [${resource ? `${resource}:` : ''}${label}] ${message}^7`) +} + +// Get the path or internal config from a resource fxmanifest.lua file +function getResourceConfigs(resourceName) { + let configs = []; + let resourcePath = GetResourcePath(resourceName); + // Get embedded configs + for(let i = 0; i < GetNumResourceMetadata(resourceName, 'esbuild'); i++) { + let embeddedConfig = JSON.parse(GetResourceMetadata(resourceName, 'esbuild_extra', i)); + if(embeddedConfig != null) { + configs.push({ + label: GetResourceMetadata(resourceName, 'esbuild', i), + type:'embedded', + path: path.resolve(resourcePath, 'fxmanifest.lua'), + value: embeddedConfig + }); + } + } + // Get the config paths + for(let i = 0; i < GetNumResourceMetadata(resourceName, 'esbuild_config'); i++) { + + let configPath = GetResourceMetadata(resourceName, 'esbuild_config', i); + if (fs.existsSync(path.resolve(resourcePath,configPath))) { + try { + let configData = require(path.resolve(resourcePath,configPath)); + if(!!configData) { + configs.push({ + label:configPath, + type:'external', + path: path.resolve(resourcePath,configPath), + value:configData + }); + } + } catch(e) { + console.log(e); + } + } else { + DebugPrint('error', `Could not find the build config file: ${configPath}`, resourceName); + } + } + return configs; +} + +function getFileStat(path) { + try { + const stat = fs.statSync(path); + + return stat ? { + mtime: stat.mtimeMs, + size: stat.size, + inode: stat.ino, + } : null; + } catch { + return null; + } +} + +module.exports = {DebugPrint, getResourceConfigs, getFileStat}; \ No newline at end of file diff --git a/resources/[system]/[builders]/esbuild/yarn.lock b/resources/[system]/[builders]/esbuild/yarn.lock new file mode 100644 index 000000000..d48c7a27d --- /dev/null +++ b/resources/[system]/[builders]/esbuild/yarn.lock @@ -0,0 +1,55 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +errno@~0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== + dependencies: + prr "~1.0.1" + +esbuild@^0.12.28: + version "0.12.28" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.28.tgz#84da0d2a0d0dee181281545271e0d65cf6fab1ef" + integrity sha512-pZ0FrWZXlvQOATlp14lRSk1N9GkeJ3vLIwOcUoo3ICQn9WNR4rWoNi81pbn6sC1iYUy7QPqNzI3+AEzokwyVcA== + +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7"