Skip to content

Extending

Storefront X enables you to extend almost every file with some new add-on. Extensions are files with .ext.* suffix. They are special because they don't override each other. If two modules have the same extensions with the same names, both of the extensions will be applied.

You have to meet a few conditions before extending is possible:

  • Before creating extension, original file has to exist
  • Extensions must be placed in the same concept directory as the original file
  • Extensions must have the same name as the file they extend (except the .ext.* suffix)
  • Extension files must have .ext.* suffix at the end of their filename

Example of creating extension file

module-a/mappers/ToProduct.ts is extended by module-b/mappers/ToProduct.ext.ts, module-a/graphql/fragments/Product.ts is extended by module-b/graphql/fragments/Product.ext.ts.

WARNING

Native support for extensions is enabled for files generated by IocConcept (all files imported from #ioc directory).

What are extensions used to?

As it was said at the beginning, extensions can be used to extend functionality of existing files. For example, we have a mapper in our common-module, and we want to add an extra key mapping in our custom-module, extension is ideal solution to our problem.

Example of using extensions in mappers concept

Let's imagine, we have a ToProduct.ts mapper (in a module-a), which is used to properly map back-end response to our key/value pairs.

ts
// module-a/mappers/ToProduct.ts

import ToProduct from '#ioc/mappers/ToProduct'
import ToMoney from '#ioc/mappers/ToMoney'

export default (data: any) => ({
  __typename: data.__typename ?? '',
  id: (data.id ?? 0) as number,
  sku: (data.sku ?? '') as string,
  name: (data.name ?? '') as string,
  finalPrice: ToMoney(data.price_range?.minimum_price?.final_price ?? {}),
  relatedProducts: (data.related_products ?? []).map(ToProduct),
  // ...
})
// module-a/mappers/ToProduct.ts

import ToProduct from '#ioc/mappers/ToProduct'
import ToMoney from '#ioc/mappers/ToMoney'

export default (data: any) => ({
  __typename: data.__typename ?? '',
  id: (data.id ?? 0) as number,
  sku: (data.sku ?? '') as string,
  name: (data.name ?? '') as string,
  finalPrice: ToMoney(data.price_range?.minimum_price?.final_price ?? {}),
  relatedProducts: (data.related_products ?? []).map(ToProduct),
  // ...
})

INFO

As we can see, the original file exports a function which takes one parameter (data to be mapped) and returns a mapped object.

We want to add to that key/value pairs new property with our module-b. So, we create the same concept (folder structure) and the same file (with .ext.ts suffix) in a new module.

ts
// module-b/mappers/ToProduct.ext.ts

import ToProductLabel from '#ioc/mappers/ToProductLabel'
import Extension from '#ioc/types/base/Extension'

interface Labels {
  labels: ReturnType<typeof ToProductLabel>[]
}

const ToProduct: Extension<Labels> = (ToProduct) => (data) => {
  const product = ToProduct(data)

  product.labels = data.product_labels?.items.map(ToProductLabel) ?? []

  return product
}

export default ToProduct
// module-b/mappers/ToProduct.ext.ts

import ToProductLabel from '#ioc/mappers/ToProductLabel'
import Extension from '#ioc/types/base/Extension'

interface Labels {
  labels: ReturnType<typeof ToProductLabel>[]
}

const ToProduct: Extension<Labels> = (ToProduct) => (data) => {
  const product = ToProduct(data)

  product.labels = data.product_labels?.items.map(ToProductLabel) ?? []

  return product
}

export default ToProduct

Extensions are simple functions with one input parameter: the thing they are extending. In this case it is the ToProduct mapper. Extensions need to return something that can be used same way as the thing they are extending. In this case, ToProduct mapper is a function that takes one argument (data) and returns object representing the product, this extension thus returns function with data input argument and mapped product as a result. Between that, we can do some stuff with the original data (eg. add labels property).

Example of using extensions in services concept

We have a useGetCartId.ts service inside a services concept. This service is a composable, so it returns a function inside the exported function. Its purpose is to retrieve current cart token and return it as an ID. You can see the example service bellow.

ts
// cart-magento/services/useGetCartId.ts

import useCartToken from '#ioc/composables/useCartToken'

export default () => {
  const cartToken = useCartToken()

  return async (): Promise<{
    id: string | null
  }> => {
    const id = cartToken.get()

    return {
      id,
    }
  }
}
// cart-magento/services/useGetCartId.ts

import useCartToken from '#ioc/composables/useCartToken'

export default () => {
  const cartToken = useCartToken()

  return async (): Promise<{
    id: string | null
  }> => {
    const id = cartToken.get()

    return {
      id,
    }
  }
}

We have another module customer-magento, where we want to return cart ID based on the condition of customer existence. That is perfect fit for extensions. So, we have to extend the existing service useGetCartId.ts. To do that, we create the same file (with .ext) in the same concept (services/useGetCartId.ext.ts).

ts
// customer-magento/services/useGetCartId.ext.ts

import useGetCustomerCartId from '#ioc/services/useGetCustomerCartId'
import useCustomerStore from '#ioc/stores/useCustomerStore'
import waitForStore from '#ioc/utils/vuePinia/waitForStore'

export default <T extends (...args: any[]) => any>(useGetCartId: T) => {
  return (): (() => Promise<any>) => {
    const getCartId = useGetCartId()
    const getCustomerCartId = useGetCustomerCartId()
    const customerStore = useCustomerStore()

    return async () => {
      return await waitForStore(
        customerStore,
        () => customerStore.customer !== undefined,
        async () => {
          if (customerStore.customer) {
            return getCustomerCartId()
          } else {
            return getCartId()
          }
        },
      )
    }
  }
}
// customer-magento/services/useGetCartId.ext.ts

import useGetCustomerCartId from '#ioc/services/useGetCustomerCartId'
import useCustomerStore from '#ioc/stores/useCustomerStore'
import waitForStore from '#ioc/utils/vuePinia/waitForStore'

export default <T extends (...args: any[]) => any>(useGetCartId: T) => {
  return (): (() => Promise<any>) => {
    const getCartId = useGetCartId()
    const getCustomerCartId = useGetCustomerCartId()
    const customerStore = useCustomerStore()

    return async () => {
      return await waitForStore(
        customerStore,
        () => customerStore.customer !== undefined,
        async () => {
          if (customerStore.customer) {
            return getCustomerCartId()
          } else {
            return getCartId()
          }
        },
      )
    }
  }
}

Extension is once again a function which receives original service as a parameter (useGetCartId) and returns a function (outer function with context as in use()). This outer context function then returns async function which calls the service. As you can see, we first call original service and then return something based on the condition.

And, that's it. Again, we can use extensions almost everywhere, in all concepts generated by IocConcept. Let's look, how it works in the background.

How it works in the background?

Storefront X will look during build if there are some extension files for an original file. If there are, it will go through all of them and generate one file. Inside this file all extensions and original file are imported. This file exports the latest extension (the latest module) as parameter it receives extension from previous module and so on. The final parameter is the original file.

ts
// generated file by SFX
// .sfx/ioc/mappers/ToProduct.ts

import self from '~/modules/catalog-magento/mappers/ToProduct'
import ext0 from '~/modules/catalog-labels-magento/mappers/ToProduct.ext'
import ext1 from '~/modules/catalog-attributes-magento/mappers/ToProduct.ext'

export default ext1(ext0(self))
// generated file by SFX
// .sfx/ioc/mappers/ToProduct.ts

import self from '~/modules/catalog-magento/mappers/ToProduct'
import ext0 from '~/modules/catalog-labels-magento/mappers/ToProduct.ext'
import ext1 from '~/modules/catalog-attributes-magento/mappers/ToProduct.ext'

export default ext1(ext0(self))