Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,23 @@ import { uploadMixpanelEvent, MIXPANEL_EVENT } from './mixpanel-service';
import { tokenizedFetch } from './tokenizedFetch';
import { getAnswerFromQuery } from './utils/graphql/nlsService/nls-answer-service';
import { createLiveboardWithAnswers } from './utils/liveboardService/liveboardService';
import { UIPassthroughEvent } from './embed/hostEventClient/contracts';
import {
UIPassthroughEvent,
UIPassthroughContractBase,
UIPassthroughRequest,
UIPassthroughResponse,
UIPassthroughArrayResponse,
HostEventRequest,
HostEventResponse,
TriggerPayload,
TriggerResponse,
LiveboardTab,
ApplicabilityLevel,
Applicability,
FilterUpdate,
LiveboardFilter,
LiveboardParameter,
} from './embed/hostEventClient/contracts';

export {
init,
Expand Down Expand Up @@ -170,6 +186,24 @@ export {
VizPoint,
CustomActionPayload,
UIPassthroughEvent,
// Host event payload / response contracts. These describe what to send with
// `embed.trigger(HostEvent.X, payload)` and what the returned promise
// resolves with. See `UIPassthroughContractBase` for the per-event
// request/response shapes.
UIPassthroughContractBase,
UIPassthroughRequest,
UIPassthroughResponse,
UIPassthroughArrayResponse,
HostEventRequest,
HostEventResponse,
TriggerPayload,
TriggerResponse,
LiveboardTab,
ApplicabilityLevel,
Applicability,
FilterUpdate,
LiveboardFilter,
LiveboardParameter,
ListPageColumns,
DataPanelCustomColumnGroupsAccordionState,
CustomActionsPosition,
Expand Down
195 changes: 195 additions & 0 deletions src/react/all-types-export.parity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* Parity gate: every symbol exported from `src/index.ts` must also be exported
* from `src/react/all-types-export.ts`.
*
* `package.json` `exports` restricts consumers to two entry points — `.` and
* `./react` — and the React surface is a HAND-MAINTAINED duplicate export list,
* not a `export * from '../index'`. So a symbol added only to `src/index.ts` is
* invisible to every React consumer.
*
* The sibling `all-types-export.spec.ts` cannot catch this: it inspects the
* module at runtime, and type-only exports (interfaces, type aliases) are
* erased by the compiler. This spec therefore compares the two files as TEXT.
*/
import * as fs from 'fs';
import * as path from 'path';

const INDEX_PATH = path.join(__dirname, '..', 'index.ts');
const REACT_EXPORT_PATH = path.join(__dirname, 'all-types-export.ts');

/**
* Symbols intentionally absent from the React surface.
* Add here only with a reason — every entry is a thing React users cannot use.
*/
const REACT_EXEMPT = new Set<string>([
// React has its own component wrappers for these; the raw classes are not
// part of the /react surface.
'SearchEmbed',
'SearchBarEmbed',
'LiveboardEmbed',
'AppEmbed',
'SpotterEmbed',
'SpotterAgentEmbed',
'ConversationEmbed',
'BodylessConversation',
'PreRenderedSearchEmbed',
'PreRenderedSearchBarEmbed',
'PreRenderedLiveboardEmbed',
'PreRenderedAppEmbed',
'PreRenderedConversationEmbed',
'PinboardEmbed',
'TsEmbed',
'V1Embed',
]);

/**
* PRE-EXISTING gaps, discovered when this spec was introduced. These are
* symbols React consumers cannot import today — most look like oversights
* rather than deliberate omissions (e.g. `ContextType`, `CustomActionPayload`,
* `VizPoint`, the Spotter view configs).
*
* This list may only ever SHRINK. Do not add to it — a new entry means a
* symbol was added to `src/index.ts` without adding it to the React surface,
* which is exactly what this spec exists to prevent. Removing entries (by
* exporting them from `all-types-export.ts`) is a welcome follow-up; it is
* deliberately out of scope for the change that introduced this gate.
*/
const KNOWN_REACT_GAPS = new Set<string>([
'AnswerService',
'AutoMCPFrameRendererViewConfig',
'BackgroundFormatType',
'BodylessConversationViewConfig',
'ConditionalFormattingComparisonType',
'ConditionalFormattingOperator',
'ContextMenuTriggerOptions',
'ContextType',
'ConversationViewConfig',
'CustomActionPayload',
'DataLabelFilterOperator',
'EmbedErrorCodes',
'EmbedErrorDetailsEvent',
'ErrorDetailsTypes',
'HomeLeftNavItem',
'HomePage',
'HomePageSearchBarMode',
'HomepageModule',
'LegendPosition',
'ListPage',
'ListPageColumns',
'LogLevel',
'MIXPANEL_EVENT',
'PrimaryNavbarVersion',
'SessionInterface',
'SpotterAgentEmbedViewConfig',
'SpotterChatViewConfig',
'SpotterEmbedViewConfig',
'SpotterQueryMode',
'SpotterShareConversationConfig',
'SpotterSidebarViewConfig',
'SpotterVizConfig',
'SpotterVizLoaderTip',
'SpotterVizStarterPrompt',
'TableContentDensity',
'TableTheme',
'UnderlyingDataPoint',
'VisualizationOverrides',
'VizPoint',
// Standalone helpers re-exported from src/index.ts outside the main block.
'createLiveboardWithAnswers',
'executeTML',
'executeTMLInput',
'exportTML',
'exportTMLInput',
'getAnswerFromQuery',
'startAutoMCPFrameRenderer',
'tokenizedFetch',
]);

/**
* Extract identifiers from every `export { ... }` block in a source file.
* Handles `a`, `a as b` (records the exported name `b`), comments and trailing
* commas.
*/
const extractExportedNames = (source: string): Set<string> => {
const names = new Set<string>();
const blockRe = /export\s*\{([\s\S]*?)\}/g;
let match = blockRe.exec(source);
while (match !== null) {
match[1]
// strip line and block comments
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/[^\n]*/g, '')
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
.forEach((entry) => {
// `foo as bar` exports the name `bar`
const parts = entry.split(/\s+as\s+/);
const exported = (parts[parts.length - 1] || '').trim();
if (/^[A-Za-z_$][\w$]*$/.test(exported)) {
names.add(exported);
}
});
match = blockRe.exec(source);
}
return names;
};

describe('react/all-types-export parity with index', () => {
it('exports every symbol that src/index.ts exports', () => {
const indexNames = extractExportedNames(fs.readFileSync(INDEX_PATH, 'utf8'));
const reactNames = extractExportedNames(fs.readFileSync(REACT_EXPORT_PATH, 'utf8'));

// Sanity: the extractor found something. Guards against a regex that
// silently matches nothing after a refactor.
expect(indexNames.size).toBeGreaterThan(50);
expect(reactNames.size).toBeGreaterThan(50);

const missing = [...indexNames]
.filter((name) => !reactNames.has(name))
.filter((name) => !REACT_EXEMPT.has(name))
.filter((name) => !KNOWN_REACT_GAPS.has(name))
.sort();

if (missing.length > 0) {
throw new Error(
'These symbols are exported from src/index.ts but NOT from '
+ 'src/react/all-types-export.ts, so React consumers cannot '
+ `import them:\n ${missing.join('\n ')}\n\n`
+ 'Add them to src/react/all-types-export.ts. Do NOT add them '
+ 'to KNOWN_REACT_GAPS — that list is a shrinking backlog of '
+ 'pre-existing gaps and must never grow.',
);
}
});

it('KNOWN_REACT_GAPS only lists symbols that are genuinely still missing', () => {
// Ratchet: once a gap is fixed, its entry must be deleted, so the list
// can only shrink.
const indexNames = extractExportedNames(fs.readFileSync(INDEX_PATH, 'utf8'));
const reactNames = extractExportedNames(fs.readFileSync(REACT_EXPORT_PATH, 'utf8'));

const stale = [...KNOWN_REACT_GAPS]
.filter((name) => reactNames.has(name) || !indexNames.has(name))
.sort();

if (stale.length > 0) {
throw new Error(
'These entries in KNOWN_REACT_GAPS are no longer gaps (they are '
+ 'now exported from the React surface, or no longer exported '
+ `from src/index.ts). Delete them from the list:\n ${stale.join('\n ')}`,
);
}
});

it('exposes the host event contract types on the React surface', () => {
const reactNames = extractExportedNames(fs.readFileSync(REACT_EXPORT_PATH, 'utf8'));
[
'UIPassthroughContractBase',
'HostEventRequest',
'HostEventResponse',
'TriggerPayload',
'TriggerResponse',
].forEach((name) => expect(reactNames.has(name)).toBe(true));
});
});
16 changes: 16 additions & 0 deletions src/react/all-types-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ export {
RuntimeParameter,
resetCachedAuthToken,
UIPassthroughEvent,
// Host event payload / response contracts — kept in sync with src/index.ts
// (enforced by all-types-export.parity.spec.ts).
UIPassthroughContractBase,
UIPassthroughRequest,
UIPassthroughResponse,
UIPassthroughArrayResponse,
HostEventRequest,
HostEventResponse,
TriggerPayload,
TriggerResponse,
LiveboardTab,
ApplicabilityLevel,
Applicability,
FilterUpdate,
LiveboardFilter,
LiveboardParameter,
DataPanelCustomColumnGroupsAccordionState,
InterceptedApiType,
CustomActionsPosition,
Expand Down
22 changes: 22 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4305,6 +4305,7 @@ export enum HostEvent {
* const url = await appEmbed.trigger(HostEvent.GetIframeUrl, {}, ContextType.Answer);
* console.log("iFrameURL", url);
* ```
* @returns iframeUrl - The URL currently loaded in the embedded iframe.
* @version SDK: 1.35.0 | ThoughtSpot: 10.4.0.cl
*/
GetIframeUrl = 'GetIframeUrl',
Expand Down Expand Up @@ -4501,6 +4502,10 @@ export enum HostEvent {
* liveboardEmbed.trigger(HostEvent.getExportRequestForCurrentPinboard).then(
* data=>console.log(data))
* ```
* @returns data - Object with a `v2Content` string: the export request
* payload for the current Liveboard.
* @returns type - The passthrough event name,
* `getExportRequestForCurrentPinboard`.
* @version SDK: 1.13.0 | ThoughtSpot: 8.5.0.cl, 8.8.1.sw
*/
getExportRequestForCurrentPinboard = 'getExportRequestForCurrentPinboard',
Expand Down Expand Up @@ -4603,6 +4608,9 @@ export enum HostEvent {
* }, ContextType.Spotter);
* ```
*
* @returns liveboardId - GUID of the Liveboard the Answer was pinned to.
* @returns tabId - GUID of the tab within that Liveboard.
* @returns vizId - GUID of the newly created visualization.
* @version SDK: 1.15.0 | ThoughtSpot: 8.7.0.cl, 8.8.1.sw
*/
Pin = 'pin',
Expand Down Expand Up @@ -5092,6 +5100,7 @@ export enum HostEvent {
* console.log(tml.answer);
* });
* ```
* @returns The TML representation of the current Answer, as an object.
* @version SDK: 1.18.0 | ThoughtSpot: 8.10.0.cl, 9.0.1.sw
* @important
*/
Expand Down Expand Up @@ -5536,6 +5545,8 @@ export enum HostEvent {
* const data = await liveboardEmbed.trigger(HostEvent.GetFilters, {}, ContextType.Liveboard);
* console.log('filters', data);
* ```
* @returns liveboardFilters - Filters applied on the Liveboard.
* @returns runtimeFilters - Runtime filters applied on the Liveboard.
* @version SDK: 1.23.0 | ThoughtSpot: 9.4.0.cl
*/
GetFilters = 'getFilters',
Expand Down Expand Up @@ -5685,6 +5696,9 @@ export enum HostEvent {
* console.log('tabs', tabDetails);
* });
* ```
* @returns orderedTabIds - Tab GUIDs in the order they appear.
* @returns numberOfTabs - Total number of tabs on the Liveboard.
* @returns Tabs - Tab details, each with at least `id` and `name`.
* @version SDK: 1.26.0 | ThoughtSpot: 9.7.0.cl
*/
GetTabs = 'getTabs',
Expand Down Expand Up @@ -5759,6 +5773,9 @@ export enum HostEvent {
* // Alternative direct usage (not recommended)
* const {session} = await embed.trigger( HostEvent.GetAnswerSession )
* ```
* @returns session - Session identifier for the Answer, for use with the
* `AnswerService`.
* @returns embedAnswerData - Data backing the Answer, when available.
* @version SDK: 1.26.0 | ThoughtSpot: 9.10.0.cl, 10.1.0.sw
*/
GetAnswerSession = 'getAnswerSession',
Expand Down Expand Up @@ -5892,6 +5909,7 @@ export enum HostEvent {
* console.log('parameters', parameters);
* });
* ```
* @returns parameters - Parameters currently applied on the Liveboard.
* @version SDK: 1.29.0 | ThoughtSpot: 10.1.0.cl, 10.1.0.sw
*/
GetParameters = 'GetParameters',
Expand Down Expand Up @@ -6021,6 +6039,10 @@ export enum HostEvent {
* description: "Generated from Spotter"
* }, ContextType.Spotter);
* ```
* @returns answerId - GUID of the saved Answer.
* @returns saveResponse - Raw response from the save operation.
* @returns shareResponse - Raw response from the share operation, when the
* Answer was also made discoverable.
* @version SDK: 1.36.0 | ThoughtSpot: 10.6.0.cl
*/
SaveAnswer = 'saveAnswer',
Expand Down
Loading