Skip to main content
Version: 3.x

📑 Manual Migration

This manual migration process should be run after the automated migration process, to complete the missing parts, or debug issues in the migration CLI output.

Transitioning to ESM

Starting 3.0, in a effort to modernize the codebase, we transitioned to ESM. This migration allows a better alignment with browsers standards, an improved static analysis, enhanced language features and overall a better consistency in the JavaScript ecosystem. To learn more about it, see:

Updates in your codebase

Updating exports

To cope with this transition, you should check that your code does not use module.exports = or module.exports.someNamedExport = syntax, and replace it with export default or export {} otherwise.

info

On linux-based systems, you can use the grep command on your project to find those occurences:

grep "module.exports" src/ -r

Uses of __dirname

__dirname isn't available anymore in ESM. To cope with this change, you will need to replace its usage. For example:

import { dirname } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));

For more information about alternatives to __dirname, see:

Updates in your dependencies

You may also have issues with some of your project dependencies that are not ESM-compatible.

First of all, we recommend that you check if the dependencies you are using have a newer version that is ESM-compatible. If so, you can update them to the latest version. If it is not the case, we encourage you to reconsider their usage in your project, and remove them if possible or replace them with an ESM-compatible alternative. It also is a sign of a library that is not actively maintained. You can also contribute to the library by opening an issue or a pull request to make it ESM-compatible, of course!

As a last resort, you can adapt your Vite configuration (vite.config.ts) to handle those cases by tweaking the optimization settings. See the Vite documentation for more information. More particularly these pages:

Components

SEO Components

The xxxSeo components (eg: HomeSeo) have been removed in favor of Remix Meta.

If you use SEO components or have overriden Seo components, you need to:

  • Remove import and usage of the component from your page
  • Use the meta function with our generateMetas helper to inject your custom metadatas in your Route Module.

Refactor pages to Remix routes

Page Enhancer HOC usually were responsible for data loading in 2.x. As we now embrace Remix Server/Client Model, page enhancers must be removed in favor of Remix route loaders.

You can see an example in the category route file on how the data loading is now used with the loader method.

You need to pay attention to two thing if you've overriden this file :

  • Did you update the query to fetch category?

We now recommend you to update the GraphQL query used in category route and pass down any new props to the Category component.

  • Did you added more HOC to the Enhancer?

You can either add your custom HOC to the Category component or replace HOC with hooks if relevant.

Product page

In v3, the base theme got completely rid of any HOC, by leveraging on standard mechanics from Remix loaders and hooks:

  • withTrackPage and withProductTracking responsibilities have been transfered to useTrackPage and useProductPage hooks
  • Loading state management is done directly at route loader level

Account pages

Similarly, page-base HOCs on the Account page have been completely removed, and replaced with standard Remix and React mechanics. Here's a list of all removed Enhancers in those pages:

withTrackOnMount HOC depreciation

withTrackOnMount has been deprecated in favor of a new hook useTrackOnMount. The new hook has a slightly different signature:

Old propNew propProp type
isResolvedFromPropshasResolvedfunctionboolean
mapPropsToPropertiescreatePropertiesfunction

shouldMountEvent, shouldUpdateEvent and trackEvent have been left unchanged.

// Before (HOC)
withTrackOnMount({
event: "Product Viewed",
isResolvedFromProps: (props) => !props.loading && props.product,
mapPropsToProperties: (props) => {
const finalPrice = props.product?.prices?.finalPrice?.priceInclTax?.value;
return {
sku: props.product.sku,
name: props.product.name,
price: finalPrice?.amount,
currency: finalPrice?.currency,
};
},
});

// After (Hook)
useTrackOnMount({
event: "Product Viewed",
hasResolved: !props.loading && Boolean(props.product),
createProperties: () => {
const finalPrice = props.product?.prices?.finalPrice?.priceInclTax?.value;
return {
sku: props.product?.sku,
name: props.product?.name,
price: finalPrice?.amount,
currency: finalPrice?.currency,
};
},
});

withProductTracking replaced with useProductTracking

Similarly, the page-based HOC withProductTracking has been completely removed, in favor of a new useProductTracking hook.

If you used this HOC in your code, you will have to replace it by calling useProductTracking in the component the HOC was previously used on. useProductTracking only takes the product as parameter.

withProductAddedPopIn removed

The HOC withProductAddedPopIn has been completely removed, in favor of using a local useState insied the Product component.

If you used this HOC in your code, you will have to replace it by using useState in the component the HOC was previously used on:

import { useState } from "react";
// ...

const MyComponent = () => {
const [productAdded, setProductAdded] = useState(null);

// ...

return (
<div>
<ProductAddedPopIn product={productAdded} hidePopin={() => setProductAdded(null)} />

<ProductView
onProductAdded={(product) => setProductAdded(product)}
{/* ... */}
/>
</div>
);
}

withCartTracking replaced with useCartTracking

The page-based HOC withCartTracking has been completely removed, in favor of a new useCartTracking hook.

If you used this HOC in your code, you will have to replace it by calling useCartTracking in the component the HOC was previously used on. useCartTracking only takes the cart as parameter.

withLogoutMutation has been removed

The page-based HOC withLogoutMutation has been completely removed, and has been replaced with a /logout route.

If you used this HOC in your code, you can remove it and use a link to /logout instead. See skeleton/app/routes/logout.tsx and theme/pages/Account/Logout/Logout.jsx for examples.

makeCommandDispatcherWithCommandFeedback API change

makeCommandDispatcherWithCommandFeedback API has changed. In version 2, the second parameter of makeCommandDispatcherWithCommandFeedback was an optional boolean (default true) and if this parameter was evaluated to a truthy value, the resulting component had the responsibility of displaying the result of the command with the InlineAlert component. In version 3, makeCommandDispatcherWithCommandFeedback now expects an optional React component to display the result of the command.

To cope with this change, you need to review all components using makeCommandDispatcherWithCommandFeedback:

  • if makeCommandDispatcherWithCommandFeedback is called with a falsy second parameter, you don't have to change anything
  • if makeCommandDispatcherWithCommandFeedback is called with a truthy second parameter or without the second parameter, you have to adapt your code. To have exactly the same behavior as before, the new call should look like:
    makeCommandDispatcherWithCommandFeedback(
    {
    /* commands definition */
    },
    ({ success, message }) => {
    return (
    <InlineAlert appearance={success ? "success" : "error"}>
    {message}
    </InlineAlert>
    );
    }
    );

withEntityNotFound API change

withEntityNotFound API has changed. In version 2, the parameter NotFoundComponent was optional (defaulted to NotFound component). This HOC is now deprecated and is now in the theme-agnostic @front-commerce/compat package. If you still want to use withEntityNotFound, you must now provide a NotFoundComponent component parameter.

To cope with this change, you need to review all component using withEntityNotFound to make sure a NotFoundComponent parameter is passed to it. For example, by using the component it was previously defaulted to:

import NotFound "theme/modules/PageError/NotFound/NotFound"

/*
... your component
*/

export default withEntityNotFound({
isFound: () => false,
NotFoundComponent: NotFound,
})(MyComponent);

A even better way would be to get rid of withEntityNotFound, and instead handle this mechanism directly withing the related loader by throwing a 404 response there. For more information about this practice, see Remix's guide on handling 404s.

PartialProductQuery removal

In version 3, PartialProductQuery has been completely removed as it is not used by the base theme anymore. If it was used in your project, you will have to replace it with your own query or fragment.

For reference, the query was:

#import "theme/modules/ProductView/ProductItem/ProductItemFragment.gql"

query PartialProductQuery($sku: String!) {
product(sku: $sku) {
...ProductItemFragment
}
}

Sass Migration

In the latest version of Front-Commerce we converted all global Sass variables to CSS variables during the automated migration. This allows for a more modular approach to theming and a better developer experience. However, there are still some manual steps that need to be taken to complete the migration.

Breakpoints Removal

The following scss helpers have been removed. If you were using them, you have to manually add them back to your project or update implementation

To replace usage of these scss methods

Instead of relying on the removed Sass mixins, we recommend to now use a standardized approach that involves directly using media queries in your CSS code. This approach provides better control, readability, and maintenance of responsive styles.

Here are the breakpoints values we used:

  • xs: 0
  • sm: 576px
  • md: 768px
  • lg: 992px
  • xl: 1200px

For each breakpoint where you were previously using the breakpoint-min function, replace it with a media query using the min-width property. For example:

Before:

$min: breakpoint-min("sm");
@media (min-width: $min) {
// Styles for 'sm' and wider
}

After:

@media (min-width: 576px) {
// Styles for 'sm' and wider
}

For cases where you were using the media-breakpoint-up, you can use a media query with min-width too. For example:

Before:

@include media-breakpoint-up("md") {
// Styles for 'md' and wider
}

After:

@media (min-width: 768px) {
// Styles for 'sm' and wider
}

And you can remove @include media-breakpoint-up("xs") { as it applied CSS rules systematically.

To restore the usage of these scss methods

You can copy it from the v2.x code and add it to a breakpoints override or a custom file, then you should import it in any of your scss files which require either the variable, mixin or function, for example:

theme/components/atoms/Container/Container.scss
@import "theme/components/atoms/Breakpoints/breakpoints";

.container {
background-color: red;

@media screen and (min-width: $menuBreakpoint) {
max-width: 40em;
}

@include media-breakpoint-up(sm) {
background-color: blue;
}

@include media-breakpoint-up(md) {
background-color: green;
}

@include media-breakpoint-up(lg) {
background-color: yellow;
}
}
info

After running the codemods on your project, you can find these deprecations by searching TODO (Codemod generated) in your project.

Media queries

If you use any media queries which use global sass variables, you need to manually import the file defining the variables.

If you have the following example component, You will need to import the breakpoints.scss file.

theme/components/atoms/Form/Fieldset/Fieldset.scss
@import "theme/components/atoms/Breakpoints/breakpoints";

fieldset {
border: none;
font-size: var(--regularSize);
.fieldset--large {
max-width: 30em;
@media screen and (min-width: $menuBreakpoint) {
max-width: 40em;
}
}
}

If you're using the $boxSizeMargin, $smallContainerWidth, or $containerWidth variables you should to import @import "theme/variables"; instead.

info

After running the codemods on your project, you can easily find these occurrences by searching TODO (Codemod generated) in your project.

Color functions

If you use any Sass color functions you will need to directly import the color.scss file at the location where it is used. This is because Sass can not process the functions on CSS variables.

If you have the following example component:

theme/components/atoms/Example/Example.scss
.Example {
color: darken(var(--colorPrimary), 10%);
}

You would need to import the color.scss directly in the Example.scss file

theme/components/atoms/Example/Example.scss
@import "theme/components/atoms/Colors/colors";

.Example {
color: darken(var(--colorPrimary), 10%);
color: darken($colorPrimary, 10%);
}

Calculations

The automated migration step takes care of changing SASS global variables to CSS custom properties and of transforming declarations value with a unique multiplication and the usage of math.div so that it becomes a valid CSS declaration:

theme/components/atoms/Example/Example.scss
.example {
padding: $boxSizeMargin;
padding: var(--boxSizeMagin);

margin-top: $boxSizeMargin * 2;
margin-top: calc(var(--boxSizeMagin) * 2);

margin-bottom: math.div($boxSizeMargin, 4);
margin-bottom: calc(var(--boxSizeMargin) / 4);
}

However for more complex calculations or for declarations with several values, only the usage of the SASS variable will be updated. After this step, a manual fix is needed. When starting Front-Commerce, sass is executed and it will report any invalid expressions. Here is a list of common pattern to fix:

theme/components/atoms/Example/Example.scss
.example {
padding: var(--boxSizeMargin) * 2 var(--boxSizeMargin) * 4 var(
--boxSizeMargin
) * 2 0;
padding: calc(var(--boxSizeMargin) * 2) calc(var(--boxSizeMargin) * 4) calc(
var(--boxSizeMargin) * 2
) 0;

max-width: calc(#{var(--boxSizeMargin) * 24} + 50ch);
max-width: calc(var(--boxSizeMargin) * 24 + 50ch);

margin: calc(-var(--boxSizeMargin) / 4) * 3;
margin: calc(-var(--boxSizeMargin) * 3 / 4);

padding-left: 1.5rem - calc(var(--boxSizeMargin) / 2);
padding-left: calc(1.5rem - calc(var(--boxSizeMargin) / 2));

padding-right: 1.5rem - calc(var(--boxSizeMargin) / 2);
padding-right: calc(1.5rem - calc(var(--boxSizeMargin) / 2));
}

@extend handling

If @extend is used with a selector defined in another stylesheet, you need to manually import that stylesheet. For example, sass would fail compiling the following stylesheet:

theme/components/atoms/Example/Example.scss
.example {
@extend .another-selector;
}

This is happening because .another-selector is unknown, so the solution is to import the stylesheet defining it:

theme/components/atoms/Example/Example.scss
@import "theme/some/path/defining/another-selector";

.example {
@extend .another-selector;
}

In V2, a modal state parameter facilitated rendering arbitrary routes in a modal during routing. In Remix, this is non-trivial unless the target route is a child route. We have introduced a ModalLink component to replace the previous mechanism. It generates a link that displays a component in a modal upon user click, with the modal state reflected in the URL hash for sharing and consistent UI presentation across users.

Here is an example of the required changes:

theme/components/organisms/Header/Header.js
import { ModalLink } from "theme/components/organisms/Modal";
import Login from "theme/components/organisms/Login";

export function Header() {
return (
<header>
<Link to={{ pathname: "/whatever", state: { modal: true } }}>Login</Link>
<ModalLink to="/login" linkContent="Login">
<Login />
</ModalLink>
</header>
);
}

Environment variables

Front-Commerce

In V3, the FRONT_COMMERCE_DISABLE_OFFLINE_MODE environment variable has been removed completely.

Renamed variables:

V2V3
FRONT_COMMERCE_FACEBOOK_CLIENT_IDFRONT_COMMERCE_LOGIN_FACEBOOK_CLIENT_ID
FRONT_COMMERCE_FACEBOOK_CLIENT_SECRETFRONT_COMMERCE_LOGIN_FACEBOOK_CLIENT_SECRET
FRONT_COMMERCE_GOOGLE_CLIENT_IDFRONT_COMMERCE_LOGIN_GOOGLE_CLIENT_ID
FRONT_COMMERCE_GOOGLE_CLIENT_SECRETFRONT_COMMERCE_LOGIN_GOOGLE_CLIENT_SECRET
FRONT_COMMERCE_COOKIE_MAX_AGE_IN_MSFRONT_COMMERCE_COOKIE_MAX_AGE_IN_SECONDS
caution

FRONT_COMMERCE_COOKIE_MAX_AGE_IN_SECONDS is now a duration in seconds and not in milliseconds as it was in v2.

Magento

In V2, Magento1 and Magento2 environment variables were prefixed the same way. FRONT_COMMERCE_MAGENTO_*, in V3, we have split them into FRONT_COMMERCE_MAGENTO1_* and FRONT_COMMERCE_MAGENTO2_*.

- FRONT_COMMERCE_MAGENTO_CONSUMER_KEY=token
- FRONT_COMMERCE_MAGENTO_CONSUMER_SECRET=token
- FRONT_COMMERCE_MAGENTO_ACCESS_TOKEN=token
- FRONT_COMMERCE_MAGENTO_ACCESS_TOKEN_SECRET=token

## Magento 1
+ FRONT_COMMERCE_MAGENTO1_CONSUMER_KEY=token
+ FRONT_COMMERCE_MAGENTO1_CONSUMER_SECRET=token
+ FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN=token
+ FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN_SECRET=token

## Magento 2
+ FRONT_COMMERCE_MAGENTO2_CONSUMER_KEY=token
+ FRONT_COMMERCE_MAGENTO2_CONSUMER_SECRET=token
+ FRONT_COMMERCE_MAGENTO2_ACCESS_TOKEN=token
+ FRONT_COMMERCE_MAGENTO2_ACCESS_TOKEN_SECRET=token

Other optional variables updated:

2.x3.x - Magento13.x - Magento2
FRONT_COMMERCE_MAGENTO_ADMIN_PATHFRONT_COMMERCE_MAGENTO1_ADMIN_PATHFRONT_COMMERCE_MAGENTO2_ADMIN_PATH
FRONT_COMMERCE_MAGENTO_TIMEOUTFRONT_COMMERCE_MAGENTO1_TIMEOUTFRONT_COMMERCE_MAGENTO2_TIMEOUT
FRONT_COMMERCE_MAGENTO_ADMIN_TIMEOUTFRONT_COMMERCE_MAGENTO1_ADMIN_TIMEOUTFRONT_COMMERCE_MAGENTO2_ADMIN_TIMEOUT
FRONT_COMMERCE_XRAY_MAGENTO_VERSIONFRONT_COMMERCE_XRAY_MAGENTO1_VERSION❌ N/A

makeImageProxyRoutercreateResizedImageResponse

The makeImageProxyRouter function has been removed in favor of the new createResizedImageResponse function. You can read more about the implementation details in the createResizedImageResponse documentation.

All images which are located under public/images/resized/* should now be moved to public/images/*. This will allow for the /images/resized/* resource route to be reserved for the new createResizedImageResponse function.

Removal of SmartLayout component

In v3 we removed the SmartLayout component. This component was used to enhance the Layout component with navigationMenu for the header and footer.

The Layout component now requires two props headerNavigationMenu and footerNavigationMenu to be passed to it.

This is can be retrieved via a loader: see example in _main.tsx layout

app/routes/_main.tsx
export const loader = async ({ context }: LoaderFunctionArgs) => {
const app = new FrontCommerceApp(context.frontCommerce);
const headerResponse = await app.graphql.query(HeaderQueryDocument, {
name: "data",
});

const footerResponse = await app.graphql.query(NavigationMenuDocument, {
name: "categories",
});

return json({
headerNavigationMenu: headerResponse?.navigationMenu || [],
footerNavigationMenu: footerResponse?.navigationMenu || [],
});
};

react-helmet-async removal

In version 3, react-helmet-async has been removed as it does not make sense in a remix application. If you used this in any custom components or pages, you will now need to move it the meta function of the route.

Client side log handler removal

The client side log mechanism has been removed from Front-Commerce 3.0. For future version, we plan to bring back this feature. In the meantime, if you are using this feature or you have overridden a component using this feature, you need to remove it. In version 2.x, the API was imported from web/core/logs/logHandler.

@loadable/component removal

@loadable/component is no longer a dependency of Front-Commerce. That means components that were previously made loadable are now loaded synchronously as any other component. In terms of usage, in most cases this change should be transparent. That's not the case if you are manually preloading a component with Component.preload() or if you have overridden a file that is doing it. In those case, you have to remove .preload() calls.

In addition, you can still use @loadable/component in your custom components and then you have to make sure @loadable/component is a dependency of your project.

CartSeo removal

In version 3, CartSeo has been removed as it is not used by the base theme anymore. If you overrode this component or one using it, you will need to move any of the customisation to the cart route meta function.

EmptyCart refactor

The component EmptyCart has been refactored and its HOC EnhanceEmptyCart has been removed to benefit from data served directly from the cart route loader. It is now rendered directly as a page in _main.cart.tsx instead of rendered as a component in Cart. If you have overridden this component in you project, you will need to ensure the changes are compatible with the new version.

See related MR (!2525) for more informations.

Removed MenuNavigation component

The component MenuNavigation was only used in the default theme of Front-Commerce, and was subsequently removed in version 3.0 (see related MR)

react-apollo usage removed from theme-chocolatine

In an effort to remove react-apollo dependency in theme chocolatine, we removed all occurences of graphql(), useQuery() and useLazyQuery() calls in it, and replace them by either direct API calls using remix's useFetcher, or move them to the related route loader when possible. This means a good number of Enhancers have also been reworked, and sometimes removed completely. Here's the list of the Enhancers that have been removed:

  • EnhanceEmptyCart (see MR): EmptyCart component is now used as a page rendered directly under /cart route
  • EnhanceCart (see MR) was moved to the /cart route loader
  • withCountries (see MR): replaced with useCountries hook
  • EnhanceChoosePayment (see MR): replaced with usePaymentMethods hook
  • EnhanceHeader (see MR): replaced with useCustomer and useCart
  • checkCart (see MR): refactor
  • withCartId (see MR): replaced with withCart
  • EnhanceMiniCartContent (see MR): replaced with useCart
  • EnhanceCartRecap (see MR): replaced with useCart
  • withCheckoutSuccessTracking (see MR): replaced withuseCheckoutSuccessTracking
  • GscText 's HOC (see MR): replaced with useStore
  • EnhanceProductReviews (see MR): replaced with useProductReviews
  • withIsSubscribedToInStockAlert (see MR): replaced with useIsSubscribedToStockAlert
  • EnhanceSearchBar (see MR): moved logic to useSearchBar

Additionally, some Enhancers and HOCs were modified for the same purpose. If you those were overriden in your project, you will have to check what change and adapt it in your code:

  • EnhanceChooseShippingMethod (see MR)
  • EnhanceAddToCart (see MR)
  • EnhanceAddress (see MR)
  • withCheckoutTracking (see MR)
  • EnhanceProductGallery (see MR)
  • EnhancePublishProductreview and EnhanceLoginForm (see MR)
  • EnhanceSubscribeToInStockAlert (see MR)
  • EnhanceCurrencySelector (see MR)
  • EnhanceAddToCartModal (see MR)

useDevice() removal

We removed the useDevice() hook in version 3.0. It was unused in the base theme, and we had no use cases in mind for it.

Device information are still available in the configuration. Access it using config.device. Please contact us if you have a use case we don't support anymore.

Sitemap Changes

Please refer to the the Customize the Sitemap documentation for more details on how to implement dynamic sitemaps in v3.

Sitemap URL change

In v3, the sitemap URL has been changed from /sitemaps/sitemap.xml to /sitemap.xml.

SitemapLoader

GraphQL Schema

  • The Sitemapable GraphQL interface has been deprecated for removal. It can be removed from your project.
  • The SitemapImage GraphQL type has been deprecated for removal. It can be removed from your project.

Deprecation of PageLoader

The page loader has been deprecated in favor of the DynamicRoutes service.

The Routable types in the schema will eventually be deprecated, as UrlMatchers no longer use GraphQL as they are decoupled into a service.

This also means that the route query will be deprecated, if you still require this functionality you can implement it using the DynamicRoutes service:

import { createGraphQLRuntime } from "@front-commerce/core/graphql";

export default createGraphQLRuntime({
resolvers: {
Query: {
route: async (_, { url }, { services, loaders }) => {
const route = await services.DynamicRoutes.matchUrl(url);
if (!route) {
return null;
}

if (route.type === "category") {
return loaders.Category.load(route.params.id);
} else if (route.type === "product") {
return loaders.Product.load(route.params.id);
}

//...
},
},
},
});