Digging Deeper
JavaScript Libraries
Bundle browser libraries with lazy js while keeping app-owned scripts easy to debug.
Split app code from library code
lazy js builds JavaScript libraries, not application scripts.
The sample app keeps app-owned browser code as a normal module:
app/public/javascript/application.js
That file is loaded directly by the layout:
<script type="module" src="/javascript/application.js"></script>
Library code is generated separately:
app/public/assets/lazyshaft/
app/public/assets/importmap.json
This keeps third-party code production-ready while user scripts stay easy to inspect in the browser with their normal filenames.
Declare library entrypoints
The default manifest is intentionally short:
[entrypoint.turbo]
module = "@hotwired/turbo"
[entrypoint.stimulus]
module = "@hotwired/stimulus"
Each [entrypoint.<name>] block declares one library bundle. module is the
package import that esbuild should bundle.
Entrypoints also have a group. The default is default, so the short
manifest above keeps the current behavior.
The command supplies defaults for the common paths:
package.json
app/public/assets/lazyshaft
app/public/assets/importmap.json
Run the pipeline from the application module root:
lazy js
The command prepares dependencies, runs the package manager install, bundles the manifest entrypoints, and writes the importmap.
Group shared dependencies
By default, all entrypoints are in the same group:
[entrypoint.turbo]
module = "@hotwired/turbo"
[entrypoint.stimulus]
module = "@hotwired/stimulus"
When multiple entrypoints in a group import the same resolved dependency, esbuild can write a shared chunk for that group. If there is nothing to share, the group behaves like a no-op and only the entrypoint bundles are written.
Use explicit groups when bundles should not share runtime chunks:
[entrypoint.public]
group = "site"
module = "public-library"
[entrypoint.admin]
group = "admin"
module = "admin-library"
With shared bundling enabled, lazy js runs esbuild once per group. The default
manifest still runs one esbuild build. If [bundle] shared = false, groups do
not affect output.
Package versions are resolved by the package manager and lockfile, not by the
group setting. If two libraries resolve the same dependency to the same
installed package, esbuild can share it inside a group. If they resolve
different physical versions, esbuild treats them as different modules and does
not merge them. To use two top-level versions of one package, declare package
manager aliases in package.json; lazy js preserves existing dependency
versions and only adds missing manifest packages as latest.
Import libraries from app scripts
App scripts import libraries by package name:
import "@hotwired/turbo"
import { Application } from "@hotwired/stimulus"
window.Stimulus = Application.start()
The generated importmap maps those names to bundled public assets:
{
"imports": {
"@hotwired/stimulus": "/assets/lazyshaft/stimulus-FNW3RRR2.js",
"@hotwired/turbo": "/assets/lazyshaft/turbo-QDEQVE6U.js"
}
}
Layouts inline the importmap through the asset helper:
{{importmap "/assets/importmap.json"}}
<script type="module" src="/javascript/application.js"></script>
The importmap must be available before the module script that uses it.
Use explicit imports when needed
By default, an entrypoint maps its module value in the importmap. If the
browser import name should be different from the bundled module path, set
imports:
[entrypoint.monaco]
module = "monaco-editor/esm/vs/editor/editor.api.js"
imports = ["monaco-editor"]
Application code can then import the shorter name:
import * as monaco from "monaco-editor"
Include workers and assets
Some libraries load extra JavaScript workers or supporting files dynamically. Declare those files in the same entrypoint block:
[entrypoint.monaco]
module = "monaco-editor/esm/vs/editor/editor.api.js"
imports = ["monaco-editor"]
workers = [
"monaco-editor/esm/vs/editor/editor.worker.js",
"monaco-editor/esm/vs/language/json/json.worker.js",
"monaco-editor/esm/vs/language/css/css.worker.js",
"monaco-editor/esm/vs/language/html/html.worker.js",
"monaco-editor/esm/vs/language/typescript/ts.worker.js",
]
assets = [
"node_modules/monaco-editor/min/vs/**/*",
]
workers are bundled as extra JavaScript outputs. assets are copied into the
lazyshaft output directory.
Use extra_files as a general alias when the extra JavaScript file is not a
worker:
[entrypoint.library]
module = "library"
extra_files = ["library/extra-runtime.js"]
Modify the manifest from Go
Tools can edit js.toml through the same manifest and pipeline code used by
lazy js:
import jscommand "github.com/golazy/lazy/commands/js"
editor, err := jscommand.OpenManifest(appDir)
if err != nil {
return err
}
if err := editor.AddEntrypoint(jscommand.Entrypoint{
Name: "monaco",
Group: "editors",
Module: "monaco-editor/esm/vs/editor/editor.api.js",
Imports: []string{"monaco-editor"},
}); err != nil {
return err
}
if err := editor.Close(); err != nil {
return err
}
Close writes the manifest, runs the JavaScript pipeline, and returns an error
if dependency preparation, installation, or bundling fails. On failure it
restores js.toml, package manager metadata, the importmap, and the lazyshaft
output directory to their previous state. The returned
*jscommand.ManifestCloseError includes the manifest diff and the complete
pipeline output.
Commit generated outputs
lazy js writes files that production Go builds embed:
package-lock.json
app/public/assets/importmap.json
app/public/assets/lazyshaft/*.js
Commit those files with the manifest and package changes. Do not commit
node_modules.
Before committing JavaScript-related changes, run:
lazy js
go test ./...
For release or deployment checks, also run the race detector, go vet, and a
production build.