It’s been a hot minute since I ventured deep into the innards of Component Builder. I’d like to think I learned a few things since the last time. Maybe yes…Maybe no…Maybe maybe.!

Why do I care? We’ve had challenges. To refresh:

  1. We were running our Nextjs project with the stories files next to the component file itself. This was super efficient for our team
  2. We upgraded to JSS 21. This introduced the Component Builder, which didn’t filter the stories out.
  3. When we tried to run the solution, bad things happened.
  4. Read this: https://rockpapersitecore.com/2023/07/storybook-stories-gotcha-when-upgrading-to-jss-21-2-1/

Our solution at the time was to move our stores into a parallel folder called “stories” (naming things is sometimes easy.). This was a royal pain though, since you had to scroll up and down the solution to find the right story for the component you were working on.

I’m back at it though, and have a solution. It’s largely straight forward, but it was a bit more complicated than it had to be. Let’s take a little history lesson/deep dive.

Everything starts with npm run bootstrap. When you run the bootstrap command, there’s a whole slew of things that happen. At the highest level, a script file is run, specifically: \scripts\bootstrap.ts. The contents of this are below:

/*
  BOOTSTRAPPING
  The bootstrap process runs before build, and generates JS that needs to be
  included into the build - specifically, plugins, the global config module,
  and the component name to component mapping.
*/

/*
   PLUGINS GENERATION
*/
import './generate-plugins';

/*
  CONFIG GENERATION
*/
import './generate-config';

/*
  COMPONENT BUILDER GENERATION
*/
import './generate-component-builder';

/*
  META DATA GENERATION
*/
import './generate-metadata';

The ones that matter to us here are #1 and #3. First, you have to generate your plugins. Plugin generation is pretty simple. There’s a control file at: \scripts\generate-plugins.ts that tells the application what plugins need processing. Here’s a snip of the definitions:

const pluginDefinitions: PluginDefinition[] = [
{
  distPath: 'scripts/temp/generate-component-builder-plugins.ts',
  rootPath: 'scripts/generate-component-builder/plugins',
  moduleType: ModuleType.ESM,
},

Essentially, we’ll grab all the files from that rootPath and then generate the file in the distPath. That file (in this scenario) looks like the below:

export { componentBuilderPlugin } from 'scripts/generate-component-builder/plugins/component-builder';
export { componentsPlugin } from 'scripts/generate-component-builder/plugins/components';
export { feaasPlugin } from 'scripts/generate-component-builder/plugins/feaas';
export { packagesPlugin } from 'scripts/generate-component-builder/plugins/packages';

Yeah, that’s a good chunk of logic to generate a list of exports, but it’s flexible and that matters. Once we have these list of exports, we can reference that file in our plugin itself here: \scripts\generate-component-builder\index.ts

With the exception of the first item, each of these plugins doesn’t add a thing to the Component Builder’s components list (generated at \src\temp\componentBuilder.ts), which out of the box generates to look like this:

/* eslint-disable */
// Do not edit this file, it is auto-generated at build time!
// See scripts/generate-component-builder/index.ts to modify the generation of this file.


import { ComponentBuilder } from '@sitecore-jss/sitecore-jss-nextjs';

import { BYOCWrapper, FEaaSWrapper } from '@sitecore-jss/sitecore-jss-nextjs';

import * as CdpPageView from 'src/components/Defaults/CdpPageView';
import * as ColumnSplitter from 'src/components/Defaults/ColumnSplitter';
import * as Container from 'src/components/Defaults/Container';
import * as ContentBlock from 'src/components/Defaults/ContentBlock';
import * as FEAASScripts from 'src/components/Defaults/FEAASScripts';
import * as Image from 'src/components/Defaults/Image';
import * as LinkList from 'src/components/Defaults/LinkList';
import * as Navigation from 'src/components/Defaults/Navigation';
import * as PageContent from 'src/components/Defaults/PageContent';
import * as PartialDesignDynamicPlaceholder from 'src/components/Defaults/PartialDesignDynamicPlaceholder';
import * as Promo from 'src/components/Defaults/Promo';
import * as RichText from 'src/components/Defaults/RichText';
import * as RowSplitter from 'src/components/Defaults/RowSplitter';
import * as Title from 'src/components/Defaults/Title';

export const components = new Map();
components.set('BYOCWrapper', BYOCWrapper);
components.set('FEaaSWrapper', FEaaSWrapper);

components.set('CdpPageView', CdpPageView);
components.set('ColumnSplitter', ColumnSplitter);
components.set('Container', Container);
components.set('ContentBlock', ContentBlock);
components.set('FEAASScripts', FEAASScripts);
components.set('Image', Image);
components.set('LinkList', LinkList);
components.set('Navigation', Navigation);
components.set('PageContent', PageContent);
components.set('PartialDesignDynamicPlaceholder', PartialDesignDynamicPlaceholder);
components.set('Promo', Promo);
components.set('RichText', RichText);
components.set('RowSplitter', RowSplitter);
components.set('Title', Title);

export const componentBuilder = new ComponentBuilder({ components });

export const moduleFactory = componentBuilder.getModuleFactory();

Back to the plugin list, the meat of that first plugin for the Component Builder generation is…not that deep:

import { generateComponentBuilder } from '@sitecore-jss/sitecore-jss-dev-tools/nextjs';
import {
  ComponentBuilderPluginConfig,
  ComponentBuilderPlugin as ComponentBuilderPluginType,
} from '..';

/**
 * Generates the component builder file.
 */
class ComponentBuilderPlugin implements ComponentBuilderPluginType {
  order = 9999;

  exec(config: ComponentBuilderPluginConfig) {
    generateComponentBuilder(config);

    config.components = [];

    return config;
  }
}

export const componentBuilderPlugin = new ComponentBuilderPlugin();

One thing that stands out here is that Sitecore is loading all the components, and then setting the component array to empty. That seems…weird. If this is the only logic in play, then there shouldn’t be any components out of the box, yeah? Mmmmm that’s a hefty IF, yo.

Ok, so you have to dig a little bit, but in short, Sitecore is taking the components you pass in from these builders, and then it scours your folders for them anyway…even if you don’t want them. The offensive line of code is here: https://github.com/Sitecore/jss/blob/v22.1.3/packages/sitecore-jss-dev-tools/src/templating/nextjs/generate-component-builder.ts#L94

You’re reading that right… take in all the packages I have specified. Take in all the components that I have specified….and go get them all again anyway. Why do they do this? I don’t know. How do you fix this? A little tool called patch-package. Patch-Package lets you modify the code in node_modules and save the delta. Then there’s you add a post-install step to apply this update every time you do a npm install. Read the docs for the details, but when we comment out that line, and only use the packages and components from the parameter, we get the following patch:

diff --git a/node_modules/@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/templating/nextjs/generate-component-builder.js b/node_modules/@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/templating/nextjs/generate-component-builder.js
index 0b1276c..ee9f65e 100644
--- a/node_modules/@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/templating/nextjs/generate-component-builder.js
+++ b/node_modules/@sitecore-jss/sitecore-jss-dev-tools/dist/cjs/templating/nextjs/generate-component-builder.js
@@ -60,7 +60,7 @@ function writeComponentBuilder({ componentRootPath, componentBuilderOutputPath,
     const items = [
         ...packages,
         ...components,
-        ...(0, components_1.getComponentList)(componentRootPath),
+      //  ...(0, components_1.getComponentList)(componentRootPath),
     ];
     const componentBuilderPath = path_1.default.resolve(componentBuilderOutputPath);
     const fileContent = (0, component_builder_1.getComponentBuilderTemplate)(items);

Notice the lines with a -/+ on them. Remove one line, replace it with another. Looks like git, yeah?

Have you heard of Patch Package before? Me either. Is it amazing? Hell yes it is! A super special shout out to Corey Smith who told me about this tool. It’s been a GAME CHANGER.

Now when we run bootstrap, we get something a LOT more expected:

/* eslint-disable */
// Do not edit this file, it is auto-generated at build time!
// See scripts/generate-component-builder/index.ts to modify the generation of this file.


import { ComponentBuilder } from '@sitecore-jss/sitecore-jss-nextjs';

import { BYOCWrapper, FEaaSWrapper } from '@sitecore-jss/sitecore-jss-nextjs';



export const components = new Map();
components.set('BYOCWrapper', BYOCWrapper);
components.set('FEaaSWrapper', FEaaSWrapper);



export const componentBuilder = new ComponentBuilder({ components });

export const moduleFactory = componentBuilder.getModuleFactory();

This is what we want! Those two components actually come from \scripts\generate-component-builder\plugins\feaas.ts which is good. We’ll let that file do all the magic it has to do.

Now that we’ve stopped the extra crap from getting in there, we can create our own list of components just the way we want to inside \scripts\generate-component-builder\plugins\components.ts. Here’s that file, which loads components using getComponentList.

import { getComponentList } from '@sitecore-jss/sitecore-jss-dev-tools';
import { ComponentBuilderPlugin, ComponentBuilderPluginConfig } from '..';

/**
 * Provides custom components configuration
 */
class ComponentsPlugin implements ComponentBuilderPlugin {
  order = 0;

  exec(config: ComponentBuilderPluginConfig) {
    /**
     * You can specify components which you want to import using custom path in format:
     * {
     *   path: string; // path to component
     *   moduleName: string; // module name to import
     *   componentName: 'component name'; // component rendering name
     * }
     *
     * Or you can register all components from a path using the below approach:
     * import { getComponentList } from '@sitecore-jss/sitecore-jss-dev-tools';
     * ...
     * const componentsPath = 'src/extra';
     * config.components = getComponentList(componentsPath);
     */
    config.components = [];

    const allComponents = getComponentList('src/components');

    allComponents.forEach((c) => {
      if (String(c.componentName).toLowerCase().indexOf('.stories') > 0) {
        console.debug(`Skipping Stories Component: ${c.componentName}`);
      } else {
        console.debug(`Adding JSS Component: ${c.componentName}`);
        config.components.push(c);
      }
    });

    return config;
  }
}

export const componentsPlugin = new ComponentsPlugin();

NOTE: You’re going to see the noise about the “Registering JSS component” which comes from the getComponentList method. Is it actually registering it? No. It just found it. No, I’m not sure why. In theory, we could write our own logic to pull those components into a list and send it in…but that might be overkill here.

When you run bootstrap now, you’ll see this beauty:

And when you open your \src\temp\componentBuilder.ts, you’ll see this:

/* eslint-disable */
// Do not edit this file, it is auto-generated at build time!
// See scripts/generate-component-builder/index.ts to modify the generation of this file.


import { ComponentBuilder } from '@sitecore-jss/sitecore-jss-nextjs';

import { BYOCWrapper, FEaaSWrapper } from '@sitecore-jss/sitecore-jss-nextjs';

import * as CdpPageView from 'src/components/Defaults/CdpPageView';
import * as ColumnSplitter from 'src/components/Defaults/ColumnSplitter';
import * as Container from 'src/components/Defaults/Container';
import * as ContentBlock from 'src/components/Defaults/ContentBlock';
import * as FEAASScripts from 'src/components/Defaults/FEAASScripts';
import * as Image from 'src/components/Defaults/Image';
import * as LinkList from 'src/components/Defaults/LinkList';
import * as Navigation from 'src/components/Defaults/Navigation';
import * as PageContent from 'src/components/Defaults/PageContent';
import * as PartialDesignDynamicPlaceholder from 'src/components/Defaults/PartialDesignDynamicPlaceholder';
import * as Promo from 'src/components/Defaults/Promo';
import * as RichText from 'src/components/Defaults/RichText';
import * as RowSplitter from 'src/components/Defaults/RowSplitter';
import * as Title from 'src/components/Defaults/Title';
import * as Hero from 'src/components/Hero/Hero';

export const components = new Map();
components.set('BYOCWrapper', BYOCWrapper);
components.set('FEaaSWrapper', FEaaSWrapper);

components.set('CdpPageView', CdpPageView);
components.set('ColumnSplitter', ColumnSplitter);
components.set('Container', Container);
components.set('ContentBlock', ContentBlock);
components.set('FEAASScripts', FEAASScripts);
components.set('Image', Image);
components.set('LinkList', LinkList);
components.set('Navigation', Navigation);
components.set('PageContent', PageContent);
components.set('PartialDesignDynamicPlaceholder', PartialDesignDynamicPlaceholder);
components.set('Promo', Promo);
components.set('RichText', RichText);
components.set('RowSplitter', RowSplitter);
components.set('Title', Title);
components.set('Hero', Hero);

export const componentBuilder = new ComponentBuilder({ components });

export const moduleFactory = componentBuilder.getModuleFactory();

We have our Hero, and no Hero.stories, even though they’re in the same folder. That’s pretty magical, I say.

Facebooktwitterredditlinkedinmail