Upgrading Brightspot Styleguide

Note: This guide covers the upgrade of Brightspot Styleguide to version 4.2.22 or later.

Goals

  • Remove gulp in favor of webpack
  • Faster start-up
  • Handlebars rendering in browser

Latest Version

The latest version of styleguide can be found on NPM

Preparation

Before actually upgrading the packages, start preparing some of the items that need be changed. These items are things such as disabling non updated themes in orded to work through the themes one at a time, as well as updates to the various linting configurations in order to align with the newer practices, and even stuff like not including zip bundle in project.

Tell git to ignore Zip bundle in gitignore

In your project .gitignore add an entry to ignore *.zip

.gitignore

*.zip

build.gradle

In your project root directory’s build.gradle file:

Look for the line ext.brightspotGradlePluginVersion = and upgrade version to match ext.brightspotGradlePluginVersion = '4.2.12'

Look for the node version and update node as well as the yarn version.

node {
    version = '18.7.0'
    yarnVersion = '1.22.19'
    download = true
}

Disable some themes (if needed), in order to update them one at a time

In your project’s root directory, look for the file settings.gradle and in that file look for the lines which include and build your themes. These lines look like include(':myproject-theme-themename') & project(':myproject-theme-themename').projectDir = file('themes/myproject-theme-themename') where myproject-theme-themename is really the name and folder of your theme.

So now with that in mind, comment out those lines for the “all” themes (which are not already updated), and leave only the theme you are updating at the moment uncommented (and any which you’ve already updated). The goal here is to update the themes one at a time such that we can actually build the project, before moving to the next theme. Part of the reason is the old and new themes may need different versions of Node.

Disable some themes (again, just like above step, but this time in the file site/build.gradle)

Similar to the above steps in this file you’ll see lines such as api project(':myproject-theme-themename')

.npmrc files

Remove them. At least with the typical packages in modern brightspot you should no longer need this file Not much info to explain. Literally delete or comment out the contents of file. This applies to both the one in root as well as the one in each theme.

Updates to package.json

This is the last real part of preparing the upgrade. In some ways this step IS the upgrade.

PLEASE NOTE: updating the node version and dependencies may affect other dependencies specific to your site not covered in this upgrade. Please test accordingly

  • In the following diff do observe that we are updating various modules. There are some things I want to highlight:
    • Brightspot styleguide version is newer, which of course is the main bit of this process. Remove the brightspot-styleguide dependency and add @brightspot/styleguide with the version wanted
    • We added @babel/eslint-parser and removed the older equivalent
    • We added the copy-webpack-plugin which is whats helping us achieve the copy tasks previously done in gulp
    • We removed gulp. It is no longer needed (your project could vary, but ideally if you have any tasks that need gulp, the advice would be to convert those to webpack as well).
    • (Optional) We added web components package for loading custom elements, which is replacing a commonly used hardcoded polyfill in each theme’s respective Page-head.hbs.
    • Navigate to your theme’s Page-head.hbs and look for this script:
      <!--This is needed for custom elements to function in browsers that
      support them natively but that are using es6 code transpiled to es5.
      This will cause a non-fatal error to show up in the IE11 console.
      It can be safely ignored. https://github.com/webcomponents/webcomponentsjs/issues/749 -->
      <script>
        (function () {
        'use strict';
        (()=>{'use strict';if(!window.customElements)return;const a=window.HTMLElement,b=window.customElements.define,c=window.customElements.get,d=new Map,e=new Map;let f=!1,g=!1;window.HTMLElement=function(){if(!f){const a=d.get(this.constructor),b=c.call(window.customElements,a);g=!0;const e=new b;return e}f=!1;},window.HTMLElement.prototype=a.prototype;Object.defineProperty(window,'customElements',{value:window.customElements,configurable:!0,writable:!0}),Object.defineProperty(window.customElements,'define',{value:(c,h)=>{const i=h.prototype,j=class extends a{constructor(){super(),Object.setPrototypeOf(this,i),g||(f=!0,h.call(this)),g=!1;}},k=j.prototype;j.observedAttributes=h.observedAttributes,k.connectedCallback=i.connectedCallback,k.disconnectedCallback=i.disconnectedCallback,k.attributeChangedCallback=i.attributeChangedCallback,k.adoptedCallback=i.adoptedCallback,d.set(h,c),e.set(c,h),b.call(window.customElements,c,j);},configurable:!0,writable:!0}),Object.defineProperty(window.customElements,'get',{value:(a)=>e.get(a),configurable:!0,writable:!0});})();
        /**
        @license
        Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
        This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
        The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
        The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
        Code distributed by Google as part of the polymer project is also
        subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
        */
        }());
      </script>
      
    • Delete all contents of this script block and replace it with:
      <script src="{{cdn "/webcomponents-loader/webcomponents-loader.js"}}"></script>
      
    • Next, add this dependency in your theme’s package.json: "@webcomponents/webcomponentsjs": "2.5.0"
    • Then, navigate to your theme’s webpack.common.js and add the following Plugin. There may be other patterns present, keep them and add this one as well.
       plugins: [
      new CopyPlugin({
        patterns: [
          {
            from: 'node_modules/@webcomponents/webcomponentsjs/bundles/*.js',
            to: 'webcomponents-loader/bundles/[name][ext]'
          },
          {
            from: 'node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js',
            to: 'webcomponents-loader/'
          }
        ]
      })
      ],
      
    • All done! This may not be applicable to your project, but if it is, it’s worth updating as it’s easier to keep that up to date this way vs a hardcoded script. One way to know if it’s applicable to a project is if you see a similar console error as below that occurs as a result of the custom script: (Instead of Banner it may say something else)

      • Uncaught TypeError: Class constructor Banner cannot be invoked without 'new'
        at new j (new-gallery:316:613)
        at CustomElementRegistry.value (new-gallery:316:889)
        at registerCustomElements (All.min.d60fa7e791797bfff55f465499097ec6.gz.js:12272:18)
        
    • LAST BUT ALSO MOST IMPORTANT The scripts portion was all updated to use the new webpack configs and more. Replace your scripts with these new ones.
      • You no longer need to use gulp.js at all, so you can use webpack-dev-server directly to start the local Styleguide server
         {
          "scripts": {
            "server:styleguide": "webpack serve --config webpack.dev.js"
          }
        }
        
      • And same goes for creating the theme bundle:
         {
          "scripts": {
            "build": "yarn run eslint \"styleguide/**/*.js\" && webpack --config webpack.prod.js && node node_modules/@brightspot/styleguide/build/bundle -b build/styleguide -o build/theme.zip",
          }
        }
        

package.json example:

{
  "name": "site-theme-default",
  "version": "1.0.0-SNAPSHOT",
  "private": true,
  "license": "UNLICENSED",
  "engines": {
    "node": "18.7.0"
  }
  "devDependencies": {
    "@babel/core": "7.18.10",
    "@babel/eslint-parser": "7.18.9",
    "@babel/plugin-proposal-class-properties": "7.18.6",
    "@babel/plugin-syntax-dynamic-import": "7.8.3",
    "@babel/plugin-transform-runtime": "7.18.10",
    "@babel/plugin-transform-classes": "7.18.9",
    "@babel/preset-env": "7.18.10",
    "@brightspot/styleguide": "4.7.8",
    "autoprefixer": "10.4.8",
    "babel-loader": "8.2.5",
    "copy-webpack-plugin": "11.0.0",
    "css-loader": "6.7.1",
    "cssnano": "5.1.12",
    "eslint": "8.21.0",
    "eslint-config-prettier": "8.5.0",
    "eslint-config-standard": "17.0.0",
    "eslint-plugin-import": "2.26.0",
    "eslint-plugin-jsx-a11y": "6.6.1",
    "eslint-plugin-n": "15.2.4",
    "eslint-plugin-node": "11.1.0",
    "eslint-plugin-prettier": "4.2.1",
    "eslint-plugin-promise": "6.0.0",
    "eslint-plugin-react": "7.30.1",
    "eslint-plugin-react-hooks": "4.6.0",
    "eslint-webpack-plugin": "3.2.0",
    "file-loader": "6.2.0",
    "fs-extra": "1.0.0",
    "graphql-tag": "2.12.6",
    "less": "4.1.3",
    "less-loader": "11.0.0",
    "mini-css-extract-plugin": "2.6.1",
    "postcss": "8.4.16",
    "postcss-loader": "7.0.1",
    "prettier": "2.7.1",
    "style-loader": "3.3.1",
    "url-loader": "4.1.1",
    "webpack": "5.74.0",
    "webpack-bundle-analyzer": "4.5.0",
    "webpack-cli": "4.10.0",
    "webpack-dev-server": "3.11.3",
    "webpack-merge": "5.8.0"
  },
  "dependencies": {
    "@babel/helper-get-function-arity": "7.16.7",
    "@babel/runtime": "7.18.9",
    "picturefill": "3.0.3",
    "sanitize.css": "13.0.0"
  },
  "scripts": {
    "build": "yarn run eslint \"styleguide/**/*.js\" && webpack --config webpack.prod.js && node node_modules/@brightspot/styleguide/build/bundle -b build/styleguide -o build/theme.zip",
    "format": "yarn run eslint \"styleguide/**/*.js\" --fix",
    "profile": "webpack serve --config webpack.profile.js",
    "server:styleguide": "webpack serve --config webpack.dev.js"
  }
}

Installing the packages

  • Once all the above its done we are now ready to install the update.
    • Make sure you are on AT LEAST node version 12
    • On your theme directory run yarn install (If you encounter any issues you can delete the node_modules dir as well as yarn.lock file, and run yarn cache clean and try again… It’s a clean start in a sense)

.eslintrc - Configure this file to use some new things

  • Here, change the line "parser": "babel-eslint" to the line "parser": "@babel/eslint-parser" as that will be the new package.
  • Make a mental note that we may need to add some rules to change some linting rules if we face some difficult to update errors when linting. Providing an example of the .eslintrc configurations you’ll need to update. That’s not the whole file, it’s just an example highlighting what you’ll want to change.

.eslintrc example:

{
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:prettier/recommended"
    ],
    "parser": "@babel/eslint-parser",
    "parserOptions": {
        "allowImportExportEverywhere": true,
        "sourceType": "module",
        "requireConfigFile": false
    }
}

.nvmrc - Create one as ideally you should be using NVM for management of Node

  • In the root of each theme directory, create a file called .nvmrc
  • In package.json, note the version of Node you are using. It will look something like this:
  "engines": {
    "node": "18.14.2"
  },
  • Copy that value into .nvmrc. This will define the Node version your theme is using.
v18.14.2

.gulpfile - IMPORTANT: Take note of what’s in the gulpfile. We want to remove it in its entirety, but some tasks may need to be moved into webpack.

  • This upgrade step list doesnt cover the scope of a potential upgrade with some extremely unique and custom gulp task that may need to be maintained. The reason is customizations could be an infinite variety of things.
  • Some common tasks in gulp to be moved to webpack are tasks related to moving an assets folder. Often times we have found such task relating to directory assets|static. The webpack copy plugin is the solution should you encounter such a case.
plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: '../../styleguide/svgs',
          to: 'svgs/',
        },
        { 
          from: 'styleguide/assets',
          to: 'styleguide/assets',
        }
      ],
    })
  ],

ABOVE is an example which (1) copies an svg folder from an outside styleguide into the bundle & (2) copies an assets folder into the bundle following the same dir strucutre

  • Additionally there may be tasks related to AMP. These taks usually run an Amp.less file and convert it into an includable .hbs file. If you encounter this one you will likely need to achieve the same (but within webpack) by creating an Amp.js file and importing Amp.less in it (similar to how All.js imports All.js). Once that is done (in the same manner the All.js works), you’ll need to add the corresponding entry or entries in the webpack common file as well as webpack prod. // In webpack prod plugins: [ new MiniCssExtractPlugin({ filename: (pathData) => { if (pathData.chunk.name === 'styleguide/Amp.min.js') { return pathData.chunk.name.replace('.js', '') + '.css.amp.hbs' } else { return pathData.chunk.name.replace('.js', '') + '.css' } }, }), ], Above is a snippet for use in the plugins configuration in webpack prod file.

The new Styleguide breaks the theme compatibility checker

It does far less processing on your example files (i.e. you now need to set both _template and _styledTemplate if you want to display one of the style variations). To fix this, you can run: npx node ./node_modules/@brightspot/styleguide/build/upgrade

Theme _config.json - A bit of cleanup

The lines "sources": [], & ` “vars”: {},` were empty. Remove them. This isn’t really part of the upgrade but take the opportunity to clean that up by removing those lines.

The webpack files in your theme directory

  • There should be 4 additional files going forward in your theme directory. If they already exist within your theme directory make any changes specified in the next few sections.
    • webpack.profile.js - this one may be new to your project. Copy and paste the file below with the same name in your theme`s folder
    • webpack.dev.js
    • webpack.common.js
    • webpack.prod.js

The changes we need on these webpack files

  • For the webpack.profile.js file as the old projects are likely missing it, simply add it. A copy of it is provided herein.

webpack.profile.js example:

const { merge } = require('webpack-merge')

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin

module.exports = merge(require('./webpack.prod.js'), {
  plugins: [new BundleAnalyzerPlugin()]
})
  • For the webpack.dev.js file, make the following tweaks
  • Observe the styleguide variable is now lowercase and the import is different.
  • Observe the merge import is now written as a non default named import hence the { around it.
  • Moved the common require statement to the top for clarity.
  • The line Styleguide.webpackDevServerConfig(webpack, { is now styleguide.webpack('./styleguide', webpack, {
  • Remember those tasks from gulp I mentioned should be commented out? Look at the use of the copy plugin herein. That does the same copying into the bundle we were previously using webpack for.

webpack.dev.js example:

const styleguide = require('@brightspot/styleguide')
const webpack = require('webpack')
const { merge } = require('webpack-merge')

module.exports = merge(
  require('./webpack.common.js'),
  styleguide.webpack('./styleguide', webpack, {
    mode: 'development',

    module: {
      rules: [
        {
          test: /\.less$/,
          use: [
            'style-loader',
            {
              loader: 'css-loader',
              options: {
                sourceMap: true
              }
            },
            {
              loader: 'postcss-loader',
              options: {
                sourceMap: true
              }
            },
            {
              loader: 'less-loader'
            }
          ]
        }
      ]
    }
  })
)
  • For the webpack.prod.js file, make the following tweaks:
    • Same as above for some of the var names.

webpack.prod.js example:

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
const { merge } = require('webpack-merge')

const entryLessFile = path.resolve(__dirname, './styleguide/All.less')

module.exports = merge(require('./webpack.common.js'), {
  mode: 'production',
  output: {
    chunkFilename: '[name].[contenthash].js'
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styleguide/All.min.css'
    })
  ],

  module: {
    rules: [
      {
        test: entryLessFile,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      }
    ]
  }
})
  • Additionally now we have a new change to how we extract the CSS into files. Observe also there are some special lines to do the CSS conversion into an HBS file just like we were doing previously for amp. This takes care that portion we were previously handling via gulp (now in webpack)

Example:

  plugins: [
    new MiniCssExtractPlugin({
      filename: pathData => {
        if (
          pathData.chunk.name === 'styles/amp/Amp.js' ||
          pathData.chunk.name === 'newsletter/NewsletterInline.js' ||
          pathData.chunk.name === 'newsletter/NewsletterEmbed.js'
      ) {
          return pathData.chunk.name.replace('.js', '') + '.css.hbs'
        } else {
          return pathData.chunk.name.replace('.js', '') + '.css'
        }
      }
    })
  ],
  • For the webpack.common.js file, make the following tweaks:
    • You’ll also need to explicitly set the webpack output directory module.exports = { output: { path: path.resolve(__dirname, './build/styleguide'),
    • The main additions here is we remove eslint-loader and instead, in plugins run the copy and eslint plugins, to imitate those that setup gulp was doing previously where it was moving the asset directories and such.

webpack.common.js example:

const CopyPlugin = require('copy-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const path = require('path')

module.exports = {
  entry: {
    'styleguide/All.min.js': './styleguide/All.js',
    'styleguide/util/IEPolyfills.js': './styleguide/util/IEPolyfills.js'
  },

  output: {
    path: path.resolve(__dirname, './build/styleguide'),
    filename: '[name]',
    chunkFilename: 'styleguide/chunk/[name].[contenthash].js',
    publicPath: '/'
  },

  plugins: [
    new CopyPlugin({
      patterns: [
        { from: 'styleguide/PreviewPage.css', to: 'styleguide' },
        { from: 'styleguide/assets/**', to: '.' }
      ]
    }),
    new ESLintPlugin()
  ],
  module: {
    rules: [
      // Split out large binary files into separate chunks.
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset'
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        type: 'asset/resource'
      },
      // Transpile JS.
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  },

Prepare the styleguides menu (previously _pages.config.json)

  • Look for _pages.config.json and rename it to _navigation.config.json
  • Other than the name it needs some minor tweaks. Observe the changes with the following before and after.
    • The top level pages should now be navigation
    • The htmlextension is now the json extension just like the files in styleguide
    • The key name is now displayName
    • The key content is now page
    • Also use explicit example rather than any wildcards (not shown in example below however) BEFORE
      {
       "pages":[
        {
           "name":"Globals",
           "pages":[
              {
                 "name":"Typography",
                 "content":"/_resource/TypographyPage.html"
              }
           ]
        }
       ]
      }
      

      AFTER

      {
       "navigation":[
        {
           "displayName":"Globals",
           "pages":[
              {
                 "displayName":"Typography",
                 "page":"/_resource/TypographyPage.json"
              }
           ]
        }
       ]
      }
      

Running the (UPGRADED) styleguide

We’ve updated the dependencies, but some things will still need to be updated in the FE templates and js.

Run the styleguide.

Run it using yarn server:styleguide in your terminal while in the theme directory (this is that script in the package.json).

  • The build process should be much faster now, and visit localhost:8080 on your browser.
    • Unless you are super lucky the expectation is now you should see styleguide starting to run, but… (onto next section)

What to expect

Now you are likely seeing some errors. If all things are going as expected, these are lots of js errors, in your terminal regarding linting rules. The quickest way to get through these is:

  • Run yarn format to have the formatted quickly format your js based on its expectations.
  • You may still have some errors that need to be manually updated. Go ahead and do so.
  • Additionally you can go back to the .eslintrc file, and add rules, should there be some rules that are easier fixed by disabling some lint rules (example of such rules provided)

The end

Once all the above steps have been done, styleguide should be rendering fine.

Potential gotchas

  • Your project may contain empty string values within certain _config files i.e. "". Null strings are no longer accepted in 4.5. If your project throws errors like /fields/blogPageStyle/values/0/value: Should be at least 1 characters you should search your the pertinent files for these null strings. To resolve this issue, we recommend to remove the Key/Value pair that threw the error and place the key into the field, cms.ui.placeholder.
  • In ImageSizes.config, an error I found in some projects is mispelling keys in the image sizes such as previewwidth which should be previewWidth now throws an error.
  • In the new _navigation.config.json the hideTabs throws error. I’ve removed those entries
  • In projects that were using displayName to rename a content type (the actual content or module, and not a style) that now throws an error. Removing such usages of displayName.
  • If you are running styleguide before completing the Gradle Plugin Upgrade step, please ensure that brightspotGradlePluginVersion, located within /build.gradle is set to at least 4.2.9. Failing to do so may cause errors such as zsh: command not found: webpack or Unknown argument: build. These errors are caused by changes in the way yarn processes commands changed in 4.2.9.
  • You may encounter issues with handlebar helpers such as rendering errors for example pages after running yarn server:styleguide, inspect elements and within the console look for index.ts:825 ReferenceError: java is not defined. This error may be caused by code in your styleguide/_helpers.js within your theme folder that looks like this:
    Handlebars.registerHelper('svgPlaceholder', function (width, height) {
    const svg =
      '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="' +
      (parseInt(height, 10) || 1) +
      'px" width="' +
      (parseInt(width, 10) || 1) +
      'px"></svg>'
    
    if (typeof Buffer !== 'undefined') {
      return 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64')
    } else {
      return (
        'data:image/svg+xml;base64,' +
        java.util.Base64.encoder.encodeToString(
          svg.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)
        )
      )
    }
    })
    

The fix is to add an else if like so:

Handlebars.registerHelper('svgPlaceholder', function (width, height) {
  const svg =
    '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="' +
    (parseInt(height, 10) || 1) +
    'px" width="' +
    (parseInt(width, 10) || 1) +
    'px"></svg>'

  if (typeof Buffer !== 'undefined') {
    return 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64')
  } else if (typeof btoa !== 'undefined') {
    return 'data:image/svg+xml;base64,' + btoa(svg)
  } else {
    return (
      'data:image/svg+xml;base64,' +
      java.util.Base64.encoder.encodeToString(
        svg.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)
      )
    )
  }
})