Skip to content

Fix/node cjs packaging - #1

Open
fc-anjos wants to merge 637 commits into
mainfrom
fix/node-cjs-packaging
Open

fc-anjos wants to merge 637 commits into
mainfrom
fix/node-cjs-packaging

Conversation

@fc-anjos

Copy link
Copy Markdown
Owner

Node: Standardize dual-format packaging to match established codebase patterns

This Pull Request fixes the packaging for CJS consumers of the Node package.
Previously, the CommonJS entry failed due to a Rollup bundling configuration issue in @herb-tools/node. The package used separate .cts/.mts source files, but Rollup wasn't properly bundling local dependencies for the CJS build, leaving runtime requires that failed.
This change adopts the established pattern: a single src/index.ts source file that Rollup transforms into both ESM and CJS outputs. This matches the proven strategy used by @herb-tools/core, @herb-tools/formatter, @herb-tools/highlighter, and @herb-tools/linter.

This change adopts the established pattern: a single src/index.ts source file that Rollup transforms into both ESM and CJS outputs. This matches the strategy used by @herb-tools/core, @herb-tools/formatter, @herb-tools/highlighter, and @herb-tools/linter.

No public API changes.

Expected vs Actual

  • Expected
    • Both ESM and CJS consumers can import/require @herb-tools/node, call await Herb.load(), and use the API.
  • Actual (before)
    • ESM import works.
    • CJS import fails at runtime when using the previously split sources.

Reproduction (before)

Using the built artifacts, with the previous split .mts/.cts sources:

  1. ESM import (works)
// test-esm.mjs
import { Herb } from "@herb-tools/node"

await Herb.load()
console.log("ESM works:", Herb.version)

Run:

node test-esm.mjs
# ✓ Loads and runs successfully
  1. CJS import (fails)
// test-cjs.cjs
const { Herb } = require("@herb-tools/node")

Run:

node test-cjs.cjs
# ✗ Error: Cannot find module './node-backend' (from dist/herb-node.cjs)

Even with the path changed, there'd still be the error:

TypeError: this.backendPromise is not a function
    at HerbBackendNode.load (.../node_modules/@herb-tools/core/src/herb-backend.ts:...)

Diagnosis (before)

CJS build problem:

  • src/index-cjs.cts contained require("./node-backend.js")
  • Rollup processed this as a runtime dependency, not a bundled dependency
  • The built dist/herb-node.cjs expected ./node-backend.js to exist at runtime, but it wasn't emitted
  • Additionally, the CJS version passed new Promise(...) (Promise instance) instead of () => new Promise(...) (Promise factory)

ESM build worked because:

  • Rollup properly resolved and bundled the local node-backend.ts dependency
  • It used the correct Promise factory pattern: () => new Promise(...)

Fix

Adopt the standard, project-wide packaging pattern:

  1. Use a single source file: src/index.ts for both formats.
  2. Update Rollup to emit both ESM and CJS outputs from that single source.
  3. Remove the separate .cts/.mts sources to avoid divergence and bundling issues.
  4. Update package.json type entries to the standard ./dist/types/index.d.ts.

This mirrors the approach in other packages and eliminates the non-working CJS path.


Files Changed

  • src/index-esm.mts → src/index.ts (renamed, now single source)
  • src/index-cjs.cts (deleted, was non-working code path)
  • rollup.config.mjs (updated to use standard pattern)
  • package.json (updated type declarations)

@fc-anjos
fc-anjos force-pushed the fix/node-cjs-packaging branch from 1ad4f6b to 1c7f6d3 Compare August 15, 2025 13:12
marcoroth and others added 29 commits December 12, 2025 22:48
This pull request changes the way the parser treats multiple control
flow elements in a single ERB tag.

Since we need to be able to group the ERB tags, we need to have the ERB
tags independently. This pull request therefore introduces a
`ERBMultipleBlocksInTagError` that gets added to a `ERBContentNode` that
contains multiple unclosed control flow elements.

The following is not valid:

```html+erb
<%
  list1.each do |i|
    list2.each do |j| %>
    
  <% end %>
<% end %>
```

Which now gets parsed as:
```js
@ DocumentNode (location: (1:0)-(6:9))
└── children: (3 items)
    ├── @ ERBBlockNode (location: (1:0)-(5:11))
    │   ├── errors: (1 item)
    │   │   └── @ ERBMultipleBlocksInTagError (location: (1:0)-(3:24))
    │   │       └── message: "Multiple unclosed control flow blocks in a single ERB tag. Split each block into its own ERB tag, or close all blocks within the same tag."
    │   │       
    │   ├── tag_opening: "<%" (location: (1:0)-(1:2))
    │   ├── content: "
    │     list1.each do |i|
    │       list2.each do |j| " (location: (1:2)-(3:22))
    │   ├── tag_closing: "%>" (location: (3:22)-(3:24))
    │   ├── body: (1 item)
    │   │   └── @ HTMLTextNode (location: (3:24)-(5:2))
    │   │       ├── errors: []
    │   │       └── content: "\n    \n  "
    │   │   
    │   └── end_node: 
    │       └── @ ERBEndNode (location: (5:2)-(5:11))
    │           ├── tag_opening: "<%" (location: (5:2)-(5:4))
    │           ├── content: " end " (location: (5:4)-(5:9))
    │           └── tag_closing: "%>" (location: (5:9)-(5:11))
    │           
    ├── @ HTMLTextNode (location: (5:11)-(6:0))
    │   └── content: "\n"
    │   
    └── @ ERBContentNode (location: (6:0)-(6:9))
        ├── errors: (1 item)
        │   └── @ ERBControlFlowScopeError (location: (6:0)-(6:9))
        │       ├── message: "`<% end %>` appears outside its control flow block. Keep ERB control flow statements together within the same HTML scope (tag, attribute, or content)."
        │       └── keyword: "`<% end %>`"
        │       
        ├── tag_opening: "<%" (location: (6:0)-(6:2))
        ├── content: " end " (location: (6:2)-(6:7))
        ├── tag_closing: "%>" (location: (6:7)-(6:9))
        ├── parsed: true
        └── valid: false
```

Previously, this failed with:
```js
@ DocumentNode (location: (1:0)-(6:9))
└── children: (3 items)
    ├── @ ERBBlockNode (location: (1:0)-(5:11))
    │   ├── tag_opening: "<%" (location: (1:0)-(1:2))
    │   ├── content: "
    │     list1.each do |i|
    │       list2.each do |j| " (location: (1:2)-(3:22))
    │   ├── tag_closing: "%>" (location: (3:22)-(3:24))
    │   ├── body: (1 item)
    │   │   └── @ HTMLTextNode (location: (3:24)-(5:2))
    │   │       ├── errors: []
    │   │       └── content: "\n    \n  "
    │   │       
    │   └── end_node: 
    │       └── @ ERBEndNode (location: (5:2)-(5:11))
    │           ├── tag_opening: "<%" (location: (5:2)-(5:4))
    │           ├── content: " end " (location: (5:4)-(5:9))
    │           └── tag_closing: "%>" (location: (5:9)-(5:11))
    │           
    ├── @ HTMLTextNode (location: (5:11)-(6:0))
    │   └── content: "\n"
    │   
    └── @ ERBContentNode (location: (6:0)-(6:9))
        ├── errors: (1 item)
        │   └── @ ERBControlFlowScopeError (location: (6:0)-(6:9))
        │       ├── message: "`<% end %>` appears outside its control flow block. Keep ERB control flow statements together within the same HTML scope (tag, attribute, or content)."
        │       └── keyword: "`<% end %>`"
        │       
        ├── tag_opening: "<%" (location: (6:0)-(6:2))
        ├── content: " end " (location: (6:2)-(6:7))
        ├── tag_closing: "%>" (location: (6:7)-(6:9))
        ├── parsed: true
        └── valid: false
```

At least, this makes the error more actionable.

The `ERBContentNode` can contain multiple control flow elements, if they
also get closed in the same tag. So this is still valid:

```html+erb
<%
  if something?
    if something_else?
      call_something(argument)
    end
  end
%>
```

Resolves marcoroth#604
Resolves marcoroth#969
This pull request updates the lexer to support detecting `<%grapqhl` as
a valid ERB tag opening. That way, we can treat them differently and
don't their content as Ruby code and parse them using Prism.

The
[`graphql-client`](https://github.com/github-community-projects/graphql-client?tab=readme-ov-file#rails-erb-integration)
gem exposes an ERB extension that allows to use custom `<%grapqhl %>`
tags in ERB files.

```html+erb
<%# app/views/humans/human.html.erb %>
<%graphql
  fragment HumanFragment on Human {
    name
    homePlanet
  }
%>

<p><%= human.name %> lives on <%= human.home_planet %>.</p>
```

With this pull request, the example from marcoroth#972 now gets parsed with a
`"<%graphql"` as the `tag_opening` and with no Ruby parse errors:

```js
@ DocumentNode (location: (1:0)-(15:2))
└── children: (1 item)
    └── @ ERBContentNode (location: (1:0)-(15:2))
        ├── tag_opening: "<%graphql" (location: (1:0)-(1:9))
        ├── content: "
          fragment Comment on Comment {
            ... on PullRequestReviewComment {
              pullRequest {
                id
                repository {
                  name
                  owner {
                    login
                  }
                }
              }
            }
          }
        " (location: (1:9)-(15:0))
        ├── tag_closing: "%>" (location: (15:0)-(15:2))
        ├── parsed: false
        └── valid: true
```


At runtime, these tags are [getting converted to ERB
comments](https://github.com/github-community-projects/graphql-client/blob/ba1328fe77a35c3558758db2cbe302f50f64e283/lib/graphql/client/erubi_enhancer.rb#L17).
In the future, we might also want to update the `Herb::Engine` to skip
these tags from being compiled.

Resolves marcoroth#972
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 7.1.11 to 7.3.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…h#985)

Bumps
[@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)
from 8.48.0 to 8.49.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [prettier](https://github.com/prettier/prettier) from 3.6.2 to
3.7.4.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ind-class-sorter (marcoroth#979)

Bumps
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
from 24.10.4 to 25.0.2.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…-lowercase` offense (marcoroth#976)

This pull request fixes marcoroth#975

Updates autofix() so that after fixing the offending tag name, it walks
up to the parent element and also fixes the related counterpart tag name
(opening/closing) in the same element.

Let me know if you see any improvements or have any suggestions! Thank
you.
Bumps [rimraf](https://github.com/isaacs/rimraf) from 6.1.0 to 6.1.2.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…h#988)

Follow up to: marcoroth#973

This pull request updates the `Herb::Engine` to not compile `<%graphql
%>` tags when compiling HTML+ERB templates. This mirrors the behavior in
`ErubiEnhancer` from the
[`graphql-client`](https://github.com/github-community-projects/graphql-client/blob/ba1328fe77a35c3558758db2cbe302f50f64e283/lib/graphql/client/erubi_enhancer.rb#L17)
gem.
…#974)

This pull request fixes the parsing of `<` and `>` characters in text
content where are a non-ambiguous.

Previously, `<kbd>></kbd>` and similar patterns would raise an
UnexpectedError because `>` wasn't recognized as valid text content.
Similarly, `<` followed by non-tag characters (like spaces or digits)
wasn't handled correctly.

Now, the following is valid: `<kbd>></kbd>` 

And gets parsed as:
```js
@ DocumentNode (location: (1:0)-(1:12))
└── children: (1 item)
    └── @ HTMLElementNode (location: (1:0)-(1:12))
        ├── open_tag: 
        │   └── @ HTMLOpenTagNode (location: (1:0)-(1:5))
        │       ├── tag_opening: "<" (location: (1:0)-(1:1))
        │       ├── tag_name: "kbd" (location: (1:1)-(1:4))
        │       ├── tag_closing: ">" (location: (1:4)-(1:5))
        │       ├── children: []
        │       └── is_void: false
        │       
        ├── tag_name: "kbd" (location: (1:1)-(1:4))
        ├── body: (1 item)
        │   └── @ HTMLTextNode (location: (1:5)-(1:6))
        │       └── content: ">"
        │       
        ├── close_tag: 
        │   └── @ HTMLCloseTagNode (location: (1:6)-(1:12))
        │       ├── tag_opening: "</" (location: (1:6)-(1:8))
        │       ├── tag_name: "kbd" (location: (1:8)-(1:11))
        │       ├── children: []
        │       └── tag_closing: ">" (location: (1:11)-(1:12))
        │       
        ├── is_void: false
        └── source: "HTML"
```

Resolves marcoroth#914
Bumps [vitepress](https://github.com/vuejs/vitepress) from
2.0.0-alpha.12 to 2.0.0-alpha.15.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The blog post’s mp4 demos weren’t rendering because they used
image-style embeds, so I replaced them with video tags
(autoplay/loop/muted/controls). Thank you!

<img width="659" height="165" alt="image"
src="https://github.com/user-attachments/assets/36c7c929-8b85-4d12-8d32-0b5bd0c121f0"
/>

ref. https://herb-tools.dev/blog/whats-new-in-herb-v0-8
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)
from 1.13.0 to 1.35.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@vscode/vsce](https://github.com/Microsoft/vsce) from 3.6.0 to
3.7.1.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps
[@shikijs/vitepress-twoslash](https://github.com/shikijs/shiki/tree/HEAD/packages/vitepress-twoslash)
from 3.15.0 to 3.20.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@mapbox/node-pre-gyp](https://github.com/mapbox/node-pre-gyp)
from 2.0.0 to 2.0.3.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…verlay (marcoroth#999)

When showing the Herb overlay:

- For non-static positions (absolute, fixed, sticky, relative), the
existing position value should be respected.
- Non-static elements already serve as the positioning context for
absolutely positioned children, so overriding them is unnecessary.
Bumps [nx](https://github.com/nrwl/nx/tree/HEAD/packages/nx) from 22.1.3
to 22.3.3.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… element (marcoroth#1000)

When attaching the Herb overlay to the `<html>` element, the label may
be positioned outside of the viewport because the default CSS uses a
negative top value.

This adds a small conditional so the label remains visible when the
overlay is attached to `<html>`.

I wasn’t sure whether adding this kind of conditional styling in
JavaScript is acceptable here, or if you’d prefer a CSS-based solution.
Please let me know if you have a preferred direction.

---------

Signed-off-by: Marco Roth <marco.roth@intergga.ch>
Co-authored-by: Marco Roth <marco.roth@intergga.ch>
This PR adds a new version of the `hb_array` called `hb_narray` that
implements an array that can actually hold the elements it stores.

The existing `hb_array` only allows to store pointers to heap allocated
elements, the new `hb_narray` acts as a container that also provides the
backing memory for the elements it holds.

It's important to note that you can still make the elements pointers if
you want to.

## Why? 

- Improves locality of the elements
- Reduces the number of individual heap allocations for elements

## Next steps

1. Get this merged
2. Refactor existing `hb_array` usages
3. Remove `hb_array` and rename `hb_narray` to `hb_array` 

## Memory layout


![herb_new_array](https://github.com/user-attachments/assets/8012ff75-38c0-418c-a188-6a8d0fa90baa)
marcoroth and others added 30 commits March 2, 2026 17:40
…marcoroth#1271)

This pull request adds a matching `shouldLintFile()` check that respects
`files.exclude`, `linter.exclude`, and default `exclude` patterns (like
`vendor/**/*`, `node_modules/**/*`) before linting a document in the
language server.

Resolves marcoroth#1179
Bumps
[@shikijs/vitepress-twoslash](https://github.com/shikijs/shiki/tree/HEAD/packages/vitepress-twoslash)
from 3.22.0 to 4.0.1.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)
from 1.50.0 to 1.51.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [yeoman-generator](https://github.com/yeoman/generator) from 7.5.1
to 8.0.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…uage-server (marcoroth#1272)

Bumps
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
from 22.19.13 to 25.3.3.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…h#1274)

Bumps
[@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin)
from 8.55.0 to 8.56.1.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Closes marcoroth#1264

This PR refactors the linter code to use a static `ruleName` property
instead of `name` on the instance.
…rcoroth#1280)

This pull request updates the formatter to detect `white-space`
significant elements by either looking at the `style` attribute and
seeing if it includes the `white-space:` property with the relevant
value, or if it includes one of the Tailwind whitespace-preserving
classes.

Additionally, it moves some of the AST utils related to attributes from
the linter package to `core` so we can also use them in the formatter.

Resolves marcoroth#1095
…oroth#1281)

This pull request changes the native extension loading from
`require_relative` to `require` to ensure compatibility with the latest
RubyGems, which no longer installs native extensions into the gem's
`lib/` directory by default. See
ruby/rubygems#9240.

Using `require` resolves the extension through `$LOAD_PATH`, which
RubyGems populates with both the `lib/` and extension directories, so
this works on both old and new versions of RubyGems.

Resolves marcoroth#1244

Co-Authored-By: Daniel Colson <composerinteralia@github.com>
…arcoroth#1085)

The `tailwind-class-sorter` was silently corrupting class names in
attributes when ERB was used for interpolating class names. For example:

**Input:**
```html
<div class="foo-<%= bar %>-100">
```

**Output:**
```html
<div class="foo--100 <%= bar %>">
```

This happened because the sorter separated all literal nodes from ERB
nodes and joined them together, losing the ERB's position within the
class name.

This pull request adds detection for string interpolation patterns and
skips sorting when they're found:

  - Case 1: `literal → ERB → literal` with no whitespace at boundaries
    e.g., `foo-<%= bar %>-100`

  - Case 2: ERB at start followed by literal without leading whitespace
    e.g., `<%= prefix %>-blue-500`

  - Case 3: Literal ending with hyphen followed by ERB at end
    e.g., `bg-<%= suffix %>`

When interpolation is detected, sorting is skipped for that attribute
value to prevent silent corruption. Classes inside nested conditionals
are still sorted correctly.

Resolves marcoroth#879
This pull request extracts a `hb_allocator_T` for providing an interface
for allocating memory in the lexer and parser. In this pull request we
implement both a `malloc`-based and a `hb_arena_T`-based allocator.

Additionally, this updates all call-sites and functions to accept a new
allocator that can later be swapped to use the `hb_arena_T`-based
allocator in marcoroth#726.
Bumps the npm_and_yarn group with 1 update in the / directory:
[underscore](https://github.com/jashkenas/underscore).

Updates `underscore` from 1.13.7 to 1.13.8

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This pull request introduces arena allocation for the lexer and parser,
replacing "per-object" `malloc`/`free` calls with bulk allocation from a
memory arena.

All allocated AST nodes, tokens, and internal strings are placed into a
single arena that is freed in one shot after the parse tree has been
converted to the binding's native objects.

The arena is accessed through `hb_allocator_T`, a vtable-based allocator
abstraction introduced in marcoroth#1287. This pull request switches the default
backend from `malloc` to `hb_arena_T` across all bindings and the CLI.

The `malloc` backend remains available as a fallback. The intent is to
validate the arena approach in production and then, once confident,
simplify the abstraction away and use `hb_arena_T` directly.

The `hb_allocator_T` interface was extended with a `destroy` function
pointer and a high-level `hb_allocator_init(allocator, type)`
constructor that takes an `hb_allocator_type_T` enum
(`HB_ALLOCATOR_ARENA`, `HB_ALLOCATOR_MALLOC`).

A separate `hb_allocator_init_with_size` variant accepts a custom
initial arena size for edge cases. The default arena size is controlled
by a compile-time `HB_ALLOCATOR_DEFAULT_ARENA_SIZE` flag.

The extract API (`herb_extract`, `herb_extract_ruby_*`,
`herb_extract_html_*`) was also updated to accept an `hb_allocator_T*`.

Depends on marcoroth#1287
…#1293)

```
❯ bundle exec exe/herb parse examples/test.html.erb --silent --arena-stats
╔════════════════════════════════════════════════════════════════════════════════════════════════════╗
║                                        ARENA MEMORY LAYOUT                                         ║
╠════════════════════════════════════════════════════════════════════════════════════════════════════╣
║  Statistics:                                                                                       ║
║    • Pages: 1                                                                                      ║
║    • Default Page Size: 16 KB                                                                      ║
║    • Total Capacity: 16 KB                                                                         ║
║    • Total Used: 7 KB                                                                              ║
║    • Total Available: 8 KB                                                                         ║
║    • Usage: 44.6%                                                                                  ║
║    • Allocations: 274                                                                              ║
║    • Fragmentation: 0 B                                                                            ║
╠════════════════════════════════════════════════════════════════════════════════════════════════════╣
║  Page 0 @ 0x104b2c000 ← CURRENT                                                                    ║
║    [████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]  7 KB / 16 KB (45%) ║
║    Unused: 8 KB (available for allocations)                                                        ║
║                                                                                                    ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════╝
Success
```

```
irb(main):001> Herb.parse("<div></div>", arena_stats: true)
╔════════════════════════════════════════════════════════════════════════════════════════════════════╗
║                                        ARENA MEMORY LAYOUT                                         ║
╠════════════════════════════════════════════════════════════════════════════════════════════════════╣
║  Statistics:                                                                                       ║
║    • Pages: 1                                                                                      ║
║    • Default Page Size: 16 KB                                                                      ║
║    • Total Capacity: 16 KB                                                                         ║
║    • Total Used: 1 KB                                                                              ║
║    • Total Available: 14 KB                                                                        ║
║    • Usage: 8.0%                                                                                   ║
║    • Allocations: 48                                                                               ║
║    • Fragmentation: 0 B                                                                            ║
╠════════════════════════════════════════════════════════════════════════════════════════════════════╣
║  Page 0 @ 0x102f38000 ← CURRENT                                                                    ║
║    [█████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]  1 KB / 16 KB (8%) ║
║    Unused: 14 KB (available for allocations)                                                       ║
║                                                                                                    ║
╚════════════════════════════════════════════════════════════════════════════════════════════════════╝
=>
#<Herb::ParseResult:0x0000000121ad6ef8 ...>
```
…#1294)

This pull request updates the `analyze` subcommand in the Ruby CLI to
also accept the `--arena-stats` flag.

**`herb analyze --arena-stats`**

<img width="2718" height="1792" alt="CleanShot 2026-03-04 at 11 14
44@2x"
src="https://github.com/user-attachments/assets/4be6bab4-2b10-4b93-b1ef-f3a21c2b17ab"
/>

...


<img width="2676" height="1842" alt="CleanShot 2026-03-04 at 11 14
04@2x"
src="https://github.com/user-attachments/assets/d8c79175-f193-4125-829e-597b0f19375f"
/>
Context: https: //github.com/marcoroth/pull/726#pullrequestreview-3811782126

Co-Authored-By: Michael Kohl <me@citizen428.net>
Sadly the require change in marcoroth#1281 is now causing the `require "herb"` locally to favor the locally installed herb gem over the local herb version
Based on the arena pages proposed in marcoroth#708 by @marcoroth, this PR builds
on the concept and adds a more refined reset behavior that merges
obsolete pages into one big page.

This has the advantage that we won't have a bunch of small pages, and
instead have more contiguous memory available.

## How it works

```
# Initial state
| Page 1 (4kb) | - next > | Page 2 (4kb) | - next > | Page 1 (16kb) | 

# reset to  4kb
| Page 1 (4kb) | - next > | Page 2 (24kb) |

# reset to 0kb
| Page 1 (4kb) | - next > | Page 2 (24kb) |
```

For now the first page is never deallocated and merged into a bigger
page for two reasons:

1. It's an edge case that needs to be handled.
2. If the allocation of a new arena page fails at least you still have
the first page. 🤷‍♂️

Related: marcoroth#1155

---------

Co-authored-by: Marco Roth <marco.roth@intergga.ch>
marcoroth#1295)

Previously, the formatter would inline ERB children in block elements
when the content fit within `maxLineLength`, even when the user
intentionally placed them on separate lines.

This pull request extends the existing mixed-content newline
preservation (marcoroth#1279) to also cover block elements with ERB children.

Resolves marcoroth#1181
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.