Skip to content

Add a TypeScript binding emitter - #9

Merged
colinjlacy merged 4 commits into
runtimeconditions:mainfrom
itvi-1234:add-typescript-binding-emitter
Oct 23, 2026
Merged

colinjlacy merged 4 commits into
runtimeconditions:mainfrom
itvi-1234:add-typescript-binding-emitter

Conversation

@itvi-1234

Copy link
Copy Markdown
Member

No description provided.

A native emitter, written in TypeScript, turns a normalized binding model
into a typed npm package: src/bindings.ts and src/index.ts, package.json,
tsconfig.json, and the structural binding manifest. It follows the Python
emitter's command contract: emit, verify, and api.

- Declarations are functions taking a per-kind marker interface. Object
  shapes are classes that declare the marker method, so a field belonging
  to another kind, or to another package's kind, does not type-check.
  String domains are enums, arrays are ReadonlyArray, and maps are
  index-signature types, which stay valid for recursive schemas.
- Names derive from exact source names with Pascal and camel case. A
  reserved word gains a trailing underscore; a TypeScript global, a fixed
  symbol, or an unresolvable collision stops emission without renaming.
- The model is read with the Python emitter's restrictions and validated
  against the shared binding-model schema.
- verify parses the generated source with the TypeScript compiler API and
  compares its public API and the manifest with a fresh rendering.
- The shared binding-manifest schema accepts the typescript language, with
  minimumNodeVersion, bindings.ts, and the boolean, number, and null
  builtins. Go and Python manifests validate unchanged.
- toolchain.lock.yaml records the TypeScript, Prettier, and Node pins under
  languages.typescript, and a test keeps the emitter's pins equal to it.
- The extension binding tooling workflow gains a TypeScript emitter job.

Tests cover the shared conformance models and the committed first-party
models: three byte-identical emissions, Prettier stability, strict tsc
compilation with provider stubs, a generated exercise of every type, and
negative diagnostics, loader, target, naming, and CLI cases.

Signed-off-by: itvi-1234 <rjsumit71@gmail.com>
@itvi-1234
itvi-1234 force-pushed the add-typescript-binding-emitter branch from 0c29123 to cca11d8 Compare October 19, 2026 17:18

@colinjlacy colinjlacy left a comment

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.

In general, looks good. Big picture, the thing I'd like to see changed is to remove hard-coded versions and see if we can inject those dynamically. The idea of tracking versions in code means that we'd have to update the code to the right version before we can actually tag the version. And that becomes a bit of a mess.

Comment on lines +9 to +11
export const EMITTER_VERSION = "0.1.0";
export const TYPESCRIPT_VERSION = "5.9.3";
export const PRETTIER_VERSION = "3.9.10";

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.

Is there a reason we'd want these hard-coded here? Seems like we'd have to maintain this every time we make a change and push a new version, use a new version of TS, etc. I wonder if there's a way to pull this in dynamically instead.

Comment on lines +27 to +28
const [major, minor] = value.split("-", 1)[0]!.split(".").map(Number) as [number, number];
const next = major === 0 ? `0.${minor + 1}.0` : `${major + 1}.0.0`;

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.

Oof, that is a really big assumption. It would be good to be able to set this dynamically.

Comment on lines +78 to +94
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": false,
"types": []
},
"include": ["src"]
}

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.

Instead of hard-coding this here, I wonder if there's a way to configure this externally with a template file.

export const KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
export const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9-]*\/)?[a-z0-9][a-z0-9-]*$/;
export const SHA_PATTERN = /^[0-9a-f]{64}$/;
const MINIMUM_NODE = [20, 0] as const;

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.

This is another one I wouldn't want to hard-code, if we can avoid it

properties:
type: {$ref: '#/$defs/nativeIdentifier'}
builtin: {enum: [string, bool, int64, float64, 'struct{}', any, str, int, float, 'None', JSONValue]}
builtin: {enum: [string, bool, int64, float64, 'struct{}', any, str, int, float, 'None', JSONValue, boolean, number, 'null']}

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.

This is fine for now, so don't worry about changing this yet. I'm worried about this growing into every language-specific type. I wonder if we need to start thinking about some sort of Karpenter style YAML join templating to dynamically compile a schema for binding generation. And if we do that, are we really working with a schema? 🤔

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.

Do we need this file? Looks more like a README. If that's what it is, let's change the name. I'd imagine your agent saw https://github.com/runtimeconditions/extensions/blob/main/tooling/extension-bindings/emitters/python/CONTRACT.md and thought that this is a paradigm it needed to follow. I need to rename and update that file to be more of a README than what it currently is, which is an LLM artifact from the process of setting up the origin binding generation workflows.

Review feedback asked for the emitter to stop tracking versions and
settings in code.

- The emitter name and version come from its package.json, and the
  TypeScript and Prettier versions come from the installed packages.
  The Prettier version check against a constant is gone because it
  compared the tool with itself.
- A dependency's npm range defaults to ^<version>, so the next-breaking
  rule is npm's own, and a dependency can set versionRange to override it.
- The generated package.json and tsconfig.json are filled from templates
  in templates/, and the Prettier options are read from templates/.
- The package target is validated against package-target.schema.yaml
  instead of patterns repeated in code. Only the checks that need two
  fields stay in code.
- CONTRACT.md is removed.

Signed-off-by: itvi-1234 <rjsumit71@gmail.com>
Keep the resolved type of scalar, any, and chained definitions instead of
discarding it, so a property that references one no longer gets an undefined
type. Compare package.json and tsconfig.json during verification, read inputs
through a bounded reader so an oversized file is rejected before it is held in
memory, and compile root, middle, and leaf packages together.

Make the npm publication workflow state that it is a placeholder, reject
dry_run false, and record in its summary that nothing was verified or
published.

Signed-off-by: itvi-1234 <rjsumit71@gmail.com>
}
}

for (;;) {

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.

does this ever exit? I'm not seeing it...

@itvi-1234
itvi-1234 force-pushed the add-typescript-binding-emitter branch from e6cb4a1 to 4cbe392 Compare October 23, 2026 06:37
Replace for (;;) and its inner break with a while loop whose condition names
what is left to plan, and explain why it ends: each pass removes the
definitions it plans from pendingDefinitions, so the set only shrinks.

Signed-off-by: itvi-1234 <rjsumit71@gmail.com>
@itvi-1234
itvi-1234 force-pushed the add-typescript-binding-emitter branch from 4cbe392 to d943981 Compare October 23, 2026 06:39
@colinjlacy
colinjlacy merged commit 5ec5a58 into runtimeconditions:main Oct 23, 2026
3 of 4 checks passed
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