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/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()],})export class AppModule {}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.js', 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.js', url: 'https://unpkg.com/htmx.org@2/dist/htmx.min.js',});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.js';
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; /** Add the defer attribute to the generated script tag. */ defer?: boolean; /** Embed content inline in a <script> or <style> tag instead of linking. */ inline?: boolean; /** Ordering constraints relative to other assets. */ dependencies?: Dependency[];}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.
Dependency ordering
Section titled “Dependency ordering”Use Before() and After() to declare load order constraints. The asset manager performs a topological sort before injecting tags:
import { StaticAsset, Before } from '@voltage/asset-manager';
export const VendorScript = StaticAsset({ name: 'vendor.js', file: join(__dirname, 'vendor.js'),});
export const AppScript = StaticAsset({ name: 'app.js', file: join(__dirname, 'app.js'), dependencies: [After(VendorScript)],});After(VendorScript) ensures app.js is rendered after vendor.js. Before(target) is the inverse — use it when the asset being declared should precede another.