Skip to content

feat: shared veil-types package (generators.types) - #15

Merged
vercel-eddie merged 4 commits into
mainfrom
feat/veil-types-package
Jun 26, 2026
Merged

feat: shared veil-types package (generators.types)#15
vercel-eddie merged 4 commits into
mainfrom
feat/veil-types-package

Conversation

@vercel-eddie

Copy link
Copy Markdown
Collaborator

Why

A change to a kind-independent "host" type (Std/Os/Fetch, File, Resource, RegistryVariables, ValidationResult, …) re-emitted the inlined copy in every generated veil-types.ts, turning a single-field change into a 100-file PR (e.g. vercel/api#77630). This factors those shared declarations into one place so they change once.

Summary

  • kinds entries are now polymorphic — a bare path string (inline, unchanged) or { path, import: { name, value } } that opts the kind into a shared types package. Existing string-array configs keep working.
  • generators.types.output_dir holds the package: host.ts (the shared half, written once) + one module per opted-in kind importing it, a package.json with per-kind subpath exports, and the dependency wired into each consuming hooks package.json. Hooks import @platform/veil-types/<kind> instead of a sibling veil-types.ts.
  • Kinds without import keep the inline veil-types.ts — no behavior change. In-memory render --build never touches the source tree.

Validation

  • Built the binary and ran it against the 10 real api kinds in package mode: every generated module typechecks under tsc --strict — including the cross-kind queue → service dependents (ServiceDependentHook/ServiceFS) — and a hook importing @platform/veil-types/queue resolves via the exports map. Dependency wiring (alphabetical, preserving existing entries) and stale-veil-types.ts removal confirmed.
  • A 4-dimension adversarial review surfaced 8 real bugs — HTML-escaping mangling unrelated package.json fields, object-form config rejected by veil new kind, the dependency wired into the monorepo-root package.json, missing validation (import-without-output_dir, empty import.value, colliding subpaths) — all fixed, several with regression tests.

Supersedes #14 (the earlier single inline-module approach).

vercel-eddie and others added 3 commits June 24, 2026 17:10
Generate kind types into one shared package instead of inlining the
~150 kind-independent host declarations (Std/Os/Fetch, File, Resource,
RegistryVariables, …) into every per-hook veil-types.ts, so a
shared-type change touches one file rather than every generated module.

- `kinds` entries are polymorphic: a bare path string (inline,
  unchanged) or `{ path, import: { name, value } }` that opts the kind
  into the shared package.
- `generators.types.output_dir` holds the package: host.ts (the shared
  half, once) + one module per opted-in kind importing it, a
  package.json with per-kind subpath exports, and the dependency wired
  into each consuming hooks package.json. Hooks import
  `@platform/veil-types/<kind>` instead of a sibling veil-types.ts.
- Kinds without `import` keep the inline veil-types.ts (no behavior
  change). In-memory render builds never touch the source tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
veil no longer names or versions the shared types package — it sets
nothing but the `exports` entries for the generated type files. The
package.json (name/version/private/…) is owned by the repo and must
exist with a name matching the import package; veil validates that up
front and otherwise leaves it untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pecheck

- veil new kind, when generators.types is configured, scaffolds the kind in
  package mode: the hook imports @<pkg>/<kind>, a hooks/package.json carries
  the workspace dep, and the kind registers in object form — instead of a
  local veil-types.ts. The package name is read from the repo-owned
  package.json (veil names nothing).
- Write the package.json exports up front (in writeShared, for every opted-in
  kind) rather than after the per-kind loop, so a brand-new kind's
  @<pkg>/<kind> import resolves during the same build's typecheck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel-eddie
vercel-eddie marked this pull request as ready for review June 25, 2026 20:46
Comment thread pkg/commands/typespackage.go Outdated
// Write the package.json exports up front — for every opted-in kind — so a
// hook's `@pkg/<kind>` import resolves during the typecheck that follows,
// including a brand-new kind whose module this same build generates.
return tp.writeManifest()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If writeKindModule fails mid-loop, the build aborts but leaves a committed package.json exporting ./B to ./B.ts for a module that was never written, so @pkg/B fails to resolve. We should write the manifest after the per-kind loop, scoped to kinds that succeeded, so that we can meet the goal stated in the function comment above.

Comment thread pkg/config/config.go
return nil, fmt.Errorf("marshalling kind entry: %w", err)
}
ref := &veilv1.KindRef{}
if err := protoencode.Unmarshal.Unmarshal(raw, ref); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

protoencode.Unmarshal runs with DiscardUnknown, so a typo in a top-level key, like {path, imprt:{…}}, is silently dropped: ref.GetImport() is nil, no error, and the kind quietly degrades to inline mode (never emitted into the package, never wired). A typo inside import (like value instead of val) is caught by the name==""||value=="" guard below, but a misspelled import key is not. We should reject unknown keys for this entry type.

for _, sub := range tp.subpath {
exports["./"+sub] = "./" + sub + ".ts"
}
m["exports"] = exports

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This reassignment is unconditional, so whenever the assertion on line 196 doesn't yield a map the original value is overwritten. The common case would be the valid npm string-sugar form ("exports": "./index.ts") - line 196 returns nil, line 198 swaps in a fresh map, and this line drops the repo's entry with no error or warning, which veil shouldn't do. We shoul either error out when m["exports"] is present but not an object, or merge into the existing value instead of replacing it. (This doesn't affect api#77828, which uses the object form, but it's a general data-loss path.)

…uard)

- Write the package.json exports incrementally in the build loop, scoped to
  kinds whose module was actually written, so a kind that fails mid-loop is
  never exported to a missing module (while still being present before that
  kind's typecheck).
- Reject unknown keys in a `kinds` entry (e.g. a typo'd `imprt:`) instead of
  silently dropping them under DiscardUnknown and degrading to inline mode.
- Error rather than overwrite a repo's non-object `exports` (e.g. the
  string-sugar "exports": "./index.ts"), so veil never drops a repo's own entry.

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

@bkonkle bkonkle left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀

@vercel-eddie
vercel-eddie merged commit d91067f into main Jun 26, 2026
3 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