@rhinostone/swig 2.0.0 → 2.1.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.
@@ -0,0 +1,8 @@
1
+ [2.0.1](https://github.com/gina-io/swig/tree/v2.0.1) / 2026-05-10
2
+ -----------------------------------------------------------------
3
+
4
+ * **Fixed** Fixed bracket-access expressions failing on unspaced binary arithmetic in both `@rhinostone/swig` and `@rhinostone/swig-twig`. The lexer NUMBER rule's optional sign prefix (`[+\-]?`) greedy-ate a leading operator inside bracket expressions — `arr[arr.length-1]` and `arr[idx-1]` lexed as VAR + DOTKEY/VAR + NUMBER(-1) + `]`, and parsePostfix then bailed with "Unexpected closing square bracket". Dropped the optional sign from both `lib/lexer.js` and `packages/swig-twig/lib/lexer.js`; parsePrimary's OPERATOR branch already wraps unary `-` / `+` as IRUnaryOp via parseUnary, so signed-literal paths (`{% set x = -5 %}`, `{{ a + -5 }}`) continue to work and the backend's emitUnaryOp produces byte-identical JS for negative numbers (the `emit('-1.5') === '-1.5'` round-trip is unchanged). Three pre-existing parse/lexer assertions documented the old "NUMBER rule, not UnaryOp" choice and were rewritten to assert the new IRUnaryOp shape; a regression suite covers `arr[arr.length-1]` / `arr[idx-1]` / `arr[idx+0]` / `arr[idx-2]` plus a `*` / `/` round-trip sweep on both render surfaces.
5
+
6
+ * **Fixed** Fixed missing whitespace-control parity in `@rhinostone/swig-twig`. `{{- … -}}` and `{%- … -%}` markers now strip surrounding whitespace at chunk boundaries, matching the native swig surface and the upstream Twig spec. Previously absent — `packages/swig-twig/lib/parser.js` had only a basic strip regex and no `varStripBefore` / `*After` / `tagStripBefore` / `*After` detection regexes, so `{{- foo -}}` either rendered as junk or threw "Unexpected end of expression". Implementation mirrors the native chunk-boundary pattern (`stripPrev` / `stripNext` flags, `stripPrevToken` helper) with the corrected strip-regex shape baked in from day one (`-?` adjacent to the open / close marker only, not after `\s*`), so negative-literal expressions like `{{ -5 }}` and `{{- -5 -}}` compose correctly. New `tests/swig-twig/whitespace.test.js` covers the six mirror-of-native cases (strips before / after / both, on `{{...}}` and `{%...%}`) plus four negative-literal composition cases. Same one-level-deep limitation as native — a `{%- endif %}` strips the trailing whitespace of the immediately enclosing tag's last child only, not deeper.
7
+
8
+ * **Fixed** Fixed `varStrip` / `tagStrip` regexes in `lib/parser.js` greedy-eating the leading `-` of negative-number expressions. The strip patterns were `^{{-?\s*-?|-?\s*-?}}$` (and the tag-form equivalent) — the second `-?` after `\s*` matched the leading `-` of an expression as if it were a whitespace-control marker, so `{{ -5 }}` and `{{ -1.5 }}` rendered as `"5"` / `"1.5"` (the sign was eaten before the lexer ever saw the chunk). Dropped the inner `-?` from both regexes; whitespace-control now fires only when `-` is immediately adjacent to the open / close marker (`{{-`, `-}}`, `{%-`, `-%}`), matching the standard Twig/Jinja2 contract. Existing whitespace strip-control tests continue to pass, and a new regression case covers negative literals plain, with strip-control before / after / both, plus a check that strip-control on regular variables still composes correctly. `@rhinostone/swig-twig` was unaffected — its parser-side strip regex (`packages/swig-twig/lib/parser.js`) is a simpler `^{{ \s*|\s* }}$` with no `-?` markers, so the same shape doesn't apply (a side-effect of the verification surfaced that swig-twig has no whitespace-control support today; tracked separately).
@@ -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.
package/.eslintrc.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "env": {
3
3
  "node": true,
4
- "mocha": true
4
+ "mocha": true,
5
+ "es2017": true
6
+ },
7
+ "parserOptions": {
8
+ "ecmaVersion": 2017
9
+ },
10
+ "globals": {
11
+ "Promise": "readonly"
5
12
  },
6
13
  "rules": {
7
14
  "max-len": ["error", { "code": 600 }],
package/AUTHORS CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Paul Armstrong <paul@paularmstrongdesigns.com>
4
4
 
5
- # Fork Maintainer
5
+ # Maintainer
6
6
 
7
7
  Rhinostone <contact@gina.io>
8
8
 
package/HISTORY.md CHANGED
@@ -1,3 +1,21 @@
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.
9
+
10
+ [2.0.1](https://github.com/gina-io/swig/tree/v2.0.1) / 2026-05-10
11
+ -----------------------------------------------------------------
12
+
13
+ * **Fixed** Fixed bracket-access expressions failing on unspaced binary arithmetic in both `@rhinostone/swig` and `@rhinostone/swig-twig`. The lexer NUMBER rule's optional sign prefix (`[+\-]?`) greedy-ate a leading operator inside bracket expressions — `arr[arr.length-1]` and `arr[idx-1]` lexed as VAR + DOTKEY/VAR + NUMBER(-1) + `]`, and parsePostfix then bailed with "Unexpected closing square bracket". Dropped the optional sign from both `lib/lexer.js` and `packages/swig-twig/lib/lexer.js`; parsePrimary's OPERATOR branch already wraps unary `-` / `+` as IRUnaryOp via parseUnary, so signed-literal paths (`{% set x = -5 %}`, `{{ a + -5 }}`) continue to work and the backend's emitUnaryOp produces byte-identical JS for negative numbers (the `emit('-1.5') === '-1.5'` round-trip is unchanged). Three pre-existing parse/lexer assertions documented the old "NUMBER rule, not UnaryOp" choice and were rewritten to assert the new IRUnaryOp shape; a regression suite covers `arr[arr.length-1]` / `arr[idx-1]` / `arr[idx+0]` / `arr[idx-2]` plus a `*` / `/` round-trip sweep on both render surfaces.
14
+
15
+ * **Fixed** Fixed missing whitespace-control parity in `@rhinostone/swig-twig`. `{{- … -}}` and `{%- … -%}` markers now strip surrounding whitespace at chunk boundaries, matching the native swig surface and the upstream Twig spec. Previously absent — `packages/swig-twig/lib/parser.js` had only a basic strip regex and no `varStripBefore` / `*After` / `tagStripBefore` / `*After` detection regexes, so `{{- foo -}}` either rendered as junk or threw "Unexpected end of expression". Implementation mirrors the native chunk-boundary pattern (`stripPrev` / `stripNext` flags, `stripPrevToken` helper) with the corrected strip-regex shape baked in from day one (`-?` adjacent to the open / close marker only, not after `\s*`), so negative-literal expressions like `{{ -5 }}` and `{{- -5 -}}` compose correctly. New `tests/swig-twig/whitespace.test.js` covers the six mirror-of-native cases (strips before / after / both, on `{{...}}` and `{%...%}`) plus four negative-literal composition cases. Same one-level-deep limitation as native — a `{%- endif %}` strips the trailing whitespace of the immediately enclosing tag's last child only, not deeper.
16
+
17
+ * **Fixed** Fixed `varStrip` / `tagStrip` regexes in `lib/parser.js` greedy-eating the leading `-` of negative-number expressions. The strip patterns were `^{{-?\s*-?|-?\s*-?}}$` (and the tag-form equivalent) — the second `-?` after `\s*` matched the leading `-` of an expression as if it were a whitespace-control marker, so `{{ -5 }}` and `{{ -1.5 }}` rendered as `"5"` / `"1.5"` (the sign was eaten before the lexer ever saw the chunk). Dropped the inner `-?` from both regexes; whitespace-control now fires only when `-` is immediately adjacent to the open / close marker (`{{-`, `-}}`, `{%-`, `-%}`), matching the standard Twig/Jinja2 contract. Existing whitespace strip-control tests continue to pass, and a new regression case covers negative literals plain, with strip-control before / after / both, plus a check that strip-control on regular variables still composes correctly. `@rhinostone/swig-twig` was unaffected — its parser-side strip regex (`packages/swig-twig/lib/parser.js`) is a simpler `^{{ \s*|\s* }}$` with no `-?` markers, so the same shape doesn't apply (a side-effect of the verification surfaced that swig-twig has no whitespace-control support today; tracked separately).
18
+
1
19
  [2.0.0](https://github.com/gina-io/swig/tree/v2.0.0) / 2026-05-06
2
20
  -----------------------------------------------------------------
3
21
 
package/README.md CHANGED
@@ -3,9 +3,9 @@ Swig
3
3
 
4
4
  [![CI](https://github.com/gina-io/swig/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/gina-io/swig/actions/workflows/ci.yml) [![NPM version](http://img.shields.io/npm/v/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![NPM Downloads](http://img.shields.io/npm/dm/@rhinostone/swig.svg?style=flat)](https://www.npmjs.com/package/@rhinostone/swig) [![Socket Badge](https://socket.dev/api/badge/npm/package/@rhinostone/swig)](https://socket.dev/npm/package/@rhinostone/swig)
5
5
 
6
- > **Maintained, multi-flavor template engine.** Originally forked from the abandoned [paularmstrong/swig](https://github.com/paularmstrong/swig); now [gina-io/swig](https://github.com/gina-io/swig), an actively developed workspace covering native Swig syntax (Jinja2/Django-inspired) and Twig syntax via dedicated frontends sharing one IR backend. Security and bug fixes ship here; the original project has not had a release since 2014.
6
+ > **Multi-flavor template engine** for Node.js and browsers native Swig syntax (Jinja2/Django-inspired) and Twig syntax via dedicated frontends sharing one IR backend. [gina-io/swig](https://github.com/gina-io/swig) started as a maintained continuation of the abandoned [paularmstrong/swig](https://github.com/paularmstrong/swig) (last released 2014) and is now a standalone project. Security and bug fixes ship here.
7
7
 
8
- > **Part of the [Gina](https://github.com/gina-io/gina) ecosystem.** This fork is the built-in template engine for [Gina](https://gina.io) ([npm](https://www.npmjs.com/package/gina)), a Node.js MVC framework with HTTP/2, multi-bundle architecture, and scope-based data isolation.
8
+ > **Part of the [Gina](https://github.com/gina-io/gina) ecosystem.** This is the built-in template engine for [Gina](https://gina.io) ([npm](https://www.npmjs.com/package/gina)), a Node.js MVC framework with HTTP/2, multi-bundle architecture, and scope-based data isolation.
9
9
 
10
10
  Swig is a **Jinja2/Django-inspired** template engine for node.js and browsers. The syntax will feel familiar to Jinja2 and Django users, but Swig is **not drop-in compatible** with either — porting templates from an existing project requires a handful of changes. See the [Migration Guide](https://gina.io/docs/swig/migration) for the full parity list and workaround patterns.
11
11
 
@@ -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
- * Automatically escapes all output for safe HTML rendering.
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
@@ -8,25 +8,36 @@ For bug reports and feature requests, file an issue at [gina-io/swig](https://gi
8
8
 
9
9
  ## Next
10
10
 
11
- | Status | Item |
12
- | --- | --- |
13
- | Planned | Unfork from `paularmstrong/swig` on GitHub. Multi-flavor track is now stable; the repo is substantively its own project. Attribution stays preserved via `LICENSE` and `package.json.author`. |
11
+ _No near-term scheduled items. See [Future (post-2.0)](#future-post-20) for upcoming work._
14
12
 
15
13
  ## Future (post-2.0)
16
14
 
17
15
  | Status | Item |
18
16
  | --- | --- |
19
17
  | Planned | Ship Jinja2 and Django frontends as additional `@rhinostone/swig-*` packages. On demand — when there's concrete user demand. |
20
- | Planned | Engine bump + test framework migration. Move to Node 18, `node:test` + `node:assert/strict`, swap mocha-phantomjs for a modern browser-test harness, swap blanket for `c8`. |
18
+ | 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.) |
21
19
 
22
20
  ---
23
21
 
24
22
  ## Completed
25
23
 
24
+ ### v2.1.0 (May 2026)
25
+
26
+ - 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.
27
+ - 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.
28
+ - 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.
29
+
30
+ ### v2.0.1 (May 2026)
31
+
32
+ - 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.
33
+ - Fixed `varStrip` / `tagStrip` regexes in `lib/parser.js` greedy-eating the leading `-` of negative-number expressions. `{{ -5 }}` and `{{ -1.5 }}` were rendering as `"5"` / `"1.5"` because the strip patterns matched the sign before the lexer saw it. Whitespace-control markers (`{{-`, `-}}`, `{%-`, `-%}`) now fire only when `-` is immediately adjacent to the open / close marker, matching the standard Twig/Jinja2 contract.
34
+ - Fixed missing whitespace-control parity in `@rhinostone/swig-twig`. `{{- … -}}` and `{%- … -%}` markers now strip surrounding whitespace at chunk boundaries, matching the native swig surface and the upstream Twig spec. Same one-level-deep limitation as native — a `{%- endif %}` strips the trailing whitespace of the immediately enclosing tag's last child only, not deeper.
35
+
26
36
  ### v2.0.0 (May 2026)
27
37
 
28
38
  - Multi-flavor template-engine workspace shipped: `@rhinostone/swig` (native syntax, drop-in for `1.x`), `@rhinostone/swig-twig` (Twig syntax), `@rhinostone/swig-core` (shared IR backend). Production-ready cut of the changeset introduced across `2.0.0-alpha.1` through `2.0.0-alpha.5`. No functional or API changes since `2.0.0-alpha.5`. IR ABI is stable from this release onward; cross-package dependencies pin exact versions and frontends + core release in lockstep.
29
39
  - README messaging refreshed across all three packages to reflect production-ready status; package descriptions cleaned of historical internal-tracking references; stale documentation URLs refreshed.
40
+ - Repository unforked from `paularmstrong/swig` on GitHub once the multi-flavor track stabilised — gina-io/swig is now a standalone project rather than a fork. Attribution preserved via `LICENSE` and `package.json.author`.
30
41
 
31
42
  ### v2.0.0-alpha.8 (April 2026)
32
43