feat: shared veil-types package (generators.types) - #15
Conversation
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>
| // 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() |
There was a problem hiding this comment.
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.
| return nil, fmt.Errorf("marshalling kind entry: %w", err) | ||
| } | ||
| ref := &veilv1.KindRef{} | ||
| if err := protoencode.Unmarshal.Unmarshal(raw, ref); err != nil { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
Why
A change to a kind-independent "host" type (
Std/Os/Fetch,File,Resource,RegistryVariables,ValidationResult, …) re-emitted the inlined copy in every generatedveil-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
kindsentries 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_dirholds the package:host.ts(the shared half, written once) + one module per opted-in kind importing it, apackage.jsonwith per-kind subpath exports, and the dependency wired into each consuming hookspackage.json. Hooks import@platform/veil-types/<kind>instead of a siblingveil-types.ts.importkeep the inlineveil-types.ts— no behavior change. In-memoryrender --buildnever touches the source tree.Validation
tsc --strict— including the cross-kindqueue → servicedependents (ServiceDependentHook/ServiceFS) — and a hook importing@platform/veil-types/queueresolves via the exports map. Dependency wiring (alphabetical, preserving existing entries) and stale-veil-types.tsremoval confirmed.package.jsonfields, object-form config rejected byveil new kind, the dependency wired into the monorepo-rootpackage.json, missing validation (import-without-output_dir, emptyimport.value, colliding subpaths) — all fixed, several with regression tests.Supersedes #14 (the earlier single inline-module approach).