@stripe/extensibility-script-build-tools
v1.5.7
Published
ESBuild plugin for injecting platform dispatch into Stripe extension scripts
Downloads
639
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) {
// Full platform dispatch shim will be installed.
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
plugins: [createPlatformDispatchPlugin(analysis)],
});
} 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)
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.
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, ProtoWireToType, wireArgs)
2. transformConfig(wireConfig)
3. new MyClass().computeDiscounts(request, config, ctx)
4. apply(ResultDescriptor, TypeToProtoWire, result)
└─► wireResultFailure 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__,__stripe_shim_active__). The author must rename the colliding identifier.BuildSystemErrorthrown by the plugin. An unexpected error during config transformer resolution. Wraps the original error.
