Syncpack is a Rust binary crate that creates a command line application for ensuring consistency in the contents of multiple package.json files, particularly focusing on dependency versions.
If you're using an LLM (like Claude, ChatGPT, or Copilot) to assist with development, see .notes/index.md for a comprehensive development hub optimised for LLM context. It includes:
- Essential file locations and their purposes
- Core data structures and their relationships
- Common development patterns and tasks
- Test writing guidelines with examples
- Naming conventions and code organization
- Common pitfalls and how to avoid them
This document (CONTRIBUTING.md) provides the high-level architecture and workflow. The .notes/ directory provides concrete patterns and examples for day-to-day development tasks.
main - The most recently published version of the Rust v14 alpha version of the codebase.
13.x.x - The most recently published version of the TypeScript v13 version of the codebase which was replaced.
- All source code is located in files at
./src/**/*.rs - All tests are located in files at
./src/**/*_test.rs - The documentation website is located in
./site/src/** - The
./fixtures/fluid-frameworkdirectory is an example project which can be used to run the local development version of syncpack against for testing locally. - The
./npmdirectory contains files used when deploying syncpack to npm, a rust binary for each major OS needs publishing as npm packages which are then set as optionalDependencies of the main syncpack package, which then has a small node.js script to run the appropriate binary. This is not needed during local development.
The source code for the documentation website is located in ./site/src/**, the sitemap for the published website is located at https://syncpack.dev/sitemap-index.xml. Read this sitemap to find what documentation is available to help you with a given topic.
Important commands:
just test- Run all testsjust lint- Run all linting checksjust coverage- Run all tests and generate a coverage report, this can help find unused code or identify real world use cases we do not have tests for.just benchmark- When making performance improvements, run this command before and after each change to compare the performance of the current version of syncpack with the previous version.just format- Fix formatting, indentation etc of all files
Run just to see a list of all other available commands and their descriptions.
When deployed and installed globally, the end user help documentation for syncpack as a whole can be found by running:
syncpack --helpThe equivalent command when running a local development version of syncpack is:
cargo run -- --helpTo view the help documentation for each command:
cargo run -- lint --help
cargo run -- fix --help
cargo run -- format --help
cargo run -- update --help
cargo run -- list --help
cargo run -- json --help- Unit tests are co-located with source files as
*_test.rsand tend to test complex functions in isolation. - The preferred tests are those at
src/visit_packages/**/*_test.rsandsrc/visit_formatting/**/*_test.rsas they are integration tests which resemble real world use cases. - Integration tests use the builder pattern in
src/test/builder.rsvia theTestBuilderstruct. TheTestBuilderstruct provides a fluent API for creating test cases that consist of package.json files, syncpack configuration files, and command line inputs. - The
TestBuilderhas a.build()method which returns aContextstruct in the correct state to reproduce the required test scenario. There is also abuild_and_visit_packages()method which returns aContextstruct which has also been passed throughvisit_packages()for convenience. - Mock utilities are available in
src/test/mock.rs - The
expectfunction atsrc/test/expect.rsreceives aContextstruct and asserts that it is in the expected state. theVecit receives is a list which must contain every expected package.json file that should be present in the context after the test has been run, in the expected state. - Examples of good tests to emulate can be found in
src/visit_packages/banned_test.rs.
Every syncpack command follows the same pattern:
This phase is read only and must happen in this order:
- Nothing can happen until the command line arguments are known
- We can then use that information to locate the configuration file
- Only then can we know which package.json files to read
- When the package.json files are read, we can collect all of their versions and dependencies, and assign them to the appropriate version and semver groups defined in the user's configuration.
More information on each of these steps is as follows:
Determine which command and each CLI options were chosen and collect them into a Cli struct. Any options which were not provided are assigned default values.
- src/cli.rs is responsible for this.
- Determine path to config file, first one wins:
--configCLI Option- Search in the root directory of the project for the first file whose name matches a specific list of config file names
- Once a config file path has been determined, read its contents. It must be one of:
- TypeScript
- JavaScript
- YAML
- JSON
- src/rcfile.rs is responsible for finding and reading the config file as an
Rcfilestruct. - src/config.rs defines a
Configstruct which combines theCliandRcfilestructs into one.
Now that we have a Config struct, we can use it to get paths to package.json files:
- Find globs to package.json files, first one wins:
--sourceCLI Options- Syncpack config
- npm workspace config in the root package.json file
- pnpm workspace config in pnpm's config file
- Yarn workspace config in the root package.json file
- Lerna workspace config in Lerna's config file
- Syncpack defaults
- Resolve globs
- Read and Parse package.json files
- src/packages.rs is responsible for reading and parsing package.json files into a
Packagesstruct containing eachPackageJsonstruct for each package.json file.
Now that we have a Config struct and Packages struct, we can collect the project's dependencies and assign them to version groups.
- Partition the monorepo by versioning policy ("version groups")
- Load every "instance" (eg. @effect/schema in devDependencies of @effect/platform-node) of every "dependency" (eg. @effect/schema)
- Parse and tag its version specifier (eg.
"1.2.1","workspace:*","catalog:","git://github.com/user/repo.git")
- Parse and tag its version specifier (eg.
- Assign every instance to one version group, first one wins
- src/context.rs is responsible for collecting project dependencies and assigning them to version groups. These are all returned in a
Contextstruct alongside all other data we have collected such as theConfigandPackagesstructs.
In terms of Rust's ownership and borrowing, the Context struct has ownership of all of the data related to the project being operated on. The Context struct is given in its entirety to either the visit_packages or visit_formatting functions.
Located at src/visit_packages.rs, this function will:
- Visit each version group, each dependency within it, and each instance within that.
- Tag every instance with an instance of an
InstanceStateenum to describe if it is valid, or specifically how it is not. - Return ownership of the
Contextstruct.
Located at src/visit_formatting.rs, this function will:
- Visit each package.json file
- Tag every package.json file with multiple status codes describing if its formatting is valid, or specifically how it is not
- Return ownership of the
Contextstruct
Finally, the command chosen by the user is passed the Context struct and has full ownership of it. Each command will perform its own side effects such as updating or synchronising the project's dependencies, or formatting the project's files. Every command must finish by returning and exit code of 1 or 0 to exit the program with.