@rhinostone/swig 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changes/v2.1.0.md +8 -0
- package/.changes/v2.2.0.md +8 -0
- package/.eslintrc.json +8 -1
- package/HISTORY.md +18 -0
- package/README.md +13 -1
- package/ROADMAP.md +13 -0
- package/dist/swig.js +609 -67
- package/dist/swig.min.js +24 -6
- package/dist/swig.min.js.map +1 -1
- package/lib/async/pre-walker.js +279 -0
- package/lib/filters.js +23 -20
- package/lib/swig.js +157 -2
- package/lib/tags/import.js +42 -21
- package/lib/tags/include.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[2.1.0](https://github.com/gina-io/swig/tree/v2.1.0) / 2026-05-10
|
|
2
|
+
-----------------------------------------------------------------
|
|
3
|
+
|
|
4
|
+
* **Added** `@rhinostone/swig` and `@rhinostone/swig-twig` gain `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` for use with async-only loaders (S3, Redis, CDN, fetch-backed). The implementation pre-walks the template dependency graph through the user loader's cb-shape arm (`loader.load(path, cb)`), builds an in-memory map keyed by resolved path, then runs the existing sync render path against an in-memory wrapper for the duration of the call. The pre-walker text-scans for `extends` / `include` / `import` (and Twig `from`) string-literal targets only — dynamic paths (`{% extends parent_var %}`) are not pre-resolved and surface a `Pre-walked map missing path` error at render time, with full async-parse support for dynamic paths tracked separately. Isolation strategy is transactional `self.options.loader` mutation for the duration of the sync render — safe under JS event-loop semantics because sync render never yields, so concurrent `renderFileAsync` calls don't interleave their render phases; documented caveat is not to externally mutate `swig.options.loader` mid-flight on the same instance while async renders are pending. Pre-walker modules live at `lib/async/pre-walker.js` (native) and `packages/swig-twig/lib/async/pre-walker.js` (Twig) — per-flavor copies because the rawTag (`raw` vs `verbatim`) and keyword set differ. Existing sync `renderFile` / `compileFile` consumers are unaffected.
|
|
5
|
+
|
|
6
|
+
* **Changed** Internal scaffolding for a future async parse path. `@rhinostone/swig-core` gains four deferred-resolution IR shapes (`IRExtendsDeferred`, `IRIncludeDeferred`, `IRImportDeferred`, `IRFromImportDeferred`) and matching backend emit branches that produce `AsyncFunction`-wrapped template bodies. The async emit flows through a new `_swig.getTemplate(<path>, options)` runtime helper that returns `Promise<TemplateFn>`, and async-mode template functions return `Promise<{output, exports}>` so importers can pick up macro definitions at runtime. Block override resolution rides on a new sixth `_blocks` positional parameter on the wrapped AsyncFunction. All gated behind an explicit `options.codegenMode === 'async'` flag — default is sync and no behavior changes for existing consumers; the frontend tag wiring and public API dispatch that would activate this path are not yet shipped. Mock coverage at `tests/swig-core/async-emit.test.js` (63 cases across the four deferred shapes plus macro exports and block override). ESLint `parserOptions.ecmaVersion` bumped to 2017 for the AsyncFunction literal.
|
|
7
|
+
|
|
8
|
+
* **Changed** Internal cleanup. `@rhinostone/swig-core` `IRVarRef` emit shape simplified from a nested-ternary dot-walk into a single-evaluation ternary expression with the same runtime semantics, producing smaller compiled bodies and easier-to-read `precompile().tpl.toString()` output. The legacy twin in `lib/tokenparser.js`'s `checkMatch` updated to match. Phase 2 byte-identity test rewritten to assert the new shape; multi-segment substring assertions still hold unchanged. New `benchmarks/` directory with a runnable render-throughput suite comparing `@rhinostone/swig` against Nunjucks (`benchmarks/render.js`), excluded from the published tarball via `.npmignore`. README clarifications surfaced the prototype-pollution hardening across CVE-2023-25345 / CVE-2021-44906 and added a benchmark pointer.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[2.2.0](https://github.com/gina-io/swig/tree/v2.2.0) / 2026-05-11
|
|
2
|
+
-----------------------------------------------------------------
|
|
3
|
+
|
|
4
|
+
* **Added** `renderFile(path, locals, cb)` and `compileFile(path, options, cb)` automatically route to the async-codegen path when the configured loader signals async support via `loader.async === true`. The async path defers template resolution from parse time to render time via a new `_swig.getTemplate(path, options)` runtime helper that returns `Promise<TemplateFn>` — `extends`, `include`, `import`, and `from` emit deferred IR shapes from the frontend (`IRExtendsDeferred`, `IRIncludeDeferred`, `IRImportDeferred`, `IRFromImportDeferred`) and the shared backend wraps the compiled body in an `AsyncFunction`. Block overrides thread through the inheritance chain via a sixth `_blocks` positional argument on the wrapped template function; macro imports pick up exports via the new `Promise<{output, exports}>` template-fn return shape. Both `@rhinostone/swig` and `@rhinostone/swig-twig` flavors — parity across the two surfaces. Static template targets (string literals in `extends` / `include` / `import` / `from`) work end-to-end against async loaders; dynamic targets (`{% extends parent_var %}`, `{% include user_template %}`) are not yet supported on the async path and surface a clear runtime error with the unresolved expression visible in the message — full dynamic-target support is tracked as a follow-up. The sync render path is unchanged — loaders without `loader.async === true` continue to use the established sync `_swig.compileFile(...)` resolution, including the built-in `loaders.fs` and `loaders.memory` which remain dual-mode. End-to-end coverage at `tests/async/render-file-cb-dispatch.test.js` (native, 11 cases) and `tests/swig-twig/async/render-file-cb-dispatch.test.js` (Twig, 11 cases) covers static extends chains, includes, macro imports, mixed graphs, dynamic include paths surfacing the runtime error, the `ignoreMissing` flag, `with-context` isolation, bare-name and aliased `from` import, and error propagation.
|
|
5
|
+
|
|
6
|
+
* **Changed** `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` on both `@rhinostone/swig` and `@rhinostone/swig-twig` are soft-deprecated via JSDoc only — no runtime warning, no `console.warn`. Use `renderFile(path, locals, cb)` / `compileFile(path, options, cb)` with an async loader (`loader.async === true`) instead; the dispatch is automatic. The legacy pre-walker entry points remain fully functional in 2.x and will be removed in 3.0.
|
|
7
|
+
|
|
8
|
+
* **Changed** Improved the performance of the `escape` / `e` filter in both `@rhinostone/swig` and `@rhinostone/swig-twig`. The HTML default branch now runs a two-pass form — an entity-aware first pass that preserves already-escaped sequences (`&`, `<`, `>`, `"`, `'`) followed by a single character-class regex `[<>"']` with a lookup function for the rest — instead of five sequential single-character regex passes. A scalar fast-path also skips `iterateFilter.apply` when input is null, undefined, or a non-object. Output is byte-identical to the previous behavior on every input, including the entity-preservation semantics swig has shipped since the upstream fork. Measured against `benchmarks/render.js` (medians of 5 runs, autoescape on, Node 25), simple-var-output goes from ~2.46M to 3.86M ops/s (+57%, flipping the verdict from `nunjucks 1.32x faster` to `swig 1.18x faster`); filter chain +37%; for-loop (5 items) +54%; if/else branch +71%; nested for+if+filter +56%. The `case 'js'` branch and array-iteration fallback through `iterateFilter` are unchanged. All 1443 tests pass including the 9 CVE regressions.
|
package/.eslintrc.json
CHANGED
package/HISTORY.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
[2.2.0](https://github.com/gina-io/swig/tree/v2.2.0) / 2026-05-11
|
|
2
|
+
-----------------------------------------------------------------
|
|
3
|
+
|
|
4
|
+
* **Added** `renderFile(path, locals, cb)` and `compileFile(path, options, cb)` automatically route to the async-codegen path when the configured loader signals async support via `loader.async === true`. The async path defers template resolution from parse time to render time via a new `_swig.getTemplate(path, options)` runtime helper that returns `Promise<TemplateFn>` — `extends`, `include`, `import`, and `from` emit deferred IR shapes from the frontend (`IRExtendsDeferred`, `IRIncludeDeferred`, `IRImportDeferred`, `IRFromImportDeferred`) and the shared backend wraps the compiled body in an `AsyncFunction`. Block overrides thread through the inheritance chain via a sixth `_blocks` positional argument on the wrapped template function; macro imports pick up exports via the new `Promise<{output, exports}>` template-fn return shape. Both `@rhinostone/swig` and `@rhinostone/swig-twig` flavors — parity across the two surfaces. Static template targets (string literals in `extends` / `include` / `import` / `from`) work end-to-end against async loaders; dynamic targets (`{% extends parent_var %}`, `{% include user_template %}`) are not yet supported on the async path and surface a clear runtime error with the unresolved expression visible in the message — full dynamic-target support is tracked as a follow-up. The sync render path is unchanged — loaders without `loader.async === true` continue to use the established sync `_swig.compileFile(...)` resolution, including the built-in `loaders.fs` and `loaders.memory` which remain dual-mode. End-to-end coverage at `tests/async/render-file-cb-dispatch.test.js` (native, 11 cases) and `tests/swig-twig/async/render-file-cb-dispatch.test.js` (Twig, 11 cases) covers static extends chains, includes, macro imports, mixed graphs, dynamic include paths surfacing the runtime error, the `ignoreMissing` flag, `with-context` isolation, bare-name and aliased `from` import, and error propagation.
|
|
5
|
+
|
|
6
|
+
* **Changed** `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` on both `@rhinostone/swig` and `@rhinostone/swig-twig` are soft-deprecated via JSDoc only — no runtime warning, no `console.warn`. Use `renderFile(path, locals, cb)` / `compileFile(path, options, cb)` with an async loader (`loader.async === true`) instead; the dispatch is automatic. The legacy pre-walker entry points remain fully functional in 2.x and will be removed in 3.0.
|
|
7
|
+
|
|
8
|
+
* **Changed** Improved the performance of the `escape` / `e` filter in both `@rhinostone/swig` and `@rhinostone/swig-twig`. The HTML default branch now runs a two-pass form — an entity-aware first pass that preserves already-escaped sequences (`&`, `<`, `>`, `"`, `'`) followed by a single character-class regex `[<>"']` with a lookup function for the rest — instead of five sequential single-character regex passes. A scalar fast-path also skips `iterateFilter.apply` when input is null, undefined, or a non-object. Output is byte-identical to the previous behavior on every input, including the entity-preservation semantics swig has shipped since the upstream fork. Measured against `benchmarks/render.js` (medians of 5 runs, autoescape on, Node 25), simple-var-output goes from ~2.46M to 3.86M ops/s (+57%, flipping the verdict from `nunjucks 1.32x faster` to `swig 1.18x faster`); filter chain +37%; for-loop (5 items) +54%; if/else branch +71%; nested for+if+filter +56%. The `case 'js'` branch and array-iteration fallback through `iterateFilter` are unchanged. All 1443 tests pass including the 9 CVE regressions.
|
|
9
|
+
|
|
10
|
+
[2.1.0](https://github.com/gina-io/swig/tree/v2.1.0) / 2026-05-10
|
|
11
|
+
-----------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
* **Added** `@rhinostone/swig` and `@rhinostone/swig-twig` gain `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` for use with async-only loaders (S3, Redis, CDN, fetch-backed). The implementation pre-walks the template dependency graph through the user loader's cb-shape arm (`loader.load(path, cb)`), builds an in-memory map keyed by resolved path, then runs the existing sync render path against an in-memory wrapper for the duration of the call. The pre-walker text-scans for `extends` / `include` / `import` (and Twig `from`) string-literal targets only — dynamic paths (`{% extends parent_var %}`) are not pre-resolved and surface a `Pre-walked map missing path` error at render time, with full async-parse support for dynamic paths tracked separately. Isolation strategy is transactional `self.options.loader` mutation for the duration of the sync render — safe under JS event-loop semantics because sync render never yields, so concurrent `renderFileAsync` calls don't interleave their render phases; documented caveat is not to externally mutate `swig.options.loader` mid-flight on the same instance while async renders are pending. Pre-walker modules live at `lib/async/pre-walker.js` (native) and `packages/swig-twig/lib/async/pre-walker.js` (Twig) — per-flavor copies because the rawTag (`raw` vs `verbatim`) and keyword set differ. Existing sync `renderFile` / `compileFile` consumers are unaffected.
|
|
14
|
+
|
|
15
|
+
* **Changed** Internal scaffolding for a future async parse path. `@rhinostone/swig-core` gains four deferred-resolution IR shapes (`IRExtendsDeferred`, `IRIncludeDeferred`, `IRImportDeferred`, `IRFromImportDeferred`) and matching backend emit branches that produce `AsyncFunction`-wrapped template bodies. The async emit flows through a new `_swig.getTemplate(<path>, options)` runtime helper that returns `Promise<TemplateFn>`, and async-mode template functions return `Promise<{output, exports}>` so importers can pick up macro definitions at runtime. Block override resolution rides on a new sixth `_blocks` positional parameter on the wrapped AsyncFunction. All gated behind an explicit `options.codegenMode === 'async'` flag — default is sync and no behavior changes for existing consumers; the frontend tag wiring and public API dispatch that would activate this path are not yet shipped. Mock coverage at `tests/swig-core/async-emit.test.js` (63 cases across the four deferred shapes plus macro exports and block override). ESLint `parserOptions.ecmaVersion` bumped to 2017 for the AsyncFunction literal.
|
|
16
|
+
|
|
17
|
+
* **Changed** Internal cleanup. `@rhinostone/swig-core` `IRVarRef` emit shape simplified from a nested-ternary dot-walk into a single-evaluation ternary expression with the same runtime semantics, producing smaller compiled bodies and easier-to-read `precompile().tpl.toString()` output. The legacy twin in `lib/tokenparser.js`'s `checkMatch` updated to match. Phase 2 byte-identity test rewritten to assert the new shape; multi-segment substring assertions still hold unchanged. New `benchmarks/` directory with a runnable render-throughput suite comparing `@rhinostone/swig` against Nunjucks (`benchmarks/render.js`), excluded from the published tarball via `.npmignore`. README clarifications surfaced the prototype-pollution hardening across CVE-2023-25345 / CVE-2021-44906 and added a benchmark pointer.
|
|
18
|
+
|
|
1
19
|
[2.0.1](https://github.com/gina-io/swig/tree/v2.0.1) / 2026-05-10
|
|
2
20
|
-----------------------------------------------------------------
|
|
3
21
|
|
package/README.md
CHANGED
|
@@ -29,11 +29,23 @@ Features
|
|
|
29
29
|
* [Express](http://expressjs.com/) compatible.
|
|
30
30
|
* Object-Oriented template inheritance.
|
|
31
31
|
* Apply filters and transformations to output in your templates.
|
|
32
|
-
*
|
|
32
|
+
* **Hardened against prototype-pollution** — `__proto__` / `constructor` / `prototype` blocked at parser, tag-side, and IR-emission layers. CVE-2023-25345 fully patched. 9 regression cases under [`tests/regressions.test.js`](./tests/regressions.test.js).
|
|
33
|
+
* Automatically escapes all variable output (HTML by default; configurable per-call).
|
|
33
34
|
* Lots of iteration and conditionals supported.
|
|
34
35
|
* Robust without the bloat.
|
|
35
36
|
* Extendable and customizable — register custom filters, tags, and loaders per-instance.
|
|
36
37
|
|
|
38
|
+
Benchmarks
|
|
39
|
+
----------
|
|
40
|
+
|
|
41
|
+
[`benchmarks/render.js`](./benchmarks/render.js) measures sync-render throughput across five workload shapes against [Nunjucks](https://www.npmjs.com/package/nunjucks).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd benchmarks && npm install && node render.js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
In production-typical settings (autoescape on), `@rhinostone/swig` outperforms Nunjucks on iteration-heavy templates by 2–3.5× and ties on simple control flow. See [`benchmarks/README.md`](./benchmarks/README.md) for the methodology, the full result table, and how to reproduce on your own hardware.
|
|
48
|
+
|
|
37
49
|
Need Help? Have Questions? Comments?
|
|
38
50
|
------------------------------------
|
|
39
51
|
|
package/ROADMAP.md
CHANGED
|
@@ -14,6 +14,7 @@ _No near-term scheduled items. See [Future (post-2.0)](#future-post-20) for upco
|
|
|
14
14
|
|
|
15
15
|
| Status | Item |
|
|
16
16
|
| --- | --- |
|
|
17
|
+
| Planned | Async parse path for dynamic targets — full support for `{% extends parent_var %}`, `{% include user_template %}`, and runtime-resolved `import` / `from` paths on the async-codegen branch. Static-target async dispatch shipped in 2.2.0; dynamic-target support is on hold pending consumer demand. |
|
|
17
18
|
| Planned | Ship Jinja2 and Django frontends as additional `@rhinostone/swig-*` packages. On demand — when there's concrete user demand. |
|
|
18
19
|
| Planned | Test framework migration. Replace mocha 1.x + expect.js with `node:test` + `node:assert/strict`, swap mocha-phantomjs for a modern browser-test harness, swap blanket for `c8`. (The Node engines bump is upstream-driven by gina and is being treated as done.) |
|
|
19
20
|
|
|
@@ -21,6 +22,18 @@ _No near-term scheduled items. See [Future (post-2.0)](#future-post-20) for upco
|
|
|
21
22
|
|
|
22
23
|
## Completed
|
|
23
24
|
|
|
25
|
+
### v2.2.0 (May 2026)
|
|
26
|
+
|
|
27
|
+
- `renderFile(path, locals, cb)` and `compileFile(path, options, cb)` now automatically route to the async-codegen path when the configured loader signals async support via `loader.async === true`. The async path defers template resolution from parse time to render time via a new `_swig.getTemplate(path, options)` runtime helper that returns `Promise<TemplateFn>`; `extends`, `include`, `import`, and `from` emit deferred IR shapes and the shared backend wraps the compiled body in an `AsyncFunction`. Block overrides thread through the inheritance chain via a sixth `_blocks` positional argument; macro imports pick up exports via a `Promise<{output, exports}>` template-fn return shape. Both `@rhinostone/swig` and `@rhinostone/swig-twig` flavors — parity across the two surfaces. Static template targets (string literals in `extends` / `include` / `import` / `from`) work end-to-end against async loaders; dynamic targets surface a clear runtime error and are tracked as a follow-up. The sync render path is unchanged — loaders without `loader.async === true` continue to use the established sync `_swig.compileFile(...)` resolution, including the built-in `loaders.fs` and `loaders.memory` which remain dual-mode.
|
|
28
|
+
- `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` on both `@rhinostone/swig` and `@rhinostone/swig-twig` are soft-deprecated via JSDoc only — no runtime warning. Use `renderFile` / `compileFile` with an async loader (`loader.async === true`) instead; the dispatch is automatic. The legacy pre-walker entry points remain fully functional in 2.x and will be removed in 3.0.
|
|
29
|
+
- Performance improvement to the `escape` / `e` filter in both flavors. The HTML default branch switched from a five-replace chain to an entity-preserving two-pass form (entity-aware first pass that preserves already-escaped sequences, followed by a single character-class regex with a lookup function). A scalar fast-path skips the array/object iteration when input is null, undefined, or a non-object. Output is byte-identical to the previous behavior on every input. Measured against `benchmarks/render.js` (medians of 5 runs, autoescape on, Node 25): simple-var-output `+57%` (flipping the bench verdict from `nunjucks 1.32x faster` to `swig 1.18x faster`); filter chain `+37%`; for-loop (5 items) `+54%`; if/else branch `+71%`; nested for+if+filter `+56%`.
|
|
30
|
+
|
|
31
|
+
### v2.1.0 (May 2026)
|
|
32
|
+
|
|
33
|
+
- Async loader support via `renderFileAsync(path, locals, cb)` and `compileFileAsync(path, options, cb)` on `@rhinostone/swig` and `@rhinostone/swig-twig`. The implementation pre-walks the template dependency graph through the user loader's cb-shape arm, builds an in-memory map keyed by resolved path, then runs the existing sync render against an in-memory wrapper for the duration of the call. Supports `extends`, `include`, `import`, and Twig `from import` with string-literal paths; dynamic paths surface a `Pre-walked map missing path` error at render time. Existing sync `renderFile` / `compileFile` consumers are unaffected.
|
|
34
|
+
- Internal scaffolding for a future async parse path: new deferred-resolution IR shapes (`IRExtendsDeferred`, `IRIncludeDeferred`, `IRImportDeferred`, `IRFromImportDeferred`) and matching `AsyncFunction`-wrapped backend emit branches land on `@rhinostone/swig-core`, all gated behind `options.codegenMode === 'async'` (default sync, no behavior change for existing consumers). The frontend tag wiring and public API dispatch that would activate this path are not yet shipped.
|
|
35
|
+
- Internal cleanup: simplified `IRVarRef` emit shape into a single-evaluation ternary (smaller compiled bodies, same runtime semantics); added a runnable render-throughput benchmark suite at `benchmarks/render.js` (excluded from the published tarball); README clarifications surfaced the prototype-pollution hardening across CVE-2023-25345 and CVE-2021-44906.
|
|
36
|
+
|
|
24
37
|
### v2.0.1 (May 2026)
|
|
25
38
|
|
|
26
39
|
- Fixed bracket-access expressions failing on unspaced binary arithmetic in `@rhinostone/swig` and `@rhinostone/swig-twig`. The lexer's NUMBER rule was greedy-eating leading `+` / `-` operators inside bracket expressions like `arr[arr.length-1]` and `arr[idx-1]`, causing the parser to bail with "Unexpected closing square bracket". Dropped the optional sign prefix from the NUMBER rule; signed-literal paths continue to work via the existing unary-operator wrapping.
|