diff --git a/.eslintrc.yml b/.eslintrc.yml
index 18aea8da..c0e36524 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -26,3 +26,4 @@ rules:
jest/no-focused-tests: 2
'@typescript-eslint/no-explicit-any': 0
'@typescript-eslint/no-empty-interface': 0
+ '@typescript-eslint/triple-slash-reference': 0
diff --git a/README.md b/README.md
index 83af3476..df71b4e6 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,18 @@ one. When you call `loadStripe`, it will use the existing script tag.
```
+### Importing `loadStripe` without side effects
+
+If you would like to use `loadStripe` in your application, but defer loading the
+Stripe.js script until `loadStripe` is first called, use the alternative
+`@stripe/stripe-js/pure` import path:
+
+```
+import {loadStripe} from '@stripe/stripe-js/pure';
+
+// Stripe.js will not be loaded until `loadStripe` is called
+```
+
## Stripe.js Documentation
- [Stripe.js Docs](https://stripe.com/docs/stripe-js)
diff --git a/package.json b/package.json
index 0f799502..764aa9cd 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,9 @@
"files": [
"dist",
"src",
- "types"
+ "types",
+ "pure.js",
+ "pure.d.ts"
],
"devDependencies": {
"@babel/core": "^7.7.2",
diff --git a/pure.d.ts b/pure.d.ts
new file mode 100644
index 00000000..7d7f7fa1
--- /dev/null
+++ b/pure.d.ts
@@ -0,0 +1,3 @@
+///
+
+export const loadStripe: typeof import('@stripe/stripe-js').loadStripe;
diff --git a/pure.js b/pure.js
new file mode 100644
index 00000000..8c390fc0
--- /dev/null
+++ b/pure.js
@@ -0,0 +1 @@
+module.exports = require('./dist/pure.js');
diff --git a/rollup.config.js b/rollup.config.js
index fdc48fcf..260a9c98 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -1,8 +1,20 @@
import babel from 'rollup-plugin-babel';
-import pkg from './package.json';
import ts from 'rollup-plugin-typescript2';
import replace from '@rollup/plugin-replace';
-import {version} from './package.json';
+
+import pkg from './package.json';
+
+const PLUGINS = [
+ ts({
+ tsconfigOverride: {exclude: ['**/*.test.ts']},
+ }),
+ babel({
+ extensions: ['.ts', '.js', '.tsx', '.jsx'],
+ }),
+ replace({
+ _VERSION: JSON.stringify(pkg.version),
+ }),
+];
export default [
{
@@ -11,16 +23,11 @@ export default [
{file: pkg.main, format: 'cjs'},
{file: pkg.module, format: 'es'},
],
- plugins: [
- ts({
- tsconfigOverride: {exclude: ['**/*.test.ts']},
- }),
- babel({
- extensions: ['.ts', '.js', '.tsx', '.jsx'],
- }),
- replace({
- _VERSION: JSON.stringify(version),
- }),
- ],
+ plugins: PLUGINS,
+ },
+ {
+ input: 'src/pure.ts',
+ output: [{file: 'dist/pure.js', format: 'cjs'}],
+ plugins: PLUGINS,
},
];
diff --git a/src/index.test.ts b/src/index.test.ts
index e44cfe0d..3342f72a 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -84,13 +84,14 @@ describe('Stripe module loader', () => {
});
});
- describe('loadStripe', () => {
+ describe.each(['./index', './pure'])('loadStripe (%s.ts)', (requirePath) => {
beforeEach(() => {
+ jest.restoreAllMocks();
jest.spyOn(console, 'warn').mockReturnValue();
});
it('resolves loadStripe with Stripe object', async () => {
- const {loadStripe} = require('./index');
+ const {loadStripe} = require(requirePath);
const stripePromise = loadStripe('pk_test_foo');
await new Promise((resolve) => setTimeout(resolve));
@@ -101,7 +102,7 @@ describe('Stripe module loader', () => {
});
it('rejects when the script fails', async () => {
- const {loadStripe} = require('./index');
+ const {loadStripe} = require(requirePath);
const stripePromise = loadStripe('pk_test_foo');
await Promise.resolve();
@@ -114,6 +115,20 @@ describe('Stripe module loader', () => {
expect(console.warn).not.toHaveBeenCalled();
});
+ it('rejects when Stripe is not added to the window for some reason', async () => {
+ const {loadStripe} = require(requirePath);
+ const stripePromise = loadStripe('pk_test_foo');
+
+ await Promise.resolve();
+ dispatchScriptEvent('load');
+
+ return expect(stripePromise).rejects.toEqual(
+ new Error('Stripe.js not available')
+ );
+ });
+ });
+
+ describe('loadStripe (index.ts)', () => {
it('does not cause unhandled rejects when the script fails', async () => {
require('./index');
@@ -127,17 +142,5 @@ describe('Stripe module loader', () => {
new Error('Failed to load Stripe.js')
);
});
-
- it('rejects when Stripe is not added to the window for some reason', async () => {
- const {loadStripe} = require('./index');
- const stripePromise = loadStripe('pk_test_foo');
-
- await Promise.resolve();
- dispatchScriptEvent('load');
-
- return expect(stripePromise).rejects.toEqual(
- new Error('Stripe.js not available')
- );
- });
});
});
diff --git a/src/index.ts b/src/index.ts
index 4b51d7ae..8f3c8a81 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,77 +1,17 @@
-// eslint-disable-next-line @typescript-eslint/triple-slash-reference
///
import {Stripe as StripeInstance, StripeConstructor} from '@stripe/stripe-js';
+import {loadScript, initStripe} from './shared';
-// `_VERSION` will be rewritten by `@rollup/plugin-replace` as a string literal
-// containing the package.json version
-declare const _VERSION: string;
-
-const V3_URL = 'https://js.stripe.com/v3';
-
-const injectScript = (): HTMLScriptElement => {
- const script = document.createElement('script');
- script.src = V3_URL;
-
- const headOrBody = document.head || document.body;
-
- if (!headOrBody) {
- throw new Error(
- 'Expected document.body not to be null. Stripe.js requires a
element.'
- );
- }
-
- headOrBody.appendChild(script);
-
- return script;
-};
-
-const registerWrapper = (stripe: any): void => {
- if (!stripe || !stripe._registerWrapper) {
- return;
- }
-
- stripe._registerWrapper({name: 'stripe-js', version: _VERSION});
-};
-
-// Execute our own script injection after a tick to give users time to
-// do their own script injection.
-const stripePromise: Promise = Promise.resolve().then(
- () => {
- if (typeof window === 'undefined') {
- // Resolve to null when imported server side. This makes the module
- // safe to import in an isomorphic code base.
- return null;
- }
-
- if (window.Stripe) {
- return window.Stripe;
- }
-
- const script: HTMLScriptElement =
- document.querySelector(
- `script[src="${V3_URL}"], script[src="${V3_URL}/"]`
- ) || injectScript();
-
- return new Promise((resolve, reject) => {
- script.addEventListener('load', () => {
- if (window.Stripe) {
- resolve(window.Stripe);
- } else {
- reject(new Error('Stripe.js not available'));
- }
- });
-
- script.addEventListener('error', () => {
- reject(new Error('Failed to load Stripe.js'));
- });
- });
- }
-);
+// Execute our own script injection after a tick to give users time to do their
+// own script injection.
+const stripePromise = Promise.resolve().then(loadScript);
let loadCalled = false;
stripePromise.catch((err) => {
- if (!loadCalled) console.warn(err);
+ if (!loadCalled) {
+ console.warn(err);
+ }
});
export const loadStripe = (
@@ -79,13 +19,5 @@ export const loadStripe = (
): Promise => {
loadCalled = true;
- return stripePromise.then((maybeStripe) => {
- if (maybeStripe === null) {
- return null;
- }
-
- const stripe = maybeStripe(...args);
- registerWrapper(stripe);
- return stripe;
- });
+ return stripePromise.then((maybeStripe) => initStripe(maybeStripe, args));
};
diff --git a/src/pure.test.ts b/src/pure.test.ts
new file mode 100644
index 00000000..f1d32485
--- /dev/null
+++ b/src/pure.test.ts
@@ -0,0 +1,29 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+const SCRIPT_SELECTOR =
+ 'script[src="https://js.stripe.com/v3"], script[src="https://js.stripe.com/v3/"]';
+
+describe('pure module', () => {
+ afterEach(() => {
+ const script = document.querySelector(SCRIPT_SELECTOR);
+ if (script && script.parentElement) {
+ script.parentElement.removeChild(script);
+ }
+
+ delete window.Stripe;
+ jest.resetModules();
+ });
+
+ test('does not inject the script if loadStripe is not called', async () => {
+ require('./pure');
+
+ expect(document.querySelector(SCRIPT_SELECTOR)).toBe(null);
+ });
+
+ test('it injects the script if loadStripe is called', async () => {
+ const {loadStripe} = require('./pure');
+ loadStripe('pk_test_foo');
+
+ expect(document.querySelector(SCRIPT_SELECTOR)).not.toBe(null);
+ });
+});
diff --git a/src/pure.ts b/src/pure.ts
new file mode 100644
index 00000000..1c881929
--- /dev/null
+++ b/src/pure.ts
@@ -0,0 +1,8 @@
+///
+import {Stripe as StripeInstance, StripeConstructor} from '@stripe/stripe-js';
+import {loadScript, initStripe} from './shared';
+
+export const loadStripe = (
+ ...args: Parameters
+): Promise =>
+ loadScript().then((maybeStripe) => initStripe(maybeStripe, args));
diff --git a/src/shared.ts b/src/shared.ts
new file mode 100644
index 00000000..927e73cd
--- /dev/null
+++ b/src/shared.ts
@@ -0,0 +1,87 @@
+import {Stripe, StripeConstructor} from '@stripe/stripe-js';
+
+// `_VERSION` will be rewritten by `@rollup/plugin-replace` as a string literal
+// containing the package.json version
+declare const _VERSION: string;
+
+const V3_URL = 'https://js.stripe.com/v3';
+
+const injectScript = (): HTMLScriptElement => {
+ const script = document.createElement('script');
+ script.src = V3_URL;
+
+ const headOrBody = document.head || document.body;
+
+ if (!headOrBody) {
+ throw new Error(
+ 'Expected document.body not to be null. Stripe.js requires a element.'
+ );
+ }
+
+ headOrBody.appendChild(script);
+
+ return script;
+};
+
+const registerWrapper = (stripe: any): void => {
+ if (!stripe || !stripe._registerWrapper) {
+ return;
+ }
+
+ stripe._registerWrapper({name: 'stripe-js', version: _VERSION});
+};
+
+let stripePromise: Promise | null = null;
+
+export const loadScript = (): Promise => {
+ // Ensure that we only attempt to load Stripe.js at most once
+ if (stripePromise !== null) {
+ return stripePromise;
+ }
+
+ stripePromise = new Promise((resolve, reject) => {
+ if (typeof window === 'undefined') {
+ // Resolve to null when imported server side. This makes the module
+ // safe to import in an isomorphic code base.
+ resolve(null);
+ return;
+ }
+
+ if (window.Stripe) {
+ resolve(window.Stripe);
+ return;
+ }
+
+ const script: HTMLScriptElement =
+ document.querySelector(
+ `script[src="${V3_URL}"], script[src="${V3_URL}/"]`
+ ) || injectScript();
+
+ script.addEventListener('load', () => {
+ if (window.Stripe) {
+ resolve(window.Stripe);
+ } else {
+ reject(new Error('Stripe.js not available'));
+ }
+ });
+
+ script.addEventListener('error', () => {
+ reject(new Error('Failed to load Stripe.js'));
+ });
+ });
+
+ return stripePromise;
+};
+
+export const initStripe = (
+ maybeStripe: StripeConstructor | null,
+ args: Parameters
+): Stripe | null => {
+ if (maybeStripe === null) {
+ return null;
+ }
+
+ const stripe = maybeStripe(...args);
+ registerWrapper(stripe);
+ return stripe;
+};