Asset Manager
@voltage/asset-manager manages client-side assets — JavaScript files, stylesheets, or any static resource — for NestJS applications. Assets are served at hashed URLs for long-lived caching, included per-request via a decorator or programmatically, and injected into <head> automatically after rendering. Load order is resolved through a dependency graph, so you never need to worry about script ordering by hand.
Installation
Section titled “Installation”yarn add @voltage/asset-manager @voltage/label @voltage/logger @voltage/jsxBasic usage
Section titled “Basic usage”forRoot()
Section titled “forRoot()”Call forRoot() once in your root module. It registers the AssetManager service globally and installs the include interceptor:
import { AssetModule } from '@voltage/asset-manager';
@Module({ imports: [AssetModule.forRoot({ watch: process.env.NODE_ENV === 'development' })],})export class AppModule {}Pass watch: true to enable chokidar file watchers. When active, BundleAsset instances recompile whenever any of their input files change. See Configuration for all options.
forFeature()
Section titled “forFeature()”Register asset classes in the module that owns them. forFeature() makes each asset available to the DI container and registers its HTTP middleware for serving:
import { AssetModule } from '@voltage/asset-manager';import { AppScript } from './app.script';
@Module({ imports: [AssetModule.forFeature([AppScript])],})export class AppModule {}Defining assets
Section titled “Defining assets”Static assets
Section titled “Static assets”StaticAsset() creates an asset backed by a file on disk. The MIME type is inferred from the file extension:
import { StaticAsset } from '@voltage/asset-manager';import { join } from 'path';
export const AppScript = StaticAsset({ name: 'app', file: join(__dirname, 'app.js'),});External assets
Section titled “External assets”ExternalAsset() fetches a remote resource. The content is downloaded at bootstrap to compute the hash; subsequent requests are served from the remote URL:
import { ExternalAsset } from '@voltage/asset-manager';
export const HtmxScript = ExternalAsset({ name: 'htmx', url: 'https://unpkg.com/htmx.org@2/dist/htmx.min.js',});Bundled assets
Section titled “Bundled assets”BundleAsset() compiles a JavaScript or TypeScript entry point with esbuild into a single in-memory bundle. The hash is computed from the bundle output, so the immutable-cache contract is maintained automatically.
import { BundleAsset } from '@voltage/asset-manager';import { join } from 'path';
export const AppScript = BundleAsset({ name: 'app', entry: join(__dirname, 'client/app.ts'), tags: [$$attribute({ defer: true })], esbuild: { external: ['petite-vue'], minify: true, target: 'es2020', },});Any option that esbuild’s BuildOptions accepts can be passed through esbuild, except entryPoints, bundle, write, metafile, outfile, and outdir — those are managed internally. When watch: true is set in forRoot(), the bundle recompiles on every file change and all input files discovered by esbuild (including transitive imports) are watched automatically.
Custom assets
Section titled “Custom assets”Extend Asset directly when you need to generate content at runtime:
import { Asset } from '@voltage/asset-manager';import { Readable } from 'stream';
export class InlineConfigAsset extends Asset { name = 'config';
content(): Readable { return Readable.from(`window.__CONFIG__ = ${JSON.stringify(this.config)};`); }
type(): string { return 'application/javascript'; }
length(): number { return Buffer.byteLength(/* ... */); }}Options
Section titled “Options”All asset factories accept a common set of options:
interface AbstractAssetOptions { /** Identifier used in the served URL: /assets/managed/{name} */ name: string; /** Labels that control load order, rendered attributes, and other behaviours. */ tags?: AnyLabel[]; /** Embed content inline in a <script> or <style> tag instead of linking. */ inline?: boolean;}Including assets
Section titled “Including assets”@Include()
Section titled “@Include()”Attach @Include() to a controller method or a component render() method. The asset is added to the current request’s inclusion list automatically:
import { Include } from '@voltage/asset-manager';import { AppScript } from './app.script';
@Controller('dashboard')export class DashboardController { @Get() @Include(AppScript) index() { return this.view.render(); }}On a JSX component, @Include() fires when the component is rendered — useful when the component itself owns the asset it needs:
@Injectable()export class ChartComponent extends InjectableComponent { @Include(ChartScript) render() { return <canvas id="chart" />; }}Programmatic inclusion
Section titled “Programmatic inclusion”Inject AssetManager and call include() when you need conditional logic:
@Injectable()export class WidgetService { constructor(private readonly assets: AssetManager) {}
async render(type: string) { if (type === 'chart') { this.assets.include(ChartScript); } }}Serving and caching
Section titled “Serving and caching”Assets are served at /assets/managed/{name}. The content hash is appended as a query string — assetManager.url(AppScript) returns /assets/managed/app.js?v=a1b2c3d4. Use this when you need to reference an asset URL in a template:
@Injectable()export class AppView extends InjectableComponent { constructor(private readonly assets: AssetManager) { super(); }
render() { return ( <img src={this.assets.url(FaviconAsset)} alt="Logo" /> ); }}Responses for hashed URLs carry cache-control: public, max-age=31536000, immutable. Browsers cache them forever; the hash changes when the content changes. ETag validation is also supported — unchanged assets return 304 Not Modified.
Load order
Section titled “Load order”Use $$before and $$after to declare load order constraints. The asset manager performs a topological sort before injecting tags into the page:
import { StaticAsset, $$after } from '@voltage/asset-manager';
export const VendorScript = StaticAsset({ name: 'vendor', file: join(__dirname, 'vendor.js'),});
export const AppScript = StaticAsset({ name: 'app', file: join(__dirname, 'app.js'), tags: [$$after(VendorScript)],});$$after(target) ensures app.js is rendered after vendor.js. $$before(target) is the inverse — declare it on the asset that should come first.
Tag attributes
Section titled “Tag attributes”Use $$attribute to add arbitrary HTML attributes to the rendered <script> or <link> tag. Pass true for boolean attributes, a string for valued attributes, or false to omit:
import { StaticAsset, $$attribute } from '@voltage/asset-manager';
export const PetiteVueScript = StaticAsset({ name: 'petite-vue', file: join(__dirname, 'petite-vue.js'), tags: [$$attribute({ defer: true, init: true })],});This produces <script src="..." defer init></script>. Multiple $$attribute tags are merged in order.
Configuration
Section titled “Configuration”interface AssetModuleOptions { /** * Enable chokidar file watchers for BundleAsset instances. * Intended for development only. * @default false */ watch?: boolean;}