@stripe/extensibility-script-build-tools
v2.1.3
Published
ESBuild plugin for injecting platform dispatch into Stripe extension scripts
Downloads
1,320
Maintainers
Keywords
Readme
@stripe/extensibility-script-build-tools
An esbuild plugin and dispatch shim for Stripe script extension entry points.
What it does
The plugin intercepts the extension entry point during bundling and appends a
__stripe_platform_dispatch__ export — a table mapping the default-export class and each
extension method to a pre-built factory closure. At runtime, the dispatch shim reads the
table and calls through it: wire arguments go in, the factory converts them to SDK types,
instantiates the class, calls the method, converts the SDK result back to wire format, and
returns.
This approach means transformation happens exactly once, at the true entry point, and the extension class itself is never modified. Methods can call each other freely.
The plugin also introspects the class's Config type argument and, if it's a user-defined type, generates and appends a config descriptor function that the factory uses to validate and coerce config values from wire JSON.
Usage
Pre-flight analysis
Before adding the plugin to a build, call analyzeForDispatch to check whether a full
dispatch shim can be built for the entry point:
import {
analyzeForDispatch,
createPlatformDispatchPlugin,
} from '@stripe/extensibility-script-build-tools';
import esbuild from 'esbuild';
const analysis = analyzeForDispatch('src/index.ts');
if (analysis !== null) {
// Inspect SDK capabilities to decide translation mode.
const useApiJson = analysis.capabilities.has('api_json_translation');
// Full platform dispatch shim will be installed.
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
plugins: [
createPlatformDispatchPlugin(analysis, {
translation: useApiJson ? 'api_json' : 'proto_json',
}),
],
});
} else {
// No shim. Build normally and route calls without dispatch.
await esbuild.build({ entryPoints: ['src/index.ts'], bundle: true });
}analyzeForDispatch returns null when any condition is not met:
- The entry point has no named default export
- The export does not implement a recognised extension interface
- The installed SDK predates platform dispatch (no
$platformWrap*factories)
When non-null, the returned analysis exposes a capabilities set that the
caller can inspect before choosing build options.
createPlatformDispatchPlugin accepts an optional second argument:
translation:'proto_json'(default) or'api_json'. Controls which wire format the dispatch shim expects.'proto_json'uses the full two-pass pipeline (proto wire to API-JSON to SDK types).'api_json'uses only the inner pass (API-JSON to SDK types), for callers that already produce/consume API-JSON. ThrowsBuildUserErrorif the SDK's capabilities don't include the requested mode.
For non-extension scripts and any other configuration that doesn't conform to the
extension framework, null is returned and the build proceeds without a shim.
SDK capability protocol
The SDK declares its capabilities via a @stripe/extensibility-sdk/capabilities
subpath export. The build plugin reads this at analysis time to validate that
requested features are supported. Older SDKs that lack this subpath are treated as
having no special capabilities (graceful degradation).
Currently declared capabilities:
| Capability | Meaning |
| ---------------------- | -------------------------------------------- |
| api_json_translation | SDK supports the api_json translation mode |
Dispatch
After building the bundle, import it and call the dispatch method baked
directly into the table:
import { DISPATCH_TABLE_EXPORT } from '@stripe/extensibility-script-build-tools';
// mod is the dynamically-imported bundle
const table = mod[DISPATCH_TABLE_EXPORT] as
| {
dispatch: (
exportName: string,
methodName: string,
wireArgs: unknown,
wireConfig: unknown,
ctx: unknown
) => unknown;
}
| undefined;
const result = table?.dispatch('default', 'computeDiscounts', wireArgs, wireConfig, ctx);dispatch is a closure embedded in the bundle by the plugin. Calling it does
not require importing any runtime helper from this package.
How the dispatch table works
The plugin appends a block like this to the extension source:
import * as __stripe_sdk_ext__ from '@stripe/apps-extensibility-sdk/extensions';
export const __stripe_platform_dispatch__ = (() => {
const e = {
default: {
__class__: MyExtensionClass,
computeDiscounts:
__stripe_sdk_ext__.Billing.Bill.DiscountCalculation.$platformWrapComputeDiscounts(
transformMyConfig
),
},
};
return Object.assign(e, {
dispatch: (exportName, methodName, wireArgs, wireConfig, ctx) => {
const entry = e[exportName];
// ... error checks ...
return entry[methodName](entry.__class__, wireArgs, wireConfig, ctx);
},
});
})();__class__is the default-export class constructor, used to instantiate a fresh object per call.- Each method entry is a closure
(cls, wireArgs, wireConfig, ctx) => wireResultreturned by the$platformWrap*factory in the SDK. Config transformer is baked in if present. dispatch(exportName, methodName, wireArgs, wireConfig, ctx)is a stable one-shot entry point that looks up the export and method from the closure-captured entries and calls through. Callers do not need any runtime helper from this package to invoke it.
Code flow
BUILD TIME
──────────
entry.ts ──► analyzeForDispatch()
│
├─ resolveExtension() ts-morph: reads implements clause,
│ looks up FQNs in BY_SDK_FQN registry
│
├─ hasPlatformWrap() require() the SDK; check $platformWrap*
│ exists → true (else return null)
│
└─ returns DispatchAnalysis (opaque)
entry.ts ──► esbuild plugin (onLoad) — only if analysis is non-null
│
├─ resolveConfigTransformer() analyse Config type arg; emit
│ descriptor function if user-defined
│
└─ append to source:
import * as __stripe_sdk_ext__ from '…/extensions';
export const __stripe_platform_dispatch__ = (() => {
const e = {
default: {
__class__: MyClass,
computeDiscounts:
__stripe_sdk_ext__.Billing.Bill
.DiscountCalculation
.$platformWrapComputeDiscounts(transformConfig),
}
};
return Object.assign(e, { dispatch: … });
})();
RUNTIME
───────
platform ──► mod['__stripe_platform_dispatch__'].dispatch(
'default', 'computeDiscounts', wireArgs, wireConfig, ctx)
│
└─ $platformWrapComputeDiscounts closure:
1. apply(ArgsDescriptor, strategy, wireArgs)
2. transformConfig(wireConfig)
3. new MyClass().computeDiscounts(request, config, ctx)
4. apply(ResultDescriptor, strategy, result)
└─► wireResult
strategy is selected by translation mode:
proto_json → ProtoWireToType / TypeToProtoWire (two-pass)
api_json → ApiJsonToType / TypeToApiJson (inner pass)Failure behaviour
analyzeForDispatchreturnsnull. Entry points without a named default export, without a recognised interface, or pinned to a pre-dispatch SDK version all returnnull. The caller builds without the plugin and records that calls should bypass dispatch.BuildUserErrorthrown byanalyzeForDispatch. The source contains a reserved identifier (__stripe_sdk_ext__,__stripe_platform_dispatch__) that the author must rename.BuildUserErrorthrown bycreatePlatformDispatchPlugin.translation: 'api_json'was requested but the installed SDK does not declare theapi_json_translationcapability.BuildSystemErrorthrown by the plugin. An unexpected error during config transformer resolution. Wraps the original error.
