Documentation
¶
Overview ¶
Package jswork discovers the packages of a JavaScript workspace, and knows which one depends on which.
npm, pnpm, Yarn (classic and Berry) and Bun are four package managers with four lockfiles and one idea of what a workspace is, so this graph is one graph and not four:
verify.Expand("test", jswork.Packages()).
Affected(change.FromTrigger(ev)).
Template(func(u senro.Unit) *senro.StepBuilder {
return senro.NewStep(exec.Command("npm", "test")).WorkDir(u.Dir)
})
A unit is one workspace package: its ID and Dir are the package directory relative to the root, and its Name is the "name" from its package.json, which is what `pnpm --filter` and `npm -w` want.
It reads manifests, and runs nothing ¶
No package manager is needed and none is run, so this works with no node installed and nothing yet installed into the tree. The lockfiles are not read at all: four formats, and none says anything about the workspace graph the manifests do not already say.
Discovery is the only part that differs by manager ¶
Every directory matching the workspace's member patterns that holds a package.json is a unit. The patterns come from two places, unioned: the root package.json's "workspaces" (an ARRAY, or Yarn v1's OBJECT with "packages" in it), and pnpm-workspace.yaml's (or .yml's) "packages" list. Both are read because a pnpm repository's root package.json usually has NO "workspaces" field at all, so reading only package.json would find nothing there; the union also covers a repository mid-migration.
A pattern beginning with "!" EXCLUDES, wherever it came from. An excluded package is owned by no unit, so a change to one runs everything rather than nothing.
A tree with neither declaration is an error rather than an empty graph, and so is a declaration matching no package: an expansion that silently produced no steps is indistinguishable from one whose root was wrong. So is a pnpm-workspace.yaml whose member list this reader cannot read; see pnpmPackages.
The dependency graph itself does not differ by manager: an edge is one package.json naming another package's "name" in one of the four dependency fields. See ReverseDeps, and Owns for the ownership rules.
The honest limit: this is the DECLARED graph ¶
Nothing here parses JavaScript, so a package that imports another WITHOUT declaring it has no edge, and a change to the imported package will not run it. That is the same hole every tool in this ecosystem has (turbo, nx, lerna and `pnpm --filter` all read the declared graph), and an undeclared import is already a bug pnpm's isolated node_modules fails on. If your repository hoists and relies on undeclared imports, run every unit or declare the dependency. TypeScript project references and tsconfig "paths" are not read either; a package wired up only that way needs the dependency in its package.json too.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Graph ¶
type Graph struct {
// contains filtered or unexported fields
}
Graph is a jswork unit graph. Build one with Packages.
One Graph memoizes one reading per root, so the three calls an affected set makes walk the tree once between them. See gowork's Graph for why the memo has no expiry.
func (*Graph) Owns ¶
Owns reports which package each of files belongs to, in three rules:
- A file DIRECTLY in the workspace root belongs to EVERY package. That is the root package.json, the lockfile, pnpm-workspace.yaml, the shared tsconfig, the eslint config and the turbo.json, every one of which can change what every package builds.
- Otherwise the nearest package directory at or above the file owns it.
- Otherwise NO package owns it, and unit.Affected runs everything. A docs/ directory beside the packages is that case, and so is a package the workspace patterns EXCLUDE: it is not a unit, so nothing owns its files, and a change to it is a change nothing can be concluded about.
Paths are slash-separated, relative to root, and never stat'ed, so a file a change deleted is answered for exactly like one it added.
func (*Graph) ReverseDeps ¶
ReverseDeps reports the packages that DIRECTLY depend on each package, sorted.
All four dependency fields are read: dependencies, devDependencies, peerDependencies and optionalDependencies. A dev dependency is an edge for the same reason a test-only import is one in Go, and a peer dependency is an edge because a package that has to be rebuilt when its peer changes is exactly what a peer dependency describes.
A dependency resolves to a package two ways, and either draws the edge:
- its NAME matching another package's "name", whatever the version range says: "workspace:*", "^1.2.3" and "*" all draw the edge. Resolving ranges is the package manager's job, and discarding unparsed ones would drop every edge in a pnpm or Yarn Berry repository. A range that means "fetch the published one instead" over-runs; the opposite mistake skips a build. "npm:@acme/core@^1" resolves through to @acme/core. A package's DIRECTORY name is never what an edge is matched on.
- a "file:", "link:" or "portal:" specifier's PATH, resolved against the depending package's own directory.
Direct edges only. The transitive closure is unit.Affected's.