@es-pack/espack

<p align="center"> <img src="https://user-images.githubusercontent.com/38193720/116794320-7b15ab00-aacc-11eb-8b4f-71aafd37b5eb.png" alt="espack: A build tool running on esbuild"> <br> <a href="https://github.com/Csszabi98/es-pack/tree/main/plugi

Usage no npm install needed!

<script type="module">
  import esPackEspack from 'https://cdn.skypack.dev/@es-pack/espack';
</script>

README

A build tool running on esbuild

espack: A build tool running on esbuild
Plugins

espack

Espack on its own is just a nice wrapper api around esbuild, with an easy single file configuration for any number of builds.

However once you start using espack plugins it shows its true power!

With pre-written plugins such as EspackHtmlPlugin and EspackCopyPlugin it's super easy to throw together a production ready web development setup in no time! (see templates)

Installation

pnpm

pnpm add -D esbuild @es-pack/espack

yarn

yarn add -D esbuild @es-pack/espack

npm

npm install -D esbuild @es-pack/espack

Main features:

  • easy configuration with a single typescript or javascript file
  • execute any number of builds simultaneously
  • written in 100% typescript
  • easy to hook into plugin api
  • exposing all the esbuild features while building upon them

Future goals:

  • Raising the test coverage
  • Creating documentation website
  • Writing more plugins
  • Warn about possible overwrite of output files when it might happen.
    • Explanation:
      • Currently, you need to make sure yourself that your output scripts are not overwriting each other. As this is a really rare case and easy to diagnose, but can be complicated to implement, it has been delayed to future releases.

Running espack:

You can use either npx or pnpx (or anything capable of running bin scripts) to run the pre included espack binary.

npx espack

CLI options:

  • profile: The current build profile. There are two pre included build profiles: development/production. Defaults to production.
pnpx espack --profile production
  • config: The path to the config to use. Defaults to espack.config.ts/espack.config.js if not present. The priority order is as follows for config resolution: custom config > espack.config.ts > espack.config.js
pnpx espack --config ./configs/espack-node.config.ts
  • watch: Triggers watch mode for builds. If this flag is provided espack will not terminate on its own after the builds have completed. It will stay active and watch for code changes (plugins also implement their own watch).
pnpx espack --watch
  • The above defined CLI options can all be used together in any order.

Configuration options:

  • buildsDir: Output base directory of espack (defaults to dist)
  • defaultBuildProfiles: This entry allows you to describe any custom build profile to be used by esbuild, and also an option to override the default build profile options for every build.
    • Defining your own build profiles:
      {
          defaultBuildProfiles: {
              "my-custom-build-profile": {
                  //Options come inside here
              }
          }
      }
      
    • Overriding default build profile options:
      {
          defaultBuildProfiles: {
              development: {
                  minify: true // Minify by default when using the development profile
              }
          }
      }
      
    • Overriding these options can get tedious and verbose, so espack provides a helper function to make this process easier and more readable:
      createBuildProfiles(commonOptions, profileOverrides, disableDefaultProfileExtension)
      
      • commonOptions: Object defining esbuild options to spread into either the default profiles
      • profileOverrides: Record whose properties either define profile names or override the default ones (development, production)
      • disableDefaultProfileExtension: Boolean, if true the profileOverrides will be used for profile creation rather than for overriding the default espack profile options.
      • Examples:
        • Overriding default options:
        import { DefaultBuildProfiles, ImportFormat, Platforms, createBuildProfiles } from '@es-pack/espack';
        import { dependencies } from './package.json';
        
        const buildProfiles = createBuildProfiles(
            {
                platform: Platforms.NODE, // Set platfrom to node
                format: ImportFormat.COMMON_JS, // Set the import format to commonjs
                external: Object.keys(dependencies) // Mark all packages under the dependencies external
            }, // These options are spread into both development and production profiles
            {
                [DefaultBuildProfiles.PROD]: {
                    minify: false // Disable minification for just the production profile
                }
            }
        );
        
        • Define your own profiles:
        import { ImportFormat, Platforms, createBuildProfiles } from '@es-pack/espack';
        
        const buildProfiles = createBuildProfiles(
            {
                platform: Platforms.NODE,
                format: ImportFormat.COMMON_JS,
                minify: false
            },
            {
                'my-own-profile': {},
                'my-second-profile': {}
            }, // Espack will spread the above defined common options into both of these profiles
            true // Flags espack to not override the default build profiles
        );
        
    • While defining your own profiles you don't have to fine tune every single option esbuild provides, you just have to define the ones you care about and any missing option will be filled up with the espack production profile options by default.
  • Build profile options:
    • These options are being passed directly into esbuild by espack
    • Any esbuild option can be used from the build api except for the following:
      • watch, entryPoints, write, outfile, stdin
      • These options have been disabled because espack sets them by itself automatically, or they would interfere with the way espack works.
    • For all the possible options see esbuild
  • builds:
    • An array of builds to be executed
    • A build is an object of the following structure:
      • buildProfiles
        • same functionalities as defaultBuildProfiles
        • provides override of buildProfile options a per-build basis
      • plugins:
        • An array of espack plugins to be used by all the builds
        • espack plugins extend on the features of espack
        • for example using the following plugin:
        plugins: [
            espackHtmlPluginFactory()
        ]
        
        • will result in injecting all your generated files let them be javascript or css into a html template and generate the output file under dist/index.html
        • espack plugins work on a per build bases rather than a per script one!
        • This is really important as any functionality which would require a per script execution should be written as an esbuild plugin rather than an espack plugin and used accordingly.
        • esbuild plugins go under the buildProfile option definitions and any of them can be hooked up just like with using esbuild by itself.
        • for defining your own espack plugins see the How to define espack plugins? section
        • for defining esbuild plugins see esbuild
      • scripts
        • A collection of entry points of the build
        • Object of the following structure:
        {
            src: "./src/index.ts", // Entry point of the build
            buildProfiles: { // Optional build profile overrides on a per entry point basis
                //...
            }
        }
        
        • Or a simple string representing the source path.
        • You can mix both of them freely inside the scripts array.

Example espack configs

  • javascript:
import { createBuildProfiles, ImportFormat, Platforms } from '@es-pack/espack';

export default {
    defaultBuildProfiles: createBuildProfiles(
        {
            platform: Platforms.NODE,
            format: ImportFormat.COMMON_JS
        },
        { production: { minify: false } }
    ),
    builds: [
        {
            scripts: [
                './src/index.ts'
            ]
        }
    ]
};
  • typescript:
import { createBuildProfiles, IEspackBuilds, ImportFormat, Platforms } from '@es-pack/espack';

const builds: IEspackBuilds = {
    defaultBuildProfiles: createBuildProfiles(
        {
            platform: Platforms.NODE,
            format: ImportFormat.COMMON_JS
        },
        { production: { minify: false } }
    ),
    builds: [
        {
            scripts: [
                './src/index.ts'
            ]
        }
    ]
};

export default builds;
  • Using cjs:
const { createBuildProfiles, ImportFormat, Platforms } = require('@es-pack/espack');

exports.default = {
    defaultBuildProfiles: createBuildProfiles(
        {
            platform: Platforms.NODE,
            format: ImportFormat.COMMON_JS
        },
        { production: { minify: false } }
    ),
    builds: [
        {
            scripts: [
                './src/index.ts'
            ]
        }
    ]
};
  • for espack configs on common templates like web development see templates

How to define espack plugins?

  • Every espack plugin must be an object with name property (string)
  • You can choose which lifecycles of the build process you want to hook into with your plugin by providing the enabled lifecycles to the object as properties.
  • Espack currently lets you hook into the following lifecycles of the build process:
    • beforeResourceCheck: Executed by esbuild before checking for build resource accessibility. Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles (for current build), the current build profile
    • onResourceCheck: Executed parallel to espack's own resource checking mechanisms. You can check for any required resources by your plugin, espack will wait for every plugin to complete before proceeding onward. Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles, the current build profile
    • afterResourceCheck: Executed after the resource check process has been done. You can do any additional modifications you wish to make to the context before the scripts are processed in the next cycle. Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles, the current build profile
    • beforeBuild: Executed right before the build process begins with esbuild. The input scripts and build profiles have been processed by this point, into a digestible format by esbuild. Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles, the current build profile, buildReadyScripts
      • buildReadyScripts structure:
      {
          "src": "./entry-point.js",
          "buildProfile": {
              // esbuild build options...
          }
      }
      
    • onBuild: The buildReadyScripts are grouped together based on their build options, if possible. Espack runs esbuild on these grouped options with the previously determined options. You can define your own resource heavy build operation here, which will be executed parallel to the esbuild build. The result of your build operation will be passed into your plugin in the next lifecycle, so there is no need for you to save it's result. Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles (for current build), the current build profile, buildReadyScripts
    • afterBuild: This lifecycle is run after esbuild has finished, or in watch mode after every rebuild. The results of esbuild and plugin build (only esbuild result in watch mode) are passed onto the plugin. Espack will write the results of esbuild after this lifecycle to the disk, so you can modify the esbuild results if you want to, though this is generally not recommended! (such operations should be done through esbuild plugins!) Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles (for current build), the current build profile, buildReadyScripts, buildResults, pluginBuildResult (if any, also generic)
      • buildResults structure:
      {
          "buildId": "build_0", // Unique identifier of this build
          "build": {
              "buildProfile": {
                  // esbuild options
              },
              "builds": [ // Array of entry points for the build
                  {
                      "src": './index.js'
                  }
              ]
          },
          "buildResult": { // The actual esbuild result, straight from esbuild itself
              "warnings": [
                  {
                      text: "",
                      location: {
                          file: "",
                          namespace: "",
                          line: 1, // 1-based
                          column: 0, // 0-based, in bytes
                          length: 0, // in bytes
                          lineText: "",
                          suggestion: ""
                      },
                      notes: [
                          {
                              "text": "",
                              "location": {} // same as above
                          }
                      ],
                      // Optional user-specified data that is passed through unmodified. You can
                      // use this to stash the original error, for example.
                      detail: {} // any
                  }
              ], // Array of warnings
              "outputFiles": [
                  {
                      "path": "", // Output path
                      "contents": "",// Build result as bytes
                      "text": "" // Build result as string
                  }
              ], // Only when "write: false"
              "rebuild": {}, // function to trigger a rebuild
              "stop": {}, // function to stop esbuild watch
              "metafile": {} // Only when "metafile: true", see esbuild for detailed structure
          }
      }
      
      • most of this structure has been copied over from esbuild, so this is subject to change, for the latest structure see the esbuild documentation.
    • afterWrite: This lifecycle is being run after espack has finished writing the build results to the disk, or in watch mode on every rebuild after writing to the disk. Context available in this lifecycle: The results of esbuild and plugin build (only esbuild result in watch mode) are passed onto the plugin.
    • registerCustomWatcher: This lifecycle lets you register your custom watcher functions. You need to return a cleanup function which frees up your resources if watch is cancelled. Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles (for current build), the current build profile, buildReadyScripts, buildResults, pluginBuildResult
    • onCleanup: This lifecycle is being executed along with the espack shutdown process. Free up any resources you want here. Context available in this lifecycle: buildsDir, scripts, defaultBuildProfiles, buildProfiles (for current build), the current build profile, buildReadyScripts, buildResults, pluginBuildResult
  • When any lifecycle is provided on the plugin object as a property the plugin execution for that lifecycle will be automatically enabled. So if you don't want your plugin to run on certain lifecycles simply don't provide a method for them.
  • Example plugin to console.log every output file's path:
const espackReportPluginFactory = () => {
    // Return a function for the lifecycles you want to hook into
    const afterBuild = (context) => {
        const { buildResults } = context;
        buildResults.forEach(
            ({ buildResult }) =>
                buildResult.outputFiles && console.log(buildResult.outputFiles.map(outputFile => outputFile.path))
        );
    };

    return {
        name: '@es-pack/report-plugin',
        afterBuild
    };
}

Webstorm

When using webstorm the "Backup files before saving" (previously "Safe Write") can interfere with the watch mode, so it is recommended to disable this option.