This document is intended for developers interest in making contributions to Preact and document our internal processes like releasing a new version.
This steps will help you to set up your development environment. That includes all dependencies we use to build Preact and developer tooling like git commit hooks.
- Clone the git repository:
git clone [email protected]:preactjs/preact.git
- Go into the cloned folder:
cd preact/
- Install all dependencies:
npm install
This repository contains Preact itself, as well as several addons like the debugging package for example. This is reflected in the directory structure of this repository. Each package has a src/
folder where the source code can be found, a test
folder for all sorts of tests that check if the code in src/
is correct, and a dist/
folder where you can find the bundled artifacts. Note that the dist/
folder may not be present initially. It will be created as soon as you run any of the build scripts inside package.json
. More on that later ;)
A quick overview of our repository:
# The repo root (folder where you cloned the repo into)
/
src/ # Source code of our core
test/ # Unit tests for core
dist/ # Build artifacts for publishing on npm (may not be present)
# Sub-package, can be imported via `preact/compat` by users.
# Compat stands for react-compatibility layer which tries to mirror the
# react API as close as possible (mostly legacy APIs)
compat/
src/ # Source code of the compat addon
test/ # Tests related to the compat addon
dist/ # Build artifacts for publishing on npm (may not be present)
# Sub-package, can be imported via `preact/hooks` by users.
# The hooks API is an effect based API to deal with component lifcycles.
# It's similar to hooks in React
hooks/
src/ # Source code of the hooks addon
test/ # Tests related to the hooks addon
dist/ # Build artifacts for publishing on npm (may not be present)
# Sub-package, can be imported via `preact/debug` by users.
# Includes debugging warnings and error messages for common mistakes found
# in Preact application. Also hosts the devtools bridge
debug/
src/ # Source code of the debug addon
test/ # Tests related to the debug addon
dist/ # Build artifacts for publishing on npm (may not be present)
# Sub-package, can be imported via `preact/test-utils` by users.
# Provides helpers to make testing Preact applications easier
test-utils/
src/ # Source code of the test-utils addon
test/ # Tests related to the test-utils addon
dist/ # Build artifacts for publishing on npm (may not be present)
# A demo application that we use to debug tricky errors and play with new
# features.
demo/
# Contains build scripts and dependencies for development
package.json
Note: The code for rendering Preact on the server lives in another repo and is a completely separate npm package. It can be found here: https://github.com/preactjs/preact-render-to-string
It's a special file that can be used to specify how terser
(previously known as uglify
) will minify variable names. Because each sub-package has it's own distribution files we need to ensure that the variable names stay consistent across bundles.
Unique to Preact we do support several ways to hook into our renderer. All our addons use that to inject code at different stages of a render process. They are documented in our typings in internal.d.ts
. The core itself doesn't make use of them, which is why the file only contains an empty object
.
We merge every PR into the main
branch which is the one that we'll use to publish code to npm. For the previous Preact release line we have a branch called 8
which is in maintenance mode. As a new contributor you won't have to deal with that ;)
We try to make it as easy as possible to contribute to Preact and make heavy use of GitHub's "Draft PR" feature which tags Pull-Requests (short = PR) as work in progress. PRs tend to be published as soon as there is an idea that the developer deems worthwhile to include into Preact and has written some rough code. The PR doesn't have to be perfect or anything really ;)
Once a PR or a Draft PR has been created our community typically joins the discussion about the proposed change. Sometimes that includes ideas for test cases or even different ways to go about implementing a feature. Often this also includes ideas on how to make the code smaller. We usually refer to the latter as "code-golfing" or just "golfing".
When everything is good to go someone will approve the PR and the changes will be merged into the main
branch and we usually cut a release a few days/ a week later.
The big takeaway for you here is, that we will guide you along the way. We're here to help to make a PR ready for approval!
The short summary is:
- Make changes and submit a PR
- Modify change according to feedback (if there is any)
- PR will be merged into
main
- A new release will be cut (every 2-3 weeks).
Scripts can be executed via npm run [script]
or yarn [script]
respectively.
build
- compiles all packages ready for publishing to npmbuild:core
- builds just Preact itselfbuild:debug
- builds the debug addon onlybuild:hooks
- builds the hook addon onlybuild:test-utils
- builds the test-utils addon onlytest:ts
- Run all tests for TypeScript definitionstest:karma
- Run all unit/integration tests.test:karma:watch
- Same as above, but it will automatically re-run the test suite if a code change was detected.
But to be fair, the only ones we use ourselves are build
and test:karma:watch
. The other ones are mainly used on our CI pipeline and we rarely use them.
Note: Both test:karma
and test:karma:watch
listen to the environment variable COVERAGE=true
. Disabling code coverage can significantly speed up the time it takes to complete the test suite.
Note2: The test suite is based on karma
and mocha
. Individual tests can be executed by appending .only
:
it.only('should test something', () => {
expect(1).to.equal(1);
});
vnode
-> shorthand forvirtual-node
which is an object that specifies how a Component or DOM-node looks likecommit
-> A commit is the moment in time when you flush all changes to the DOMc
-> The variablec
always refers to acomponent
instance throughout our code base.diff/diffing
-> Diffing describes the process of comparing two "things". In our case we compare the previousvnode
tree with the new one and apply the delta to the DOM.root
-> The topmost node of avnode
tree
- Check the JSDoc block right above the function definition to understand what it does. It contains a short description of each function argument and what it does.
- Check the callsites of a function to understand how it's used. Modern editors/IDEs allow you to quickly find those, or use the plain old search feature instead.
We have a benchmark suite that we use to measure the performance of Preact. Our benchmark suite lives in our preactjs/benchmarks repository, but is included here as Git submodule. To run the benchmarks, first ensure PNPM is installed on your system and initialize and setup the submodule (it uses pnpm
as a package manager):
pnpm -v # Make sure pnpm is installed
git submodule update --init --recursive
cd benchmarks
pnpm i
Then you can run the benchmarks:
# In the benchmarks folder
pnpm run bench
Checkout the README in the benchmarks folder for more information on running benchmarks.
Note: When switching branches, git submodules are not automatically updated to the commit of the new branch - it stays at the commit of the previous branch. This can be a feature! It allows you to work in different branches with the latest versions of the benchmarks - especially if you have made changes to the benchmarks.
However if you want to switch branches and also update the benchmarks to the latest commit of the new branch, you can run
git submodule update --recursive
after switching branches, or rungit checkout --recurse-submodules
when checking out a new branch.
Several members of the team are very fond of TypeScript and we wanted to leverage as many of its advantages, like improved autocompletion, for Preact. We even attempted to port Preact to TypeScript a few times, but we ran into many issues with the DOM typings. Those would force us to fill our codebase with many any
castings, making our code very noisy.
Luckily TypeScript has a mode where it can somewhat reliably typecheck JavaScript code by reusing the types defined in JSDoc blocks. It's not perfect and it often has trouble inferring the correct types the further one strays away from the function arguments, but it's good enough that it helps us a lot with autocompletion. Another plus is that we can make sure that our TypeScript definitons are correct at the same time.
Check out the official TypeScript documentation for more information.
Note that we have separate tests for our TypeScript definition files. We only use ts-check
for local development and don't check it anywhere else like on the CI.
There is no real reason for that other a historical one. Back before auto-formatting via prettier was a thing and minifiers weren't as advanced as they are today we used a pretty terse code-style. The code-style deliberately was aimed at making code look as concise and short as possible. The let
keyword is a bit shorter than const
to write, so we only used that. This was done only for stylistic reasons.
This helped our minds to not lose sight of focusing on size, but made it difficult for newcomers to start contributing to Preact. For that reason alone we switched to prettier
and loosened our rule regarding usage of let
or const
. Today we use both, but you can still find many existing places where let
is still in use.
In the end there is no effect on size regardless if you use const
, let
or use both. Our code is downtranspiled to ES5
for npm so both will be replaced with var
anyways. Therefore it doesn't really matter at all which one is used in our codebase.
This will only become important once shipping modern JavaScript code on npm becomes a thing and bundlers follow suit.
To be able to fix issues we need to see them on our machine. This is only possible when we can reproduce the error. The easiest way to do that is narrow down the problem to specific components or combination of them. This can be done by removing as much unrelated code as possible.
The perfect way to do that is to make a codesandbox. That way you can easily share the problematic code and ensure that others can see the same issue you are seeing.
For us a codesandbox says more than a 1000 words 🎉
We closely watch our issues and have a pretty active Slack workspace. Nearly all our communication happens via these two forms of communication.
This guide is intended for core team members that have the necessary rights to publish new releases on npm.
- Make a PR where only the version number is incremented in
package.json
and everywhere else. A simple search and replace works. (note: We followSemVer
conventions) - Wait until the PR is approved and merged.
- Switch back to the
main
branch and pull the merged PR - Create and push a tag for the new version you want to publish:
git tag 10.0.0
git push --tags
- Wait for the Release workflow to complete
- It'll create a draft release and upload the built npm package as an asset to the release
- Fill in the release notes in GitHub and publish them
- Run the publish script with the tag you created
node ./scripts/release/publish.mjs 10.0.0
- Make sure you have 2FA enabled in npm, otherwise the above command will fail.
- If you're doing a pre-release add
--npm-tag next
to thepublish.mjs
command to publish it under a different tag (default islatest
)
- Tweet it out
ATTENTION: Make sure that you've cleared the project correctly when switching from a 10.x branch.
- Run
rm -rf dist node_modules && npm i
to make sure to have the correct dependencies. - Write the release notes and keep them as a draft in GitHub
- I'd recommend writing them in an offline editor because each edit to a draft will change the URL in GitHub.
- Make a PR where only the version number is incremented in
package.json
(note: We followSemVer
conventions) - Wait until the PR is approved and merged.
- Switch back to the
main
branch and pull the merged PR - Run
npm run build && npm publish
- Make sure you have 2FA enabled in npm, otherwise the above command will fail.
- If you're doing a pre-release add
--tag next
to thenpm publish
command to publish it under a different tag (default islatest
)
- Publish the release notes and create the correct git tag.
- Tweet it out
The release notes have become a sort of tiny blog post about what's happening in preact-land. The title usually has this format:
Version Name
Example:
10.0.0-beta.1 Los Compresseros
The name is optional, we just have fun finding creative names 😉
To keep them interesting we try to be as concise as possible and to just reflect where we are. There are some rules we follow while writing them:
- Be nice, use a positive tone. Avoid negative words
- Show, don't just tell.
- Be honest.
- Don't write too much, keep it simple and short.
- Avoid making promises and don't overpromise. That leads to unhappy users
- Avoid framework comparisons if possible
- Highlight awesome community contributions (or great issue reports)
- If in doubt, praise the users.
After this section we typically follow with a changelog part that's divided into 4 groups in order of importance for the user:
- Features
- Bug Fixes
- Typings
- Maintenance
We generate it via this handy cli program: changelogged. It will collect and format
the descriptions of all PRs that have been merged between two tags.
The usual command is changelogged 10.0.0-rc.2..HEAD
similar to how
you'd diff two points in time with git. This will get you 90% there,
but you still need to divide it into groups. It's also a good idea
to unify the formatting of the descriptions, so that they're easier
to read and don't look like a mess.