New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

als-pack

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

als-pack

In-memory, modifier-first JS bundler that outputs an eval-ready string. Build once, bundle many.

latest
npmnpm
Version
0.1.2
Version published
Maintainers
1
Created
Source

als-pack

In‑memory, modifier‑first JS bundler (eval‑ready).

als-pack is a tiny, dependency‑light bundler focused on on‑the‑fly packaging of ES modules into a single string you can eval or run in a sandbox. It parses your ESM graph, rewrites imports to a lazy runtime, and lets you apply modifiers to transform code (JSX/TS/minify/instrumentation) at bundle time.

  • In‑memory: build once, generate many bundles from the same graph.
  • 🧩 Modifier‑first: plug any transforms without complex config.
  • 🧪 Eval‑ready: output is a single template literal string.
  • 🌐 Node + browser: file resolvers included; bare specifiers can be handled externally.
  • 🧭 Deterministic: no dev server, no HMR, no hidden async; what you see is what runs.

Use it for editors, REPLs, SDKs, demos, and anywhere a heavyweight toolchain is overkill.

Installation

npm i als-pack
# or
pnpm add als-pack
# or
yarn add als-pack

Usage

Use in Nodejs:

import { pack } from 'als-pack';

Or use in browser:

<script src="/node_modules/als-pack/pack.js"><script>
or
<script src="/node_modules/als-pack/pack.min.js"><script>

Quick start

Minimal two‑file example

project/
  entry.js
  util.js

util.js

export const greet = (name) => `Hello, ${name}!`;

entry.js

import { greet } from "./util.js";
export default () => greet("world");

bundle & run (Node):

import { pack } from "als-pack";

const p = await pack(resolve("./mini-project/entry.js"));
p.build();

// Get a *pure expression* that evaluates to the exports object:
const expr = p.bundle();

// Execute in a sandbox (recommended) and grab exports:
const { default: greet } = eval(expr);

By default bundle(varName) returns a statement const ${varName} = (bundleExpr) which is handy for REPLs, but most apps prefer bundle() to get a pure expression and explicitly capture the value.

Result (bundle expression):

new (class {
   constructor() {
      void this.entry
   }
   
   get util() {
      if(this._util) return this._util
      const greet = (name) => `Hello, ${name}!`;
      this._util = {greet:greet}
      return this._util
   }

   get entry() {
      if(this._entry) return this._entry
      const { greet } = this.util;;

      this._entry = {default:() => greet('world')}
      return this._entry
   }
})().entry

And pack after build should look like this:

{
  commonPath: "/examples/demo",
  defaultAlias: "main",
  entry: "entry",
  modifiers: [],
  modules: {util: {…}, entry: {…}},
  order: (2) ['/examples/demo/util.js', '/examples/demo/entry.js'],
  orderByName: (2) ['util', 'entry'],
  sep: "/",
}

API

pack(entry: string): Promise<Pack>

Builds a module graph starting at entry. Detects environment (Node/browser) and uses the proper file/resolve helpers.

Throws on cyclic dependencies during preparation (message includes importer, imported and original import string).

class Pack

Properties

  • modules: Record<string, Module> — internal module table (after build() it contains modified sources and exportKeys).
  • order: string[] — absolute file paths in topological order (leaf → entry).
  • orderByName: string[] — same as above but normalized to export names.
  • commonPath: string — resolved common root of all module paths.
  • entry: string — normalized name of the entry module.
  • modifiers: Array<Modifier> — transformation pipeline applied per‑module during bundling (see below).

Methods

build(): void

Prepares modules for bundling (rewrites imports/exports, computes exportKeys, throws on cycles). Must be called before bundle().

bundle(varName): string

Returns bundle code string.

  • When varName passed, returns a statement like:
    const ${varName} = (/* bundle expression */)
    
    Useful in REPLs; eval(code) will declare those bindings in the current scope.
  • When no varName, returns a pure expression that evaluates to an exports object:
    const expr = pack.bundle();
    const exportsObj = (new Function(`return (${expr})`))();
    

Module shape (internal)

A Module object (internal, for advanced users and modifiers):

type Module = {
  modulePath: string;   // absolute path
  expName: string;      // normalized export name ('dir_file_js' etc.)
  imported: Array<{
    importString: string;
    relativePath: string;
    fullPath: string;     // '' for external/builtin
    command: 'import' | 'export';
    isBuiltin: boolean;   // node:* modules
    isExternal: boolean;  // bare specifiers (kept external)
  }>;
  exported: Array<{ asname: string; name: string; exportString: string; declaration: boolean; }>;
  modified: string;      // source with imports/exports rewritten
  exportKeys: Array<[asname: string, name: string]>; // collected during prepare
};

Modifiers

A modifier transforms each module right before it is placed into the bundle.

Signature:

type Modifier = ([modname, code, toReturn]: [string, string, string]) => [string, string, string];
  • modname — the normalized module name (same as getter name).
  • code — final, prepared source of the module (imports/exports already rewritten).
  • toReturn — string representation of { ... } returned from the module getter.

! The modName has to remain the same! It passed only for understanding which module is it.

Example: inject a feature flag

pack.modifiers.push(([m, c, t]) => [m, c.replace('ENABLE_X=false', 'ENABLE_X=true'), t]);

Example: JSX (using @babel/standalone in the browser)

pack.modifiers.push(([m, c, t]) => {
  const out = Babel.transform(c, { presets: ['react'] });
  return [m, out.code, t];
});

Modifiers run per bundle call. You can generate multiple different bundles from the same Pack by changing modifiers between calls.

Resolving modules

  • Relative/absolute imports are read from the file system (Node) or your fetch adapter (browser).
  • Built‑in Node modules (node:* or bare names like fs, path) are treated as externals and removed from the code. You can provide bindings yourself (e.g. via globalThis or a small prelude).
  • Bare specifiers (e.g. react) are treated as externals by default. To include a package in the graph, pass a full file path to its entry instead of a bare name, or use the browser resolver to turn a specifier into a URL.

Browser helper: getNodeModule(spec: string): Promise<string>

Resolves spec to /node_modules/<pkg>/<target> by reading /node_modules/<pkg>/package.json over fetch and using exports / module / main (simplified).

import { getNodeModule } from 'als-pack/lib/browser/node-module.js';

const url = await getNodeModule('react/jsx-runtime'); 
// → '/node_modules/react/dist/jsx-runtime.js'

Node helper: getNodeModule(spec: string): Promise<string | "node:*">

Resolves using import.meta.resolve (Node ≥ 20) or Module.createRequire().resolve and converts file:// URLs to filesystem paths.

import { getNodeModule } from 'als-pack/lib/node-module.js';
console.log(await getNodeModule('node:path')); // 'node:path'
console.log(await getNodeModule('react'));     // '/abs/path/to/.../react/...'

Runtime semantics (how it differs from ESM)

  • Lazy module getters. Each module is generated as a getter on an internal runtime object. The module body executes on first access to that module from another module.
  • Bare imports import 'x' become void this.x; inside the importer. This means side‑effects of x run when the importer module is first evaluated, not eagerly at graph load like in native ESM.
  • Cyclic dependencies are not allowed. als‑pack throws during preparation if a module depends on another that isn’t ready (message shows importer, imported, and the import string). There’s no simulation of live‑bindings.
  • Top‑level await in regular modules is not executed/awaited by the runtime. If you need async, return a Promise from the entry module and await the bundle’s result at the call site.

Limitations

  • Do not rename exported identifiers in modifiers. If a modifier changes names that other modules import, the prepared graph may break.
  • Combined declaration export like export const A = 1, B = 2; is not supported. Use either separate declarations or export { A, B };.
  • No live‑binding simulation across modules (ESM semantics are approximated with object snapshots).
  • Bare imports are lazy (see semantics). If you rely on eager side‑effects, place the bare import at the top of the importer module.

Limitations for node_modules

Browser resolver (getNodeModule for browsers)

  • Assumes packages are hosted under /node_modules/<package>/package.json and accessible via fetch; this requires proper static hosting and CORS configuration.
  • Only a subset of the exports field is honored (e.g., browser, import, module, default). Conditional exports (e.g., { "production": ..., "development": ... }), pattern exports, and advanced condition matching are not fully implemented.
  • Scoped packages (@scope/name) and subpath exports (pkg/subpath) are supported, but resolution semantics are simplified compared to Node’s resolver.
  • Fallbacks rely on module or main; if neither is present, the resolver guesses index.js. Extension and directory resolution (index.*, missing extensions) are not performed beyond what the package declares.
  • Does not automatically resolve transitive dependencies of the resolved file; it only returns a URL for the requested specifier.
  • Built‑in Node modules are mapped to node:* URLs; there are no polyfills shipped for the browser.
  • No support for the package "imports" map, self‑references ("name": "pkg" + import "pkg/..."), or Node’s conditional flags (e.g., require, node, deno, workerd).
  • Network errors or hosts that do not support HEAD requests must be handled by using GET; servers that block /package.json cannot be resolved.

Node resolver (getNodeModule for Node.js)

  • Relies on import.meta.resolve (Node ≥ 20) when available; otherwise falls back to Module.createRequire(...).resolve. Environments that restrict either API may not be supported.
  • Returns node:* for built‑ins; no polyfills are provided.
  • The function delegates real package resolution to Node; custom resolution conditions or non‑standard loaders are not considered.
  • The returned value is a filesystem path when Node returns a file:// URL. Only node: and file: schemes are supported.
  • Does not attempt to implement Node’s entire resolution algorithm; behavior is whatever your Node version provides (differences may exist across Node versions).

Keywords

bundler

FAQs

Package last updated on 20 Aug 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts