Skip to main content

Upgrade to Ralph UI v22.0.0

Information about version 21.0.0

Shortly after the release of v21.0.0, a memory leak was discovered. We strongly recommend that you skip that version and upgrade straight to v22.0.0 instead, where this leak has been fixed. This guide will cover upgrading from v20.x.x straight to v22.0.0.

See release notes for v21.0.0

Why you should upgrade

Ralph Storefront now supports Node v16.x.x (since v21.0.0) and this version (v22.0.0) has a lot of performance improvements and bug fixes. For example, no more Apollo smart queries are used for fetching anything from the api.

Also, a lot of bloating has been removed from the package and the name of the package has been changed to @geins/ralph-ui and is now available on npm: @geins/ralph-ui.

See release notes for more information

NOTE

This is a HUGE update with a lot of required changes. Make sure to set aside enough time to go through the changes and test your storefront thoroughly after the upgrade.

Pre-requisites

You will need to install and use Node v16.x.x, we recommend running it on Node v16.20.0

Before upgrading your to Ralph UI v22.0.0 in your storefront, make sure you have already upgraded to v20.x.x. See guide here

Options for upgrading

Since v21.0.0 of Ralph UI now runs on Node v16.x.x, you will need to update your storefront to Ralph Storefront v2.2.0, to ensure compatibility.

There are two ways to upgrade to Ralph UI v22.0.0, either merge the changes from the Ralph Storefront repository into your project, or manually upgrade your project. Normally it would be recommended to merge the changes, but since this update also includes new linting, it might be easier to manually upgrade your project.

Here follows a guide for both options.

1. Update to Ralph Storefront v2.2.0 through merging

This way of updating ensures you get all important updates. It can sometimes be a bit time consuming dealing with conflicts, but this is usually how we do it.

Start by setting up Ralph Storefront as a remote in git and then merge the changes from the Ralph Storefront repository into your project.

Merge from remote

Firstly, make sure you have a GitHub personal access token. You can create one here.

Run the following commands in your Ralph Storefront project:

git remote add ralph https://${yourPersonalToken}@github.com/geins-io/ralph-storefront.git
git fetch ralph
git merge ralph/master

Dealing with merge conflicts

You will surely encounter merge conflicts when merging the changes from the Ralph Storefront repository into your project. Methodically go through each file with conflicts and resolve them.

Also, look through the auto merged ones. A lot of components and files have been removed (and in many cases moved to Ralph UI), so you will need to make sure that you are not removing anything that you are using or that you made custom changes to in your project.

When you are done, you can commit the changes and push them to your repository.

2. Manual upgrade

If the merge conflicts are too many or too complex, you can manually upgrade your project to Ralph Storefront v2.2.0. This section will try to guide you through the process.

Update dependencies

In package.json, the list of dependencies should be:

"dependencies": {
"@apollo/client": "^3.8.1",
"@geins/ralph-ui": "^22.0.0",
"@nuxtjs/apollo": "4.0.1-rc.5",
"@nuxtjs/i18n": "^7.3.1",
"@nuxtjs/pwa": "^3.3.5",
"apollo-link": "^1.2.14",
"cookie": "^0.5.0",
"cookie-universal-nuxt": "^2.2.2",
"focus-visible": "^5.2.0",
"graphql": "^16.8.0",
"nuxt": "^2.17.2",
"nuxt-polyfill": "^1.0.3",
"nuxt-user-agent": "^1.2.2",
"vue-slide-up-down": "^2.0.1"
},
"devDependencies": {
"@babel/core": "^7.22.10",
"@babel/eslint-parser": "^7.22.10",
"@babel/runtime-corejs3": "^7.22.10",
"@nuxtjs/eslint-config": "^10.0.0",
"@nuxtjs/eslint-module": "^3.1.0",
"@nuxtjs/router": "^1.7.0",
"@nuxtjs/style-resources": "^1.2.1",
"@nuxtjs/stylelint-module": "^4.2.0",
"@vuese/cli": "^2.14.3",
"concurrently": "^8.2.1",
"core-js": "^3.32.0",
"cross-fetch": "^4.0.0",
"csv-parser": "^3.0.0",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-nuxt": "^3.1.0",
"eslint-plugin-prettier": "^5.0.0",
"file-save": "^0.2.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.0",
"postcss": "^8.1.9",
"postcss-html": "^1.5.0",
"postcss-import": "12.0.1",
"prettier": "^3.0.2",
"prompts": "^2.4.2",
"sass": "^1.49.9",
"sass-loader": "^10.4.1",
"stylelint": "^15.10.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-config-standard-vue": "^1.0.0",
"svg-inline-loader": "^0.8.2",
"uppercamelcase": "^3.0.0",
"zen-observable-ts": "^0.8.21"
},

Add and remove dependencies accordning to your needs. A lot of the dependencies in the list above are added with specific versions, even if they are just used by other dependencies. This is to ensure that the versions are compatible with each other and with Node v16.x.x. If you need to add or remove any dependencies, you can use the list above as referenece if something breaks.

tip

Before updating your dependencies, make sure you ar running on Node v16.x.x. To make sure to start from a clean slate, remove your node_modules folder and your package-lock.json file. Then clean your cache with npm cache clean --force and then run npm install to install the new dependencies.

Changing names

Some functions/variables/other has changed names.

  • In pages/product/_alias.vue, change the name of sizeChangeHandler to skuChangeHandler
  • In pages/list/*.vue, change the name of the middleware list-page-routing to ralph-list-page-routing
  • Search your project for @ralph/ralph-ui and replace it with @geins/ralph-ui
  • In the middleware folder, change the names of all three files to have ralph- in front of them, like so: ralph-authenticated.js, ralph-list-page-routing.js and ralph-default.js, also change the name of the import in these files to the same as the file name.
  • In CaCheckout, change CaUdc ref="udc" to CaNshift ref="nshift" and change CaCheckoutCarismar ... ref="checkoutCarismar" to CaCheckoutInvoice ...ref="checkoutInvoice"
  • Search your project for eventbus and replace it with this.$ralphBus. Also remove all imports of eventbus in components.

Replace functionality with mixins

In some cases, it's recommended to replace all functionality with mixins, to be able to receive new updates from Ralph UI.

  • In pages/content/_preview.vue, replace all functionality with MixWidgetPreview
  • In pages/content/_alias.vue, replace all functionality with MixContentPage
  • In pages/account/settings.vue, replace all functionality with MixAccountSettings
  • In pages/account/reset-password.vue, replace all functionality with MixResetPassword
  • In pages/account/order.vue, replace all functionality with MixAccountOrders
  • In pages/account/index.vue, replace all functionality with MixAccountIndex
  • In pages/account/balance.vue, replace all functionality with MixAccountBalance

Language keys

  • Change PICK_SIZE to PICK_SKU in your language files
  • Add PICK_VARIANT to your language files (if you want to use it)
  • Add FEEDBACK_ORGANIZATION_ID_NOT_VALID if you're using CaCheckoutInvoice

Add files

Add a folder named config to your root and these files to it:

config/route-paths.js
export const routePaths = {
category: '/c',
brand: '/b',
product: '/p',
search: '/s',
discountCampaign: '/dc',
list: '/l',
};
config/market-settings.js
export function getMarketSettings() {
// Setting the domain settings and market settings based on if env-variable DOMAINS exists
// Default settings for multi market / multi language
let domainSettings = {
differentDomains: false,
strategy: 'prefix',
};
let domainUrls = null;

// Default settings for market for publicRuntimeConfig
let marketSettings = {
isMultiLanguage: true,
marketInPath: true,
};

if (process.env.DOMAINS) {
const domains = process.env.DOMAINS.split(',');

domainUrls = domains
?.map((domain) => {
const domainArr = domain?.split('|');
return {
[domainArr[0]]: domainArr[1] || '',
};
})
.reduce((result, item) => {
const key = Object.keys(item)[0];
result[key] = item[key];
return result;
}, {});

// If using DOMAINS, turn off multilang and marketInPath
marketSettings = {
isMultiLanguage: false,
marketInPath: false,
};

// If site should have only language prefix and no market prefix, remove the following declaration
domainSettings = {
differentDomains: false,
strategy: 'prefix_except_default',
};

if (domains.length > 1) {
// If more than one domain, set diffrentDomains to true
domainSettings = {
differentDomains: true,
strategy: 'no_prefix',
};
}
}
return {
domainSettings,
domainUrls,
marketSettings,
};
}
config/image-sizes.js
import fs from 'fs';
import csv from 'csv-parser';

const imageSizesFile = './config/ImageSize.csv';

export async function getImageSizes() {
const imageSizesStream = fs.createReadStream(imageSizesFile);
const imageSizeObject = {};

const imageSizesStreamRead = new Promise((resolve) => {
imageSizesStream
.pipe(csv())
.on('data', (row) => {
// Get the value from the PartitionKey (Image type) Column and make it lowercase (Because of input inconsistencies in the source document)
const PartitionKey = row.PartitionKey.toLowerCase();
// Create the data for this row
const imageRow = {
folder: row.Folder,
descriptor: row.Width + 'w',
};
// Check if the imagesizeobject has the current image tyoe already, if so add to it, otherwise create it
if (imageSizeObject[PartitionKey]) {
imageSizeObject[PartitionKey].push(imageRow);
} else {
imageSizeObject[PartitionKey] = [imageRow];
}
})
.on('end', () => {
resolve(imageSizeObject);
});
});
return await imageSizesStreamRead;
}
config/fallback-data.js
import { gql } from 'graphql-tag';
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core';
import fetch from 'cross-fetch';

const apolloClient = new ApolloClient({
cache: new InMemoryCache({}),
link: new HttpLink({ uri: process.env.API_ENDPOINT, fetch }),
});

const defaultMetaQuery = gql`
query listPageInfo(
$channelId: String
$languageId: String
$marketId: String
) {
listPageInfo(
channelId: $channelId
languageId: $languageId
marketId: $marketId
) {
meta {
description
title
}
}
}
`;

const getMarketsQuery = gql`
query channel($channelId: String) {
channel(channelId: $channelId) {
defaultMarketId
markets {
id
defaultLanguageId
alias
virtual
onlyDisplayInCheckout
groupKey
allowedLanguages {
id
name
code
}
country {
name
code
}
currency {
name
code
}
}
}
}
`;

const fallbackChannelId = process.env.FALLBACK_CHANNEL_ID;
const fallbackMarketAlias = process.env.FALLBACK_MARKET_ALIAS;
const defaultLocale = process.env.DEFAULT_LOCALE;

export async function getFallbackMeta() {
const getMetaQuery = await apolloClient.query({
query: defaultMetaQuery,
variables: {
channelId: fallbackChannelId,
marketId: fallbackMarketAlias,
languageId: defaultLocale,
},
context: {
headers: {
'X-ApiKey': process.env.API_KEY,
},
},
});

return await getMetaQuery.data.listPageInfo.meta;
}

export async function getFallbackMarkets() {
const getMarkets = await apolloClient.query({
query: getMarketsQuery,
variables: {
channelId: fallbackChannelId,
},
context: {
headers: {
'X-ApiKey': process.env.API_KEY,
},
},
});
return await getMarkets.data.channel.markets;
}
config/channel-settings.js
export const channelSettings = [
{
channelId: '1|se',
siteName: 'Ralph Storefront', // Change this name to your sites name
// All theme settings will be converted into css variables and added to the root html element
theme: {
'accent-color': '#363636', // Change to your accent color
// Add more variables here if you want to get them converted into css variables
},
},
];

export const currentChannelSettings =
channelSettings.find(
(i) => i.channelId === process.env.FALLBACK_CHANNEL_ID,
) || channelSettings[0];

Also, add the config folder to your Dockerfile file like this (last line):

...
ARG NODE_ENV
ENV NODE_ENV=${NODE_ENV}
RUN npm run build \
&& node-prune \
&& mkdir output \
&& cp -r package.json nuxt.config.js node_modules .nuxt static scripts config output
...

Update your nuxt.config.js file

A lot of bloating of nuxt.config.js has been moved to external config files. Remove all code above the export default async() => { and replace it with this:

nuxt.config.js
import path from 'path';
import { getImageSizes } from './config/image-sizes';
import { getFallbackMarkets, getFallbackMeta } from './config/fallback-data';
import { getMarketSettings } from './config/market-settings';
import {
channelSettings,
currentChannelSettings,
} from './config/channel-settings';
import { routePaths } from './config/route-paths';
import DirectoryNamedWebpackPlugin from './config/directory-named-webpack-resolve';

export default async() => {
...

Then after the export default async() => { add this:

nuxt.config.js
  ...
export default async() => {
const ralphEnv = process.env.RALPH_ENV || 'prod';
const imageSizes = await getImageSizes();
const fallbackMarkets = await getFallbackMarkets();
const fallbackMeta = await getFallbackMeta();
const { domainSettings, domainUrls, marketSettings } = getMarketSettings();
return {
...
};
};

Then change the rest of the file to work with this setup. For exampel defaultMeta has changed namne to fallbackMeta.

Plugins

The plugins-section should be:

nuxt.config.js
  ...
plugins: [
{
src: '~/node_modules/@geins/ralph-ui/plugins/ralph.js',
},
{
src: '~/node_modules/@geins/ralph-ui/plugins/set-css-variables.js',
mode: 'client',
},
{
src: '~/node_modules/@geins/ralph-ui/plugins/broadcast-channel.js',
mode: 'client',
},
{
src: '~/node_modules/@geins/ralph-ui/plugins/headers-control.js',
mode: 'server',
},
],
...

Modules

To the modules and buildModules, add:

nuxt.config.js
  ...
modules: [
...
// Doc: https://github.com/nuxt-community/style-resources-module
'@nuxtjs/style-resources',
// Doc: https://www.npmjs.com/package/nuxt-polyfill
'nuxt-polyfill',
...
],
...
buildModules: [
...

[
// Internal build module
'~/node_modules/@geins/ralph-ui/modules/ralph-build',
...
],
...
],
...

Config

Change router middleware from default to ralph-default.

nuxt.config.js
  ...
router: {
middleware: ['ralph-default'],
},
...

Add/replace the following variables in the publicRuntimeConfig:

nuxt.config.js
  ...
publicRuntimeConfig: {
...
fallbackChannelId: process.env.FALLBACK_CHANNEL_ID,
fallbackMarketAlias: process.env.FALLBACK_MARKET_ALIAS,
currentChannelSettings,
channelSettings,
fallbackMarkets,
imageSizes,
...marketSettings,
...
},
...

Also add statesToPersist and choose what modulles/states in your store that you want to be persisted. The list below is the default list, but you can add or remove states as you like, although its strongly recommended to keep the list/relocateAlias and list/relocatePage states.

nuxt.config.js
  ...
publicRuntimeConfig: {
...
statesToPersist: [
'favorites',
'customerType',
'vatIncluded',
'list/relocateAlias',
'list/relocatePage',
],
...
},
...

We also introduced a variable called ralphLog from which you can set what to log to the console in development mode, Add this to your publicRuntimeConfig:

nuxt.config.js
  ...
publicRuntimeConfig: {
...
ralphLog: {
all: false,
api: false,
events: false,
checkout: false,
warnings: false,
},
...
},
...

Config files

Remove stylelint.config.js and add .stylelintrc with the following content (or the content you prefer):

{
"extends": ["stylelint-config-standard"],
"overrides": [
{
"files": ["*.scss", "**/*.scss"],
"extends": ["stylelint-config-standard-scss"]
},
{
"files": ["*.vue", "**/*.vue"],
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-standard-vue/scss"
]
}
],
"rules": {
"scss/operator-no-newline-after": null,
"no-empty-source": null,
"rule-empty-line-before": [
"always-multi-line",
{ "ignore": ["after-comment", "first-nested"] }
]
}
}

Add a folder named .husky to your root and add a file named pre-commit with the following content:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

Add a file called .npmrc to your root with the following content:

install-links=true
legacy-peer-deps=true

In package.json, add/replace the following and remove the husky section.

...
"scripts": {
...
"lint": "npm run lint:js && npm run lint:scss",
"lint:js": "eslint --fix --ext .js,.vue --ignore-path .gitignore .",
"lint:scss": "npx stylelint \"**/*.{css,scss,vue}\" --fix",
...
"ralph-create": "node ./scripts/ralph-create.cjs",
"ralph-ui-docs": "npm --prefix ./node_modules/@geins/ralph-ui/ run documentation-serve"
},
"lint-staged": {
"*.{js,vue}": "npm run lint:js",
"*.{css,scss,vue}": "npm run lint:scss"
},
...

Style changes

Since we are now running postcss isntead of node-sass, you will need to make a few changes to your style files. Also, we have decided to change the names of for example mixins to kebab-case instead of camelCase.

Change yout styles/helpers/_sizing.scss file to:

@use 'sass:math';

$rem-base: 16px;

@function strip-unit($num) {
@return math.div($num, ($num * 0) + 1);
}

@function rem-calc($value, $base-value: $rem-base) {
$value: math.div(strip-unit($value), strip-unit($base-value)) * 1rem;

@if $value == 0 {
$value: 0;
}

// Turn 0rem into 0

@return $value;
}

Change your styles/helpers/_layout.scss file to:

@mixin product-image-overlay {
&::after {
content: '';
display: block;
width: 100%;
height: 100%;
background: radial-gradient(
farthest-corner at center,
transparent 27%,
rgb(9 9 9 / 0%) 31%,
rgb(170 170 170 / 10%) 100%
);
position: absolute;
top: 0;
left: 0;
z-index: 1;
pointer-events: none;
}
}

@mixin icon-circle {
@include flex-calign;

background-color: $c-white;
width: 36px;
height: 36px;
transition: all 200ms ease;
border-radius: 50%;
box-shadow: 0 0 6px rgb(0 0 0 / 10%);
border: $border-light;
font-size: $font-size-m;

&:focus-visible {
box-shadow: 0 0 0 $focus-border;
}
}

@mixin scrollbar-style($color: $c-medium-gray, $hover: $c-darkest-gray) {
scrollbar-color: $color transparent;
scrollbar-width: thin;

&::-webkit-scrollbar {
background-color: transparent;
width: $px16 * 0.35;
height: $px16 * 0.35;
}
&::-webkit-scrollbar-track {
border-radius: 100vw;
}
&::-webkit-scrollbar-thumb {
background-color: $color;
border-radius: 100vw;
}
&::-webkit-scrollbar-thumb:hover {
background-color: $hover;
}
}
@mixin hide-scrollbar {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}

In styles/variables/_colors.scss, add @use 'sass:color'; at the top of the file and change the file to use the new syntax. For exampel, instead of lighten you can use color.adjust.

Other changes

  • Move the file static/ImageSize.csv to the config folder
  • In CaProductCard, change all NuxtLink to CaClickable and the click.native event handler to clicked
  • In Dockerfile, change node:14.15.5-alpine to node:16.20.0-alpine
  • In layouts/error.vue, change so that the loading/end is called on mounted instead of created
  • In CaListPage change $apollo.queries.products.loading passed to CaListPaginationto prevPageLoading and nextPageLoading
  • To fix an error with both internal and external confirm page showing, make this change to your pages/checkout/confirm.vue file:
confirm.vue
 <template>
...
<CaCheckoutHeader :title="$t('ORDER_CONFIRM_TITLE')" />
<CaCheckoutSection :loading="loading" :bottom-arrow="false">
<CaCheckoutExternal
v-if="$route.query.kid || $route.query.sid || $route.query.wid"
:type="type"
:data="confirmSnippet"
/>
<div v-else class="ca-checkout-confirm">
...
</template>
...
  • In pages/content/_alias.vue, change so that CaWidgetArea uses widgetData to use the data fetched on the server side in MixContentPage, like so:
_alias.vue
<template>
...
<CaWidgetArea :widget-data="widgetData" :alias="$route.params.alias" />
...
</template>
...
  • In pages/product/_alias.vue, make sure to only use one mixin: MixProductPage and remove the others, like so:
_alias.vue
...
export default {
...
mixins: [MixProductPage],
...
};
...