fix(typegen): derive interface names from slug, not label#1349
Conversation
Generated TS interface names in emdash-env.d.ts were derived from the collection's human label (labelSingular). Labels are arbitrary and user-controlled, so a label with spaces/punctuation produced an invalid identifier (e.g. `Book (do not use)` -> `Book(donotuse)`) and two collections sharing a label collapsed to a duplicate identifier -- both rejected by astro check/tsc. Derive names from the slug instead, which is constrained to /^[a-z][a-z0-9_]*$/ and unique, so PascalCasing it always yields a valid, collision-free identifier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 60fc425 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
Review: fix(typegen): derive interface names from slug, not label
Approach judgment
Solving the right problem the right way. Deriving TypeScript interface names from the constrained, unique collection slug instead of the arbitrary human labelSingular eliminates the two real bugs reported: invalid identifiers from punctuation/spaces and duplicate identifiers from shared labels. The code is clean, well-tested, and the test suite directly covers the regression cases.
Headline concern: unacknowledged breaking change
Where this PR falls short is packaging. Removing singularize and dropping the labelSingular fallback changes interface names for every collection that previously relied on them — not just the broken edge cases. Examples from the existing test fixture and demos:
slug="pages"→ oldPage, newPagesslug="blog_posts"withlabelSingular="Blog Post"→ oldBlogPost, newBlogPosts
Any user code that directly imports or references a generated interface name (rather than going through the EmDashCollections map) will fail to compile after updating. That is a real breaking change. AGENTS.md says breaking changes need an explicit decision, a package bump, and a changeset that calls the break out clearly — the current changeset frames this as a straight patch fix and omits the breakage entirely.
Other issues found
- Overly-optimistic JSDoc on
getInterfaceNameclaims PascalCasing slugs "always yields a valid, collision-free TS identifier." The "valid" part is true (slugs are constrained), but "collision-free" is not:test_1andtest1both map toTest1. The collision risk is low in practice, but the absolute guarantee is mathematically false. - Stale checked-in fixtures: all demo
emdash-env.d.tsfiles and the integration test fixture still declarePage/Post(old singularized convention). After this PR the dev-server auto-generator will emitPages/Posts, so developers running the demos or integration tests will get dirty git state. Regenerating or updating those fixtures before merge would avoid the drift.
Verdict
The implementation is correct and the tests are thorough. Address the changeset bump/callout before merge.
- Changeset bumped patch -> minor and rewritten to call out the interface-name rename (e.g. Page -> Pages) as breaking, per AGENTS.md. - Drop the inaccurate "collision-free" guarantee from getInterfaceName JSDoc: test_1 and test1 both PascalCase to Test1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Using slug is better, but an interface should be singular: the type is for a single Post, not a collection of Posts |
Review feedback: an interface describes a single entry, not the collection, so the name should be singular -- `Post`, not `Posts`. Keep deriving from the slug (valid, constrained identifier) but singularize it first. Singularization can map two distinct slugs onto the same name (`book` and `books` both -> `Book`), which would reintroduce the duplicate-identifier error this PR fixes, so resolve collisions in generateTypesFile with a deterministic numeric suffix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@ascorbic Agreed and updated. Wanted to know what you think first. |
There was a problem hiding this comment.
Re-review: what's fixed versus still open
The three prior findings are all resolved:
- Changeset & breaking change — Fixed. The changeset now bumps
emdashtominorand explicitly calls out the rename as breaking. - Overly-optimistic JSDoc — Fixed.
getInterfaceNamenow accurately notes that singularization can cause collisions and callers must dedupe. - Stale fixtures — Resolved by approach change. Re-introducing
singularizekeeps common slugs likepages/postsmapping back toPage/Post, so existing demo and test fixtures remain valid.
New issues from the latest commit
The singularize + dedupe approach is the right idea, but two gaps remain:
uniqueInterfaceNamescan still emit duplicates. The collision algorithm tracks counts per base name, but it never checks whether a deduped name likeBook2collides with another slug's base name. A site with slugsbook,books, andbook2ends up with twoBook2interfaces.- The CLI type-export endpoint (
/_emdash/api/schema?format=typescript) was not updated. It still callsgenerateTypeScriptin a loop without deduping. Before this PR thelabelSingularfallback often prevented collisions; nowsingularize(slug)makes them deterministic. Runningemdash typeson a project withbook+bookscollections will emit duplicateBookinterfaces and break the user's build.
Both should be addressed before merge.
…I export Address review on emdash-cms#1349: - uniqueInterfaceNames now picks suffixes against the set of names already emitted rather than a per-base counter, so a suffixed name can't collide with another slug's base name (slugs book/books/book2 no longer emit Book2 twice). - The schema export route (?format=typescript) now resolves names via uniqueInterfaceNames before generating, so `emdash types` on a project with colliding singularized slugs no longer emits duplicate interfaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Both findings from the re-review are addressed in 0f13ed9:
Tests pass (28), lint clean, typecheck clean. |
|
I think these failing tests may be from something else |
|
@ascorbic Green |
What does this PR do?
Generated TypeScript interface names in
emdash-env.d.tswere derived from a collection's human display label (labelSingular) instead of its slug. Labels are arbitrary and user-controlled, which produced broken type output:Book (do not use)→export interface Book(donotuse)(parentheses are illegal in an identifier).Both are rejected by
astro check/tsc, breaking the generatedemdash-env.d.ts.The fix derives interface names from the slug instead. Slugs are constrained to
/^[a-z][a-z0-9_]*$/and are unique, so PascalCasing one always yields a valid identifier (blog_posts→BlogPosts). TheEmDashCollectionsaugmentation map references the same names.Closes #
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain. — n/a, no admin UI strings changed (type-generation code only)AI-generated code disclosure
Screenshots / test output
🤖 Generated with Claude Code