Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alternative side-effect free import path #45

Merged
merged 3 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ one. When you call `loadStripe`, it will use the existing script tag.
<script src="https://js.stripe.com/v3" async></script>
```

### 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)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"files": [
"dist",
"src",
"types"
"types",
"pure.js",
"pure.d.ts"
],
"devDependencies": {
"@babel/core": "^7.7.2",
Expand Down
3 changes: 3 additions & 0 deletions pure.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
///<reference path='./types/index.d.ts' />

export const loadStripe: typeof import('@stripe/stripe-js').loadStripe;
1 change: 1 addition & 0 deletions pure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/pure.js');
33 changes: 20 additions & 13 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -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 [
{
Expand All @@ -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,
},
];
33 changes: 18 additions & 15 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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();
Expand All @@ -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');

Expand All @@ -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')
);
});
});
});
82 changes: 6 additions & 76 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,21 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
///<reference path='../types/index.d.ts' />
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 <body> 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<StripeConstructor | null> = 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'));
});
});
}
);
const stripePromise = loadScript();

let loadCalled = false;

stripePromise.catch((err) => {
if (!loadCalled) console.warn(err);
if (!loadCalled) {
console.warn(err);
}
});

export const loadStripe = (
...args: Parameters<StripeConstructor>
): Promise<StripeInstance | null> => {
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));
};
29 changes: 29 additions & 0 deletions src/pure.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
hofman-stripe marked this conversation as resolved.
Show resolved Hide resolved
const script = document.querySelector(SCRIPT_SELECTOR);

if (script && script.parentElement) {
script.parentElement.removeChild(script);
}
});

test('does not inject the script if loadStripe is not called', async () => {
require('./pure');
await Promise.resolve();

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');
await Promise.resolve();
hofman-stripe marked this conversation as resolved.
Show resolved Hide resolved

expect(document.querySelector(SCRIPT_SELECTOR)).not.toBe(null);
});
});
17 changes: 17 additions & 0 deletions src/pure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
///<reference path='../types/index.d.ts' />
import {Stripe as StripeInstance, StripeConstructor} from '@stripe/stripe-js';
import {loadScript, initStripe} from './shared';

let stripePromise: Promise<StripeConstructor | null> | null = null;

export const loadStripe = (
...args: Parameters<StripeConstructor>
): Promise<StripeInstance | null> => {
// Ensure we only attempt to load Stripe.js once, no matter how many times
// `loadStripe` is called
if (!stripePromise) {
stripePromise = loadScript();
}

return stripePromise.then((maybeStripe) => initStripe(maybeStripe, args));
};
84 changes: 84 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 <body> element.'
);
}

headOrBody.appendChild(script);

return script;
};

const registerWrapper = (stripe: any): void => {
if (!stripe || !stripe._registerWrapper) {
return;
}

stripe._registerWrapper({name: 'stripe-js', version: _VERSION});
};

export const loadScript = (): Promise<StripeConstructor | null> => {
// Execute our own script injection after a tick to give users time to
// do their own script injection.
const stripePromise: Promise<StripeConstructor | null> = Promise.resolve().then(
christopher-stripe marked this conversation as resolved.
Show resolved Hide resolved
christopher-stripe marked this conversation as resolved.
Show resolved Hide resolved
() => {
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'));
});
});
}
);

return stripePromise;
};

export const initStripe = (
maybeStripe: StripeConstructor | null,
args: Parameters<StripeConstructor>
): Stripe | null => {
if (maybeStripe === null) {
return null;
}

const stripe = maybeStripe(...args);
registerWrapper(stripe);
return stripe;
};