Skip to content

feat(typescript): add opt-in packageJsonMergeStrategy for deep-merging packageJson overrides - #17609

Merged
adidavid014 merged 5 commits into
mainfrom
devin/1788286575-deep-merge-package-json-exports
Sep 2, 2026
Merged

feat(typescript): add opt-in packageJsonMergeStrategy for deep-merging packageJson overrides#17609
adidavid014 merged 5 commits into
mainfrom
devin/1788286575-deep-merge-package-json-exports

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Description

Linear ticket: Refs (TrueFoundry request, no ticket)

Adds an opt-in packageJsonMergeStrategy: shallow | deep option to the TypeScript SDK generator. Default shallow is the exact released behavior (byte-for-byte); deep recursively merges packageJson overrides so a user can add a single custom exports condition without redeclaring the generated import/require/default ones.

Motivation: with the released (shallow) merge, a mergeWith customizer returning {...obj, ...src} stops recursion, so overriding exports["."] with only {"trueforge-dev": ...} replaces the whole entry and the package becomes unresolvable (ERR_PACKAGE_PATH_NOT_EXPORTED). Making deep merge the default would resurrect generated keys for anyone relying on wholesale replacement, so it is gated behind the flag.

config:
  packageJsonMergeStrategy: deep
  packageJson:
    exports:
      ".":
        "trueforge-dev":
          types: "./dist/cjs/index.d.ts"
          default: "./src/index.ts"
# => ".": { "trueforge-dev": {...}, "import": {...}, "require": {...}, "default": "..." }

deep semantics: user keys win at every level; user keys are emitted first in user order (a custom condition precedes generated ones; a full entry written as require, import, default keeps that order); unmentioned generated keys are inherited; arrays are unioned user-first (same as shallow). Removing a generated key is not expressible under either strategy.

Changes Made

  • TypescriptCustomConfigSchema: packageJsonMergeStrategy: z.optional(z.enum(["shallow", "deep"])); SdkGeneratorCli defaults to "shallow".
  • Threaded SdkCustomConfigSdkGeneratorTypescriptProject.Init.extraConfigsMergeStrategymergeExtraConfigs(packageJson, extraConfigs, strategy = "shallow"). PHP's separate mergeExtraConfigs is untouched.
  • mergeExtraConfigs: shallowMerge is the original customizer verbatim; deepMerge is the recursive source-order-first merge (uses Object.hasOwn for prototype-name keys).
  • Changelog changes/unreleased/feat-package-json-merge-strategy.yml (type feat); documented packageJson / packageJsonMergeStrategy in generators/typescript/README.md. Public docs PR: fern-api/docs (see linked PR).
  • Seed snapshot seed/ts-sdk/simple-api/custom-package-json/package.json reverted to main — regenerating under the default produces zero diff.
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated — 24 tests in mergeExtraConfigs.test.ts: shared cases run under both strategies (incl. useLegacyExports/no generated exports passthrough); shallow suite locks in released behavior, including the explicit "condition-only override of . drops generated conditions" case; deep suite covers add/override/order/subset/non-removal/new subpath/arrays/prototype keys.
  • Manual testing completed — pnpm seed test --generator ts-sdk --fixture simple-api --outputFolder custom-package-json --local regenerates the snapshot byte-identical to main; earlier revision validated end-to-end with TrueFoundry's real config (require.resolve under plain, -C trueforge-dev, and ESM).

Link to Devin session: https://app.devin.ai/sessions/df07e774e6de4b6e9f4a78074bffa093
Open in Devin Desktop: https://app.devin.ai/desktop/session/df07e774e6de4b6e9f4a78074bffa093?variant=devin


Devin Review

…nditions keep generated ones

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot 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.

AI Review Summary

Solid fix: the old customizer short-circuited lodash's recursion, so nested exports entries were replaced wholesale. The new explicit deepMerge with source-first key ordering is correct and well-tested. One nit: wrapping deepMerge in mergeWith is now redundant and leaves a subtle undefined-return fallback path in lodash.

  • 🟡 1 warning(s)
  • 🔵 1 suggestion(s)

To request another review, comment /ai-review on this pull request.

Comment thread generators/typescript/utils/commons/src/typescript-project/mergeExtraConfigs.ts Outdated

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Devin Review

devin-ai-integration Bot and others added 2 commits September 1, 2026 18:19
Co-Authored-By: bot_apk <apk@cognition.ai>
Co-Authored-By: bot_apk <apk@cognition.ai>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-09-02T04:06:59Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
ts-sdk square 178s (n=5) 187s (n=5) 147s -31s (-17.4%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-09-02T04:06:59Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-09-02 16:30 UTC

devin-ai-integration Bot and others added 2 commits September 1, 2026 20:49
…ocument merge semantics

Co-Authored-By: bot_apk <apk@cognition.ai>
…erge as default

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration devin-ai-integration Bot changed the title fix(typescript): deep-merge packageJson overrides so custom export conditions keep generated ones feat(typescript): add opt-in packageJsonMergeStrategy for deep-merging packageJson overrides Sep 2, 2026
@adidavid014
adidavid014 merged commit a3ed1ab into main Sep 2, 2026
75 checks passed
@adidavid014
adidavid014 deleted the devin/1788286575-deep-merge-package-json-exports branch September 2, 2026 20:52
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