Repository navigation
SDK regeneration (TypeScript generator 3.107.1) - #533
anubhav-intercom wants to merge 4 commits into
Conversation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
levindixon
left a comment
There was a problem hiding this comment.
Ran this against master (identical to the published 7.0.3) for breaking changes and the version bump. Everything below is against 36dd082. Version strings agree on 7.1.0 in package.json, src/version.ts and both SDK headers, and 7.1.0 is unpublished on npm. CI workflows are disabled (#523), so build, unit (332 tests) and wire (966 tests, against the unchanged 7.0.3 fixtures) ran locally, all green, plus a set of consumer compile and runtime probes against both builds (script at the bottom).
Requesting changes. Two things block at any version, then there's a SemVer call on five compatibility changes.
Nearly all of it is generator output rather than anything hand-written in this PR. Flagging it here so we can decide what to patch locally behind .fernignore and what to take upstream to Fern.
Blocking: README ships imports that don't resolve
README.md is in the npm files list, so this goes out in the tarball.
- Lines 38 and 96 import from
"./src/Client", lines 51/64/77/131/226/310 import from"Intercom". Both should be"intercom-client", which doesn't appear anywhere in the new README. - Line 121 advertises
import { AdminsClient } from 'Intercom/admins'. Even corrected tointercom-client/adminsit fails withERR_PACKAGE_PATH_NOT_EXPORTED, the exports map only has.and./package.json. Either add the subpath exports or drop that example. - The Installation section (and the npm badge) from 7.0.3 are gone, and line 165 refers to the
retryStatusCodesgenerator setting, which means nothing to a package consumer. - README isn't in
.fernignore, so a hand fix gets overwritten on the next regeneration. Setting the npm package name in the Fern generator config is the durable fix, or.fernignoreit and hand-edit.
Blocking: client.fetch() fails in its default configuration
All three are in generated core and reproduce with a mocked fetch. The README documents the method, so they'd ship as documented behaviour.
- No default environment.
new IntercomClient({ token }).fetch('/contacts')throwsFailed to parse URLin Node, and.fetch('https://api.intercom.io/contacts')on the same client goes out without an Authorization header, because the origin check has no base URL to compare against. Settingenvironment: IntercomEnvironment.UsProductionexplicitly fixes both. Generated endpoints already fall back to that environment, so the passthrough should resolve it before the URL and auth checks (src/Client.ts:225passesthis._options.baseUrl ?? this._options.environmentwith no default). client.fetch('/contacts?per_page=5&starting_after=next')produces.../contacts%3Fper_page=5&starting_after=next.join()writes the input, query included, toURL.pathname(src/core/url/join.ts:24).- A POST
Requestplus an init object ({ headers: {...} }) is sent as a GET with no body. Request properties are only copied wheninitis absent (makePassthroughRequest.ts:73). Nativenew Request(req, init)keeps the method and body and only replaces the fieldsinitsets, so losing the content-type header there is expected, losing the method and body isn't.
Compatibility: compiles and runs on 7.0.3, doesn't on this branch
- P1, silent. Custom fetchers lose query params. Generated clients call
(this._options.fetcher ?? core.fetcher)({...})withqueryStringand neverqueryParameters(310 call sites, was 0).Fetcher.Args.queryParametersis still there as deprecated, so afetcher:that builds its URL from it keeps typechecking and drops every filter and pagination param without an error.events.list({ email, type: 'user', per_page: 5 })gives a custom fetcherqueryParameters: undefined, queryString: "email=...&type=user&per_page=5". I'd treat this as breaking unless custom fetchers keep receivingqueryParameters. Since every call site readsthis._options.fetcher, wrapping the user's fetcher once where options are normalised (parsequeryStringback intoqueryParameters) covers it. - P2. Three
Intercom.*aliases are gone:CreateContactRequestTwo,CreateDataEventRequestTwo,UpdateVisitorRequestOne(TS2724). They were= unknownplaceholders only used by the unstable clients, so impact is close to nil, but they're top-level stable exports, so the "stable surface unchanged" line in the description needs adjusting too. Three deprecated re-exports in a.fernignored file keeps this a minor. - P2.
token: () => process.env.INTERCOM_API_KEYno longer compiles under strict null checks (TS2322, sync and async). The type went fromSupplier<BearerToken | undefined>toSupplier<BearerToken> | undefined(src/auth/BearerAuthProvider.ts:43). Runtime still falls back to the env var when the supplier returnsundefined, so widening the type parameter back fixes it. - P2.
requestOptions.queryParams: { per_page: undefined }used to remove the endpoint param, now the endpoint value is kept (?type=user&per_page=5vs 7.0.3's?type=user&custom=).mergeAdditionalinQueryStringBuilder.ts:65-74skips nullish overrides instead of deleting the key, and itsserialized.length > 0guard also skips overrides that serialise to nothing (empty arrays). Deleting the key onundefinedis a two-line fix. Thenullomission is in the description and is fine as a release note. - P2, type-level. Response types moved from
email?: stringtoemail?: string | undefined, which breaks consumers onexactOptionalPropertyTypes(TS2375 / TS18048 againstIntercom.Contact). I'd accept this one as a documented type-level change rather than a break: it only affects consumers on an opt-in compiler flag, and restoring it means overriding the generator's output across every response type. Flagging it so it's a deliberate call, strict SemVer would count it.
Two more type-level changes with no consumer repro, same treatment as 5: BaseClientOptions and every *Client.Options moved from interface to type (breaks declaration merging only), and Fetcher.Args.headers widened to Record<string, unknown> (breaks a custom fetcher that forwards args.headers to a narrower type).
Version
7.1.0 is the right size for what's additive (client.fetch(), auth option, additionalBodyParameters, stream, error.requestId / error.cause). It stays a minor if compat items 2, 3 and 4 are restored and item 1 gets the adapter. If the adapter isn't workable in generated core, I'd go 8.0.0 with migration notes rather than ship a silent request change under a minor. Item 5 and the two type-level changes go in the release notes either way. The release tag has to equal 7.1.0 (leading v optional) or publish.yml aborts.
Release-note items and nits (not blocking)
Consumers will notice these, none are breaking:
IntercomTimeoutErrornow extendsIntercomError(wasError), soinstanceof IntercomErrorbranches catch timeouts too.error.nameis the class name instead of"Error", and the auth-missing message text changed.- The default 20s timeout now covers reading the body for non-streaming responses (
keepTimeoutUntilBodyRead,Fetcher.ts:188). 7.0.3 cleared it when headers arrived. Big responses on slow links can newly time out. - Path params
.and..now throw. 2xx responses labelled JSON that fail to parse now throw instead of returning{ ok: false }, which is a fix. - Unstable: required
requestoncreateContact/createDataEvent/updateVisitorandmessages.createMessage, requireduser/visitoronConvertVisitorRequest,ReplyConversationRequestis nowReplyConversationBodyandUpdateDataAttributeRequestis nowUpdateDataAttributeBody. Matches the description and isn't covered by semver, just a line for unstable users.
Nit: wire tests weren't regenerated. They still pass against the 7.0.3 fixtures, which is decent evidence the wire format for the covered endpoints hasn't moved.
Reproduction script (mocked fetch, placeholder token, expects base/ and head/ builds with deps alongside)
const fs = require('node:fs');
const path = require('node:path');
const ts = require('./head/node_modules/typescript');
const cases = {
stable_aliases: `
import type { Intercom } from 'SDK';
type ContactHelper = Intercom.CreateContactRequestTwo;
type EventHelper = Intercom.CreateDataEventRequestTwo;
type VisitorHelper = Intercom.UpdateVisitorRequestOne;
`,
optional_token_supplier: `
import { IntercomClient } from 'SDK';
declare const readToken: () => string | undefined;
new IntercomClient({ token: readToken });
new IntercomClient({ token: async () => readToken() });
`,
exact_optional_response: `
import type { Intercom } from 'SDK';
declare const contact: Intercom.Contact;
const email: { email?: string } = contact;
if ('email' in contact) contact.email.toUpperCase();
`,
unstable_renames_and_required_requests: `
import { IntercomClient, type Intercom } from 'SDK';
declare const client: IntercomClient;
type Reply = Intercom.unstable.ReplyConversationRequest;
type Update = Intercom.unstable.UpdateDataAttributeRequest;
client.unstable.contacts.createContact();
`,
};
function compileCase(version, name, source) {
const filename = path.join(__dirname, `${version}-${name}.ts`);
fs.writeFileSync(filename, source.replaceAll('SDK', `./${version}`));
const program = ts.createProgram([filename], {
strict: true, noEmit: true, skipLibCheck: true,
exactOptionalPropertyTypes: name === 'exact_optional_response',
target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
typeRoots: [path.join(__dirname, 'head/node_modules/@types')],
});
return ts.getPreEmitDiagnostics(program).map(d => {
const pos = d.file && d.start != null ? d.file.getLineAndCharacterOfPosition(d.start) : undefined;
return { file: d.file && path.relative(__dirname, d.file.fileName), line: pos && pos.line + 1,
code: d.code, message: ts.flattenDiagnosticMessageText(d.messageText, '\n') };
});
}
async function runtimeCases(version) {
const { IntercomClient } = require(`./${version}/dist/cjs/index.js`);
const received = [];
const client = new IntercomClient({ token: 'review-placeholder', fetcher: async args => {
received.push({ queryParameters: args.queryParameters ?? null, queryString: args.queryString ?? null });
return { ok: true, body: { type: 'event.list', events: [] }, rawResponse: { headers: new Headers(), status: 200 } };
}});
await client.events.list({ email: 'review@example.com', type: 'user', per_page: 5 });
const urls = [];
const fetchClient = new IntercomClient({ token: 'review-placeholder', fetch: async (url, init) => {
urls.push(String(url));
return new Response(JSON.stringify({ type: 'event.list', events: [] }), { headers: {'Content-Type': 'application/json'} });
}});
await fetchClient.events.list({ type: 'user', per_page: 5 }, {queryParams: { per_page: undefined, custom: null }});
return { custom_fetcher: received, undefined_null_query_override: urls };
}
async function passthroughCases() {
const { IntercomClient, IntercomEnvironment } = require('./head/dist/cjs/index.js');
const observations = [];
for (const explicitEnvironment of [false, true]) {
const captured = [];
const client = new IntercomClient({ token: 'review-placeholder',
...(explicitEnvironment ? {environment: IntercomEnvironment.UsProduction} : {}),
fetch: async (url, init) => {
captured.push({url: String(url), hasAuth: new Headers(init.headers).has('authorization')});
new Request(url, init);
return new Response('{}');
},
});
for (const input of ['/contacts', 'https://api.intercom.io/contacts']) {
try { await client.fetch(input); observations.push({explicitEnvironment, input, result: 'ok'}); }
catch(error) { observations.push({explicitEnvironment, input, result: error.message}); }
}
observations.push({explicitEnvironment, captured});
}
return observations;
}
async function passthroughInputCases() {
const { IntercomClient, IntercomEnvironment } = require('./head/dist/cjs/index.js');
const captured = [];
const client = new IntercomClient({ token: 'review-placeholder', environment: IntercomEnvironment.UsProduction,
fetch: async (url, init) => {
captured.push({url: String(url), method: init.method, hasBody: init.body != null,
contentType: new Headers(init.headers).get('content-type')});
return new Response('{}');
},
});
await client.fetch('/contacts?per_page=5&starting_after=next');
await client.fetch(new Request('https://api.intercom.io/contacts', {
method: 'POST', body: JSON.stringify({email: 'review@example.com'}),
headers: {'Content-Type': 'application/json'},
}), {headers: {'X-Review': 'true'}});
return captured;
}
(async () => {
const results = {typechecks: {}, runtime: {}, passthrough: []};
for (const [name, source] of Object.entries(cases)) {
results.typechecks[name] = {};
for (const version of ['base', 'head']) results.typechecks[name][version] = compileCase(version, name, source);
}
for (const version of ['base', 'head']) results.runtime[version] = await runtimeCases(version);
results.passthrough = await passthroughCases();
results.passthroughInputs = await passthroughInputCases();
fs.writeFileSync(path.join(__dirname, 'compatibility-results.json'), JSON.stringify(results, null, 2) + '\n');
console.log(JSON.stringify(results, null, 2));
})().catch(error => { console.error(error); process.exitCode = 1; });Restore package-user README, default environment and query/Request handling in client.fetch(), query parameters for custom fetchers, deprecated type aliases, optional token supplier typing and queryParams removal semantics. Fernignore the patched files. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
levindixon
left a comment
There was a problem hiding this comment.
Re-ran everything against d167574: build, unit (347, the 332 from before plus the 15 new ones), wire (966, unchanged fixtures) and the consumer probes from the first review. Everything from that review is addressed. README imports resolve, Installation and the badge are back, client.fetch() resolves the US environment and keeps query strings out of the path, the Request merge now matches new Request(req, init), custom fetchers get queryParameters again, the three aliases and the wider token type compile under strict, and queryParams: { x: undefined } removes the param. 7.1.0 holds.
The adapter plus .fernignore approach is the one I was hoping for, and the new tests pin each item down individually, which makes the next regeneration much easier to check.
Requesting changes for one bug in the new fetcher adapter. It's a one-line fix, then this is an approve from me.
Bug: repeated query keys reach custom fetchers with the first value split into characters
In withQueryParameters (src/BaseClient.ts), the second occurrence of a key does [...(existing as string[]), value], but existing is still a plain string at that point, so it spreads into characters. Through the built SDK, events.list({ type: "user" }, { queryParams: { tags: ["ab", "cd"] } }) reaches the fetcher as tags: ["a", "b", "cd"]. The new test only uses ["a", "b"], which is why it passes.
Fix is Array.isArray(existing) ? [...existing, value] : [existing, value], plus values longer than one character in the test.
Concern: .fernignore now covers src/api/types/index.ts
That's the 160-line barrel that gets a new export * line for every type the API adds. Once it's ignored, the next regeneration writes the new type files but the barrel stays frozen, so new types stop being exported from Intercom.* and nothing fails to tell us.
I'd move the three aliases into a new fernignored src/api/types/compat.ts and re-export it from src/api/index.ts (four lines, changes far less often), ignoring that file instead of the barrel. If you'd rather keep the barrel ignored, a comment at the top saying it has to be diffed by hand after each regeneration would be the minimum.
Nits, none blocking:
queryParametersvalues are now strings (per_page: "5"where 7.0.3 passed5). Fine for fetchers that serialise straight to a URL. A line in the release notes or on the wrapper would save someone a debugging session.makePassthroughRequestreadsinput.arrayBuffer()even wheninit.bodyreplaces it, so the caller'sRequestends upbodyUsed. Nativenew Request(req, init)leaves it alone. Guarding oninit?.body === undefinedmatches it.- With the US default,
client.fetch("https://api.eu.intercom.io/...")goes out withoutAuthorizationunlessenvironmentis set to EU. That's the right behaviour, but the Custom Fetch section of the README says "benefiting from ... authentication" without the condition. One sentence there would head off the question. - The PR description could mention the compat patch and the six fernignored files, so whoever runs the next regeneration knows those are hand-maintained.
Unrelated to the change, but since CI is off: on Node 25 the wire suite fails at import with localStorage.getItem is not a function (msw 2.11 meets the new localStorage global). NODE_OPTIONS=--no-experimental-webstorage gets all 966 passing.
…ut of the types barrel The second value of a repeated key was spread into characters. Leave a Request body unread when init replaces it, keep the generated types barrel regenerable by moving the 7.0.x aliases to a fernignored compat file, and note in the README that auth only goes to the configured environment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Fixed the repeated-key bug and moved the aliases to a fernignored |
Why?
The SDK was last generated with TypeScript generator 3.35.8. Regenerating on 3.107.1 picks up the generator's fixes to the shared request code, including correct encoding of values placed in request paths.
How?
Regenerated from the same API spec as 7.0.3 and brought the core tests in line with the new generated code. Released as 7.1.0 rather than a patch because query parameters set to null are now left out of the request instead of being sent empty.
Notes for reviewers
.fernignore):README.md,src/BaseClient.ts,src/api/index.ts,src/api/types/compat.ts,src/auth/BearerAuthProvider.ts,src/core/fetcher/makePassthroughRequest.ts,src/core/url/QueryStringBuilder.ts. Compare them with the generated output on the next regeneration.BaseClient.tsalso holds the SDK version string.queryParametersagain, rebuilt from the query string, so values arrive as strings (per_page: "5").unstablenamespace, two request types are renamed and some request arguments become required.Generated with Claude Code