A pnpm workspace for the Endevops XML packages, built on Vite+ for the toolchain.
| Package | Description |
|---|---|
@endevops/effect-codec-xml |
Round-trip Effect Schema codec for XML, entity decoder and XML name validation |
@endevops/benchmarks |
Private project that runs the benchmarks, never published |
One publishable package and one private project. effect-xml-codec is
standalone: it has no sibling workspace dependency, and the entity decoder and
name validators it reads documents with live inside it (merged in from the
former @endevops/common-xml). packages/benchmarks depends on it, ships
nothing, and exists only to run the benchmarks.
packages/<name>/ publishable package: src/, test/, package.json
packages/benchmarks/ private benchmark project: bench/, never built or published
vite.config.ts Oxlint, Oxfmt, staged checks — one config for the whole repo
tsconfig.shared.json compilerOptions every package extends
pnpm-workspace.yaml package globs, version catalog, overrides
A package that absorbed several of the earlier ones keeps a subdirectory per
absorbed area, and re-exports all of them flat from src/index.ts.
effect-xml-codec keeps the codec at the top of src/, and the absorbed
primitives under src/entities/ and src/naming/. Specs sit under the matching
test/ subdirectory and import from #/index.ts like any other module in the
package. Nothing is exposed per-area at the package boundary: one package, one
entry point.
vp lint, vp fmt, and vp check read the lint and fmt blocks in the root
vite.config.ts even when you run them from inside a package, and Oxlint and
Oxfmt ignore nested configs in Vite+ mode. The shared root config keeps a
package from quietly opting out of the shared lint and format rules. When one package needs
different settings, add a lint.overrides or fmt.overrides entry keyed on
packages/<name>/** rather than a config file in the package.
A package's own vite.config.ts holds only what is specific to that package,
here, the tsdown pack options. The root config has no pack block, because
vp pack at the root needs a target and would otherwise be ambiguous.
Run from the workspace root:
vp install # install, via the packageManager field
vp check # format, lint, type-check — every package
vp test # every package's test suite
vp run bench # the `*.bench.ts` benchmarks, no tests
pnpm build # vp run -r build — pack every packageFor one package, target it with -C, which behaves exactly like cd-ing there:
vp -C packages/effect-xml-codec check
vp -C packages/effect-xml-codec test
vp -C packages/effect-xml-codec packBenchmarks are Vitest benchmarks, not scripts. A
file named *.bench.ts is collected by the benchmark project, which vp test skips entirely and
vp test bench runs on its own. Keeping them out of vp test avoids the slow, noisy runs: nothing
about the suite should depend on a number that moves with the weather.
Benchmarks that drive a package's public entry point live in packages/benchmarks. It is
private, so pnpm -r publish skips it and it has no build step. It resolves each workspace
dependency to its built dist/, so run pnpm build before it, and the numbers describe what
consumers install.
vp run bench runs each package's bench script in its own project.
pnpm build # the benchmark project reads dist/
vp run bench # every benchmark
vp -C packages/benchmarks test bench # the built-output benchmarks
vp -C packages/benchmarks test bench -t "a small document" # one test nameA bare vp pack at the root refuses to guess between the root and the packages
and prints the commands to use instead. That is the intended behaviour once
more than one package exists.
mkdir -p packages/<name>with its ownpackage.json, and declare it inpnpm-workspace.yamlif the name does not matchpackages/*.- Add a
tsconfig.jsonthat extends../../tsconfig.shared.json. Keeppathsandincludethere, not in the shared file. Both resolve relative to the file that declares them, so apathsentry in the shared file would anchor to the root. - Take shared dependency versions from the
cataloginpnpm-workspace.yamlwith"<dep>": "catalog:", so one edit updates every package. - Add a
packblock to the package'svite.config.tsif it ships a build. Leave lint and format settings alone.
Releases are automatic. semantic-release runs in CI on master, develop,
feature/* and hotfix/*, derives the next version from
Conventional Commits, writes that one
version into the root manifest and every package manifest, and publishes every
public package at it with pnpm -r publish. See
docs/versioning.md for the branches, bump rules and
channels.
prepublishOnly runs vp pack in each package, so pnpm -r publish builds
each package before it goes out. The root manifest is private and is never
published, and so is packages/benchmarks: pnpm -r publish skips both, and
scripts/release/sync-versions.mjs stamps only the publishable manifests, so a
private package never appears in a release commit.
MIT. See packages/effect-xml-codec/LICENSE.