Skip to content

Civet language in Hera code and handlers - #51

Merged
STRd6 merged 16 commits into
mainfrom
multi-language-handlers
Mar 31, 2026
Merged

Civet language in Hera code and handlers#51
STRd6 merged 16 commits into
mainfrom
multi-language-handlers

Conversation

@STRd6

@STRd6 STRd6 commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

Adds support for Civet inside Hera similar to the existing TS support.

Made some changes to the generated TS and CJS to ensure it is compatible when compiled as Civet including with coffeeCompat.

Working in Civet: DanielXMoore/Civet#1876

STRd6 and others added 10 commits March 29, 2026 15:57
Adds `language` CompilerOptions field to control handler body language
(js, ts, civet). Compiler applies Civet-compatible formatting (indented
handlers, single-quoted $EXPECT strings) when language is 'civet'. TS
scaffold output is also enabled for civet. Esbuild plugin forwards the
option.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Civet language loader (CJS + ESM hooks) that compiles .hera files
with language:'civet' then transpiles the Civet output to JS using
@danielx/civet. Exports register/civet, register/civet/cjs, and
register/civet/esm from the package. Supports custom civet options via
the initialize export on the ESM hook and options.civet on the CJS hook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a hera.language VS Code setting that controls the handler body
language for generated parsers. The LSP server reads the setting and
forwards it as a compiler option.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds civet-default (plain civet loader) and civet-custom (with
coffeeCompat parseOptions via the initialize hook) loader examples.
Includes integration tests via the test-examples script and a README
section describing the civet pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Describes the repo layout, bootstrap problem, build wiring, compiler
internals, language pipelines, and testing conventions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@STRd6 STRd6 changed the title Multi language handlers Civet language in Hera code and handlers Mar 30, 2026
@STRd6
STRd6 requested a review from edemaine March 30, 2026 06:11
@edemaine

Copy link
Copy Markdown
Collaborator

@greptileai Could you review this?

@greptile-apps

greptile-apps Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a first-class Civet language pipeline to Hera, mirroring the existing TypeScript (tsc) pipeline architecture. Grammars can now declare language: 'civet' and their handler bodies are compiled by @danielx/civet before execution.

Key changes:

  • source/compiler.civetlanguage option added to CompilerOptions; types is now auto-derived from language; a new jsString helper avoids backslash-escaped \" in generated code (which Civet ≤ 0.10.x couldn't parse); handler-body tokens are unconditionally indented 4 spaces so Civet's whitespace-sensitive parser can interpret them inside function bodies; jsTail/tsTail ternary constructs are rewritten as explicit if/else statements for coffeeCompat compatibility; cjsHead is selected instead of tsHead when language === 'civet' and module is unset, avoiding import type in CJS contexts.

  • source/register/civet/ — Four new files (parallel to register/tsc/) implement the Node.js loader: a shared compileCivetToJs helper (uses { sync: true } for CJS compatibility), a CJS require.extensions hook, an ESM load hook with initialize() support for custom civetOptions, and an index that registers both hooks in the correct order.

  • source/esbuild.civet — The esbuild plugin gains Civet post-processing via civet.compile(compiled) without { js: true }, so Civet outputs TypeScript which esbuild then strips — preserving advanced TS features (const enum etc.).

  • lsp/src/server.ts — Adds hera.language VS Code configuration and a getHeraLanguage() helper that reads from VS Code settings first, falling back to package.json#hera.language. Currently only logged at initialization; not yet wired into diagnostics/completions.

  • loader-examples/civet-custom/ and loader-examples/civet-default/ — Working end-to-end examples demonstrating default and coffeeCompat Civet grammars.

Confidence Score: 5/5

Safe to merge — all remaining findings are P2 style suggestions that do not affect correctness or runtime behavior.

No P0 or P1 issues found. The Civet pipeline architecture is clean and mirrors the proven tsc pipeline. The hook registration order, CJS/ESM head selection, and handler-body indentation logic are all correct. The two P2 findings (missing filename arg in three test calls, unhandled promise rejection in LSP init) are minor and do not affect functionality.

test/register/civet/transpile.civet (missing filename args), lsp/src/server.ts (unhandled promise).

Important Files Changed

Filename Overview
source/compiler.civet Core compiler changes: adds language option, jsString helper for Civet-compatible string quoting, types auto-derivation from language, Civet-specific cjsHead selection, handler-body indentation (4 spaces, unconditional), and Civet-compatible reformatting of jsTail/tsTail constructs.
source/esbuild.civet Adds Civet post-processing pass inside the esbuild plugin when language:'civet'; correctly omits { js: true } so Civet outputs TypeScript and esbuild strips types with loader:'ts'. Adds defaultLoaderForLanguage helper.
source/register/civet/transpile.civet Shared compileCivetToJs helper; uses createRequire anchored to process.cwd() for peer-dep resolution; forces sync:true for CJS compatibility.
source/register/civet/cjs.civet CJS require-extension hook: Hera compile (with language:'civet') → compileCivetToJs → module._compile. Exports mutable options object for caller customisation.
source/register/civet/esm.civet ESM load hook post-processor; calls next() first (correct order), then applies compileCivetToJs only for .hera URLs. Receives civetOptions via initialize().
source/register/civet/index.civet Registers both ESM hooks (base Hera with language:'civet', then Civet post-processor) and imports CJS hook. Hook registration order is correct for load-hook LIFO chaining.
test/register/civet/transpile.civet Unit tests for compileCivetToJs. Three of four test calls omit the required filename argument (passes undefined at runtime, TypeScript error silently skipped by transpileModule).
lsp/src/server.ts Adds getHeraLanguage() / getLanguageFromPackageJson() helpers and hera.language VS Code configuration. Language is detected at initialization and logged but not yet wired into LSP functionality. Promise returned from getHeraLanguage() is missing a .catch() handler.
package.json Adds ./register/civet, ./register/civet/cjs, and ./register/civet/esm package exports; bumps @danielx/civet dev-dependency from 0.10.2 → 0.11.2.
build/esbuild.civet Manually adds the four new source/register/civet/*.civet entry points to the esbuild bundle — consistent with the existing pattern that requires explicit listing.

Sequence Diagram

sequenceDiagram
    participant GF as .hera file
    participant NL as Node ESM/CJS Loader
    participant HC as Hera Compiler
    participant CT as compileCivetToJs
    participant CV as @danielx/civet
    participant JS as JavaScript Module

    Note over NL: register/civet/index.civet<br/>registers two ESM hooks (LIFO)

    GF->>NL: import / require
    NL->>HC: heraCompile(src, {language:'civet'})
    Note over HC: Uses cjsHead (CJS)<br/>or tsHead (ESM)<br/>+ tsTail + indented handler bodies
    HC-->>NL: Civet source string
    NL->>CT: compileCivetToJs(civetSrc, url, opts)
    CT->>CV: civet.compile({js:true, sync:true})
    CV-->>CT: JavaScript string
    CT-->>NL: JavaScript string
    NL-->>JS: Loaded JS module

    Note over GF,JS: esbuild path differs:<br/>civet.compile() without {js:true}<br/>→ TypeScript → esbuild strips types
Loading

Reviews (1): Last reviewed commit: "remove extra async" | Re-trigger Greptile

Comment thread test/register/civet/transpile.civet
Comment thread lsp/src/server.ts
Comment on lines +88 to +90
getHeraLanguage().then(lang => {
connection.console.log(`[hera] language: ${lang}`);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unhandled promise rejection

getHeraLanguage() is awaited with .then() but has no .catch() handler. Both connection.workspace.getConfiguration(…) and the filesystem reads inside getLanguageFromPackageJson are individually wrapped in try/catch, making a throw unlikely — but connection.workspace.getConfiguration itself can reject if the connection is not yet fully ready on some clients, resulting in an unhandled rejection that surfaces as an uncaught error in the LSP host process.

Suggested change
getHeraLanguage().then(lang => {
connection.console.log(`[hera] language: ${lang}`);
});
getHeraLanguage().then(lang => {
connection.console.log(`[hera] language: ${lang}`);
}).catch(err => {
connection.console.error(`[hera] failed to read language setting: ${err}`);
});

Comment thread lsp/package.json
"description": "The language used for handlers and code blocks in .hera files."
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should document in this string that this can also be set via package.json setting, but vscode setting overrides?

I'm not actually sure about this priority order. I could imagine making one global setting for vscode in general, but I'd usually trust the package.json of the project I'm in as higher priority...

I also wonder whether it'd make sense to support a meta comment at the top of a Hera file that overrides for that file, in case of multiple .hera's with different languages. I also kind of like the specification being in the Hera file itself. But I also see that the current design doesn't allow multiple languages, and maybe this should be for a future PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the LSP side of things needs a whole other PR to work this out including editor integration and sourcemapping (like go to def from inside the handlers)

Comment thread lsp/src/server.ts
if (hasConfigurationCapability) {
const settings = await connection.workspace.getConfiguration({ section: 'hera' });
const lang = (settings as Partial<HeraSettings>).language;
if (lang === 'javascript' || lang === 'typescript' || lang === 'civet') return lang;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small isLang helper might be nice to avoid the duplication, in case we add more languages.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any other languages would be a pretty big rewrite of the compiler since all the mechanisms would need to be implemented in the target language.

Would definitely be cool to get hera for zig eventually though.

Comment thread source/compiler.civet Outdated

/**
Quote a string for JS output. Prefer single quotes when the string contains
double quotes, to avoid backslash-escaped double quotes that Civet can't parse.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a bug in Civet? Not sure what we're avoiding here, though I certainly like using single quotes when there's only double quotes in the string, and don't really have a preference when there's both quote types, so this behavior is fine.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was just the AI sneaking in an extra unnecessary function in confusion, thanks for the double check.

// bodies and uses single-quoted strings for Civet parser compatibility
register "../../esm.js", pathToFileURL(__filename), { data: { language: 'civet' } }
// register the Civet post-processor hook
register "./esm.js", pathToFileURL(__filename)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, hook composition! I take it the registration order equals the application order. I expected to see the transpiler passed into data, but I guess that's how we did TS earlier?

// createRequire needs a base path for peer dep resolution.
// process.cwd() is available in both ESM (tests) and CJS (production),
// and anchors module resolution to the project root where peer deps live.
_require := createRequire(`${process.cwd()}/package.json`)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that process.cwd() is always going to be the project root. It will be when building, but not when running scripts in subdirectories... Oh, but I guess that's still fine, because require will walk up from there. Is there an issue if package.json doesn't exist in that directory, though?

In a perfect world, I imagine we'd pass in context.parentURL from the hook?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too sure about those edge cases, at that point the project configuration is likely far off the beaten path.

Comment thread source/compiler.civet
if (options.startRule !== null && options.startRule !== undefined)
parser = grammar[options.startRule] as Parser<ParserResult<Grammar[K]>>
else
parser = Object.values(grammar)[0] as Parser<ParserResult<Grammar[K]>>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the old style is better, and just add the as casts.

Comment thread source/esbuild.civet Outdated
let civet: any
/* c8 ignore next 7 */
try
civet = (await import('@danielx/civet')).default ?? await import('@danielx/civet')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely we don't need this ?? as Civet's exports are consistent.

Comment thread source/esbuild.civet Outdated
catch
throw new Error [
"[@danielx/hera] Civet support requires '@danielx/civet' to be installed."
"Run: npm install @danielx/civet"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Run: npm install @danielx/civet"
"Run: npm install -D @danielx/civet"

It's less clear for the esm plugin, but in an esbuild context I'd say this is a dev dependency.

Comment thread test/esbuild-plugin.civet Outdated
"a" ->
return "ok" as string
"""
pluginOptions: { language: 'typescript', loader: 'ts' }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pluginOptions: { language: 'typescript', loader: 'ts' }
pluginOptions: { language: 'javascript', loader: 'ts' }

Without this change, I don't think we're testing anything.

Comment thread AGENTS.md Outdated
@@ -0,0 +1 @@
CLAUDE.md No newline at end of file

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather do the opposite: use AGENTS.md which is standard, and put See @AGENTS.md in CLAUDE.md.

I'm also not really sure we need these files. This paper finds that AGENTS files, especially those autogenerated by LLMs, worsened quality and significantly increased cost. But I leave it to you to decide.

(If we remove them, some of the content might be good for docs though.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think migrating the important info into other docs in the repo that are useful for people and agents is best.

I've also found that AGENTS.md in practice is largely wishful thinking :P

@STRd6
STRd6 merged commit f2ac628 into main Mar 31, 2026
4 checks passed
@STRd6
STRd6 deleted the multi-language-handlers branch March 31, 2026 22:03
@STRd6 STRd6 mentioned this pull request Apr 8, 2026
2 tasks
@STRd6 STRd6 mentioned this pull request Apr 26, 2026
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.

2 participants