pnpm install
pnpm start
pnpm start
- start the H5Web stand-alone demopnpm start:storybook
- start the component library's Storybook documentation site at http://localhost:6006
pnpm install
- install the dependencies of every project in the workspace and of the workspace itselfpnpm --filter <project-name> add [-D] <pkg-name>
- add a dependency to a project in the workspacepnpm [run] <script> [--<arg>]
- run a workspace scriptpnpm [run] --filter {packages/*} [--parallel] <script> [--<arg>]
- run a script in every project in thepackages
folderpnpm [exec] <binary>
- run a binary located innode_modules/.bin
(equivalent tonpx <pkg-name>
for a package installed in the workspace)pnpx <pkg-name>
- fetch a package from the registry and run its default command binary (equivalent tonpx <pkg-name>
)pnpm why -r <pkg-name>
- show all project and packages that depend on the specified packagepnpm outdated -r
- list outdated dependencies in the workspacepnpm up -rL <pkg-name>
- update a package to the latest version in every project
- Run
pnpm outdated -r
to list dependencies that can be upgraded. - Read the changelogs and release notes of the dependencies you'd like to upgrade. Look for potential breaking changes, and for bug fixes and new features that may help improve the codebase.
- Run
pnpm up -rL <pkg-name>
to upgrade a dependency to the latest version in all projects. Alternatively, you can either replace-r
with--filter
to target specific projects, or edit the relevantpackage.json
file(s) manually and runpnpm install
(but make sure to specify an exact dependency version rather than a range - i.e. don't prefix the version with a caret or a tilde). - Run
pnpm up -r @types/node @types/react @types/react-dom @types/jest
to upgrade DefinitelyTyped packages that must remain in sync with the major versions of the libraries they provide types for.
Note that pnpm
offers multiple solutions for dealing with peer dependency
version conflicts and other package resolution issues:
pnpm.overrides
,
pnpm.packageExtensions
peerDependenciesMeta
,
.pnpmfile.cjs
.
pnpm.overrides
is currently used to force the version of ESLint to the one
required by eslint-config-galex
. This is needed because, in the demo
project, vite-plugin-eslint
depends on an older version of ESLint.
To reference a workspace dependency, use pnpm's
workspace protocol
with the *
alias - e.g. "@h5web/lib": "workspace:*"
. This tells pnpm to link
the dependency to its corresponding workspace folder, and saves you from having
to keep the version of the dependency up to date. During publishing, pnpm
automatically replaces workspace:*
with the correct version.
A workspace dependency's package.json
must include a main
field pointing to
the dependency's source entry file - e.g. src/index.ts
. This is the key to
this monorepo set-up, as it avoids having to run watch tasks in separate
terminals to automatically rebuild dependencies during development.
Obviously, a package's main
field cannot point to its source TypeScript entry
file once published, as consumers may not understand TypeScript. Additionally,
package.json
needs to point to more entry files (type declarations, ESM build,
etc.) and do so in a way that is compatible with various toolchains (webpack 4,
webpack 5, Parcel, Rollup, Vite, CRA, etc.) pnpm provides a nice solution to
this problem in the form of the
publishConfig
field.
H5Web uses the Feather icon set.
Icons can be imported as React components from react-icons/fi
.
pnpm build
- build the H5Web stand-alone demopnpm build:storybook
- build the component library's Storybook documentation sitepnpm serve
- serve the built demo at http://localhost:5173pnpm serve:storybook
- serve the built Storybook at http://localhost:6006pnpm packages
- build packages (cf. details below)
The build process of @h5web/lib
works as follows:
-
First, Vite builds the JS bundles (ESM and CommonJS) in library mode starting from the package's entrypoint:
src/index.ts
. The bundles are placed in the outputdist
directory and referenced frompackage.json
.The JS build also generates a file called
style.css
in thedist
folder that contains the compiled CSS modules that Vite comes across while building the React components. These styles are called "local" styles. -
Second, we run two scripts in parallel:
build:css
andbuild:dts
.- The job of
build:css
is to build the package's global styles and concatenate them with the local styles compiled at the first step. To do so, we run Vite again but with a different config:vite.styles.config.js
, and a different entrypoint:src/styles.ts
. The output files are placed in a temporary folder:dist/temp
. We then concatenatedist/temp/style.css
(the global styles) anddist/style.css
(the local styles) and output the result todist/styles.css
, which is the stylesheet referenced frompackage.json
that consumers need to import. - The job of
build:dts
is to generate type declarations for package consumers who use TypeScript. This is a two step process: first we generate type declarations for all TS files in thedist-ts
folder withtsc
, then we use Rollup to merge all the declarations into a single file:dist/index.d.ts
, which is referenced frompackage.json
. Note that since@h5web/shared
is not a published package, it cannot be marked as an external dependency; its types must therefore be inlined intodist/index.d.ts
, so we make sure to tell Rollup where to find them.
- The job of
The build process of @h5web/app
is the same with one exception: in addition to
importing the package's global styles, src/styles.ts
also imports the lib
package's distributed styles - i.e. the output of the lib's build:css
script.
The lib's distributed styles include both its global and local styles. This
allows us to provide a single CSS bundle for consumers of @h5web/app
to
import.
The build process of@h5web/h5wasm
is also the same as the lib's, but since the
package does not include any styles, vite build
does not generate a
style.css
file and there's not build:css
script.
Finally, since @h5web/shared
is not a published package, it does not need to
be built with Vite. However, its types do need to be built with tsc
so that
other packages can inline them in their own dist/index.d.ts
.
pnpm prettier
- check that all files in the workspace have been formatted with Prettierpnpm lint
- lint and type-check every project in the workspace with ESLint and TypeScript, as well as the workspace root andcypress
folderpnpm lint:eslint
- lint every project with ESLintpnpm lint:tsc
- type-check every project with TypeScriptpnpm [--filter <project-name|{folder/*}>] lint:eslint
- lint specific projectspnpm [--filter <project-name|{folder/*}>] lint:tsc
- type-check specific projectspnpm --filter @h5web/<lib|app> analyze
- analyze a package's bundle (run only after building the package)
pnpm prettier --write
- format all files with Prettierpnpm lint:eslint --fix
- auto-fix linting issues in every projectpnpm [--filter <project-name|{folder/*}>] lint:eslint --fix
- auto-fix linting issues in specific projects
Most editors support fixing and formatting files automatically on save. The configuration for VSCode is provided out of the box, so all you need to do is install the recommended extensions.
pnpm test
- run unit and feature tests with Jestpnpm test --watch
- run tests related to changed files in watch modepnpm test --watchAll
- run all tests in watch modepnpm --filter <project-name> test
- run Jest in a specific projectpnpm cypress
- open the Cypress end-to-end test runner (local dev server must be running in separate terminal)pnpm cypress:run
- run end-to-end tests once (local dev server must be running in separate terminal)
Note that the workspace's
test
script doesn't recursively run thetest
script in every project like (i.e. it is not equivalent topnpm -r test
). Instead, it runs Jest globally using aprojects
configuration located injest.config.json
. This results in a nicer terminal output when running tests on the entire workspace.
The @h5web/app
package includes feature tests written with
React Testing Library.
They are located under src/__tests__
. Each file covers a particular subtree of
components of H5Web.
H5Web's feature tests typically consist in rendering the entire app with mock
data (i.e. inside MockProvider
), executing an action like a real user would
(e.g. clicking on a button, pressing a key, etc.), and then expecting something
to happen in the DOM as a result. Most tests, perform multiple actions and
expectations consecutively to minimise the overhead of rendering the entire app
again and again.
MockProvider
resolves most requests instantaneously to save time in tests, but
its API's methods are still called asynchronously like other providers. This
means that during tests, Suspense
loading fallbacks render just like they
would normally; they just don't stick around in the DOM for long.
This adds a bit of complexity when testing, as React doesn't like when something happens after a test has completed. In fact, we have to ensure that every component that suspends inside a test finishes loading before the end of that test. This is where Testing Library's asynchronous methods come in.
To allow developing and testing loading interfaces, as well as features like
cancel/retry, MockProvider
adds an artificial delay of 3s (SLOW_TIMEOUT
) to
some requests, notably to value requests for datasets prefixed with slow_
.
In order for this artificial delay to not slow down feature tests, we must use
fake timers. This is done
by setting the withFakeTimers
option when calling renderApp()
:
renderApp({ withFakeTimers: true });
You can use Testing Library's
prettyDOM
utility
to log the state of the DOM anywhere in your tests:
console.debug(prettyDOM()); // if you use `console.log` without mocking it, the test will fail
console.debug(prettyDOM(screen.getByText('foo'))); // you can also print out a specific element
To ensure that the entire DOM is printed out in the terminal, you may have to
set environment variable DEBUG_PRINT_LIMIT
to a large value
when calling pnpm test
.
Cypress is used for end-to-end testing but also for visual regression testing. The idea is to take a screenshot (or "snapshot") of the app in a known state and compare it with a previously approved "reference snapshot". If any pixel has changed, the test fails and a diff image highlighting the differences is created.
Taking consistent screenshots across platforms is impossible because the exact
rendering of the app depends on the GPU. For this reason, visual regression
tests are run only on the CI. This is done through an environment variable
called CYPRESS_TAKE_SNAPSHOTS
.
Visual regression tests may fail in the CI, either expectedly (e.g. when implementing a new feature) or unexpectedly (when detecting a regression). When this happens, the diff images and debug screenshots that Cypress generates are uploaded as artifacts of the workflow, which can be downloaded and reviewed.
If the visual regressions are expected, the version-controlled reference
snapshots can be updated by posting a comment in the Pull Request with this
exact text: /approve
. This triggers the Approve snapshots workflow, which
runs Cypress again but this time telling it to update the reference snapshots
when it finds differences and to pass the tests. Once Cypress has updated the
reference snapshots, the workflow automatically opens a PR to merge the new
and/or updated snapshots into the working branch. After this PR is merged, the
visual regression tests in the working branch succeed and the branch can be
merged into main
.
Here is the summarised workflow (also described with screenshots in PR #306):
- Push your working branch and open a PR.
- If the
e2e
job of the Lint & Test CI workflow fails, check out the logs. - If the fail is caused by a visual regression (i.e. if a test fails on a
cy.matchImageSnapshot()
call), download the workflow's artifacts. - Review the snapshot diffs. If the visual regression is unexpected: fix the bug, push it and start from step 2 again. If the visual regression is expected: continue to step 5.
- In the PR, post a comment with
/approve
. - Go to the Actions page and wait for the Approve snapshots workflow to complete.
- Go to the newly opened PR titled Update Cypress reference snapshots.
- Review the new reference snapshots once more and merge the PR.
- Go back to your main PR and wait for the jobs of the Lint & Test workflow to succeed.
- The project's
main
branch is continuously deployed to https://h5web.panosc.eu/ with Netlify. - The component library's Storybook documentation site is deployed to GitHub Pages on every release: https://h5web-docs.panosc.eu
To release a new version and publish the packages to NPM:
- Check out
main
and pull the latest changes. - Make sure your working tree doesn't have uncommitted changes and that the
latest commit on
main
has passed the CI. - Run
pnpm version [ patch | minor | major | <new-version> ]
This command bumps the version number in the workspace's package.json
, commits
the change and then tags the commit with the same version number. The
postversion
script then runs automatically and pushes the new commit and the
new tag to the remote repository. This, in turn, triggers the Release workflow
on the CI, which builds and publishes the packages to NPM (with pnpm publish
)
and deploys the Storybook site.
A few things happen when
pnpm publish
is run inside each package's directory:
- First, a
prepack
script is triggered that removes thetype
field from the package'spackage.json
. The reason for this workaround is explained in #1219.- Then, pnpm modifies
package.json
further by merging in the content of thepublishConfig
field.- Finally, the package gets published to NPM. Note that it's possible to publish to a local registry for testing purposes (e.g. Verdaccio) by overriding NPM's default
registry
configuration.
Once the Release workflow has completed:
- Make sure the new package versions are available on NPM and that the live Storybook site still works as expected.
- Upgrade and test the packages in apps and code sandboxes, as required.
- Write and publish release notes on GitHub.
The beta release process described below allows publishing packages to NPM with
the next
tag (instead of the default latest
tag) so they can be beta-tested
before the official release.
- Follow steps 1 and 2 of the normal release process.
- At step 3, run
pnpm version <x.y.z-beta.0>
(incrementing the beta version number as needed).
The CI will then build and deploy the packages with pnpm publish --tag next
.
Once the Release workflow has completed, check that the beta packages have
been published with the correct tag by running npm dist-tag ls @h5web/lib
.
This command should print something like:
latest: <a.b.c>
next: <x.y.z>-beta.0
You can then install the beta packages with npm install @h5web/lib@next
or the
like and make sure that they work as expected. Once you're done testing, follow
the normal release process, making sure to run pnpm version <x.y.z>
at step 3
(without the beta
suffix).
Once the release process has completed, you can remove the next
tag from the
obsolete beta packages by running
npm dist-tag rm @h5web/lib@<x.y.z>-beta.0 next