Skip to content

Commit

Permalink
feat(storybook): vite migration (#2803)
Browse files Browse the repository at this point in the history
Migrating the build for our Storybook instance from Webpack to Vite for a stronger front-of-the-front-end build system.

**Why?**

- More efficient code splitting due to using native ESM.
- Rapid cold start speed and efficient HMR for swift code modifications.
- Local and browser caching are built-in features.
- PostCSS is pre-configured and a built-in utility requiring very little (or often no) configuration.

Yes, Vite comes with a limited plugins ecosystem as compared to webpack, but what they have are focused on front-of-the-front-end tooling such as postcss and esbuild which meet our needs without adding complexity.

**Changes to support the migration**

- Wrap typekit.js loader in DOMContentLoaded (Vite is loading it more quickly causing initialization issues)
- Load styles for the context decorator by appending them to style tags as-needed
- Updated development guide for Storybook to describe changes
- Create a new loaders folder to pull out the font, icon, and token loaders
- Update main.js config in storybook to use Vite config instead
- Use ESM syntax for Storybook workspace
- Pass the context data through templates to allow us to query it in nested components
  • Loading branch information
castastrophe authored Jun 12, 2024
1 parent b322fe7 commit 2c5e5eb
Show file tree
Hide file tree
Showing 111 changed files with 3,834 additions and 4,477 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-teachers-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spectrum-css/preview": minor
---

Feature to migrate Storybook to use Vite's builder instead of Webpack. This change reduces the configuration complexity with more built-in features that align with our needs.
5 changes: 5 additions & 0 deletions .changeset/twenty-starfishes-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spectrum-css/tokens": minor
---

This feature adds the custom variables for each context (spectrum and express) to the root-named asset (dist/css/express/global-vars.css)
78 changes: 44 additions & 34 deletions .storybook/assets/typekit.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,52 @@
/* global Typekit */

// This wrapper prevents loading the font more than once
if (!window.Typekit) {
const kitId =
document.querySelector("[lang]:not([lang=\"en-US\"])") === null
? "mge7bvf"
: "rok6rmo";

window.addEventListener("DOMContentLoaded", () => {
const html = document.documentElement;
html.classList.add("wf-loading");
const root = document.head ?? document.body ?? html;
const isNotEnglish = root.querySelector("[lang]:not([lang=\"en-US\"])");

const toggleClass = (state, force = true) => {
if (html) html.classList.toggle(`wf-${state}`, force);
else if (root) root.classList.toggle(`wf-${state}`, force);
};

const t = setTimeout(function () {
html.classList.remove("wf-loading");
html.classList.add("wf-inactive");
const timeout = setTimeout(function () {
toggleClass("loading", false);
toggleClass("inactive");
}, 3000);

const tk = document.createElement("script");
let d = false;

// Always load over https
tk.src = "https://use.typekit.net/" + kitId + ".js";
tk.type = "text/javascript";
tk.async = "true";
tk.onload = tk.onreadystatechange = () => {
const a = this.readyState;
if (d || (a && a !== "complete" && a !== "loaded")) return;

d = true;
clearTimeout(t);

try {
window.Typekit = Typekit.load({
kitId,
scriptTimeout: 3000,
});
}
catch (b) {/* empty */}
const config = {
kitId: isNotEnglish ? "mge7bvf" : "rok6rmo",
scriptTimeout: 3000,
active: () => {
toggleClass("loading");
},
};

document.body.appendChild(tk);
}
// This wrapper prevents loading the font more than once
if (window && !window.Typekit) {
let d = false;
let tk = document.querySelector("#typekit");

if (!tk) {
tk = document.createElement("script");
tk.id = "typekit";
tk.src = `https://use.typekit.net/${config.kitId}.js`;
tk.type = "text/javascript";
tk.async = "true";
tk.onload = tk.onreadystatechange = function () {
const readyState = this.readyState;
if (d || (readyState && readyState !== "complete" && readyState !== "loaded")) return;

d = true;
clearTimeout(timeout);

try {
window.Typekit = Typekit.load(config);
}
catch (b) {/* empty */}
};
root.appendChild(tk);
}
}
});
65 changes: 56 additions & 9 deletions .storybook/decorators/context.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { makeDecorator, useEffect } from "@storybook/preview-api";
import { html } from "lit";

/**
* @type import('@storybook/csf').DecoratorFunction<import('@storybook/web-components').WebComponentsFramework>
Expand All @@ -8,7 +9,8 @@ export const withContextWrapper = makeDecorator({
name: "withContextWrapper",
parameterName: "context",
wrapper: (StoryFn, context) => {
const { args, argTypes, viewMode, id } = context;
const { args = {}, argTypes = {}, viewMode, id, loaded = {} } = context;
const { tokens = {} } = loaded;

const getDefaultValue = (type) => {
if (!type) return null;
Expand All @@ -27,10 +29,30 @@ export const withContextWrapper = makeDecorator({
/** @type string */
const scale = args.scale ? args.scale : getDefaultValue(argTypes.scale) ?? "medium";

const colors = ["light", "dark", "darkest"];
const scales = ["medium", "large"];

useEffect(() => {
const toggleStyles = (container, id, styleObj, add = true) => {
if (!container && !id) return;

let style = container.querySelector(`#${id}`);
const styles = styleObj ? Object.values(styleObj)[0] : undefined;

if (!add) {
if (style) style.remove();
return;
}

if (!style) {
style = document.createElement("style");
style.id = id;
container.appendChild(style);
}

if (!style) return;

if (add && styles) style.innerHTML = styles;
else style.remove();
};

let containers = [document.body];

const roots = [
Expand All @@ -42,17 +64,34 @@ export const withContextWrapper = makeDecorator({
}

for (const container of containers) {
const styleContainer = container.querySelector("#styles-container");
const globalContainer = styleContainer.querySelector("#global");
const colorsContainer = styleContainer.querySelector("#colors");
const scalesContainer = styleContainer.querySelector("#scale");
const contextContainer = styleContainer.querySelector("#context");

container.classList.toggle("spectrum", true);
container.classList.toggle("spectrum--express", isExpress);

for (const c of colors) {
toggleStyles(globalContainer, "vars-base", tokens?.global?.base, true);
toggleStyles(contextContainer, "vars-base-spectrum", tokens?.spectrum?.base, true);
toggleStyles(contextContainer, "vars-base-express", tokens?.express?.base, isExpress);

for (const c of ["light", "dark", "darkest"]) {
container.classList.toggle(`spectrum--${c}`, c === color);

toggleStyles(colorsContainer, `vars-${c}`, tokens?.global?.[c], c === color);
toggleStyles(colorsContainer, `vars-${c}-spectrum`, tokens?.spectrum?.[c], c === color);
toggleStyles(colorsContainer, `vars-${c}-express`, tokens?.express?.[c], isExpress && c === color);
}

for (const s of scales) {
for (const s of ["medium", "large"]) {
container.classList.toggle(`spectrum--${s}`, s === scale);
}

toggleStyles(scalesContainer, `vars-${s}`, tokens?.global?.[s], s === scale);
toggleStyles(scalesContainer, `vars-${s}-spectrum`, tokens?.spectrum?.[s], s === scale);
toggleStyles(scalesContainer, `vars-${s}-express`, tokens?.express?.[s], isExpress && s === scale);
}

container.style.removeProperty("background");
const hasStaticElement = container.querySelector(`.${args.rootClass}--staticWhite, .${args.rootClass}--staticBlack, .${args.rootClass}--overBackground`);
Expand All @@ -65,8 +104,16 @@ export const withContextWrapper = makeDecorator({
}
}
}
}, [color, scale, isExpress, args.staticColor]);
}, [color, scale, isExpress, tokens, args.staticColor]);

return StoryFn(context);
return html`
<div id="styles-container">
<div id="global"></div>
<div id="colors"></div>
<div id="scale"></div>
<div id="context"></div>
</div>
${StoryFn(context)}
`;
},
});
12 changes: 6 additions & 6 deletions .storybook/guides/deprecation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ Before removing the component from the codebase, we need to flag the component a
a. Edit the title of any exported stories to be prefixed with the `Deprecated` category, i.e., `title: "Quick actions"`.
b. Update any local references to point to the package name instead, i.e.,<br/>_Original_:<br/>`import { Template } from "./template";`<br/><br/>_Updated to_:<br/>`import { Template } from "@spectrum-css/quickaction/stories/template.js";`.
c. In the parameters section, there are 2 important updates to make: - Add `chromatic: { disableSnapshot: true },` to ensure it no longer runs regression tests. - Update the `status` type to `deprecated`:
`json
parameters: {
chromatic: { disableSnapshot: true },
status: { type: "deprecated" }
},
`
```json
parameters: {
chromatic: { disableSnapshot: true },
status: { type: "deprecated" }
},
```
3. Update the status of the component to `Deprecated` in the `*.yml` file. Add any additional migration notes to the `deprecationNotice` keyword. i.e.,
```yaml
name: Quick actions
Expand Down
Loading

0 comments on commit 2c5e5eb

Please sign in to comment.