Skip to content

feat: add ImportDeclaration bindings and InjectImport mutation#83

Merged
Emyrk merged 2 commits into
mainfrom
feat/import-declarations
May 29, 2026
Merged

feat: add ImportDeclaration bindings and InjectImport mutation#83
Emyrk merged 2 commits into
mainfrom
feat/import-declarations

Conversation

@Emyrk
Copy link
Copy Markdown
Member

@Emyrk Emyrk commented May 29, 2026

Adds top-level import { ... } from "...", import type { ... }, and bare import "..." support so callers can prepend imports to the generated TypeScript output.

ts.ApplyMutations(
    config.InjectImport("zod", "z"),                       // import { z } from "zod"
    config.InjectTypeImport("./types", "Foo", "Bar=Renamed"), // import type { Bar as Renamed, Foo } from "./types"
    config.InjectSideEffectImport("./polyfill"),           // import "./polyfill"
)

Foundation for a follow-up that rewrites #82 as a guts mutation composable with ExportTypes, SimplifyOptional, etc.

Design notes, merge semantics, sort order, test list

File-by-file

  • bindings/declarations.go: new ImportDeclaration (DeclarationType) and ImportSpecifier. Modeled forms: named, type-only, side-effect. Default and namespace imports are not yet supported; doc comments call this out.
  • typescript-engine/src/index.ts: importSpecifier and importDeclaration wrappers around ts.factory.createImportSpecifier / createImportDeclaration / createImportClause / createNamedImports. When the named-imports array is undefined the wrapper drops the import clause so the printer emits the side-effect form natively. dist/main.js is rebuilt with pnpm run build.
  • bindings/bindings.go: *ImportDeclaration dispatched through ToTypescriptDeclarationNode plus the goja-backed builders. Empty Alias emits just the name; a set Alias passes the original name as the TS propertyName so the printer emits Name as Alias.
  • convert.go: imports []*bindings.ImportDeclaration field on Typescript, public AppendImport(decl) that merges by (Module, IsTypeOnly, SideEffect), and an emission block in SerializeInOrder that writes the imports above the alphabetically-sorted declarations.
  • config/imports.go: the user-facing mutation API.

Merge semantics for AppendImport

Imports are keyed by (Module, IsTypeOnly, SideEffect):

  • Same key merges. Specifiers are unioned by (Name, Alias, IsTypeOnly); duplicates dropped while preserving first-occurrence order.
  • Mixed type-only + value for the same module split. This avoids silently flattening the type-only intent. InjectTypeImport("zod", "Foo") + InjectImport("zod", "bar") emits two lines, not import { Foo, bar } from "zod"; with Foo demoted.
  • Side-effect imports dedupe per module.

Deterministic emission order

Serialize sorts before emitting so output is independent of mutation order:

  • Imports: (Module asc, !SideEffect, IsTypeOnly asc). Same module groups together with side-effect first, then value, then type-only.
  • Specifiers within an import: (Name, Alias, IsTypeOnly).

Mutation API details

"Name=Alias" shorthand in InjectImport / InjectTypeImport emits Name as Alias; bare names emit as-is. InjectSideEffectImport(module) takes no names.

Tests

  • bindings/imports_test.go: goja round-trip for single, multiple, aliased, type-only, mixed, side-effect, side-effect-with-stray-IsTypeOnly.
  • config/imports_internal_test.go: Name=Alias parser.
  • imports_test.go (root), per-behavior subtests:
    • TestInjectImport_Merge — same-module dedup, specifiers sorted.
    • TestInjectImport_AliasAndType — alias shorthand + type form.
    • TestInjectImport_MixedTypeAndValueSplit — regression for the mixed-mode flatten bug.
    • TestInjectImport_SortedAcrossModules — alphabetical regardless of append order.
    • TestInjectSideEffectImport — bare emit, dedupe, side-effect-before-clausal placement.

go test ./... and go vet ./... pass (one pre-existing unreachable-code warning in convert.go predates this branch).

Follow-up

Stacked PR will replace the #82 string-builder serializer with a zod.AsSchemas mutation emitting VariableStatement + Alias pairs. That needs ImportDeclaration (this PR) plus new expression nodes for CallExpression, PropertyAccessExpression, ObjectLiteralExpression, ArrayLiteralExpression, ArrowFunction, and TypeQuery.


Generated by Coder Agents on behalf of @Emyrk.

Adds top-level ECMAScript import support to the codegen pipeline so callers
can prepend value- and type-only named imports to generated TypeScript.

- bindings/declarations.go: new ImportDeclaration (DeclarationType) and
  ImportSpecifier nodes. Only the named-imports form is modeled; default and
  namespace imports can be added later.
- typescript-engine/src/index.ts: factory wrappers importSpecifier and
  importDeclaration around ts.factory.createImportSpecifier and
  createImportDeclaration. dist/main.js is rebuilt to ship them.
- bindings/bindings.go: dispatch *ImportDeclaration through
  ToTypescriptDeclarationNode and add the goja-backed builders.
- convert.go: new imports field on *Typescript, AppendImport that merges by
  module specifier (unioning specifiers in first-seen order, IsTypeOnly only
  when all contributing imports are type-only), and emission in
  SerializeInOrder ahead of the named declarations.
- config/imports.go: InjectImport and InjectTypeImport mutations. Names may
  use the "Name=Alias" form to emit "Name as Alias".
- Tests: goja round-trip cases for ImportDeclaration, end-to-end
  ApplyMutations + Serialize coverage proving imports appear above the
  generated header's declarations and that repeated calls merge per module.

Foundation for a follow-up that rewrites the zod string-builder serializer
in PR #82 as a mutation that composes with ExportTypes, SimplifyOptional,
and the other config mutations.

Generated by Coder Agents on behalf of @Emyrk.
@Emyrk
Copy link
Copy Markdown
Member Author

Emyrk commented May 29, 2026

/coder-agent-review

Review pass on the import primitives:

- AppendImport now keys by (Module, IsTypeOnly, SideEffect) instead of
  Module alone. Mixing a type-only and a value import for the same module
  used to flatten the declaration-level IsTypeOnly flag with `&&`, silently
  demoting the type-only specifier into a value import. They now emit as
  two distinct statements.
- Serialize sorts the import block by (Module, !SideEffect, IsTypeOnly)
  and each statement's specifiers by (Name, Alias, IsTypeOnly). Output is
  deterministic across mutation orderings.
- New `SideEffect bool` on bindings.ImportDeclaration plus
  config.InjectSideEffectImport(module) emit the bare `import "module";`
  form. The TS factory wrapper drops the import clause entirely when no
  specifiers are supplied so the printer outputs the side-effect form
  natively rather than `import { } from "module"`.
- bindings/imports_test.go covers the bare and bare-plus-stray-IsTypeOnly
  shapes through the goja round trip. imports_test.go replaces the single
  smoke test with a per-behavior suite: merge dedup, alias + type form,
  mixed-mode split, alphabetical module order, side-effect dedup and
  placement before clausal imports for the same module.

Generated by Coder Agents on behalf of @Emyrk.
@Emyrk Emyrk marked this pull request as ready for review May 29, 2026 18:57
@Emyrk Emyrk merged commit 955fb63 into main May 29, 2026
1 check 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.

1 participant