Skip to content

Add a Rust port of theme-check - #1356

Open
clauderic wants to merge 2 commits into
parser-specsfrom
theme-check-rust
Open

clauderic wants to merge 2 commits into
parser-specsfrom
theme-check-rust

Conversation

@clauderic

@clauderic clauderic commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1359, the parser specs. Under it are #1347, the executable specs, and #1357, a parser fix. Review those first; this PR's base moves down the stack as they merge.

What are you adding in this PR?

Theme Check in Rust: a port of @shopify/theme-check-node and the packages it uses, in three crates under crates/.

  • liquid-ast: the syntax tree, as @shopify/liquid-html-parser builds it.
  • liquid-parser: the parser.
  • theme-check: the checks, the runner, config loading, the theme graph, the docs cache and a CLI.

It reports the same offenses as TypeScript, with the same messages, positions, fixes and suggestions:

The parser builds the same trees as TypeScript, and throws the same errors: cargo test runs the parser specs from #1359, and all 415 cases in specs/parser pass. It includes the fix from #1357: an unclosed block in {% liquid %} ends where its last line does. Past 1,000 levels of nesting, a template is a syntax error, Maximum call stack size exceeded, as it is in TypeScript once Node runs out of stack. Node's limit isn't a fixed number, so a template nested between about 900 and 1,000 levels parses in Rust only. The specs give trees in the TypeScript shape, so crates/liquid-parser/tests/specs.rs prints them that way; the crate itself has no JSON.

A full run with every dependency is about 11 to 13 times faster than theme-check-node: 125 ms against 1,608 ms on Dawn. It uses about 75 MB of memory, against 400 to 750 MB.

crates/theme-check/README.md has the rules the port follows: UTF-8 byte offsets in the AST, UTF-16 positions in offenses, and JS semantics where offenses depend on them.

There are two commits:

  1. Add a Rust port of theme-check. The crates, and CI:
    • A rust job runs cargo fmt --check and cargo test --locked.
    • code-quality checks that the crate's copies of the configs and the check metadata match the TypeScript, and a Rust test checks each check's definition against that metadata.
    • crates/theme-check/tests/cli.rs runs the binary on small themes: config files, extends, ignore and root, app mode, bad configs and exit codes, --autofix and deep nesting. Every expected value is theme-check-node's on the same files.
  2. Version and publish the Rust crates.

Versioning and publishing

Changesets versions the crates, like everything else here. crates/theme-check/package.json is a private package, theme-check-rs, outside the fixed theme-check group. To release the crates, add a changeset for it.

  • pnpm release gets a step, syncCrateVersion, that copies its version into Cargo.toml and Cargo.lock. All three crates have the same version.
  • CI checks that the crates have that version, and dry-runs the publish, which builds each crate from the files it would upload.
  • The crate is in both the root package.json workspaces and pnpm-workspace.yaml, since changesets reads the former.

release.yml publishes the crates to crates.io after the npm packages. It's a job of its own, publish-crates, in the crates-io environment, so no npm package runs where it can get a crates.io token.

  • It authenticates with trusted publishing (rust-lang/crates-io-auth-action), so there's no token secret.
  • It publishes each crate whose version isn't on crates.io yet, in dependency order, so a run that fails partway is finished by the next.
  • Until theme-check is on crates.io, it publishes nothing.
  • It fails if crates.io answers anything but found or not found, so an outage isn't a silent skip.

There's no changeset: 0.1.0 is published by hand once this merges.

After merge

  1. Create a crates.io API token with the publish-new scope, limited to theme-check, liquid-ast and liquid-parser, with a short expiry.
  2. From main, publish 0.1.0: cargo publish --workspace --locked. This needs cargo 1.90 or newer.
  3. For each crate, add the owners: cargo owner --add github:Shopify-Partners:crates-publish <crate>, and one more person.
  4. For each crate, add a trusted publisher in its crates.io settings: GitHub, owner Shopify, repository theme-tools, workflow release.yml, environment crates-io.
  5. In the repository settings, limit the crates-io environment to main. GitHub creates it, with no rules, the first time the job runs.
  6. Revoke the token.

From then on, a changeset for theme-check-rs releases the crates.

What's next? Any followup issues?

  • The agent server runs theme-check with this crate, in its own PR.
  • cargo install theme-check doesn't get the fallback docs. They're read from a data folder next to the executable, so they need to be embedded in the binary.
  • The docs cache and the remote asset checks run curl.
  • The Rust CI job runs on Ubuntu only.
  • Providers has one flag, and every caller sets it to true, so it can go.

What did you learn?

Changesets reads the root package.json workspaces before pnpm-workspace.yaml. A package that's only in pnpm-workspace.yaml is invisible to it.

Tophatting

  • cargo test runs the unit tests, the CLI tests and the specs.
  • To compare the CLI with theme-check-node. Their stderr logs differ, so compare stdout:
    pnpm build:ts
    diff <(cargo run -q --release -- packages/theme-graph/fixtures/skeleton 2>/dev/null) \
         <(node packages/theme-check-node/dist/cli.js packages/theme-graph/fixtures/skeleton 2>/dev/null)
  • To try a release, add a changeset for theme-check-rs and run pnpm release. Cargo.toml and Cargo.lock get the new version.

Before you deploy

  • My feature is backward compatible

🤖 Generated with Claude Code

@clauderic
clauderic requested a review from a team as a code owner October 20, 2026 19:17
@clauderic
clauderic added this pull request to stack #1353 October 20, 2026 19:17
@clauderic
clauderic removed this pull request from stack #1353 October 21, 2026 00:28
@clauderic
clauderic added this pull request to stack #1358 October 21, 2026 00:29
@clauderic
clauderic removed this pull request from stack #1358 October 21, 2026 00:47
@clauderic
clauderic changed the base branch from executable-specs to parser-specs October 21, 2026 00:47
@clauderic
clauderic added this pull request to stack #1360 October 21, 2026 00:48
@clauderic clauderic mentioned this pull request Oct 21, 2026
2 tasks done
@karreiro
karreiro self-requested a review October 21, 2026 15:10
clauderic and others added 2 commits October 21, 2026 18:43
Theme Check in Rust, in three crates under crates/:
- liquid-ast: the syntax tree of @shopify/liquid-html-parser.
- liquid-parser: the parser.
- theme-check: the checks, the runner, config loading, the docs cache,
  the theme graph and a CLI like theme-check-node's.

It reports the same offenses as the TypeScript, with the same
messages, positions and fixes. `cargo test` runs the specs in
specs/checks and specs/graph, and it passes all of them, and runs
the CLI on small themes. A new Rust job in CI runs it and checks the
formatting.

The crate has copies of theme-check-node's configs and of the checks'
metadata. The code quality job checks that they match the TypeScript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changesets only versions packages that have a package.json, so the
crates get one: crates/theme-check/package.json, a private package
named theme-check-rs. Its name keeps it out of the
@shopify/theme-check-* group, so the crates have their own version,
from 0.1.0. A changeset for theme-check-rs bumps it, and the release
orchestrator then copies it into Cargo.toml and Cargo.lock. The three
crates share it, and CI checks that they have it. CI also dry-runs the
publish, which builds each crate from the files it would upload.

The release workflow publishes each crate whose version isn't on
crates.io yet, with trusted publishing, so a run that fails partway
is finished by the next. It does this in a job of its own, in the
crates-io environment, so no npm package runs where it can get a
crates.io token. Until theme-check is on crates.io, it skips them:
the first publish is by hand, and so is adding the trusted publisher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant