Vistaprint

Contributing to Visage

Get to know how you can contribute your work in Visage

We follow an open-source model, and welcome contributions and pull requests from any developer! You can see our Jira backlog to see what work we have planned.

We do recommend checking in with us over at #proj-visage before you start coding, if just to minimize any potential merge issues if someone else is working on the same file(s) at the same time.

Requirements

All Visage components must be:

  • Re-usable or likely re-used. Not every component can, or should, be in Visage! Elements that are likely unique to one page are often best left in that page's repo.

  • Accessible, meeting our Site Accessibility RFC

  • Localizable, i.e. there is no text built into the component

  • On brand

  • Maintainable and scalable

  • Documented, including both developer documentation, as well as design documentation on where and how the component is meant to be used (and where it is not to be used)

Merge requests

Raise a merge request targetted to the main branch and send the requests over #proj-visage

Repository

The git repository is at https://gitlab.com/vistaprint-org/merchandising-technology/visage/visage-design-system

Setup

Prerequisites

  1. node.js, or with NVM. See the .nvmrc file for the correct version to install.

  2. VP_ARTIFACTORY_TOKEN. See the Confluence page.

Editor setup

VSCode: We recommend using Visual Studio Code, a lightweight editor which combines the simplicity of a source code editor with powerful developer tooling, like IntelliSense code completion and debugging and a large set of extensions.

ESLint: This plugin highlights any linting errors right in your editor. VSCode's ESLint plugin, does not lint TypeScript files by default. In order to enable TypeScript support, make sure that "typescript" and "typescriptreact" exist in the "eslint.validate" setting. The default value is ["javascript", "javascriptreact"].

Prettier: This plugin adds Prettier as a VSCode formatter so that when you run the VSCode "format document/selection" routines, the project's Prettier-config will be respected.

Optionally configure VSCode to auto-format whenever you save a file by setting the "editor.formatOnSave" option to true

The Prettier plugin is pretty aggressive and, by default, will try to format code in other projects too (even if they don't have their own Prettier configuration). So, I recommend that you configure the Prettier plugin so that it will only format code when there's a Prettier configuration file in the project.

Below is an example set of settings; you can always configure your own:

settings.json
json
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.formatOnSave": true,
"prettier.requireConfig": true
}

Setup

At the root of the project, execute the command:

npm run setup

This will take care of installing all the dependencies for the library and storybooks, building up the library for using it locally. This may take up some time. So, sit back and relax. You won't need to execute this step unless some dependencies are updated inside the library/.

After the process is complete, you can run either of the two storybooks(HTML and react) using the commands:

npm run start:sbh // HTML Storybook

npm run start:sbr // React Storybook

To clean the built files, run: npm run clean

You can find more details about all the available scripts and their use cases in the README.md, present at the root of the project.

Testing

We use React Testing Library and Jest to render our components "headlessly" (without rendering any visible UI) by validating all the expected behaviours, attributes and classes for a component.

In order to test a component, its corresponding test file should be component.test.tsx placed next to the component itself.

To test all the components, execute:

cd library && npm run test

Seeing is believing

Now, the unit testing output is great, but realistically we also want to see and play with our components before we ship them. That's why we've created an additional layer using Storybook that actually interprets our snapshot tests and renders each test as a Storybook story.

So, when you're ready to visually test your components, at the project root, execute- npm run start:sbr in order to start up react Storybook site that will render all of your tests in a visual playground. There is an HTML version also available which can be visualized by executing- npm run start:sbh

The unit testing should be treated as the primary way to validate a component and the storybook-rendered versions should be considered to be a convenience.

Adding new component

We have provided a script that will automatically generate a basic component (including the necessary imports and exports) that matches our convention. The script to generate the component is as follows:

cd library && npm run gen:c

After you execute the command, you will be prompted with a few questions:

  1. Component name: Name of the component in any format you like but the output component folder name will be according to our convention i.e kebab casing

  2. Parent component folder name (Ignore if this component is top-level): If you want the generated component to be part of an already existing component, provide the exact name of that component's folder, else just return.

  3. Include styles setup (Y/n): Whether to include the SCSS file for the component styling. This is optional because some of our components don't need specific styling and depend directly on core CSS.

  4. Include JS setup (Y/n): Does the component need JS support to handle events or perform some operations similar to accordion, alert-box, etc. components. If yes, the new JS will be created at the location - src/vanilla-js/

The Visage component generation will take care of everything like adding necessary imports/exports, adding entries in STYLE_KEYS/SCRIPT_KEYS, adding stories as per the answers provided for the prompts.

Our convention

All the components are expected to be created under library/src/components/. The component's corresponding SCSS, TSX, stories, and test files should live in the same directory of the component. The filenames for the component's SCSS and TSX files must match, in order for the component to be properly bundled and published via npm!

Static files should have the name all lowercase, with words separated by dashes, so some-component.scss and some-component.tsx. Stylesheets should be .scss files so that they can use the static globals and gain access to our variables and mixins.

Naming

Make sure the component name is unique; this will avoid CSS namespace collisions.

Follow kebab-case pattern for file naming, so a component would be named .some-component instead of someComponent.

Elements that are just native HTML elements but with Vistaprint-specific styling typically begin with "Stylized", e.g. stylized-checkbox or stylized-input.

Skins

UI components must support dynamic skinning. Skins should be named semantically. This allows designers to modify stylistic features of the components while preserving their semantic meaning, and without affecting their behavior. Switching the skin applied to a specific instance of a component should be trivial.

For skins, CSS class naming should be of the format .componentname-skin-skinname. Skin names should be semantic. For component options, class naming should be of the format .componentname-optionname.

The skin name should be applied to the component in addition to the generic CSS class for the component itself. Thus, a FooWidget using the skin named "Quux" might get the CSS classes foowidget foowidget-skin-quux. (This allows the class foowidget to contain any CSS that the component needs independent of a given skin.)

JavaScript standards

Components that need JavaScript behavior should have an implementation in visage inside library/src/vanilla-js/ to be written in vanilla JavaScript, to avoid a dependency on any platform.

UI components must exhibit limited, specific behavior, and not one-off behaviors. If a business owner feels strongly that a non-standard behavior is necessary, the Visage team should make an explicit decision to add this feature.

CSS standards

Property order

We prefer that properties be logically grouped (e.g. all the font-related properties together) rather than in alphabetical or random order.

The typical order is:

  • positioning, including top/left/right/bottom/z-index

  • display, including any flex properties

  • width and height

  • margin and padding

  • fonts and typography

  • background

  • border

  • box-shadow and outline

  • anything else, including transitions, transforms, and filters

Keep CSS flat

Keep your CSS as "flat" as you can, with short selectors and not much LESS/SASS nesting. (We have not always done this in the past, but it is now a best practice. Flat stylesheets help decouple styling concerns from the structure of the markup.) This means that, whenever possible, you should put classes on sub-elements rather than rely on ancestor/descendant selectors.

Styling default tags

When developing a new component, require the component to place a specific class on the element(s). Except in rare cases (e.g. basic typography), Visage should not style all instances of a given tag. For instance, even though we anticipate wanting all of our dropdowns to have our branded look, we still require the class "stylized-select" on the <select> tag.

(There are a few exceptions to this rule, notably the h1 to h6 tags.)

State

When a component needs to keep track of state via CSS, use a CSS class rather than a data attribute. This is because a change in the state often means a change in styling, and so we can then use the CSS class for that styling.

Place the "state" class as high up as you can, ideally on the outermost element. This is because you can easily create a CSS selector that checks for the presence of an ancestor, but you cannot create a cross-browser selector that checks for the presence of a descendant. So if the state affects the entire component, place it on the outermost element; if the state refers only to a portion of the component, place it as far out as you can.

Themes

Visage themes use CSS custom properties. The names of the custom properties use dashes to separate pieces, and follow this format:

  • --visage

  • then the name of the component (e.g. selection-set)

  • then the name of any sub-component inside the component (e.g. image)

  • then the CSS property being modified (e.g. margin). Exception: font color is listed as font-color rather than just color, so that it stays grouped with the other font properties.

  • then any skin or variant of the component (e.g. primary)

  • then any pseudo-element or pseudo-class (e.g. checked)

So for the Selection Set component with the "buttons with images "skin, the custom property that adjusts the margin on the image inside the component is --visage-selection-set-image-margin-buttons-with-images. For the "buttons" skin, the custom property that sets the border color on the component when it is checked is --visage-selection-set-border-color-buttons-checked.

Committing

We use a lightweight commit-message convention in order to auto-generate release notes and to automatically bump the version of the library in CI.

Every commit must specify the "type" of change that is being made

If the commit convention is not followed, the library will not be able to automatically publish a release during CI, so it's super important that everyone uses this pattern.

Valid Commit Types

  • Breaking - A breaking change (major release)

  • New - A new feature (minor release)

  • Fix - A bug fix (patch release)

  • Chore - A change that has no bearing on the exported files (e.g. eslint change or adding a test) (no release)

e.g.

bash
# add a new feature
git commit -am "New: Add TextButton component"
# fix a bug
git commit -am "Fix: Add null-check before rendering children"
# update an API
git commit -am "Breaking: Remove TextButton 'checkout' skin"
# add an eslint plugin
git commit -am "Chore: Add jsx-a11y eslint plugin"

Tools

In order to make sure that all commits follow our convention, we've set up a few scripts/tools.

Husky

Husky is a tool which allows us to run custom scripts as part of the git lifecycle. We leverage Husky in order to run a commit-message analyzer (Commitlint) before each commit so that we can provide fast feedback if you've made a typo or forgotten about the convention.

Husky is easily disabled/circumvented (even inadvertently), so--while this is great for fast feedback--it's not quite enough to ensure that only "conventional" commits make it to master.

GitLab Push Rule

GitLab allows you to configure rules for commit messages based on a regular expression. This ensures that the remote will reject any commits that don't match the desired pattern.

This is configured in the GitLab UI under Settings/Repository/Push Rules.

javascript
/^(?:Breaking|Chore|New|Fix): .+$/

Release Notes

When we push release notes to GitLab during our releases, GitLab will reject the notes because they don't match the push-rule. GitLab may eventually make the push-rule configurable so that it only validates "actual" commits, but we have to use a workaround for now.

GitLab issue

The workaround is to expand our push-rule regular expression so that the release notes commits are allowed, so the actual regular expression that we're using is:

javascript
/(^(?:Breaking|Chore|New|Fix): .+$|^(Notes added by \'git notes add\'))/

Which uses our commit convention but also permits messages that start with "Notes added by 'git notes add'".

Squashing

Be careful when squashing commits (especially when merging) because squashing causes us to lose all of the original commit messages. If multiple commits make more sense squashed together, that's fine, but just make sure that the final commit message follows the convention and has an appropriate prefix.

Configuring

If you want to make a change to the commit-message convention, you can update /commit-convention.config.js. Just make sure that you also update the GitLab push rule to take into account any changes!

CI

Our CI pipeline is described in the root .gitlab-ci.yml. At a high level, the pipeline works like this:

1. install

Installs deps into node_modules and caches them for all future jobs.

Even though the node_modules should be available in the cache, each job should assume that the cache might have expired by the time it has run. We treat the availability of the cache as an optimization, not a requirement/guarantee.

2. prebuild

We run our test suite and linter in parallel. Nothing fancy here. If tests fail or the linter complains, the pipeline will fail.

We also run our automated release tooling in "dry-run" mode at this stage in order to preview what the new version will be if we publish.

We need to know the new version number at build-time for our sites (e.g. so that the docs site can display the current version number in its header)

Knowing the new version number ahead of time also allows us to skip some CI steps if we don't detect a version-change.

3. build

We build the library and all of the sites in parallel in this step. Note that the library does not need to be built in order for the sites to be built. This is because we've set up our sites to build based on the library's source code rather than its built assets. This is also the reason we can develop on our sites without re-building the library on every change.

This step stores the built artifacts for the library and each site so that they can be published in a later step.

This marks the end of the CI pipeline for non-master builds.

4. prepublish

Runs some AWS Cloudformation scripts in order to make sure that the proper AWS resources are provisioned.

5. publish

Runs our automated release tooling in "write" mode.

Analyzes our commit-messages, determines the proper next-version, bumps the version in the library package.json and each of the sites' package.jsons, commits the changes and tags the commit with the version number, publishes all packages to NPM, and publishes a "release" to GitLab.

Each site is published by uploading the files to S3.

We have two different "types" of sites: versioned and un-versioned.

Versioned sites (docs, storybook, playroom) upload their files once at the root of the bucket (overwriting the previous version) and they upload the same files again inside of a folder with the corresponding version-number. These sites will show the latest content when you visit the root of the site, but also allow folks to visit historical versions of the site by including a version in the url.

Un-versioned sites (contrib) are simpler. They just upload the files once at the root and overwrite the previous version every time.

This step requires GL_TOKEN (GitLab token) to be defined as an env var in order to enable pushing/tagging commits and creating releases.

This step requires NPM_TOKEN to defined as an env var in order to enable publishing to Vistaprint Artifactory.