phug/js-transformer-filter
A Phug filter that pipes JavaScript through a configurable transformer during template rendering, enabling inline script preprocessing (such as Babel transforms) while keeping JS embedded in Pug templates.
phug/js-transformer-filter is a thin adapter: it does not transform anything by itself, it just exposes a js-transformer instance as a Phug filter. The very first thing to understand is that you need two packages: the filter and at least one concrete transformer (a Babel/TypeScript/whatever adapter from the js-transformer family, or your own object).
composer require phug/js-transformer-filter
# plus a transformer, e.g. one of the js-transformer adapters
composer require kylekatarnls/js-transformer-babel # example, pick what you need
Then register the filter on a renderer. Filters in Phug are looked up by the name you give them, and that name is what appears after the leading colon in templates:
use Phug\Renderer;
use Phug\JsTransformerFilter\JsTransformerFilter;
$renderer = new Renderer([
'pretty' => true,
'filters' => [
'js' => new JsTransformerFilter($transformer), // $transformer is any js-transformer instance
],
]);
First use case — transpile inline TypeScript inside a Pug template at render time:
script
:js
const greeting: string = 'hello';
console.log(greeting);
The block body is handed to the transformer, and whatever string it returns is embedded in place of the block. If that round-trip works, the integration is wired correctly; anything else is configuration.
Where to look first:
README.md and src/ — the package is only a couple of files, so read the whole filter class. The constructor signature tells you exactly what it expects of a "transformer" (__invoke($code) vs ->transform($code)).js-transformer core package, to see the contract your transformer must satisfy and what options it accepts.:filter syntax, 'filters' option, filter chaining) — that behaviour is defined by Phug, not by this package.One filter name per transformer. Because the filter is generic, the natural pattern is to register several instances under distinct names and use the one each template needs:
'filters' => [
'ts' => new JsTransformerFilter($typescript),
'es5' => new JsTransformerFilter($babel),
],
script
:ts
const answer: number = 42;
Register per-renderer, not globally. The filter holds a stateful transformer instance. If you have different transformer configurations for admin vs. front-end bundles, build separate Renderer instances rather than mutating filter options mid-flight.
Wrap it in a module/extension for larger apps. Since Phug supports modules/extensions, the clean pattern is a small extension that registers the filter with a container-resolved transformer. That keeps the transformer's constructor options (plugins, presets, plugin paths) in your config layer instead of in the template call site.
Chain with other filters. Phug applies filters in the :a:b order written. :js:highlight (transform first, then highlight) is usually what you want; reversed order will highlight code that is about to change.
Keep transformed blocks self-contained. The transformer sees only the indented text block, not the surrounding tag, attributes, or other blocks. Treat each block as an independent snippet; don't rely on variables defined in a previous :js block being visible to the next one unless the transformer is intentionally stateful.
Use it as a build-time step where possible. The filter runs at template render/compile time. For production assets, prefer extracting the script content once (or pre-compiling the Pug template and shipping the transpiled JS) over relying on the transformer on every request — the transform is not free, and Phug's template cache will not cache the transformer output separately from the compiled template.
It is a text-block filter, not an asset pipeline. :js only applies to indented text under a tag or at the root. It will not touch script(src="..."), inline attributes like onclick="...", or JS embedded inside string attributes. If you need those transformed, do it before passing data into the template.
Caching is the #1 source of "it didn't change". Phug caches compiled templates. When you change transformer options or bump the transformer version, the cached compiled Pug can still be served. Clear the cache directory or disable caching in development before you conclude the filter is broken.
Errors surface as template errors, not JS errors. A transformer throwing on invalid syntax will abort the render with a Phug-level exception that often points at the template line, not the actual offending JS line. Catch and rethrow with the block body prepended, or debug by feeding the same snippet directly to the transformer outside Phug first.
No source maps. The output is a plain string substituted into the template. Column/line mapping back to the Pug source is lost; if debuggability matters, emit a comment marker (e.g. /* transformed: file.pug */) in your transformer wrapper.
External tooling dependency. Most js-transformer adapters shell out to a Node binary or require a Node toolchain. That is fine locally and painful in CI/production. Check the adapter's requirements and fail loudly at container build time, not at first render.
Version pinning. The package has essentially no dependents and a very small surface; the transformer ecosystem around it moves independently. Pin both the filter and the transformer (composer require with exact constraints or a lock file) so a transformer upgrade does not silently change what your templates emit.
Extension points. If the built-in filter's constructor doesn't fit your transformer, the cleanest escape hatch is a tiny subclass overriding the invocation method, or simply a Closure registered under 'filters' — Phug accepts callables, so fn ($text) => $myTransformer->transform($text) is often enough and avoids the adapter entirely. Use the package when you want the standard contract and a named, reusable class; skip it when a closure is clearer.
Transformer options belong in config, not templates. Presets, plugin lists, and parser options are constructor concerns. Passing them through the template keeps rendering non-deterministic and makes the cache harder to reason about.
How can I help you explore Laravel packages today?