Skip to content

fix(tsgen): stop modular block interfaces colliding with other generated names - #201

Open
naman-contentstack wants to merge 1 commit into
developmentfrom
fix/DX-10385
Open

fix(tsgen): stop modular block interfaces colliding with other generated names#201
naman-contentstack wants to merge 1 commit into
developmentfrom
fix/DX-10385

Conversation

@naman-contentstack

@naman-contentstack naman-contentstack commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Problem

REST-mode type generation emits two interfaces with the same name when a modular blocks field's UID matches the UID of a content type. The generated .d.ts does not compile:

export interface Form { heading?: string }     // content type `form`
export interface Form { heading: { ... } }     // blocks field `form` in `form_basic`

TypeScript reports TS2687 (mismatched modifiers on heading) and TS2717 ({} vs string | undefined).

Root cause

Three places emit interface names and none of them knew about the others:

Emitter De-duplicated against
content types / global fields (define_interface) nothing
modular blocks (type_modular_blocks) only other blocks
builtins (stack/builtins.ts) nothing

Only block-vs-block was handled — that is the DX-1272 / DX-1333 / DX-2775 fix. A block colliding with a content type or a builtin was not.

Compounding it, content types are generated one at a time, so the factory could not know about a content type it had not reached yet. The collision therefore depended on stack ordering.

Fix

Every block name is allocated from one registry, seeded before generation with the builtin names and every top-level name in the batch.

  • interfaceNameForUid() — the single place a UID becomes a name.
  • collectBuiltinInterfaceNames() — reads the builtin names from stack/builtins.ts instead of restating them, so the list cannot drift, and passes the real emission flags so names that are not emitted are not reserved (reserving an unemitted name renames a block and consumes the shared suffix counter).
  • generateTSFromContentTypes — reserves all top-level names before the loop, so the fix does not depend on ordering.
  • The prefix is normalised once at that boundary, so the builtins, the reserved names and the generated interfaces agree. A null prefix no longer emits nullFile.

Two deliberate constraints

Top-level interfaces are never renamed. Customers import content type and global field interfaces by name. Only the derived block interface is suffixed.

Verification

22 suites / 91 tests / 18 snapshots green; no snapshot changed.

Case Result
Content type form + blocks field form Form kept, block → Form1, compiles
Same, reversed generation order identical — ordering-independent
Content type UID == its own block UID GroupTest + GroupTest1
Group field sharing a content type's UID no rename — groups are inline and claim no name
Blocks named file, link, taxonomy, build_tuple, tuple_prefixes, max_tuple all renamed, no duplicates
Multiple block-vs-block collisions Hero, Hero1, Card, Card2 — unchanged from before
Stacks with no collisions byte-identical output before and after
--prefix, null prefix, --include-system-fields correct

Notes for reviewers

A rename is now logged. Previously silent; a customer's first sign was Cannot find name 'Form' in their own build. This matches every other name-mangling path in the file.

…ted names

REST-mode type generation emitted two interfaces with the same name when a
modular blocks field's UID matched the UID of a content type, producing a
.d.ts that does not compile:

    export interface Form { heading?: string }    // content type `form`
    export interface Form { heading: { ... } }    // blocks field `form`

TypeScript then reports TS2687 and TS2717 on the merged members.

Block interface names were de-duplicated only against other block names, so
they were blind to content types, global fields and the builtin interfaces.
Content types are also generated one at a time, so the factory could not see
a content type it had not reached yet.

Route every block name through a single registry, seeded up front with the
builtin names and with every top-level name in the batch:

- interfaceNameForUid() is now the one place a UID becomes a name.
- collectBuiltinInterfaceNames() reads the builtin names from
  stack/builtins.ts rather than restating them, so the list cannot drift, and
  passes the real emission flags so names that are not emitted are not
  reserved.
- generateTSFromContentTypes reserves all top-level names before the loop,
  making the fix independent of content type ordering.
- The prefix is normalised once at that boundary so the builtins, the
  reserved names and the generated interfaces agree.

Top-level interfaces are never renamed: customers import those by name. Only
the derived block interface is suffixed, and the suffix counter is
deliberately left shared so existing output is unchanged - a stack that
produces Card2 today still produces Card2, not Card1.

A rename is now reported through the logger, matching every other
name-mangling path in the file, so a customer does not meet it first as a
"Cannot find name" error in their own build.

Group fields are unaffected: they are emitted inline and claim no name, so
they cannot collide. The ticket describes the trigger as a group field; the
customer's generated output confirms it is a modular blocks field.

Known gap, unchanged by this commit and tracked separately: a content type
whose UID maps to a builtin name still emits a duplicate. Both sides are
top-level, so neither can be renamed without breaking imports. Verified
byte-identical to the previous behaviour.

Fixes DX-10385

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@naman-contentstack
naman-contentstack requested a review from a team as a code owner August 31, 2026 08:50
@snyk-io

snyk-io Bot commented Aug 31, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

Copy link
Copy Markdown

Coverage report for commit: 0acd33a
File: coverage/clover.xml

Cover ┌─────────────────────────┐ Freq.
   0% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  10% │ █░░░░░░░░░░░░░░░░░░░░░░ │  3.0%
  20% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  30% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  40% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  50% │ █░░░░░░░░░░░░░░░░░░░░░░ │  3.0%
  60% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  70% │ ██░░░░░░░░░░░░░░░░░░░░░ │  6.1%
  80% │ ██░░░░░░░░░░░░░░░░░░░░░ │  6.1%
  90% │ ██░░░░░░░░░░░░░░░░░░░░░ │  6.1%
 100% │ ███████████████████████ │ 75.8%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution 
Summary - Lines: 81.18% | Methods: 77.97% | Branches: 64.12%
FilesLinesMethodsBranches
src/constants
   index.ts100.00%33.33%100.00%
   messages.ts66.67%58.33%50.00%
src/format
   index.ts100.00%100.00%100.00%
src/generateTS/docgen
   jsdoc.ts100.00%100.00%75.00%
   nulldoc.ts100.00%100.00%100.00%
src/generateTS
   factory.ts83.67%95.24%62.36%
   index.ts82.86%69.23%70.11%
src/generateTS/shared
   cslp-helpers.ts66.67%50.00%100.00%
   utils.ts88.68%88.24%22.73%
src/generateTS/stack
   builtins.ts95.00%100.00%45.45%
src/graphqlTS
   index.ts94.74%50.00%82.09%
   queries.ts100.00%100.00%100.00%
src/logger
   index.ts51.35%53.33%37.50%
src/sdk
   utils.ts11.54%--
tests/unit
   mock.ts100.00%100.00%100.00%
tests/unit/tsgen
   boolean.ct.js100.00%100.00%100.00%
   custom-field.ct.js100.00%100.00%100.00%
   defaults.ct.js100.00%100.00%100.00%
   global.fields.ct.js100.00%100.00%100.00%
   group.ct.js100.00%100.00%100.00%
   initialization.ct.js100.00%100.00%100.00%
   isodate.ct.js100.00%100.00%100.00%
   jsdoc.ct.js100.00%100.00%100.00%
   json-rte.ct.js100.00%100.00%100.00%
   modular.blocks.ct.js100.00%100.00%100.00%
   name-collisions.ct.js100.00%100.00%100.00%
   number.ct.js100.00%100.00%100.00%
   numeric-keys.ct.js100.00%100.00%100.00%
   options.ct.js100.00%100.00%100.00%
   references.ct.js100.00%100.00%100.00%
   select.ct.js100.00%100.00%100.00%
   string.ct.js100.00%100.00%100.00%
   taxonomies.ct.js100.00%100.00%100.00%

🤖 comment via lucassabreu/comment-coverage-clover

@github-actions

Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 0 0 10 ✅ Passed
🟠 High Severity 0 0 25 ✅ Passed
🟡 Medium Severity 0 2 500 ✅ Passed
🔵 Low Severity 0 0 1000 ✅ Passed

⏱️ SLA Breach Summary

✅ No SLA breaches detected. All vulnerabilities are within acceptable time thresholds.

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 0 0 15 / 30 days ✅ Passed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

ℹ️ Vulnerabilities Without Available Fixes (Informational Only)

The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:

  • Critical without fixes: 0
  • High without fixes: 0
  • Medium without fixes: 2
  • Low without fixes: 0

✅ BUILD PASSED - All security 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