pnpm install
pnpm start
Once the development server has started, press o
to open the development URL
in your browser, or h
to show all the available keyboard shortcuts.
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] "/<regex>/" [--<arg>]
- run multiple workspace scripts in parallelpnpm [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)pnpm dlx <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).
If you run into peer dependency warnings and other package resolution issues, note that
pnpm
offers numerous solutions for dealing with them, likepnpm.peerDependencyRules.allowedVersions
.
DefinitelyTyped packages
The major versions of @types/*
packages must be aligned with the major
versions of the packages they provide types for—i.e. foo@x.y.z
requires
@types/foo@^x
.
For convenience, some @types
packages can be quickly upgraded to their latest
minor/patch version by running pnpm up -r
.
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
.
Styles are written with CSS Modules. Global styles should be avoided.
In most cases, a component that needs styling should come with its own CSS
Modules file - e.g. MyComponent.module.css
. If multiple components need to use
the same style rules, they may import the same CSS Modules file:
import styles from './ThisComponent.module.css';
import otherComponentStyles from './OtherComponent.module.css';
However, it is better to write reusable components or take advantage of a CSS Modules feature called composition:
/* utils.module.css */
.baseBtn {
color: green;
}
/* ThisComponent.module.css */
.thisBtn {
composes: baseBtn from './utils.module.css';
color: blue;
}
.selectedBtn {
composes: thisBtn;
color: red;
}
To avoid
style ordering issues,
modules that are composed from other modules (utils.module.css
in the example
above) should be used solely for composition and never be imported. In other
words, you should never compose from a class located in the CSS Modules file of
another React component:
/*
>>> BAD <<<
If `OtherComponent.module.css` gets imported after `ThisComponent.module.css,
the `.baseBtn` rule will end up after `.thisBtn` and `color: blue` will be applied.
*/
/* OtherComponent.module.css */
.baseBtn {
color: blue;
}
/* ThisComponent.module.css */
.thisBtn {
composes: baseBtn from './OtherComponent.module.css';
color: red;
}
/*
>>> GOOD <<<
Move `.baseBtn` to a utility module.
*/
/* utils.module.css */
.baseBtn {
color: blue;
}
/* OtherComponent.module.css */
.otherBtn {
composes: baseBtn from './utils.module.css';
}
/* ThisComponent.module.css */
.thisBtn {
composes: baseBtn from './utils.module.css';
color: red;
}
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 no 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 lint
- run Prettier, ESLint and TypeScript on the entire workspacepnpm lint:prettier
- check that every file is formatted with Prettierpnpm lint:eslint
- lint every project with ESLintpnpm [--filter <project-name|{folder/*}>] lint:eslint
- lint specific projectspnpm lint:root:eslint
- lint files that don't belong to projectspnpm lint:tsc
- type-check every project with TypeScriptpnpm [--filter <project-name|{folder/*}>] lint:tsc
- type-check specific projectspnpm lint:cypress:tsc
- type-check thecypress
folderpnpm --filter @h5web/<lib|app> analyze
- analyze a package's bundle (run only after building the package)pnpm --filter storybook exec storybook doctor
- diagnose problems with Storybook installation
pnpm lint: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 Vitest in watch mode (or once when on the CI)For the providers test to work, the sample HDF5 file must first be created and the h5grove support server must be running in a separate terminal; see
pnpm support:*
scripts and Providers tests section.pnpm test run
- run unit and feature tests oncepnpm test [run] <filter>
- run tests matching the given filterpnpm test -- --project <lib|app|...>
- run Vitest on a specific projectpnpm support:setup
- create/update Poetry environments required for testing the providerspnpm support:sample
- createsample.h5
pnpm support:h5grove
- start h5grove support serverpnpm 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)
Vitest is able to run on the entire monorepo thanks to the workspace configuration defined in
vitest.workspace.ts
. It then uses each project's Vite configuration to decide how to run the tests.
The @h5web/app
package includes feature tests written with
React Testing Library
and running in a JSDOM environment.
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. To do so, you can use Testing Library's asynchronous APIs for finding elements and interacting with them, as well as Vitest's `waitFor`` utility.
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
.
Two data providers are currently tested through their respective APIs:
H5GroveApi
and H5WasmApi
. Each API test (<provider>-api.test.ts
) works as
follows:
- It instanciates the API using a sample file called
sample.h5
, located insupport/sample/dist
, that contains a lot of HDF5 datasets of various shapes and types. - It retrieves the values of all the datasets in the sample file and stores them in an object.
- It takes a snapshot of that object and compares it to the existing snapshot
(
<provider>-api.test.ts.snap
). - If the new snapshot is the same, the test succeeds; if the new snapshot differs, the test fails and the differences are shown.
To get up and running with testing the providers locally, we recommend using pyenv and Poetry:
- Install the Python version specified in
.python-version
with pyenv:pyenv install
- Install Poetry with
pipx:
pipx install poetry
- Create the Poetry environements:
pyenv exec pnpm support:setup
Once the Poetry environments are created, you can create sample.h5
, start
h5grove and run the API tests:
pyenv exec pnpm support:sample
pyenv exec pnpm support:h5grove
pyenv exec pnpm test api
If the Python version specified in
.python-version
is globally available on your system, you may run the scripts above withoutpyenv exec
.
If you need to intervene on the
support
projects and the Python version specified in.python-version
is not globally available, make sure to run all Poetry commands from the root of the monorepo as follows:
```bash
pyenv exec poetry -C support/<project> <cmd>
pyenv exec poetry -C support/<project> add <dep>
pyenv exec poetry -C support/<project> run python <script>.py
```
If you're unable to create the sample file (for instance because your
environment lacks support for float128
), you may
download it from silx.org and place
it into the support/sample/dist
folder. However, please beware that the file
may not be up to date.
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.
It is also possible to download the artifacts from the CI and manually commit/push the updated snapshots without going through the steps above. This may be the best option if, for instance, the visual diffing tests fail on the
main
branch after a commit is pushed to it directly or after a PR is forcefully merged.
- 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
Note that the version of pnpm that Netlify installs by default is outdated and incompatible with this monorepo. We use the
packageManager
entry in the rootpackage.json
to specify a more recent version.
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> ]
The
pnpm version
command:
- bumps the version in the workspace's
package.json
;- copies the new version into each package's
package.json
(via theversion
script);- commits and tags the changes, and then pushes the new commit and the new tag to the remote repository (via the
postversion
script).This, in turn, triggers the Publish packages and Deploy Storybook workflows on the CI, which builds and publishes the packages to NPM (with
pnpm -r publish
) and deploys the Storybook site.A few things happen when
pnpm publish
runs for each package:
- First, it triggers a
prepack
script 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 CI workflows have run successfully:
- 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 Publish packages workflow has run successfully, check that the beta
packages are 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 you've completed the release process, you may remove the next
tag from
the obsolete beta packages by running
npm dist-tag rm @h5web/lib@<x.y.z-beta.0> next
To test a package locally in another project without publishing it to NPM, follow these steps:
- Run
pnpm packages
. - Navigate to the package's directory - e.g.
cd packages/app
. - Run
pnpm pack
to pack the package into a tarball, optionally passing a target directory for the tarball with--pack-destination <dir>
. - Navigate to the project in which you want to install and test the package.
- Install the tarball with the project's package manager (e.g.
pnpm add <path-to-tarball>
).
Like
pnpm publish
,pnpm pack
runs the package'sprepack
script, which removes"type": "module"
frompackage.json
, so don't forget to revert this change when you're done.