@venn-lang/project 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # @venn-lang/project
2
+
3
+ > What a Venn project *is*: its manifest, its workspace members, what it builds, and where the build goes.
4
+
5
+ Every command needs the same three answers before it can do anything: which project this path
6
+ belongs to, what that project builds, and where derived files live. This package answers them once.
7
+ It touches the disk only through the `FileSystem` port from [`@venn-lang/contracts`](../contracts), and
8
+ imports no `node:*`, so the language server reads a workspace exactly the way the CLI does.
9
+
10
+ ## Usage
11
+
12
+ ```ts
13
+ import { createNodeFs } from "@venn-lang/contracts/node";
14
+ import { findProject } from "@venn-lang/project";
15
+
16
+ const { project, problems } = await findProject({ fs: createNodeFs(), from: process.cwd() });
17
+ if (!project) throw new Error(problems[0]?.title);
18
+
19
+ for (const pkg of project.packages) {
20
+ for (const target of pkg.targets) {
21
+ console.log(`${pkg.manifest.name}: ${target.kind} ${target.name} → ${target.path}`);
22
+ }
23
+ }
24
+ ```
25
+
26
+ `findProject` walks up from `from` looking for a `venn.toml`, the way `cargo` and `git` do, so a
27
+ command run three directories deep means what it means at the top. A package is claimed by an
28
+ ancestor workspace only when that workspace's `members` actually name it: sitting inside someone's
29
+ folder is not membership. When nothing is found, `project` is absent and `problems` holds one
30
+ `ProjectProblem` with code `VN2101`.
31
+
32
+ ## API
33
+
34
+ ### Discovery
35
+
36
+ | Export | What it does |
37
+ | --- | --- |
38
+ | `findProject({ fs, from })` | The project a path belongs to, members loaded and inheritance applied. Returns `FoundProject`. |
39
+ | `loadPackage({ fs, dir, workspace?, workspaceDir? })` | One package: its manifest with workspace inheritance merged, aliases reanchored, targets resolved. |
40
+ | `readManifest({ fs, dir })` | Parses the `venn.toml` in this directory, or `undefined` when there is none. |
41
+ | `conventionalTargets({ fs, dir, declared, packageName })` | The declared targets plus the conventional ones that exist on disk. |
42
+ | `MANIFEST_FILE` | `"venn.toml"`. |
43
+
44
+ ### Model
45
+
46
+ | Type | Shape |
47
+ | --- | --- |
48
+ | `Project` | `root`, `isWorkspace`, `rootManifest`, `packages`, `defaultPackages`. A lone package is a workspace of one, so nothing downstream has to ask which it is. |
49
+ | `Package` | `dir`, `manifest`, `targets`. |
50
+ | `FoundProject` | `{ project?, problems }`. |
51
+ | `ProjectProblem` | `{ code, title, path? }`. Never a raw error from the disk. |
52
+
53
+ `Manifest`, `PackageInfo`, `BuildTarget`, `Dependency` and `Profile` are re-exported from
54
+ [`@venn-lang/contracts`](../contracts), which owns the TOML reader.
55
+
56
+ ### Workspaces
57
+
58
+ | Export | What it does |
59
+ | --- | --- |
60
+ | `memberDirs({ fs, root, workspace })` | The directories the members occupy, exclusions applied, anything without a `venn.toml` dropped. |
61
+ | `inherit({ manifest, from })` | A member manifest with what the root supplies filled in. |
62
+ | `expandMembers({ fs, root, patterns })` | Expands `packages/*` against the disk. `*` is one segment; `**` is deliberately not read. |
63
+ | `matchesMember({ path, patterns })` | Whether a path *would* be caught by the globs, without looking at the disk. Needed before the directory exists. |
64
+
65
+ ### Paths
66
+
67
+ Path arithmetic on manifest paths as text with forward slashes, so it works in a Web Worker:
68
+ `normalise`, `join`, `parentOf`, `baseName`, `ancestors`, `isInside`, `relativeTo`, `reanchor`.
69
+
70
+ `reanchor({ path, declaredIn, usedIn })` is the one that is not obvious. A root writes
71
+ `"#shared" = "./shared"` once; read from `packages/api` that means somewhere else entirely, so an
72
+ inherited alias is rewritten to `"../../shared"` on the way down. An alias the member wrote itself
73
+ is left exactly as written.
74
+
75
+ ### Target directory
76
+
77
+ | Export | What it does |
78
+ | --- | --- |
79
+ | `TARGET_DIR`, `TARGET_LAYOUT` | `"target"`, and the names inside it. |
80
+ | `targetDir(root)` | `<root>/target`. |
81
+ | `modulesDir(root)` | `<root>/target/node_modules`. |
82
+ | `nativeModulesDir(root)` | `<root>/target/native_modules`. |
83
+ | `outputDir({ root, profile })` | `<root>/target/debug` or `<root>/target/release`. `ProfileName` is `"debug" \| "release"`. |
84
+ | `writeBuildRecord({ fs, root, record })` | Writes `build.json` into the profile's output directory and returns its path. |
85
+ | `RECORD_FILE`, `BuildRecord`, `BuiltTarget` | `"build.json"`, and what it holds. |
86
+
87
+ ### Dependencies and the lock
88
+
89
+ | Export | What it does |
90
+ | --- | --- |
91
+ | `packageJsonFor({ manifest, members? })` | The `package.json` the package manager is shown, generated into `target/`. Path dependencies are left out; `[patch]` becomes `overrides`. |
92
+ | `managerCommand({ manager, verb, packages?, dev?, platform })` | The command a verb becomes in pnpm, npm, bun or yarn, and whether it needs a shell. |
93
+ | `isSafeSpec(spec)` | Whether a string is a package specifier and nothing else. |
94
+ | `readInstalled({ fs, root })` | Every package under `target/node_modules`, scoped ones included, in name order. |
95
+ | `hashPackage({ fs, dir })` | `sha256-…` over the files a package installed, as a hash of hashes. |
96
+ | `writeLockfile({ fs, root, manager })` / `readLockfile({ fs, root })` | `venn.lock` at the project root. |
97
+ | `verifyLock({ fs, root, lock })` | What is installed against what the lock records, as a list of `Drift`. |
98
+ | `describeDrift(drift)` | One line per package that differs. |
99
+ | `LOCK_FILE`, `LOCK_VERSION`, `Lockfile`, `LockedPackage` | `"venn.lock"`, version `1`, and its shape. |
100
+
101
+ ### Scaffolding
102
+
103
+ `scaffold(request)` returns the files a new project starts as, as `ScaffoldFile[]`. It is pure: it
104
+ says what should exist and the caller writes it, which is what lets `--dry-run` and the tests see
105
+ the same answer as the disk. `ScaffoldKind` is `"lib" | "bin" | "workspace"`, and
106
+ `insideWorkspace` changes *what* is written, not only where: a member leaves out the version it
107
+ would inherit and carries no `.gitignore`, because the root already owns the one `target/` there is.
108
+
109
+ ## What a package builds when it does not say
110
+
111
+ | On disk | Target |
112
+ | --- | --- |
113
+ | `src/lib.vn` | a `lib` named after the package |
114
+ | `src/main.vn` | a `bin` named after the package |
115
+ | `src/bin/<name>.vn` | a `bin` named after the file |
116
+
117
+ A declared target wins, matched by kind and name, so writing `[lib]` with another `path` moves the
118
+ library rather than adding a second one.
119
+
120
+ ## What a member takes from its root
121
+
122
+ Two things are inherited and they work differently. `[workspace.package]` is a default: the
123
+ member's own value wins wherever it wrote one. A dependency marked `{ workspace = true }` is a
124
+ request, so the root's answer replaces whatever was there. `[env.*]` tables and `[paths]` merge per
125
+ key, with the member winning.
126
+
127
+ ```toml
128
+ [workspace]
129
+ members = ["packages/*"]
130
+ exclude = ["packages/legacy"]
131
+ default-members = ["packages/api"]
132
+
133
+ [workspace.package]
134
+ version = "2.0.0"
135
+ license = "MIT"
136
+
137
+ [workspace.dependencies]
138
+ zod = "^4.2.0"
139
+
140
+ [paths]
141
+ "#shared" = "./shared"
142
+
143
+ [env.staging]
144
+ BASE_URL = "https://staging.acme.dev"
145
+ ```
146
+
147
+ ```toml
148
+ # packages/api/venn.toml
149
+ [package]
150
+ name = "api" # version and licence come from the root
151
+
152
+ [dependencies]
153
+ zod = { workspace = true }
154
+ shared = { path = "../shared" }
155
+ ```
156
+
157
+ ## Why `node_modules` lives in `target/`
158
+
159
+ One directory holds every derived thing, the way Cargo's does, so deleting it costs time and
160
+ nothing else. The placement of the modules inside it is load-bearing rather than tidiness: Node
161
+ resolves a package by walking up from the importing file, and built output sits in `target/debug`
162
+ or `target/release`, one level below `target/node_modules`. The ordinary resolver finds them with
163
+ no loader, no symlink and no fight with whichever package manager is underneath. The generated
164
+ `package.json` goes into `target/` for the same reason: a manager writes `node_modules` beside the
165
+ file it was pointed at.
166
+
167
+ `venn.lock` is written from what is actually installed, not translated out of whichever manager's
168
+ own lock produced it. Each entry pins the exact version and a hash of the files that landed on
169
+ disk, which every manager produces the same way, so `verifyLock` catches a registry that answered
170
+ differently or a file edited by hand.
171
+
172
+ ## See also
173
+
174
+ - [`@venn-lang/contracts`](../contracts) - the `FileSystem` port and the TOML manifest reader.
175
+ - [`@venn-lang/cli`](../cli) - the commands built on this: `new`, `build`, `add`, `install`.
176
+ - [`@venn-lang/lsp`](../lsp) - the other reader of the same project model.