Tag your projects by layer and say which layers may depend on which. vx refuses a dependency that breaks a rule.
lock
Here UI may not depend on a utility, and packages/c (UI) depends on
packages/a:
// .vx/workspace.ts
import { defineWorkspace } from '@vzn/vx/config'
export default defineWorkspace({
run: {
rules: {
dependencies: [{ from: 'tag:type:ui', deny: ['tag:type:util'] }],
},
},
})
$ vx lock
✗ vx lock lock not written
1 dependency breaks run.rules.dependencies
packages/c depends on packages/a, which run.rules.dependencies[0] (from 'tag:type:ui') denies
fix drop the dependency, or change the project's tags or the rule
$ echo $?
1
The edge here is a task edge (c#build depends on a#build), not a
package.json dependency. Both count.
Demo workspace · vx 69617eb · 2026-10-23
Tags and rules
A project names its tags in its own config:
// packages/button/vx.config.ts
import { defineProject } from '@vzn/vx/config'
export default defineProject({
tags: ['type:ui'],
tasks: { build: { exec: { command: 'tsc -b' } } },
})
The workspace says what each tag may depend on. A selector is
tag:<glob> or a project-name glob:
// .vx/workspace.ts
import { defineWorkspace } from '@vzn/vx/config'
export default defineWorkspace({
run: {
rules: {
dependencies: [
{ from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] },
{ from: 'tag:type:ui', deny: ['tag:type:feature', 'tag:type:app'] },
],
},
},
})
A dependency must match one of allow (if given) and none of deny.
A deny beats any allow. A project no rule selects is free.
vx lock checks them, runs don't
vx lock checks each project's direct dependencies: package.json and
cross-project dependsOn. It names every broken edge and writes
nothing. Fix the edge and the lock is written:
$ vx lock
✓ vx lock 6 project configs → .vx/projects.lock.json
$ vx lock --check
✓ vx lock --check up to date (6 projects)
vx lock --check in CI fails the same way, so a pull request with a
forbidden dependency goes red.
Runs read the lock, which already passed the rules, so they pay nothing
(vx lock).
From Nx
nx() from @vzn/vx-migrate reads the depConstraints of
@nx/enforce-module-boundaries from your ESLint config and fills
run.rules.dependencies.
It reads the file without running it, so ESLint never loads on a vx run.
// eslint.config.mjs (Nx)
depConstraints: [
{ sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:ui'] },
{ sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:feature'] },
]
becomes:
dependencies: [
{ from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] },
{ from: 'tag:type:ui', deny: ['tag:type:feature'] },
]
One difference: Nx's lint
rule
checks source imports. vx checks declared dependencies.
allSourceTags, /regex/ tags and external-import bans are left out.
The vx-migrate
README
lists them.
The schema is in the configuration reference. More at
vznjs.github.io/vx.
Originally published on the vx blog. vx is open-source (MIT) task orchestration with a build cache: vznjs.github.io/vx · GitHub.
Written with AI assistance.
Top comments (0)