diff --git a/.circleci/config.yml b/.circleci/config.yml index 29489c187c..584b569c35 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ jobs: build: <<: *node_executor - resource_class: large + resource_class: xlarge steps: - ci/pre-setup - node/npm-install @@ -110,7 +110,7 @@ jobs: build_cdn: <<: *node_executor - resource_class: medium+ + resource_class: xlarge steps: - ci/pre-setup - node/npm-install diff --git a/.eslintignore b/.eslintignore index 4d6aa67925..774e45dd8b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -/packages/core/src/generated/*.ts +/packages/core/src/generated/**/*.ts /dist/* diff --git a/package.json b/package.json index 9c42099d9e..5232502b35 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,26 @@ "version": "1.795.1", "description": "BigCommerce Checkout JavaScript SDK", "license": "MIT", - "main": "dist/checkout-sdk.js", - "typings": "dist/checkout-sdk.d.ts", + "main": "dist/cjs/checkout-sdk.js", + "module": "dist/esm/checkout-sdk.js", + "typings": "dist/types/checkout-sdk.d.ts", + "exports": { + ".": { + "types": "./dist/types/checkout-sdk.d.ts", + "import": "./dist/esm/checkout-sdk.js", + "require": "./dist/cjs/checkout-sdk.js" + }, + "./essential": { + "types": "./dist/types/checkout-sdk.d.ts", + "import": "./dist/esm/checkout-sdk-essential.js", + "require": "./dist/cjs/checkout-sdk-essential.js" + }, + "./integrations/*": { + "types": "./dist/types/integrations/*.d.ts", + "import": "./dist/esm/integrations/*.js", + "require": "./dist/cjs/integrations/*.js" + } + }, "files": [ "dist/", "docs/" @@ -32,7 +50,7 @@ "build-cdn": "npx nx run core:build-cdn", "prebundle": "rm -rf dist", "bundle": "npx nx run core:build", - "bundle:analyze": "npm run --silent bundle -- --config-name umd --profile --json > webpack-stats.json && webpack-bundle-analyzer webpack-stats.json dist --default-sizes gzip", + "bundle:analyze": "npx nx run core:build-analyze", "bundle:watch": "export WATCH=true; npx nx run core:build-watch", "bundle-dts": "npx nx run core:build-dts", "docs": "npx nx run core:docs", diff --git a/packages/adyen-utils/src/index.ts b/packages/adyen-utils/src/index.ts index a9a5fba79e..367b1611c5 100644 --- a/packages/adyen-utils/src/index.ts +++ b/packages/adyen-utils/src/index.ts @@ -1,8 +1,10 @@ +import * as adyenV2Mock from './adyenv2/adyenv2.mock'; +import * as adyenV3Mock from './adyenv3/adyenv3.mock'; + export { default as AdyenV2ScriptLoader } from './adyenv2/adyenv2-script-loader'; export { default as AdyenV3ScriptLoader } from './adyenv3/adyenv3-script-loader'; export * from './types'; -export * as adyenV2Mock from './adyenv2/adyenv2.mock'; -export * as adyenV3Mock from './adyenv3/adyenv3.mock'; +export { adyenV2Mock, adyenV3Mock }; export { WithAdyenV3PaymentInitializeOptions } from './adyenv3/adyenv3-initialize-options'; export { default as AdyenV3PaymentInitializeOptions } from './adyenv3/adyenv3-initialize-options'; export { WithAdyenV2PaymentInitializeOptions } from './adyenv2/adyenv2-initialize-options'; diff --git a/packages/adyen-utils/src/types.ts b/packages/adyen-utils/src/types.ts index 4269fa662e..2e16f76464 100644 --- a/packages/adyen-utils/src/types.ts +++ b/packages/adyen-utils/src/types.ts @@ -195,11 +195,7 @@ export interface AdyenAdditionalActionCallbacks { export interface AdyenAdditionalActionErrorResponse { provider_data: AdyenAdditionalAction; - errors: [ - { - code: string; - }, - ]; + errors: [{ code: string }]; } export interface AdyenAdditionalActionOptions extends AdyenAdditionalActionCallbacks { diff --git a/packages/core/api-extractor/checkout-button.json b/packages/core/api-extractor/checkout-button.json index 0a5853af9e..ee491a5298 100644 --- a/packages/core/api-extractor/checkout-button.json +++ b/packages/core/api-extractor/checkout-button.json @@ -4,6 +4,6 @@ "entryPointSourceFile": "../../temp/core/src/bundles/checkout-button.d.ts" }, "dtsRollup": { - "mainDtsRollupPath": "checkout-button.d.ts" + "mainDtsRollupPath": "types/checkout-button.d.ts" } } diff --git a/packages/core/api-extractor/checkout-sdk.json b/packages/core/api-extractor/checkout-sdk.json index 4b497930e5..3ef6908416 100644 --- a/packages/core/api-extractor/checkout-sdk.json +++ b/packages/core/api-extractor/checkout-sdk.json @@ -17,6 +17,6 @@ }, "dtsRollup": { "enabled": true, - "mainDtsRollupPath": "checkout-sdk.d.ts" + "mainDtsRollupPath": "types/checkout-sdk.d.ts" } } diff --git a/packages/core/api-extractor/embedded-checkout.json b/packages/core/api-extractor/embedded-checkout.json index 995b1b0d87..ad32203742 100644 --- a/packages/core/api-extractor/embedded-checkout.json +++ b/packages/core/api-extractor/embedded-checkout.json @@ -4,6 +4,6 @@ "entryPointSourceFile": "../../temp/core/src/bundles/embedded-checkout.d.ts" }, "dtsRollup": { - "mainDtsRollupPath": "embedded-checkout.d.ts" + "mainDtsRollupPath": "types/embedded-checkout.d.ts" } } diff --git a/packages/core/api-extractor/internal-mappers.json b/packages/core/api-extractor/internal-mappers.json index e186d85d98..70979193e2 100644 --- a/packages/core/api-extractor/internal-mappers.json +++ b/packages/core/api-extractor/internal-mappers.json @@ -4,6 +4,6 @@ "entryPointSourceFile": "../../temp/core/src/bundles/internal-mappers.d.ts" }, "dtsRollup": { - "mainDtsRollupPath": "internal-mappers.d.ts" + "mainDtsRollupPath": "types/internal-mappers.d.ts" } } diff --git a/packages/core/auto-export.config.json b/packages/core/auto-export.config.json index 6ef0ba9e72..61136ae553 100644 --- a/packages/core/auto-export.config.json +++ b/packages/core/auto-export.config.json @@ -3,17 +3,24 @@ { "inputPath": "packages/*/src/index.ts", "outputPath": "packages/core/src/generated/payment-strategies.ts", + "packageOutputPath": "packages/core/src/generated/integrations//index.ts", "memberPattern": "^create.+PaymentStrategy$" }, { "inputPath": "packages/*/src/index.ts", "outputPath": "packages/core/src/generated/customer-strategies.ts", + "packageOutputPath": "packages/core/src/generated/integrations//index.ts", "memberPattern": "^create.+CustomerStrategy$" }, { "inputPath": "packages/*/src/index.ts", "outputPath": "packages/core/src/generated/checkout-button-strategies.ts", + "packageOutputPath": "packages/core/src/generated/integrations//index.ts", "memberPattern": "^create.+ButtonStrategy$" } - ] + ], + "apiExtractorConfig": { + "entryPointSourceFile": "../../temp/core/src/generated/integrations//index.d.ts", + "mainDtsRollupPath": "types/integrations/.d.ts" + } } diff --git a/packages/core/project.json b/packages/core/project.json index 6973ea8614..ad06d855e3 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -19,10 +19,30 @@ } ] }, + "build-analyze": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + "webpack --config webpack.config.js --config-name esm --profile --json > webpack-stats.json", + "webpack-bundle-analyzer webpack-stats.json dist --default-sizes gzip" + ], + "parallel": false + }, + "dependsOn": [ + { + "target": "generate", + "projects": "self" + }, + { + "target": "build", + "projects": "dependencies" + } + ] + }, "build-watch": { "executor": "@nrwl/workspace:run-commands", "options": { - "command": "webpack --config webpack.config.js --config-name cjs --watch --progress" + "command": "webpack --config webpack.config.js --config-name esm --watch --progress" }, "dependsOn": [ { @@ -63,6 +83,7 @@ "commands": [ "tsc --outDir ../../temp --declaration --emitDeclarationOnly", "api-extractor run --config api-extractor/checkout-sdk.json & api-extractor run --config api-extractor/checkout-button.json & api-extractor run --config api-extractor/embedded-checkout.json & api-extractor run --config api-extractor/internal-mappers.json", + "find src/generated/integrations -name 'api-extractor.json' | xargs -I {} -P 8 sh -c 'cd \"$(dirname \"{}\")\" && npx api-extractor run --config api-extractor.json'", "rm -rf ../../temp", "nx run hosted-form-v2:build-dts" ] @@ -74,7 +95,7 @@ "cwd": "packages/core", "parallel": false, "commands": [ - "mkdir -p src/generated && cp ../../dist/checkout-sdk.d.ts src/generated/checkout-sdk.d.ts", + "mkdir -p src/generated && cp ../../dist/types/checkout-sdk.d.ts src/generated/checkout-sdk.d.ts", "typedoc --plugin typedoc-plugin-markdown --options typedoc.json --tsconfig tsconfig.json src/generated/checkout-sdk.d.ts" ] }, @@ -118,6 +139,7 @@ "executor": "@nrwl/workspace:run-commands", "options": { "commands": [ + "rm -rf packages/core/src/generated", "npx nx generate @bigcommerce/checkout-sdk/workspace-tools:auto-export --projectName=core", "npx nx generate @bigcommerce/checkout-sdk/workspace-tools:extend-interface --projectName=core", "npx nx generate @bigcommerce/checkout-sdk/workspace-tools:create-enum --projectName=core" diff --git a/packages/core/src/checkout-buttons/checkout-button-initializer.spec.ts b/packages/core/src/checkout-buttons/checkout-button-initializer.spec.ts index 1fad986e3b..e3501f43c5 100644 --- a/packages/core/src/checkout-buttons/checkout-button-initializer.spec.ts +++ b/packages/core/src/checkout-buttons/checkout-button-initializer.spec.ts @@ -26,7 +26,7 @@ describe('CheckoutButtonInitializer', () => { buttonActionCreator = new CheckoutButtonStrategyActionCreator( createCheckoutButtonRegistry(store, createRequestSender(), createFormPoster(), 'en'), - createCheckoutButtonRegistryV2(createPaymentIntegrationService(store)), + createCheckoutButtonRegistryV2(createPaymentIntegrationService(store), {}), new PaymentMethodActionCreator(new PaymentMethodRequestSender(createRequestSender())), ); diff --git a/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts b/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts index b29bd22110..535f0b67b9 100644 --- a/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts +++ b/packages/core/src/checkout-buttons/checkout-button-strategy-action-creator.spec.ts @@ -48,7 +48,7 @@ describe('CheckoutButtonStrategyActionCreator', () => { ); strategy = new MockButtonStrategy(); store = createCheckoutStore(); - registryV2 = createCheckoutButtonRegistryV2(createPaymentIntegrationService(store)); + registryV2 = createCheckoutButtonRegistryV2(createPaymentIntegrationService(store), {}); registry.register(CheckoutButtonMethodType.MASTERPASS, () => strategy); jest.spyOn(paymentMethodActionCreator, 'loadPaymentMethod').mockReturnValue(() => diff --git a/packages/core/src/checkout-buttons/create-checkout-button-initializer.ts b/packages/core/src/checkout-buttons/create-checkout-button-initializer.ts index 639791e12a..9fdce0e492 100644 --- a/packages/core/src/checkout-buttons/create-checkout-button-initializer.ts +++ b/packages/core/src/checkout-buttons/create-checkout-button-initializer.ts @@ -3,6 +3,7 @@ import { createRequestSender } from '@bigcommerce/request-sender'; import { createCheckoutStore } from '../checkout'; import { ConfigState } from '../config'; +import * as defaultCheckoutButtonStrategyFactories from '../generated/checkout-button-strategies'; import { PaymentMethodActionCreator, PaymentMethodRequestSender } from '../payment'; import { createPaymentIntegrationService } from '../payment-integration'; @@ -53,7 +54,12 @@ export default function createCheckoutButtonInitializer( const requestSender = createRequestSender({ host }); const formPoster = createFormPoster({ host }); const paymentIntegrationService = createPaymentIntegrationService(store); - const registryV2 = createCheckoutButtonRegistryV2(paymentIntegrationService); + const registryV2 = createCheckoutButtonRegistryV2( + paymentIntegrationService, + defaultCheckoutButtonStrategyFactories, + // TODO: Replace once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out + // process.env.ESSENTIAL_BUILD ? {} : defaultCheckoutButtonStrategyFactories, + ); return new CheckoutButtonInitializer( store, diff --git a/packages/core/src/checkout-buttons/create-checkout-button-registry-v2.ts b/packages/core/src/checkout-buttons/create-checkout-button-registry-v2.ts index f2e708d004..d0168ca738 100644 --- a/packages/core/src/checkout-buttons/create-checkout-button-registry-v2.ts +++ b/packages/core/src/checkout-buttons/create-checkout-button-registry-v2.ts @@ -7,7 +7,6 @@ import { } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { ResolveIdRegistry } from '../common/registry'; -import * as defaultCheckoutButtonStrategyFactories from '../generated/checkout-button-strategies'; export interface CheckoutButtonStrategyFactories { [key: string]: CheckoutButtonStrategyFactory; @@ -15,7 +14,7 @@ export interface CheckoutButtonStrategyFactories { export default function createCheckoutButtonStrategyRegistry( paymentIntegrationService: PaymentIntegrationService, - checkoutButtonStrategyFactories: CheckoutButtonStrategyFactories = defaultCheckoutButtonStrategyFactories, + checkoutButtonStrategyFactories: CheckoutButtonStrategyFactories, ): ResolveIdRegistry { const registry = new ResolveIdRegistry< CheckoutButtonStrategy, diff --git a/packages/core/src/checkout/checkout-service.spec.ts b/packages/core/src/checkout/checkout-service.spec.ts index 2506a633d5..6717070213 100644 --- a/packages/core/src/checkout/checkout-service.spec.ts +++ b/packages/core/src/checkout/checkout-service.spec.ts @@ -253,8 +253,8 @@ describe('CheckoutService', () => { jest.spyOn(paymentStrategy, 'deinitialize').mockResolvedValue(Promise.resolve()); paymentStrategyRegistry = new PaymentStrategyRegistry(); - paymentStrategyRegistryV2 = createPaymentStrategyRegistryV2(paymentIntegrationService); - customerRegistryV2 = createCustomerStrategyRegistryV2(paymentIntegrationService); + paymentStrategyRegistryV2 = createPaymentStrategyRegistryV2(paymentIntegrationService, {}); + customerRegistryV2 = createCustomerStrategyRegistryV2(paymentIntegrationService, {}); // This can't be fixed until we have differences between core and payment integration api payment strategy types // TODO: remove ts-ignore and update test with related type (PAYPAL-4383) @@ -379,6 +379,7 @@ describe('CheckoutService', () => { customerStrategyActionCreator = new CustomerStrategyActionCreator( createCustomerStrategyRegistry(store, requestSender, locale), customerRegistryV2, + paymentIntegrationService, ); instrumentActionCreator = new InstrumentActionCreator(instrumentRequestSender); @@ -397,6 +398,7 @@ describe('CheckoutService', () => { paymentStrategyRegistryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ); shippingStrategyActionCreator = new ShippingStrategyActionCreator( diff --git a/packages/core/src/checkout/create-checkout-service.ts b/packages/core/src/checkout/create-checkout-service.ts index 63a022be22..5383a0bdb1 100644 --- a/packages/core/src/checkout/create-checkout-service.ts +++ b/packages/core/src/checkout/create-checkout-service.ts @@ -28,7 +28,8 @@ import { WorkerExtensionMessenger, } from '../extension'; import { FormFieldsActionCreator, FormFieldsRequestSender } from '../form'; -import * as defaultPaymentStrategyFactories from '../generated/payment-strategies'; +import * as customerStrategyFactories from '../generated/customer-strategies'; +import * as paymentStrategyFactories from '../generated/payment-strategies'; import { CountryActionCreator, CountryRequestSender } from '../geography'; import { OrderActionCreator, OrderRequestSender } from '../order'; import { @@ -136,12 +137,20 @@ export default function createCheckoutService(options?: CheckoutServiceOptions): formFieldsActionCreator, ); const paymentIntegrationService = createPaymentIntegrationService(store); + const registryV2 = createPaymentStrategyRegistryV2( paymentIntegrationService, - defaultPaymentStrategyFactories, + paymentStrategyFactories, + // TODO: Replace once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out + // process.env.ESSENTIAL_BUILD ? {} : paymentStrategyFactories, { useFallback: true }, ); - const customerRegistryV2 = createCustomerStrategyRegistryV2(paymentIntegrationService); + const customerRegistryV2 = createCustomerStrategyRegistryV2( + paymentIntegrationService, + customerStrategyFactories, + // TODO: Replace once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out + // process.env.ESSENTIAL_BUILD ? {} : customerStrategyFactories, + ); const extensionActionCreator = new ExtensionActionCreator( new ExtensionRequestSender(requestSender), ); @@ -174,6 +183,7 @@ export default function createCheckoutService(options?: CheckoutServiceOptions): new CustomerStrategyActionCreator( createCustomerStrategyRegistry(store, requestSender, locale), customerRegistryV2, + paymentIntegrationService, ), new ErrorActionCreator(), new GiftCertificateActionCreator(new GiftCertificateRequestSender(requestSender)), @@ -191,6 +201,7 @@ export default function createCheckoutService(options?: CheckoutServiceOptions): registryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ), new PickupOptionActionCreator(new PickupOptionRequestSender(requestSender)), new ShippingCountryActionCreator( diff --git a/packages/core/src/common/registry/registry.ts b/packages/core/src/common/registry/registry.ts index e7b929e010..e9c736c718 100644 --- a/packages/core/src/common/registry/registry.ts +++ b/packages/core/src/common/registry/registry.ts @@ -32,6 +32,13 @@ export default class Registry { } } + getFactory(token: string): Factory | undefined { + const resolvedToken = this._tokenResolver(token, Object.keys(this._factories)); + const factory = resolvedToken ? this._factories[resolvedToken] : undefined; + + return factory; + } + register(token: K, factory: Factory): void { if (this._hasFactory(token)) { throw new InvalidArgumentError(`'${token}' is already registered.`); @@ -50,8 +57,7 @@ export default class Registry { private _getInstance(token: string, cacheToken: string): T { if (!this._hasInstance(cacheToken)) { - const resolvedToken = this._tokenResolver(token, Object.keys(this._factories)); - const factory = resolvedToken && this._factories[resolvedToken]; + const factory = this.getFactory(token); if (!factory) { throw new InvalidArgumentError(`'${token}' is not registered.`); diff --git a/packages/core/src/common/registry/resolve-id-registry.ts b/packages/core/src/common/registry/resolve-id-registry.ts index 4b9144fde2..672d02cedc 100644 --- a/packages/core/src/common/registry/resolve-id-registry.ts +++ b/packages/core/src/common/registry/resolve-id-registry.ts @@ -18,6 +18,14 @@ export default class ResolveIdRegistry | undefined { + try { + return this._registry.getFactory(this._encodeToken(resolveId)); + } catch (error) { + return undefined; + } + } + register(resolveId: TToken, factory: Factory): void { this._registry.register(this._encodeToken(resolveId), factory); } diff --git a/packages/core/src/common/types/sentry.ts b/packages/core/src/common/types/sentry.ts new file mode 100644 index 0000000000..ffd8ba1ea7 --- /dev/null +++ b/packages/core/src/common/types/sentry.ts @@ -0,0 +1,7 @@ +export interface Sentry { + captureMessage(message: string): void; +} + +export interface SentryWindow extends Window { + Sentry?: Sentry; +} diff --git a/packages/core/src/common/utility/set-prototype-of.spec.ts b/packages/core/src/common/utility/set-prototype-of.spec.ts index 6a543486fd..cdd334b29b 100644 --- a/packages/core/src/common/utility/set-prototype-of.spec.ts +++ b/packages/core/src/common/utility/set-prototype-of.spec.ts @@ -6,8 +6,6 @@ describe('setPrototypeOf', () => { const error = new CustomError(); - expect(error instanceof CustomError).toBeFalsy(); - setPrototypeOf(error, CustomError.prototype); expect(error instanceof CustomError).toBeTruthy(); diff --git a/packages/core/src/customer/create-customer-strategy-registry-v2.ts b/packages/core/src/customer/create-customer-strategy-registry-v2.ts index b600cd45f6..0b421daacb 100644 --- a/packages/core/src/customer/create-customer-strategy-registry-v2.ts +++ b/packages/core/src/customer/create-customer-strategy-registry-v2.ts @@ -4,10 +4,10 @@ import { CustomerStrategyResolveId, isResolvableModule, PaymentIntegrationService, + toResolvableModule, } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { ResolveIdRegistry } from '../common/registry'; -import * as defaultCustomerStrategyFactories from '../generated/customer-strategies'; export interface CustomerStrategyFactories { [key: string]: CustomerStrategyFactory; @@ -15,7 +15,7 @@ export interface CustomerStrategyFactories { export default function createCustomerStrategyRegistry( paymentIntegrationService: PaymentIntegrationService, - customerStrategyFactories: CustomerStrategyFactories = defaultCustomerStrategyFactories, + customerStrategyFactories: CustomerStrategyFactories, ): ResolveIdRegistry { const registry = new ResolveIdRegistry(); @@ -30,7 +30,13 @@ export default function createCustomerStrategyRegistry( } for (const resolverId of createCustomerStrategy.resolveIds) { - registry.register(resolverId, () => createCustomerStrategy(paymentIntegrationService)); + // TODO: Remove toResolvableModule once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out + const factory = toResolvableModule( + () => createCustomerStrategy(paymentIntegrationService), + createCustomerStrategy.resolveIds, + ); + + registry.register(resolverId, factory); } } diff --git a/packages/core/src/customer/customer-request-options.ts b/packages/core/src/customer/customer-request-options.ts index 61e7d75a85..0ec282c771 100644 --- a/packages/core/src/customer/customer-request-options.ts +++ b/packages/core/src/customer/customer-request-options.ts @@ -1,3 +1,8 @@ +import { + CustomerStrategy, + CustomerStrategyFactory, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; + import { RequestOptions } from '../common/http-request'; import { MasterpassCustomerInitializeOptions } from './strategies/masterpass'; @@ -28,6 +33,11 @@ export interface CustomerRequestOptions extends RequestOptions { export interface BaseCustomerInitializeOptions extends CustomerRequestOptions { [key: string]: unknown; + /** + * @alpha + */ + integrations?: Array>; + /** * The options that are required to initialize the Masterpass payment method. * They can be omitted unless you need to support Masterpass. diff --git a/packages/core/src/customer/customer-strategy-action-creator.spec.ts b/packages/core/src/customer/customer-strategy-action-creator.spec.ts index 634dbb5730..462ba2a605 100644 --- a/packages/core/src/customer/customer-strategy-action-creator.spec.ts +++ b/packages/core/src/customer/customer-strategy-action-creator.spec.ts @@ -4,6 +4,8 @@ import { merge } from 'lodash'; import { from, of } from 'rxjs'; import { catchError, toArray } from 'rxjs/operators'; +import { PaymentIntegrationService } from '@bigcommerce/checkout-sdk/payment-integration-api'; + import { CheckoutActionCreator, CheckoutRequestSender, @@ -41,6 +43,7 @@ describe('CustomerStrategyActionCreator', () => { let state: CheckoutStoreState; let store: CheckoutStore; let strategy: DefaultCustomerStrategy; + let paymentIntegrationService: PaymentIntegrationService; beforeEach(() => { const requestSender = createRequestSender(); @@ -65,9 +68,9 @@ describe('CustomerStrategyActionCreator', () => { state = getCheckoutStoreState(); store = createCheckoutStore(state); - const paymentIntegrationService = createPaymentIntegrationService(store); + paymentIntegrationService = createPaymentIntegrationService(store); - customerRegistryV2 = createCustomerStrategyRegistryV2(paymentIntegrationService); + customerRegistryV2 = createCustomerStrategyRegistryV2(paymentIntegrationService, {}); registry = createCustomerStrategyRegistry(store, createRequestSender(), 'en'); strategy = new DefaultCustomerStrategy( store, @@ -98,7 +101,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('finds customer strategy by id', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); await from(actionCreator.initialize({ methodId: 'default' })(store)) .pipe(toArray()) @@ -108,7 +115,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('initializes customer strategy', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const options = { methodId: 'default' }; await from(actionCreator.initialize(options)(store)).pipe(toArray()).toPromise(); @@ -117,7 +128,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('does not initialize if strategy is already initialized', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const strategy = registry.get('amazon'); jest.spyOn(strategy, 'initialize').mockReturnValue(Promise.resolve(store.getState())); @@ -128,7 +143,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits action to notify initialization progress', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const actions = await from(actionCreator.initialize({ methodId: 'default' })(store)) .pipe(toArray()) .toPromise(); @@ -146,7 +165,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits error action if unable to initialize', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const initializeError = new Error(); const errorHandler = jest.fn((action) => of(action)); @@ -184,7 +207,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('finds customer strategy by id', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); await from(actionCreator.deinitialize({ methodId: 'default' })(store)) .pipe(toArray()) @@ -194,7 +221,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('deinitializes customer strategy', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const options = { methodId: 'default' }; await from(actionCreator.deinitialize(options)(store)).pipe(toArray()).toPromise(); @@ -203,7 +234,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('does not deinitialize if strategy is not initialized', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const strategy = registry.get('amazon'); jest.spyOn(strategy, 'deinitialize').mockReturnValue(Promise.resolve(store.getState())); @@ -214,7 +249,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits action to notify initialization progress', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const actions = await from(actionCreator.deinitialize({ methodId: 'default' })(store)) .pipe(toArray()) .toPromise(); @@ -232,7 +271,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits error action if unable to deinitialize', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const deinitializeError = new Error(); const errorHandler = jest.fn((action) => of(action)); @@ -264,7 +307,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('finds customer strategy by id', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); await actionCreator .signIn({ email: 'foo@bar.com', password: 'password1' }, { methodId: 'default' }) @@ -275,7 +322,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('executes customer strategy', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const credentials = { email: 'foo@bar.com', password: 'password1' }; const options = { methodId: 'default' }; @@ -285,7 +336,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits action to notify sign-in progress', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const actions = await actionCreator .signIn({ email: 'foo@bar.com', password: 'password1' }, { methodId: 'default' }) .pipe(toArray()) @@ -298,7 +353,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits error action if unable to sign in', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const signInError = new Error(); const errorHandler = jest.fn((action) => of(action)); @@ -328,7 +387,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('finds customer strategy by id', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); await actionCreator.signOut({ methodId: 'default' }).pipe(toArray()).toPromise(); @@ -336,7 +399,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('executes customer strategy', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const options = { methodId: 'default' }; await actionCreator.signOut(options).pipe(toArray()).toPromise(); @@ -345,7 +412,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits action to notify sign-out progress', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const actions = await actionCreator .signOut({ methodId: 'default' }) .pipe(toArray()) @@ -364,7 +435,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits error action if unable to sign out', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const signOutError = new Error(); const errorHandler = jest.fn((action) => of(action)); @@ -393,7 +468,11 @@ describe('CustomerStrategyActionCreator', () => { describe('#widgetInteraction()', () => { it('executes widget interaction callback', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const options = { methodId: 'default' }; const fakeMethod = jest.fn(() => Promise.resolve()); @@ -403,7 +482,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits action to notify widget interaction in progress', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const actions = await actionCreator .widgetInteraction( jest.fn(() => Promise.resolve()), @@ -425,7 +508,11 @@ describe('CustomerStrategyActionCreator', () => { }); it('emits error action if widget interaction fails', async () => { - const actionCreator = new CustomerStrategyActionCreator(registry, customerRegistryV2); + const actionCreator = new CustomerStrategyActionCreator( + registry, + customerRegistryV2, + paymentIntegrationService, + ); const signInError = new Error(); const errorHandler = jest.fn((action) => of(action)); diff --git a/packages/core/src/customer/customer-strategy-action-creator.ts b/packages/core/src/customer/customer-strategy-action-creator.ts index 9835d191da..bd6fc23ff3 100644 --- a/packages/core/src/customer/customer-strategy-action-creator.ts +++ b/packages/core/src/customer/customer-strategy-action-creator.ts @@ -1,10 +1,15 @@ import { createAction, createErrorAction, ThunkAction } from '@bigcommerce/data-store'; import { Observable, Observer } from 'rxjs'; -import { CustomerStrategy as CustomerStrategyV2 } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { + CustomerStrategy as CustomerStrategyV2, + PaymentIntegrationService, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { isExperimentEnabled } from '@bigcommerce/checkout-sdk/utility'; import { InternalCheckoutSelectors } from '../checkout'; import { Registry } from '../common/registry'; +import { matchExistingIntegrations, registerIntegrations } from '../payment-integration'; import CustomerCredentials from './customer-credentials'; import { @@ -28,6 +33,7 @@ export default class CustomerStrategyActionCreator { constructor( private _strategyRegistry: Registry, private _strategyRegistryV2: CustomerStrategyRegistryV2, + private _paymentIntegrationService: PaymentIntegrationService, ) {} signIn( @@ -140,6 +146,28 @@ export default class CustomerStrategyActionCreator { const methodId = options && options.methodId; const meta = { methodId }; + const { features } = state.config.getStoreConfigOrThrow().checkoutSettings; + const experimentEnabled = isExperimentEnabled( + features, + 'CHECKOUT-9450.lazy_load_payment_strategies', + false, + ); + + if (experimentEnabled) { + const resolveId = { id: methodId || '' }; + + matchExistingIntegrations( + this._strategyRegistryV2, + options?.integrations ?? [], + resolveId, + ); + registerIntegrations( + this._strategyRegistryV2, + options?.integrations ?? [], + this._paymentIntegrationService, + ); + } + if (methodId && state.customerStrategies.isInitialized(methodId)) { return observer.complete(); } diff --git a/packages/core/src/payment-integration/index.ts b/packages/core/src/payment-integration/index.ts index 7439c549bd..1ca6de2918 100644 --- a/packages/core/src/payment-integration/index.ts +++ b/packages/core/src/payment-integration/index.ts @@ -1 +1,2 @@ export { default as createPaymentIntegrationService } from './create-payment-integration-service'; +export { registerIntegrations, matchExistingIntegrations } from './register-integrations'; diff --git a/packages/core/src/payment-integration/register-integrations.spec.ts b/packages/core/src/payment-integration/register-integrations.spec.ts new file mode 100644 index 0000000000..f3a10820c5 --- /dev/null +++ b/packages/core/src/payment-integration/register-integrations.spec.ts @@ -0,0 +1,65 @@ +import { PaymentIntegrationService } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { PaymentIntegrationServiceMock } from '@bigcommerce/checkout-sdk/payment-integrations-test-utils'; + +import { ResolveIdRegistry } from '../common/registry'; + +import { registerIntegrations, StrategyFactory } from './register-integrations'; + +class MockStrategy { + constructor(private _service: PaymentIntegrationService) {} + + getService() { + return this._service; + } +} + +describe('registerIntegrations', () => { + let registry: ResolveIdRegistry; + let paymentIntegrationService: PaymentIntegrationService; + let mockFactory: StrategyFactory; + let anotherMockFactory: StrategyFactory; + + beforeEach(() => { + registry = new ResolveIdRegistry(); + paymentIntegrationService = new PaymentIntegrationServiceMock(); + + mockFactory = Object.assign( + (service: PaymentIntegrationService) => new MockStrategy(service), + { resolveIds: [{ id: 'mock-strategy' }] }, + ); + + anotherMockFactory = Object.assign( + (service: PaymentIntegrationService) => new MockStrategy(service), + { resolveIds: [{ id: 'another-strategy' }] }, + ); + }); + + describe('when registering integrations', () => { + it('should register resolvable integrations in the registry', () => { + const integrations = [mockFactory, anotherMockFactory]; + + registerIntegrations(registry, integrations, paymentIntegrationService); + + const registeredStrategy = registry.get({ id: 'mock-strategy' }); + const anotherRegisteredStrategy = registry.get({ id: 'another-strategy' }); + + expect(registeredStrategy).toBeInstanceOf(MockStrategy); + expect(anotherRegisteredStrategy).toBeInstanceOf(MockStrategy); + }); + + it('should skip registration if a factory is already registered for the same resolve ID', () => { + const originalFactory = mockFactory; + const duplicateFactory = Object.assign( + (service: PaymentIntegrationService) => new MockStrategy(service), + { resolveIds: [{ id: 'mock-strategy' }] }, + ); + + registerIntegrations(registry, [originalFactory], paymentIntegrationService); + registerIntegrations(registry, [duplicateFactory], paymentIntegrationService); + + const registeredStrategy = registry.get({ id: 'mock-strategy' }); + + expect(registeredStrategy).toBeInstanceOf(MockStrategy); + }); + }); +}); diff --git a/packages/core/src/payment-integration/register-integrations.ts b/packages/core/src/payment-integration/register-integrations.ts new file mode 100644 index 0000000000..eea51d7c2e --- /dev/null +++ b/packages/core/src/payment-integration/register-integrations.ts @@ -0,0 +1,73 @@ +import { isEqual } from 'lodash'; + +import { + isResolvableModule, + PaymentIntegrationService, + toResolvableModule, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import { ResolveIdRegistry } from '../common/registry'; +import { SentryWindow } from '../common/types/sentry'; + +export type StrategyFactory = ( + paymentIntegrationService: PaymentIntegrationService, +) => TStrategy; + +export function registerIntegrations( + registry: ResolveIdRegistry, + integrations: Array>, + paymentIntegrationService: PaymentIntegrationService, +): void { + integrations.forEach((factory) => { + if (!isResolvableModule, TResolveId>(factory)) { + return; + } + + factory.resolveIds.forEach((resolveId) => { + if (registry.getFactory(resolveId)) { + return; + } + + // TODO: Remove toResolvableModule once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out + const createStrategy = toResolvableModule( + () => factory(paymentIntegrationService), + factory.resolveIds, + ); + + registry.register(resolveId, createStrategy); + }); + }); +} + +// TODO: Remove this function once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out +export function matchExistingIntegrations( + registry: ResolveIdRegistry, + integrations: Array>, + resolveId: TResolveId, +): boolean { + const existingFactory = registry.getFactory(resolveId); + const matchedExisting = integrations.some( + (factory) => + isResolvableModule(existingFactory) && + isResolvableModule(factory) && + isEqual(existingFactory.resolveIds, factory.resolveIds), + ); + + // During the initial rollout, all strategies will continue to be registered with `strategyRegistryV2` + // and bundled together by default. This allows us to compare the passed-in strategies with the existing + // ones to ensure they match. Once confirmed, we can remove the comparison logic and the existing strategies, + // relying solely on the passed-in strategies. + if (existingFactory && !matchedExisting) { + const message = `A different strategy is registered for ${JSON.stringify(resolveId)}.`; + const { Sentry } = window as SentryWindow; + + if (Sentry?.captureMessage) { + Sentry.captureMessage(message); + } else { + // eslint-disable-next-line no-console + console.log(message); + } + } + + return matchedExisting; +} diff --git a/packages/core/src/payment/create-payment-strategy-registry-v2.ts b/packages/core/src/payment/create-payment-strategy-registry-v2.ts index 0980ad1de8..e9d81516ea 100644 --- a/packages/core/src/payment/create-payment-strategy-registry-v2.ts +++ b/packages/core/src/payment/create-payment-strategy-registry-v2.ts @@ -4,10 +4,10 @@ import { PaymentStrategy, PaymentStrategyFactory, PaymentStrategyResolveId, + toResolvableModule, } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { ResolveIdRegistry } from '../common/registry'; -import * as defaultPaymentStrategyFactories from '../generated/payment-strategies'; export interface PaymentStrategyFactories { [key: string]: PaymentStrategyFactory; @@ -15,7 +15,7 @@ export interface PaymentStrategyFactories { export default function createPaymentStrategyRegistry( paymentIntegrationService: PaymentIntegrationService, - paymentStrategyFactories: PaymentStrategyFactories = defaultPaymentStrategyFactories, + paymentStrategyFactories: PaymentStrategyFactories, options: { useFallback: boolean } = { useFallback: false }, ): ResolveIdRegistry { const { useFallback } = options; @@ -31,7 +31,13 @@ export default function createPaymentStrategyRegistry( } for (const resolverId of createPaymentStrategy.resolveIds) { - registry.register(resolverId, () => createPaymentStrategy(paymentIntegrationService)); + // TODO: Remove toResolvableModule once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out + const factory = toResolvableModule( + () => createPaymentStrategy(paymentIntegrationService), + createPaymentStrategy.resolveIds, + ); + + registry.register(resolverId, factory); } } diff --git a/packages/core/src/payment/create-payment-strategy-registry.ts b/packages/core/src/payment/create-payment-strategy-registry.ts index 61598e7a24..56b2ed1963 100644 --- a/packages/core/src/payment/create-payment-strategy-registry.ts +++ b/packages/core/src/payment/create-payment-strategy-registry.ts @@ -11,6 +11,7 @@ import { import { BrowserStorage } from '../common/storage'; import { ConfigActionCreator, ConfigRequestSender } from '../config'; import { FormFieldsActionCreator, FormFieldsRequestSender } from '../form'; +import * as paymentStrategyFactories from '../generated/payment-strategies'; import { HostedFormFactory } from '../hosted-form'; import { OrderActionCreator, OrderRequestSender } from '../order'; import { createPaymentIntegrationService } from '../payment-integration'; @@ -65,11 +66,17 @@ export default function createPaymentStrategyRegistry( const registry = new PaymentStrategyRegistry({ defaultToken: PaymentStrategyType.CREDIT_CARD, }); + const scriptLoader = getScriptLoader(); const paymentRequestTransformer = new PaymentRequestTransformer(); const paymentRequestSender = new PaymentRequestSender(paymentClient); const paymentIntegrationService = createPaymentIntegrationService(store); - const registryV2 = createPaymentStrategyRegistryV2(paymentIntegrationService); + const registryV2 = createPaymentStrategyRegistryV2( + paymentIntegrationService, + paymentStrategyFactories, + // TODO: Replace once CHECKOUT-9450.lazy_load_payment_strategies experiment is rolled out + // process.env.ESSENTIAL_BUILD ? {} : paymentStrategyFactories, + ); const checkoutRequestSender = new CheckoutRequestSender(requestSender); const checkoutValidator = new CheckoutValidator(checkoutRequestSender); const spamProtectionActionCreator = new SpamProtectionActionCreator( @@ -106,6 +113,7 @@ export default function createPaymentStrategyRegistry( registryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ); const formPoster = createFormPoster(); const stepHandler = createStepHandler(formPoster, paymentHumanVerificationHandler); diff --git a/packages/core/src/payment/payment-request-options.ts b/packages/core/src/payment/payment-request-options.ts index e681d473f6..b15af23bc7 100644 --- a/packages/core/src/payment/payment-request-options.ts +++ b/packages/core/src/payment/payment-request-options.ts @@ -1,4 +1,8 @@ import { CreditCardPaymentInitializeOptions } from '@bigcommerce/checkout-sdk/credit-card-integration'; +import { + PaymentStrategy, + PaymentStrategyFactory, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; import { RequestOptions } from '../common/http-request'; @@ -35,6 +39,11 @@ export interface PaymentRequestOptions extends RequestOptions { * current checkout flow. */ export interface BasePaymentInitializeOptions extends PaymentRequestOptions { + /** + * @alpha + */ + integrations?: Array>; + /** * @alpha * Please note that this option is currently in an early stage of diff --git a/packages/core/src/payment/payment-strategy-action-creator.spec.ts b/packages/core/src/payment/payment-strategy-action-creator.spec.ts index ffbc38a197..0506ab65dd 100644 --- a/packages/core/src/payment/payment-strategy-action-creator.spec.ts +++ b/packages/core/src/payment/payment-strategy-action-creator.spec.ts @@ -13,8 +13,11 @@ import { import { createNoPaymentStrategy } from '@bigcommerce/checkout-sdk/no-payment-integration'; import { OrderFinalizationNotRequiredError as OrderFinalizationNotRequiredErrorV2, + PaymentIntegrationService, + PaymentStrategyFactory, PaymentStrategyResolveId, PaymentStrategy as PaymentStrategyV2, + toResolvableModule, } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { @@ -31,8 +34,9 @@ import { } from '../checkout/checkouts.mock'; import { MissingDataError } from '../common/error/errors'; import { ResolveIdRegistry } from '../common/registry'; +import { SentryWindow } from '../common/types/sentry'; import { getCustomerState } from '../customer/customers.mock'; -import * as defaultPaymentStrategyFactories from '../generated/payment-strategies'; +import * as paymentStrategyFactories from '../generated/payment-strategies'; import { HostedFormFactory } from '../hosted-form'; import { OrderActionCreator, OrderActionType, OrderRequestSender } from '../order'; import { OrderFinalizationNotRequiredError } from '../order/errors'; @@ -75,6 +79,7 @@ describe('PaymentStrategyActionCreator', () => { let spamProtectionActionCreator: SpamProtectionActionCreator; let paymentHumanVerificationHandler: PaymentHumanVerificationHandler; let actionCreator: PaymentStrategyActionCreator; + let paymentIntegrationService: PaymentIntegrationService; beforeEach(() => { state = getCheckoutStoreState(); @@ -97,12 +102,14 @@ describe('PaymentStrategyActionCreator', () => { new CheckoutValidator(new CheckoutRequestSender(createRequestSender())), ); - const paymentIntegrationService = createPaymentIntegrationService(store); + paymentIntegrationService = createPaymentIntegrationService(store); registryV2 = createPaymentStrategyRegistryV2( paymentIntegrationService, - defaultPaymentStrategyFactories, - { useFallback: true }, + paymentStrategyFactories, + { + useFallback: true, + }, ); strategy = new CreditCardPaymentStrategy( store, @@ -126,6 +133,7 @@ describe('PaymentStrategyActionCreator', () => { registryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ); jest.spyOn(registry, 'getByMethod').mockReturnValue(strategy); @@ -269,6 +277,166 @@ describe('PaymentStrategyActionCreator', () => { expect((action as Action).payload).toBeInstanceOf(MissingDataError); } }); + + describe('with integrations', () => { + let mockStrategyFactory: PaymentStrategyFactory; + let mockStrategy: PaymentStrategyV2; + + beforeEach(() => { + mockStrategy = { + initialize: jest.fn().mockResolvedValue(store.getState()), + execute: jest.fn().mockResolvedValue(undefined), + finalize: jest.fn().mockResolvedValue(undefined), + deinitialize: jest.fn().mockResolvedValue(undefined), + }; + + mockStrategyFactory = jest.fn().mockReturnValue(mockStrategy); + + store = createCheckoutStore( + merge({}, state, { + config: { + data: { + storeConfig: { + checkoutSettings: { + features: { + 'CHECKOUT-9450.lazy_load_payment_strategies': true, + }, + }, + }, + }, + }, + }), + ); + + actionCreator = new PaymentStrategyActionCreator( + registry, + registryV2, + orderActionCreator, + spamProtectionActionCreator, + paymentIntegrationService, + ); + }); + + it('registers new strategy factory when integrations are provided', async () => { + const method = getPaymentMethod(); + const resolvableFactory = toResolvableModule(mockStrategyFactory, [ + { + id: method.id, + gateway: method.gateway, + type: method.type, + }, + ]); + + jest.spyOn(registryV2, 'getFactory').mockReturnValue(undefined); + jest.spyOn(registryV2, 'register'); + jest.spyOn(registryV2, 'get').mockReturnValue(mockStrategy); + + await from( + actionCreator.initialize({ + methodId: method.id, + gatewayId: method.gateway, + integrations: [resolvableFactory], + })(store), + ).toPromise(); + + expect(registryV2.register).toHaveBeenCalledWith( + { + id: method.id, + gateway: method.gateway, + type: method.type, + }, + expect.any(Function), + ); + + expect( + registryV2.get({ + id: method.id, + gateway: method.gateway, + type: method.type, + }), + ).toBe(mockStrategy); + }); + + it('uses provided integration strategy when registering and initializing', async () => { + const method = getPaymentMethod(); + const resolvableFactory = toResolvableModule(mockStrategyFactory, [ + { + id: method.id, + gateway: method.gateway, + type: method.type, + }, + ]); + + jest.spyOn(registryV2, 'getFactory').mockReturnValue(undefined); + jest.spyOn(registryV2, 'get').mockReturnValue(mockStrategy); + jest.spyOn(registry, 'getByMethod').mockImplementation(() => { + throw new Error('Strategy not found in registry v1'); + }); + + await from( + actionCreator.initialize({ + methodId: method.id, + gatewayId: method.gateway, + integrations: [resolvableFactory], + })(store), + ).toPromise(); + + expect(mockStrategy.initialize).toHaveBeenCalledWith({ + methodId: method.id, + gatewayId: method.gateway, + integrations: [resolvableFactory], + }); + }); + + it('logs message if provided integration strategy does not match with existing strategy', async () => { + const method = getPaymentMethod(); + + const existingFactory = toResolvableModule(mockStrategyFactory, [ + { + id: method.id, + gateway: method.gateway, + type: method.type, + }, + ]); + + const newFactory = toResolvableModule(jest.fn(), [ + { + id: 'different-id', + gateway: method.gateway, + type: method.type, + }, + ]); + + jest.spyOn(registryV2, 'getFactory').mockReturnValue(existingFactory as any); + jest.spyOn(registryV2, 'get').mockReturnValue(mockStrategy); + jest.spyOn(registry, 'getByMethod').mockImplementation(() => { + throw new Error('Strategy not found in registry v1'); + }); + + const captureMessageSpy = jest.fn(); + const actionCreatorWithLogger = new PaymentStrategyActionCreator( + registry, + registryV2, + orderActionCreator, + spamProtectionActionCreator, + paymentIntegrationService, + ); + + (window as SentryWindow).Sentry = { + captureMessage: captureMessageSpy, + }; + + await from( + actionCreatorWithLogger.initialize({ + methodId: method.id, + gatewayId: method.gateway, + integrations: [newFactory], + })(store), + ).toPromise(); + + expect(captureMessageSpy).toHaveBeenCalled(); + }); + }); }); describe('#deinitialize()', () => { @@ -518,6 +686,7 @@ describe('PaymentStrategyActionCreator', () => { registryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ); try { @@ -552,6 +721,7 @@ describe('PaymentStrategyActionCreator', () => { registryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ); const payload = { ...getOrderRequestBody(), useStoreCredit: true }; @@ -667,6 +837,7 @@ describe('PaymentStrategyActionCreator', () => { registryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ); const strategyV2 = new CreditCardPaymentStrategyV2( createPaymentIntegrationService(store), @@ -705,6 +876,7 @@ describe('PaymentStrategyActionCreator', () => { registryV2, orderActionCreator, spamProtectionActionCreator, + paymentIntegrationService, ); try { diff --git a/packages/core/src/payment/payment-strategy-action-creator.ts b/packages/core/src/payment/payment-strategy-action-creator.ts index d6aab9788d..ef3fda9727 100644 --- a/packages/core/src/payment/payment-strategy-action-creator.ts +++ b/packages/core/src/payment/payment-strategy-action-creator.ts @@ -2,7 +2,11 @@ import { createAction, ThunkAction } from '@bigcommerce/data-store'; import { concat, defer, empty, Observable, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; -import { PaymentStrategy as PaymentStrategyV2 } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { + PaymentIntegrationService, + PaymentStrategy as PaymentStrategyV2, +} from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { isExperimentEnabled } from '@bigcommerce/checkout-sdk/utility'; import { InternalCheckoutSelectors, ReadableCheckoutStore } from '../checkout'; import { throwErrorAction } from '../common/error'; @@ -15,6 +19,7 @@ import { OrderRequestBody, } from '../order'; import { OrderFinalizationNotRequiredError } from '../order/errors'; +import { matchExistingIntegrations, registerIntegrations } from '../payment-integration'; import { SpamProtectionAction, SpamProtectionActionCreator } from '../spam-protection'; import PaymentMethod from './payment-method'; @@ -41,6 +46,7 @@ export default class PaymentStrategyActionCreator { private _strategyRegistryV2: PaymentStrategyRegistryV2, private _orderActionCreator: OrderActionCreator, private _spamProtectionActionCreator: SpamProtectionActionCreator, + private _paymentIntegrationService: PaymentIntegrationService, ) { this._paymentStrategyWidgetActionCreator = new PaymentStrategyWidgetActionCreator(); } @@ -162,6 +168,32 @@ export default class PaymentStrategyActionCreator { return empty(); } + const { features } = state.config.getStoreConfigOrThrow().checkoutSettings; + const experimentEnabled = isExperimentEnabled( + features, + 'CHECKOUT-9450.lazy_load_payment_strategies', + false, + ); + + if (experimentEnabled) { + const resolveId = { + id: method.id, + gateway: method.gateway, + type: method.type, + }; + + matchExistingIntegrations( + this._strategyRegistryV2, + options.integrations ?? [], + resolveId, + ); + registerIntegrations( + this._strategyRegistryV2, + options.integrations ?? [], + this._paymentIntegrationService, + ); + } + const strategy = this._getStrategy(method); const promise: Promise = strategy.initialize({ diff --git a/packages/core/src/payment/strategies/braintree/braintree-visacheckout-payment-strategy.spec.ts b/packages/core/src/payment/strategies/braintree/braintree-visacheckout-payment-strategy.spec.ts index 3861ecdabf..945a94c328 100644 --- a/packages/core/src/payment/strategies/braintree/braintree-visacheckout-payment-strategy.spec.ts +++ b/packages/core/src/payment/strategies/braintree/braintree-visacheckout-payment-strategy.spec.ts @@ -111,7 +111,7 @@ describe('BraintreeVisaCheckoutPaymentStrategy', () => { spamProtection, 'en_US', ); - const registryV2 = createPaymentStrategyRegistryV2(paymentIntegrationService); + const registryV2 = createPaymentStrategyRegistryV2(paymentIntegrationService, {}); const checkoutRequestSender = new CheckoutRequestSender(createRequestSender()); const checkoutValidator = new CheckoutValidator(checkoutRequestSender); @@ -135,6 +135,7 @@ describe('BraintreeVisaCheckoutPaymentStrategy', () => { spamProtection, new SpamProtectionRequestSender(requestSender), ), + paymentIntegrationService, ); paymentActionCreator = new PaymentActionCreator( new PaymentRequestSender(createPaymentClient(store)), diff --git a/packages/external-integration/src/external-payment-strategy.ts b/packages/external-integration/src/external-payment-strategy.ts index 777805b99d..92ad632970 100644 --- a/packages/external-integration/src/external-payment-strategy.ts +++ b/packages/external-integration/src/external-payment-strategy.ts @@ -47,7 +47,11 @@ export default class ExternalPaymentStrategy implements PaymentStrategy { }, } = error; - return new Promise(() => this.redirectUrl(redirect_url)); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return new Promise(() => { + this.redirectUrl(redirect_url); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any; } } diff --git a/packages/google-pay-integration/src/google-pay-payment-processor.ts b/packages/google-pay-integration/src/google-pay-payment-processor.ts index 60489d1516..38d47ad618 100644 --- a/packages/google-pay-integration/src/google-pay-payment-processor.ts +++ b/packages/google-pay-integration/src/google-pay-payment-processor.ts @@ -7,6 +7,7 @@ import { guard, NotInitializedError, NotInitializedErrorType, + Omit, PaymentMethod, PaymentMethodFailedError, SDK_VERSION_HEADERS, diff --git a/packages/google-pay-integration/src/google-pay-payment-strategy.ts b/packages/google-pay-integration/src/google-pay-payment-strategy.ts index 9f04f199dc..6a3aa760c5 100644 --- a/packages/google-pay-integration/src/google-pay-payment-strategy.ts +++ b/packages/google-pay-integration/src/google-pay-payment-strategy.ts @@ -7,6 +7,7 @@ import { MissingDataErrorType, NotInitializedError, NotInitializedErrorType, + Omit, OrderFinalizationNotRequiredError, OrderRequestBody, PaymentArgumentInvalidError, diff --git a/packages/hosted-form-v2/src/common/utility/set-prototype-of.spec.ts b/packages/hosted-form-v2/src/common/utility/set-prototype-of.spec.ts index 6a543486fd..cdd334b29b 100644 --- a/packages/hosted-form-v2/src/common/utility/set-prototype-of.spec.ts +++ b/packages/hosted-form-v2/src/common/utility/set-prototype-of.spec.ts @@ -6,8 +6,6 @@ describe('setPrototypeOf', () => { const error = new CustomError(); - expect(error instanceof CustomError).toBeFalsy(); - setPrototypeOf(error, CustomError.prototype); expect(error instanceof CustomError).toBeTruthy(); diff --git a/packages/paypal-express-integration/src/paypal-express-button-initialize-options.ts b/packages/paypal-express-integration/src/paypal-express-button-initialize-options.ts index e33f9d3bcc..0b709620dd 100644 --- a/packages/paypal-express-integration/src/paypal-express-button-initialize-options.ts +++ b/packages/paypal-express-integration/src/paypal-express-button-initialize-options.ts @@ -1,4 +1,4 @@ -import { StandardError } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { Omit, StandardError } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { PaypalStyleOptions } from './paypal-express-types'; diff --git a/packages/squarev2-integration/src/create-squarev2-payment-strategy.ts b/packages/squarev2-integration/src/create-squarev2-payment-strategy.ts index d9cdcdfe63..4a36031cee 100644 --- a/packages/squarev2-integration/src/create-squarev2-payment-strategy.ts +++ b/packages/squarev2-integration/src/create-squarev2-payment-strategy.ts @@ -1,6 +1,7 @@ import { getScriptLoader } from '@bigcommerce/script-loader'; import { + PaymentStrategy, PaymentStrategyFactory, toResolvableModule, } from '@bigcommerce/checkout-sdk/payment-integration-api'; @@ -9,7 +10,7 @@ import SquareV2PaymentProcessor from './squarev2-payment-processor'; import SquareV2PaymentStrategy from './squarev2-payment-strategy'; import SquareV2ScriptLoader from './squarev2-script-loader'; -const createSquareV2PaymentStrategy: PaymentStrategyFactory = ( +const createSquareV2PaymentStrategy: PaymentStrategyFactory = ( paymentIntegrationService, ) => { return new SquareV2PaymentStrategy( diff --git a/packages/utility/src/is-experiment-enabled/is-experiment-enabled.ts b/packages/utility/src/is-experiment-enabled/is-experiment-enabled.ts index 688fc4a4f9..fd77fa72cd 100644 --- a/packages/utility/src/is-experiment-enabled/is-experiment-enabled.ts +++ b/packages/utility/src/is-experiment-enabled/is-experiment-enabled.ts @@ -2,6 +2,10 @@ export interface Features { [featureName: string]: boolean | undefined; } -export default function isExperimentEnabled(features: Features, experimentName: string): boolean { - return features[experimentName] ?? true; +export default function isExperimentEnabled( + features: Features, + experimentName: string, + fallbackValue = true, +): boolean { + return features[experimentName] ?? fallbackValue; } diff --git a/packages/workspace-tools/src/generators/auto-export/__snapshots__/auto-export.spec.ts.snap b/packages/workspace-tools/src/generators/auto-export/__snapshots__/auto-export.spec.ts.snap index 051cf26b26..98a19d670e 100644 --- a/packages/workspace-tools/src/generators/auto-export/__snapshots__/auto-export.spec.ts.snap +++ b/packages/workspace-tools/src/generators/auto-export/__snapshots__/auto-export.spec.ts.snap @@ -1,9 +1,46 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`autoExport() export matching members from files to another file 1`] = ` -"export { StrategyA } from '../__fixtures__/strategy-a'; +Object { + "apiExtractorConfigs": Map { + "workspace-tools" => "{ + \\"compiler\\": { + \\"configType\\": \\"tsconfig\\", + \\"rootFolder\\": \\"../../../..\\" + }, + \\"project\\": { + \\"entryPointSourceFile\\": \\"/__temp__/workspace-tools/index.d.ts\\" + }, + \\"validationRules\\": { + \\"missingReleaseTags\\": \\"allow\\" + }, + \\"apiReviewFile\\": { + \\"enabled\\": false + }, + \\"apiJsonFile\\": { + \\"enabled\\": false + }, + \\"dtsRollup\\": { + \\"enabled\\": true, + \\"mainDtsRollupPath\\": \\"/__temp__/workspace-tools.d.ts\\" + } +}", + }, + "packageExports": Map { + "workspace-tools" => "export { StrategyA } from '../../__fixtures__/strategy-a'; +export { StrategyB } from '../../__fixtures__/strategy-b'; +", + }, + "packageExportsGrouped": "export { StrategyA } from '../__fixtures__/strategy-a'; export { StrategyB } from '../__fixtures__/strategy-b'; -" +", +} `; -exports[`autoExport() handles scenario where no matching member is found 1`] = `""`; +exports[`autoExport() handles scenario where no matching member is found 1`] = ` +Object { + "apiExtractorConfigs": Map {}, + "packageExports": Map {}, + "packageExportsGrouped": "", +} +`; diff --git a/packages/workspace-tools/src/generators/auto-export/auto-export-config.ts b/packages/workspace-tools/src/generators/auto-export/auto-export-config.ts index 4d97a487a3..b860e19624 100644 --- a/packages/workspace-tools/src/generators/auto-export/auto-export-config.ts +++ b/packages/workspace-tools/src/generators/auto-export/auto-export-config.ts @@ -1,9 +1,16 @@ export default interface AutoExportConfig { entries: AutoExportConfigEntry[]; + apiExtractorConfig: ApiExtractorConfig; } export interface AutoExportConfigEntry { inputPath: string; outputPath: string; + packageOutputPath: string; memberPattern: string; } + +export interface ApiExtractorConfig { + entryPointSourceFile: string; + mainDtsRollupPath: string; +} diff --git a/packages/workspace-tools/src/generators/auto-export/auto-export.spec.ts b/packages/workspace-tools/src/generators/auto-export/auto-export.spec.ts index bb55daa826..1cb8cb4544 100644 --- a/packages/workspace-tools/src/generators/auto-export/auto-export.spec.ts +++ b/packages/workspace-tools/src/generators/auto-export/auto-export.spec.ts @@ -8,6 +8,11 @@ describe('autoExport()', () => { inputPath: path.join(__dirname, '/__fixtures__/**/index.ts'), outputPath: path.join(__dirname, '/__temp__/output.ts'), memberPattern: '^Strategy', + packageOutputPath: path.join(__dirname, '/__temp__//output.ts'), + apiExtractorConfig: { + entryPointSourceFile: '/__temp__//index.d.ts', + mainDtsRollupPath: '/__temp__/.d.ts', + }, }; expect(await autoExport(options)).toMatchSnapshot(); @@ -18,6 +23,11 @@ describe('autoExport()', () => { inputPath: path.join(__dirname, '/__fixtures__/**/index.ts'), outputPath: path.join(__dirname, '/__temp__/output.ts'), memberPattern: '^Test', + packageOutputPath: path.join(__dirname, '/__temp__//output.ts'), + apiExtractorConfig: { + entryPointSourceFile: '/__temp__//index.d.ts', + mainDtsRollupPath: '/__temp__/.d.ts', + }, }; expect(await autoExport(options)).toMatchSnapshot(); diff --git a/packages/workspace-tools/src/generators/auto-export/auto-export.ts b/packages/workspace-tools/src/generators/auto-export/auto-export.ts index a8ba8b7554..d6d703ee78 100644 --- a/packages/workspace-tools/src/generators/auto-export/auto-export.ts +++ b/packages/workspace-tools/src/generators/auto-export/auto-export.ts @@ -7,24 +7,108 @@ import { promisify } from 'util'; export interface AutoExportOptions { inputPath: string; outputPath: string; + packageOutputPath: string; memberPattern: string; + apiExtractorConfig: { + entryPointSourceFile: string; + mainDtsRollupPath: string; + }; } -export default async function autoExport({ +export interface AutoExportResult { + packageExports: Map; + packageExportsGrouped: string; + apiExtractorConfigs?: Map; +} + +export default async function autoExport(options: AutoExportOptions): Promise { + const { packageExports, apiExtractorConfigs } = await createPackageExports(options); + const packageExportsGrouped = await createPackageExportsGrouped(options); + + return { + packageExports, + packageExportsGrouped, + apiExtractorConfigs, + }; +} + +async function createPackageExports({ + inputPath, + packageOutputPath, + memberPattern, + apiExtractorConfig, +}: AutoExportOptions): Promise<{ + packageExports: Map; + apiExtractorConfigs?: Map; +}> { + const filePaths = await promisify(glob)(inputPath); + const results = ( + await Promise.all( + filePaths.map((filePath) => + createExportDeclaration(filePath, packageOutputPath, memberPattern), + ), + ) + ).filter(exists); + const packageExports = new Map(); + const packageGroups = new Map(); + + results.forEach((result) => { + const packageName = result.packageName; + + if (!packageGroups.has(packageName)) { + packageGroups.set(packageName, []); + } + + const declarations = packageGroups.get(packageName); + + if (declarations) { + declarations.push(result.exportDeclaration); + } + }); + + Array.from(packageGroups.entries()).forEach(([packageName, declarations]) => { + const packageContent = ts + .createPrinter() + .printList( + ts.ListFormat.MultiLine, + ts.factory.createNodeArray(declarations), + ts.createSourceFile('', '', ts.ScriptTarget.ESNext), + ); + + packageExports.set(packageName, packageContent); + }); + + const apiExtractorConfigs = createApiExtractorConfigs( + Array.from(packageGroups.keys()), + apiExtractorConfig.entryPointSourceFile, + apiExtractorConfig.mainDtsRollupPath, + ); + + return { + packageExports, + apiExtractorConfigs, + }; +} + +async function createPackageExportsGrouped({ inputPath, outputPath, memberPattern, }: AutoExportOptions): Promise { const filePaths = await promisify(glob)(inputPath); - const exportDeclarations = await Promise.all( - filePaths.map((filePath) => createExportDeclaration(filePath, outputPath, memberPattern)), - ); + const results = ( + await Promise.all( + filePaths.map((filePath) => + createExportDeclaration(filePath, outputPath, memberPattern), + ), + ) + ).filter(exists); return ts .createPrinter() .printList( ts.ListFormat.MultiLine, - ts.factory.createNodeArray(exportDeclarations.filter(exists)), + ts.factory.createNodeArray(results.map((result) => result.exportDeclaration)), ts.createSourceFile(outputPath, '', ts.ScriptTarget.ESNext), ); } @@ -33,7 +117,13 @@ async function createExportDeclaration( filePath: string, outputPath: string, memberPattern: string, -): Promise { +): Promise< + | { + exportDeclaration: ts.ExportDeclaration; + packageName: string; + } + | undefined +> { const root = await getSource(filePath); const memberNames = root.statements @@ -56,7 +146,7 @@ async function createExportDeclaration( return; } - return ts.factory.createExportDeclaration( + const exportDeclaration = ts.factory.createExportDeclaration( undefined, undefined, false, @@ -71,6 +161,13 @@ async function createExportDeclaration( ), ts.factory.createStringLiteral(getImportPath(filePath, outputPath), true), ); + + const packageName = extractPackageName(filePath); + + return { + exportDeclaration, + packageName, + }; } async function getSource(filePath: string): Promise { @@ -88,6 +185,55 @@ function getImportPath(filePath: string, outputPath: string): string { return fileName === 'index' ? importFolder : path.join(importFolder, fileName); } +function extractPackageName(filePath: string): string { + const pathParts = filePath.split(path.sep); + const packagesIndex = pathParts.findIndex((part) => part === 'packages'); + + if (packagesIndex !== -1 && packagesIndex + 1 < pathParts.length) { + return pathParts[packagesIndex + 1]; + } + + return path.basename(path.dirname(path.dirname(filePath))); +} + function exists(value?: TValue): value is NonNullable { return value !== null && value !== undefined; } + +function createApiExtractorConfigs( + packageNames: string[], + entryPointSourceFile: string, + mainDtsRollupPath: string, +): Map { + const apiExtractorConfigs = new Map(); + + packageNames.forEach((packageName) => { + const moduleName = packageName.replace(/-integration$/, ''); + const config = { + compiler: { + configType: 'tsconfig', + rootFolder: '../../../..', + }, + project: { + entryPointSourceFile: entryPointSourceFile.replace('', moduleName), + }, + validationRules: { + missingReleaseTags: 'allow', + }, + apiReviewFile: { + enabled: false, + }, + apiJsonFile: { + enabled: false, + }, + dtsRollup: { + enabled: true, + mainDtsRollupPath: mainDtsRollupPath.replace('', moduleName), + }, + }; + + apiExtractorConfigs.set(packageName, JSON.stringify(config, null, 4)); + }); + + return apiExtractorConfigs; +} diff --git a/packages/workspace-tools/src/generators/auto-export/generator.ts b/packages/workspace-tools/src/generators/auto-export/generator.ts index 7c36a8ee55..b63f8bdb2a 100644 --- a/packages/workspace-tools/src/generators/auto-export/generator.ts +++ b/packages/workspace-tools/src/generators/auto-export/generator.ts @@ -19,12 +19,62 @@ export default async function autoExportGenerator(tree: Tree, options: AutoExpor throw new Error('The specified config file does not conform to the expected schema.'); } + const allPackageExports = new Map(); + const allApiExtractorConfigs = new Map(); + await Promise.all( - config.entries.map(async (entry) => { + config.entries.map(async (entry, entryIndex) => { + const result = await autoExport({ + ...entry, + apiExtractorConfig: config.apiExtractorConfig, + }); + generateFiles(tree, join(__dirname, './templates'), parse(entry.outputPath).dir, { - content: await autoExport(entry), + content: result.packageExportsGrouped, outputName: basename(entry.outputPath), }); + + result.packageExports.forEach((packageExport, packageName) => { + const packageOutputPath = parse( + config.entries[entryIndex].packageOutputPath.replace( + '', + packageName.replace(/-integration$/, ''), + ), + ).dir; + + if (!allPackageExports.has(packageOutputPath)) { + allPackageExports.set(packageOutputPath, []); + } + + const existingExports = allPackageExports.get(packageOutputPath) || []; + + allPackageExports.set(packageOutputPath, [...existingExports, packageExport]); + }); + + result.apiExtractorConfigs?.forEach((configContent, packageName) => { + const packageOutputPath = parse( + config.entries[entryIndex].packageOutputPath.replace( + '', + packageName.replace(/-integration$/, ''), + ), + ).dir; + + allApiExtractorConfigs.set( + `${packageOutputPath}/api-extractor.json`, + configContent, + ); + }); }), ); + + allPackageExports.forEach((packageExports, packageOutputPath) => { + generateFiles(tree, join(__dirname, './templates'), packageOutputPath, { + content: packageExports.join('\n'), + outputName: 'index.ts', + }); + }); + + allApiExtractorConfigs.forEach((configContent, configPath) => { + tree.write(configPath, configContent); + }); } diff --git a/packages/workspace-tools/src/generators/auto-export/is-auto-export-config.ts b/packages/workspace-tools/src/generators/auto-export/is-auto-export-config.ts index ead6acf77e..b99711c890 100644 --- a/packages/workspace-tools/src/generators/auto-export/is-auto-export-config.ts +++ b/packages/workspace-tools/src/generators/auto-export/is-auto-export-config.ts @@ -22,6 +22,10 @@ export default function isAutoExportConfig(config: unknown): config is AutoExpor return false; } + if (!hasKey(entry, 'packageOutputPath') || typeof entry.packageOutputPath !== 'string') { + return false; + } + if (!hasKey(entry, 'memberPattern') || typeof entry.memberPattern !== 'string') { return false; } diff --git a/tsconfig.base.json b/tsconfig.base.json index 04a6f4ebf2..ce05e564e9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -6,8 +6,8 @@ "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, "importHelpers": false, - "lib": ["dom", "dom.iterable", "es6", "scripthost"], - "module": "es6", + "lib": ["dom", "dom.iterable", "esnext", "scripthost"], + "module": "esnext", "moduleResolution": "node", "noUnusedParameters": true, "noUnusedLocals": true, @@ -15,7 +15,7 @@ "sourceMap": true, "strict": true, "stripInternal": true, - "target": "es5", + "target": "es6", "baseUrl": ".", "paths": { "@bigcommerce/checkout-sdk/adyen-integration": [ @@ -47,9 +47,6 @@ "@bigcommerce/checkout-sdk/bluesnap-direct-integration": [ "packages/bluesnap-direct-integration/src/index.ts" ], - "@bigcommerce/checkout-sdk/bluesnapv2-integration": [ - "packages/bluesnapv2-integration/src/index.ts" - ], "@bigcommerce/checkout-sdk/bolt-integration": [ "packages/bolt-integration/src/index.ts" ], @@ -63,7 +60,9 @@ "@bigcommerce/checkout-sdk/checkoutcom-custom-integration": [ "packages/checkoutcom-custom-integration/src/index.ts" ], - "@bigcommerce/checkout-sdk/clearpay": ["packages/clearpay/src/index.ts"], + "@bigcommerce/checkout-sdk/clearpay-integration": [ + "packages/clearpay-integration/src/index.ts" + ], "@bigcommerce/checkout-sdk/core": ["packages/core/src/index.ts"], "@bigcommerce/checkout-sdk/credit-card-integration": [ "packages/credit-card-integration/src/index.ts" @@ -78,7 +77,7 @@ "packages/google-pay-integration/src/index.ts" ], "@bigcommerce/checkout-sdk/hosted-form-v2": ["packages/hosted-form-v2/src/index.ts"], - "@bigcommerce/checkout-sdk/humm": ["packages/humm-integration/src/index.ts"], + "@bigcommerce/checkout-sdk/humm-integration": ["packages/humm-integration/src/index.ts"], "@bigcommerce/checkout-sdk/klarna-integration": [ "packages/klarna-integration/src/index.ts" ], diff --git a/webpack-common.config.js b/webpack-common.config.js index 84115367da..174fb74434 100644 --- a/webpack-common.config.js +++ b/webpack-common.config.js @@ -1,3 +1,4 @@ +const fs = require('fs'); const path = require('path'); const { DefinePlugin } = require('webpack'); @@ -28,9 +29,10 @@ const libraryEntries = { 'bundles', 'hosted-form-v2-iframe-host.ts', ), + ...getIntegrationEntries(), }; -async function getBaseConfig() { +async function getBaseConfig(_options, argv = {}) { return { stats: { errorDetails: true, @@ -60,16 +62,41 @@ async function getBaseConfig() { plugins: [ new DefinePlugin({ LIBRARY_VERSION: JSON.stringify(await getNextVersion()), + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || argv.mode || 'production', + ), + 'process.env.ESSENTIAL_BUILD': JSON.stringify( + process.env.ESSENTIAL_BUILD || argv.essentialBuild || false, + ), }), ], }; } +function getIntegrationEntries() { + const integrationsPath = path.join(coreSrcPath, 'generated', 'integrations'); + const integrationFolders = {}; + + fs.readdirSync(integrationsPath) + .filter((file) => { + return fs.statSync(path.join(integrationsPath, file)).isDirectory(); + }) + .forEach((folder) => { + integrationFolders[`integrations/${folder}`] = path.join( + integrationsPath, + folder, + 'index.ts', + ); + }); + + return integrationFolders; +} + const babelEnvPreset = [ '@babel/preset-env', { corejs: 3, - targets: ['defaults', 'ie 11'], + targets: ['defaults'], useBuiltIns: 'usage', }, ]; diff --git a/webpack.config.js b/webpack.config.js index f2db63a8b0..5f06029f7b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,32 +1,44 @@ const path = require('path'); -const { DefinePlugin } = require('webpack'); const nodeExternals = require('webpack-node-externals'); const { babelLoaderRules, getBaseConfig, libraryEntries, - libraryName, + coreSrcPath, } = require('./webpack-common.config'); const outputPath = path.join(__dirname, 'dist'); -async function getUmdConfig(options, argv) { +async function getEsmConfig(options, argv) { const baseConfig = await getBaseConfig(options, argv); return { ...baseConfig, - name: 'umd', + name: 'esm', entry: libraryEntries, + externals: [ + nodeExternals({ + importType: 'module', + }), + ], + externalsPresets: { + node: true, + }, output: { - filename: '[name].umd.js', - library: libraryName, - libraryTarget: 'umd', - path: outputPath, + filename: '[name].js', + path: `${outputPath}/esm`, + library: { + type: 'module', + }, + environment: { + module: true, + }, }, - module: { - rules: [...babelLoaderRules, ...baseConfig.module.rules], + experiments: { + outputModule: true, }, + target: ['web', 'es6'], }; } @@ -37,23 +49,78 @@ async function getCjsConfig(options, argv) { ...baseConfig, name: 'cjs', entry: libraryEntries, - externals: [nodeExternals()], output: { filename: '[name].js', libraryTarget: 'commonjs2', - path: outputPath, + path: `${outputPath}/cjs`, + }, + module: { + rules: [...babelLoaderRules, ...baseConfig.module.rules], }, - plugins: [ - ...baseConfig.plugins, - new DefinePlugin({ - 'process.env.NODE_ENV': 'process.env.NODE_ENV', + }; +} + +async function getEssentialBuildEsmConfig(options, argv) { + const baseConfig = await getBaseConfig(options, { ...argv, essentialBuild: true }); + + return { + ...baseConfig, + name: 'esm-essential', + entry: { + 'checkout-sdk-essential': path.join(coreSrcPath, 'bundles', 'checkout-sdk.ts'), + }, + externals: [ + nodeExternals({ + importType: 'module', }), ], + externalsPresets: { + node: true, + }, + output: { + filename: '[name].js', + path: `${outputPath}/esm`, + library: { + type: 'module', + }, + environment: { + module: true, + }, + }, + experiments: { + outputModule: true, + }, + target: ['web', 'es6'], + }; +} + +async function getEssentialBuildCjsConfig(options, argv) { + const baseConfig = await getBaseConfig(options, { ...argv, essentialBuild: true }); + + return { + ...baseConfig, + name: 'cjs-essential', + entry: { + 'checkout-sdk-essential': path.join(coreSrcPath, 'bundles', 'checkout-sdk.ts'), + }, + output: { + filename: '[name].js', + libraryTarget: 'commonjs2', + path: `${outputPath}/cjs`, + }, + module: { + rules: [...babelLoaderRules, ...baseConfig.module.rules], + }, }; } async function getConfigs(options, argv) { - return [await getCjsConfig(options, argv), await getUmdConfig(options, argv)]; + return [ + await getEsmConfig(options, argv), + await getEssentialBuildEsmConfig(options, argv), + await getCjsConfig(options, argv), + await getEssentialBuildCjsConfig(options, argv), + ]; } module.exports = getConfigs;