packen
collects isomorphic JavaScript code when it runs on the server, and prepares it for shipping to and running on the client. Useful for server side rendering (SSR), static site generation (SSG), etc.
// isomorphic code:
import { packMe } from 'packen'
packMe()
// ...
node -r packen/r server-code.js
- ☕ Simple: mark isomorphic code,
packen
does the rest - 🧠 Smart: only ship what you need with conditional bundling
- 🛠️ Flexible: integrate
packen
with your own toolchain - 🧩 Extensible: write your own custom processors
You need Node.js for using packen
.
npm i packen
To use packen, you need to:
- Mark your isomorphic code
- Run your server code, which may use (some of) your isomorphic code
packen will keep track of what was used and bundle it for you (or help you bundle it using your own toolchain).
👉 Use packMe()
to mark your isomorphic code:
import { packMe } from 'packen'
packMe()
// this file will be included in the bundle
You can also conditionally mark a file:
if (condition()) {
packMe()
}
// this file will be included in the bundle
// only if condition() is true.
export function func() {
packMe()
// this file will be included in the bundle
// only when func() is called.
// ...
}
You can create the bundle either programmatically or using the CLI. packen is mainly designed to be used programmatically (as part of other tooling), but the CLI route offers a convenient out-of-the-box method suitable for static site generation (SSG), albeit with limited configuration options.
node -r packen/r sever_code.js
ts-node -r packen/r server_code.ts
☝️ This will execute server_code.js
, and bundle any marked isomorphic code in an output bundle.js
. You can customize the output file by providing the PACKEN_TO
environment variable:
PACKEN_TO=dist/chunk.js node -r packen/r server_code.js
PACKEN_TO=dist/chunk.js ts-node -r packen/r server_code.ts
import { Bundle, build } from 'packen/server'
//
// 👉 STEP 1: create a bundle
//
const bundle = new Bundle()
//
// 👉 STEP 2: run your isomorphic code
//
import './my/iso.js'
...
//
// 👉 STEP 3: build the bundle
//
await build(bundle, 'dist/chunk.js')
A Bundle
MUST have been created before the isomorphic code is executed. Calling packMe()
when no bundle is created will have no effect. Additionally, when you build a bundle, it is closed, which means it can no longer collect isomorphic code, and you need to make a new bundle.
packen
provides various methods for building bundles:
build()
: creates a bundle from collected code using esbuild and writes it to given file.pack()
: creates a bundle from collected code using esbuild and returns it as a string.write()
: creates an entry point from collected code and writes it to given file. You might need to bundle this entry file using your own bundler before shipping it to client (tools such as Vite can consume it directly).serialize()
: creates an entry point from collected code and returns it as a string.
build(bundle: Bundle, path: string, processor?: Processor): void
Builds given bundle, bundles and minifies it (using esbuild) and writes it to given path. If a processor is provided, will be used for processing earmarked entries (see Extension).
import { Bundle, build } from 'packen/server'
const bundle = new Bundle()
// ...
build(bundle, 'dist/bundle.js')
pack(bundle: Bundle, processor?: Processor): string
Builds given bundle, returning the bundled and minified code as a strin (uses esbuild). If a processor is provided, will be used for processing earmarked entries (see Extension).
import { Bundle, pack } from 'packen/server'
const bundle = new Bundle()
// ...
const code = pack(bundle)
This method can be used for generating server-side HTML:
const myHTML = html`
<html>
<head>
<script type="module">
${pack(bundle)}
</script>
</head>
<body>
<!-- ... -->
</body>
</html>
`
write(bundle: Bundle, path: string, processor?: Processor): void
Builds an entry file for given bundle, and writes it to given path. If a processor is provided, will be used for processing earmarked entries (see Extension). DOES NOT bundle or minify the code.
import { Bundle, write } from 'packen/server'
const bundle = new Bundle()
// ...
write(bundle, 'dist/entry.js')
This entry file can be used with other bundlers, like Vite:
<!-- index.html -->
<script type="module" src="dist/entry.js"></script>
<!-- ... -->
vite build
serialize(bundle: Bundle, processor?: Processor): string
Builds an entry file for given bundle, returning the code as a string. If a processor is provided, will be used for processing earmarked entries (see Extension). DOES NOT bundle or minify the code.
import { Bundle, serialize } from 'packen/server'
const bundle = new Bundle()
// ...
const code = serialize(bundle)
This is particularly useful when you want to use other bundlers programmatically (or even using esbuild with some custom configuration):
import { build } from 'esbuild'
await build({
stdin: {
contents: serialize(bundle, processor),
resolveDir: process.cwd(),
},
// your esbuild configuration
})
👉 Use packCaller()
from inside a function to pack the code that called that function:
import { packCaller } from 'packen'
// any code calling func will now be packed automatically.
export function func() {
packCaller()
// ...
}
👉 Use mark()
and collect()
to create a mark that can be called later (perhaps due to some later event):
import { mark, collect } from 'packen'
const flag = mark()
// when laterCallback is called, this file will be collected and bundled.
export function laterCallback() {
collect(flag)
// ...
}
👉 Use markCaller()
and collect()
to create a mark for the calling code that can be called later (perhaps due to some later event):
import { markCaller, collect } from 'packen'
function someFunc() {
const flag = markCaller()
// when this is called, the code that called someFunc will be collected and bundled.
return () => collect(flag)
}
By default, packen will use bare imports, collecting files as side-effect:
// entry file
import '/Path/to/some/isomorphic.js'
// ...
You can change this behavior by providing a processor function to bundling functions. This processor will be passed a CallSite
, and should turn it into some valid JavaScript string. For example, the following custom processor allows collecting specific functions, which then will be executed on client side:
export dryRun = entry => {
const func = entry.getFunctionName()
const file = entry.getFileName()
return `
import { ${func} } from '${file}';
${func}();
`
}
This processor can be used like this:
// ...
build(bundle, 'dist/bundle.js', dryRun)
// isomorphic code
import { packMe } from 'packen'
export const myFunc = () => {
// this function will be collected for client side bundling
// and executed on client bootstrap.
packMe()
// do other stuff
}
You need node, NPM to start and git to start.
# clone the code
git clone git@github.com:loreanvictor/packen.git
# install stuff
npm i
Make sure all checks are successful on your PRs. This includes all tests passing, high code coverage, correct typings and abiding all the linting rules. The code is typed with TypeScript, Jest is used for testing and coverage reports, ESLint and TypeScript ESLint are used for linting. Subsequently, IDE integrations for TypeScript and ESLint would make your life much easier (for example, VSCode supports TypeScript out of the box and has this nice ESLint plugin), but you could also use the following commands:
# run tests
npm test
# check code coverage
npm run coverage
# run linter
npm run lint