@depack/depack

The Source Code For Depack's JavaScript API.

Usage no npm install needed!

<script type="module">
  import depackDepack from 'https://cdn.skypack.dev/@depack/depack';
</script>

README

@depack/depack

npm version

@depack/depack is The Source Code For Depack's JavaScript API. Depack is the compiler of Node.JS packages into a single executable, as well as the bundler for JavaScript web files using Google Closure Compiler. It scans the entry files to detect all dependencies, to passes them to GCC.

yarn add @depack/depack

Table Of Contents

API

The package is available by importing its named functions:


    Compile, Bundle, BundleChunks, run,
    getOptions, getOutput,
    GOOGLE_CLOSURE_COMPILER, getCompilerVersion,

async run(
  args: !Array<string>,
  opts=: !RunConfig,
): string

Low-level API used by Compile and Bundle. Spawns Java and executes the compilation. To debug a possible bug in the GCC, the sources after each pass can be saved to the file specified with the debug command. Also, GCC does not add // # sourceMappingURL=output.map comment, therefore it's done by this method. Returns stdout of the Java process. Returns the stdout of the Java process.

  • args* !Array<string>: The arguments to Java.
  • opts !RunConfig (optional): General options for running of the compiler.

RunConfig: General options for running of the compiler.

Name Type & Description Default
output string -
The path where the output will be saved. Prints to stdout if not passed.
debug string -
The name of the file where to save sources after each pass. Useful when there's a potential bug in GCC.
compilerVersion string -
Used in the display message. Obtained with the getCompilerVersion method.
noSourceMap boolean false
Disables source maps.

async Compile(
  options: !CompileConfig,
  runOptions=: !RunConfig,
  compilerArgs=: !Array<string>,
): string

Compiles a Node.JS source file with dependencies into a single executable (with the +x addition). Performs regex-based static analysis of the whole of the dependency tree to construct the list of JS files. If any of the files use require, adds the --process_common_js_modules flag. Returns the stdout of the compiler, and prints to the console if output is not given in runOptions.

  • options* !CompileConfig: Options for the Node.JS package compiler. Must have the src prop at least.
  • runOptions !RunConfig (optional): General options for running of the compiler.
  • compilerArgs !Array<string> (optional): The compiler args got with getOptions and/or manually extended. getOptions needs to be called first to find out the compiler's JAR at minimum.

The actual logic that makes compilation of Node.JS packages possible is:

  • Scan the source code and dependency to find out what internal Node.JS modules are used, and creates the output wrapper with require calls to require those built-in modules, e.g., const path = require('path').
  • Add appropriate externs for the internal modules.
  • To make Closure resolve internal imports like import { join } from 'path' instead of throwing an error, mock the built-ins in node_modules folder. The mocks will reference the variable from the output wrapper generated in step 1:
    // node_modules/path/index.js
    export default path
    export * from path
    

The last argument, compilerArgs can come from the getOptions method. The output property should come from getOutput method to enable saving to directories without specifying the output filename (GCC will do it automatically, but we need to write source maps and set +x).

CompileConfig: Options for the Node.JS package compiler.

Name Type & Description Default
src* string -
The entry file to bundle. Currently only single files are supported.
noStrict boolean false
Removes use strict from the output.
verbose boolean false
Print all arguments to the compiler.
silent boolean false
If output is not given, don't print to stdout. By default, the output will be printed.
library boolean false
Whether to create a library.

For example, given the following source:

Click to expand/collapse
import { constants } from 'os'
import { createWriteStream, createReadStream } from 'fs'

// ...
;(async () => {
  const result = await new Promise((r, j) => {
    const input = process.env['INPUT'] || __filename
    const output = process.env['OUTPUT']
    const rs = createReadStream(input)
    const ws = output ? createWriteStream(output) : process.stdout
    rs.pipe(ws)
    rs.on('error', (err) => {
      if (err.errno === -constants.errno.ENOENT) {
        return j(`Cannot find file ${input}`)
      }
      return j(err)
    })
    rs.on('close', () => {
      r({ input, 'output': output })
    })
  })
  const res = {
    version: process.version,
    ...result,
  }
  console.log(res)
})()

The library can be used to start the compilation:

import { getCompilerVersion, Compile, getOptions } from '@depack/depack'

(async () => {
  const compilerVersion = await getCompilerVersion()
  const options = getOptions({
    advanced: true,
    prettyPrint: true,
    languageIn: 2018,
    languageOut: 2017,
  })
  await Compile({
    src: 'example/compile-src.js',
  }, { compilerVersion }, options)
})()

The compiled output in pretty format of advanced optimisation:

#!/usr/bin/env node
'use strict';
const os = require('os');
const fs = require('fs');             
const g = os.constants;
const h = fs.createReadStream, k = fs.createWriteStream;
(async() => {
  var d = await new Promise((l, e) => {
    const a = process.env.INPUT || __filename, b = process.env.OUTPUT, c = h(a), m = b ? k(b) : process.stdout;
    c.pipe(m);
    c.on("error", f => f.errno === -g.errno.ENOENT ? e(`Cannot find file ${a}`) : e(f));
    c.on("close", () => {
      l({input:a, output:b});
    });
  });
  d = Object.assign({}, {version:process.version}, d);
  console.log(d);
})();

Stderr:

java -jar /Users/zavr/node_modules/google-closure-compiler-java/compiler.jar \
--compilation_level ADVANCED --language_in ECMASCRIPT_2018 --language_out \
ECMASCRIPT_2017 --formatting PRETTY_PRINT --package_json_entry_names module,main \
--entry_point example/compile-src.js --externs node_modules/@externs/nodejs/v8/os.js \
--externs node_modules/@externs/nodejs/v8/fs.js --externs \
node_modules/@externs/nodejs/v8/stream.js --externs \
node_modules/@externs/nodejs/v8/events.js --externs \
node_modules/@externs/nodejs/v8/url.js --externs \
node_modules/@externs/nodejs/v8/global.js --externs \
node_modules/@externs/nodejs/v8/global/buffer.js --externs \
node_modules/@externs/nodejs/v8/nodejs.js
Built-ins: os, fs
Running Google Closure Compiler 20200112

async Bundle(
  options: !BundleConfig,
  runOptions=: !RunConfig,
  compilerArgs=: !Array<string>,
): string

Bundles the browser source code into a JavaScript file. If there are any JSX dependencies, the bundler will transpile them first using ÀLaMode/JSX. Returns the stdout of the compiler, and prints to the console if output is not given in runOptions.

  • options* !BundleConfig: Options for the web bundler. Must have the src prop at least.
  • runOptions !RunConfig (optional): General options for running of the compiler.
  • compilerArgs !Array<string> (optional): The compiler args got with getOptions and/or manually extended.

BundleBase: Options for the web bundler.

Name Type & Description Default
tempDir string depack-temp
Where to save prepared JSX files.
preact boolean false
Adds import { h } from 'preact' automatically, so that the bundle will be compiled together with Preact.
silent boolean false
If output is not given, don't print to stdout. By default, the output will be printed.
preactExtern boolean false
Adds import { h } from '@preact/extern' automatically, assuming that preact will be available in the global scope and won't be included in the compilation. It will also rename any preact imports into @externs/preact, so that the actual source code does not need manual editing.

BundleConfig extends BundleBase: Options for the Bundle method.

Name Type & Description
src* string
The entry file to bundle. Only a single file is accepted. To compile multiple files at once, use chunks.

For example, given the following single JS source:

/* eslint-env browser */
[...document.querySelectorAll('.BananaInactive')]
  .forEach((el) => {
    const parent = el.closest('.BananaCheck')
    el.onclick = () => {
      parent.classList.add('BananaActivated')
    }
  })
;[...document.querySelectorAll('.BananaActive')]
  .forEach((el) => {
    const parent = el.closest('.BananaCheck')
    el.onclick = () => {
      parent.classList.remove('BananaActivated')
    }
  })

Depack is used to make a JS file in ES2015 understood by old browsers:

import { getCompilerVersion, Bundle, getOptions } from '@depack/depack'

(async () => {
  const compilerVersion = await getCompilerVersion()
  const options = getOptions({
    advanced: true,
    prettyPrint: true,
  })
  await Bundle({
    src: 'example/bundle-src.js',
  }, { compilerVersion }, options)
})()

The bundled output:

function c(a) {
  var b = 0;
  return function() {
    return b < a.length ? {done:!1, value:a[b++]} : {done:!0};
  };
}
function e(a) {
  if (!(a instanceof Array)) {
    var b = "undefined" != typeof Symbol && Symbol.iterator && a[Symbol.iterator];
    a = b ? b.call(a) : {next:c(a)};
    for (var d = []; !(b = a.next()).done;) {
      d.push(b.value);
    }
    a = d;
  }
  return a;
}
e(document.querySelectorAll(".BananaInactive")).concat().forEach(function(a) {
  var b = a.closest(".BananaCheck");
  a.onclick = function() {
    b.classList.add("BananaActivated");
  };
});
e(document.querySelectorAll(".BananaActive")).concat().forEach(function(a) {
  var b = a.closest(".BananaCheck");
  a.onclick = function() {
    b.classList.remove("BananaActivated");
  };
});

Stderr:

java -jar /Users/zavr/node_modules/google-closure-compiler-java/compiler.jar \
--compilation_level ADVANCED --formatting PRETTY_PRINT
--js example/bundle-src.js
Running Google Closure Compiler 20200112

async BundleChunks(
  options: !ChunksConfig,
  runOptions=: !RunConfig,
  compilerArgs=: !Array<string>,
): string

Bundles the browser source code into multiple JavaScript file. Works in the same way as Bundle, generating a temp dir for JSX dependencies.

  • options* !ChunksConfig: Options for the web bundler. Must have the srcs prop with paths to source files at least.
  • runOptions !RunConfig (optional): General options for running of the compiler.
  • compilerArgs !Array<string> (optional): The compiler args got with getOptions and/or manually extended.

ChunksConfig extends BundleBase: Options for the BundleChunks method.

Name Type & Description
srcs* !Array<string>
The entry files to bundle. Chunks will be created according to the strategy (only common strategy is supported at the moment, which places any dependency which is required in more than one file in a common chunk).
rel* string
Directory to which sources of chunks are relative. By default, the basenames are used for chunk names, but if sources are from multiple dirs, this prop can be set. Because chunk names cannot contain /, separators will be substituted for -. For example, given the following input:
  • src/lib.js
  • src/source1.js
  • src/dir/source2.js
  • and using rel=src, the following chunks are created:

  • lib
  • source1
  • dir-source2
  • checkCache (analysis: !Array<!_staticAnalysis.Detection>) => !Promise<(boolean | undefined)>
    A function to be executed to compare the an existing static analysis result with the new one, to see if any files/dependencies were updated. Should return true when caches match to skip processing and return void.
    analysis* !Array<!_staticAnalysis.Detection>: New static analysis result.

    For example, given the following multiple JS sources:

    Click to expand/collapse
    // chunkA.js
    import test from './'
    import { common } from './common'
    
    console.log('chunk a')
    test()
    common()
    

    ...

    // chunkB.js
    import test from './'
    import { common } from './common'
    
    console.log('chunk b')
    test()
    common()
    
    // common.js
    export const common = (opts = {}) => {
      const { a } = opts
      if (window.DEBUG && a) console.log('test')
    }
    
    // index.js
    export default () => {
      console.log('common')
    }
    

    Depack can generate multiple output files when a number of entries are passed:

    const options = getOptions({
      chunkOutput: TEMP,
      advanced: true,
      sourceMap: false,
    })
    await BundleChunks({
      silent: true,
      srcs: ['test/fixture/chunks/chunkA.js',
        'test/fixture/chunks/chunkB.js'],
    }, { output: TEMP, noSourceMap: true }, options)
    

    The bundled output:

    # chunkA.js
    
    console.log("chunk a");console.log("common");c();
    
    
    # chunkB.js
    
    console.log("chunk b");console.log("common");c();
    
    
    # common.js
    
    function c(){var a=void 0===a?{}:a;a=a.a;window.b&&a&&console.log("test")};
    

    Stderr:

    java -jar /Users/zavr/node_modules/google-closure-compiler-java/compiler.jar \
    --compilation_level ADVANCED --chunk_output_path_prefix test/temp/ --module_resolution \
    NODE
    --js test/fixture/chunks/index.js
         test/fixture/chunks/common.js
    --chunk common:2
         test/fixture/chunks/chunkA.js
    --chunk chunkA:1:common
         test/fixture/chunks/chunkB.js
    --chunk chunkB:1:common
    Running Google Closure Compiler
    

    Caching

    This method supports caching. It will shallowly analyse source files (does not go into node_modules apart from finding out their version), and run the checkCache function if it was passed. If this callback returns true, the compilation will be skipped. See an example implementation below.

    import stat from 'async-stat'
    import deepEqual from '@zoroaster/deep-equal'
    import { BundleChunks } from '../src'
    
    const compileOurChunks = async (srcs) => {
      let cachedMap, needsCacheUpdate
    
      let map = await BundleChunks({
        srcs,
        preactExtern: true,
        async checkCache(analysis) {
          // somehow get the cache object: { chunksMap, files, deps }
          const { chunksMap, ...current } = splendid.getCache('compile-comps')
          cachedMap = chunksMap
          const deps = {}
          const entries = []
          analysis.forEach(({ name, version, entry }) =>  {
            if (name) deps[name] = version
            else entries.push(entry)
          })
          const files = await entries.reduce(async (acc, file) => {
            const accRes = await acc
            /** @type {import('fs').Stats} */
            const ls = await stat(file)
            const d = new Date(ls.mtimeMs).toLocaleString()
            accRes[file] = d
            return accRes
          }, {})
          try {
            deepEqual({ files, deps }, current, ' ')
            // this is now OK, should not need to do anything else
            splendid.log2('compile-comps', 'Comps not changed.')
            return true
          } catch (err) {
            splendid.log2('compile-comps', err.message)
            needsCacheUpdate = err.actual
          }
        },
      }, { compilerVersion, output }, options)
    
      if (needsCacheUpdate) {
        needsCacheUpdate.chunksMap = map
        // save new cache: { chunksMap, files, deps }
        await splendid.appendCache('compile-comps', needsCacheUpdate)
      } else if (!map) {
        map = cachedMap
      }
      return map
    }
    

    getOptions(
      options: !GetOptions,
    ): !Array

    Returns an array of options to pass to the compiler for Compile, Bundle and BundleChunks methods. Full list of supported arguments.

    • options* !GetOptions: The map of options to be converted into Java arguments.

    GetOptions: Parameters for getOptions.

    Name Type & Description Default
    compiler string -
    The path to the compiler JAR. Default value will be got from require.resolve('google-closure-compiler-java/compiler.jar').
    output string -
    Sets the --js_output_file flag.
    chunkOutput string -
    Sets the --chunk_output_path_prefix flag.
    level string -
    Sets the --compilation_level flag.
    advanced boolean false
    Sets the --compilation_level flag to ADVANCED.
    languageIn (string | number) -
    Sets the --language_in flag. If a year is passed, adjusts it to ECMASCRIPT_{YEAR} automatically.
    languageOut (string | number) -
    Sets the --language_out flag. If a number is passed, adjusts it to ECMASCRIPT_{YEAR} automatically.
    sourceMap boolean true
    Adds the --create_source_map %outname%.map flag.
    prettyPrint boolean false
    Adds the --formatting PRETTY_PRINT flag.
    iife boolean false
    Adds the --isolation_mode IIFE flag.
    noWarnings boolean false
    Sets the --warning_level QUIET flag.
    debug string -
    The location of the file where to save sources after each pass. Disables source maps as these 2 options are incompatible.
    argv !Array<string> -
    Any additional arguments to the compiler.

    Example:

    Click to show/hide output
    import { getOptions } from '@depack/depack'
    
    const opts = getOptions({
      advanced: true,
      iife: true,
      languageIn: 2019,
      languageOut: 2017,
      noWarnings: true,
      prettyPrint: true,
      output: 'bundle.js',
      argv: ['--externs', 'externs.js'],
    })
    console.log(opts)
    

    [ '-jar',
      '/Users/zavr/node_modules/google-closure-compiler-java/compiler.jar',
      '--compilation_level',
      'ADVANCED',
      '--language_in',
      'ECMASCRIPT_2019',
      '--language_out',
      'ECMASCRIPT_2017',
      '--create_source_map',
      '%outname%.map',
      '--formatting',
      'PRETTY_PRINT',
      '--isolation_mode',
      'IIFE',
      '--warning_level',
      'QUIET',
      '--externs',
      'externs.js',
      '--js_output_file',
      'bundle.js' ]
    

    getOutput(
      output: string,
      src: string,
    ): string

    Returns the location of the output file, even when the directory is given.

    • output* string: The path to the output dir or file.
    • src* string: The path to the source file. Will be used when the output is a dir.
    import { getOutput } from '@depack/depack'
    
    const file = getOutput('output/example.js', 'src/example.js')
    console.log('File: %s', file)
    const dir = getOutput('output', 'src/index.js')
    console.log('Dir: %s', dir)
    
    File: output/example.js
    Dir: output/index.js
    

    GOOGLE_CLOSURE_COMPILER: string

    If the GOOGLE_CLOSURE_COMPILER was set using the environment variable, it will be returned in this named exported.

    async getCompilerVersion(): string

    If GOOGLE_CLOSURE_COMPILER was set using an environment variable, returns target, otherwise reads the version from the google-closure-compiler-java package.json file.

    License & Copyright

    GNU Affero General Public License v3.0

    Art Deco © Art Deco™ for Depack 2020