@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.
@@ -0,0 +1,61 @@
1
+ // biome-ignore-all lint/suspicious/noTemplateCurlyInString: ${name} is Venn's own interpolation inside a generated file, not a JavaScript template.
2
+ import { TARGET_DIR } from "../target/index.js";
3
+ import type { ScaffoldFile, ScaffoldRequest } from "./scaffold.types.js";
4
+
5
+ /**
6
+ * The files a new project starts as.
7
+ *
8
+ * Pure: it says what should exist and the caller writes it, so `--dry-run`, the
9
+ * tests and the disk all see one answer. What is generated is deliberately
10
+ * small, because a starting point that has to be deleted before it can be used
11
+ * is worse than one that is missing something.
12
+ *
13
+ * @returns The manifest, a source file for anything that is not a workspace,
14
+ * and a `.gitignore` unless a workspace above already owns one.
15
+ */
16
+ export function scaffold(request: ScaffoldRequest): ScaffoldFile[] {
17
+ const files = [{ path: "venn.toml", content: manifestFor(request) }];
18
+ if (request.kind !== "workspace") files.push(sourceFor(request));
19
+ // One `target/` per workspace means one line ignoring it, at the root that
20
+ // owns it. A member repeating that would be ignoring something it has not got.
21
+ if (!request.insideWorkspace) files.push(ignoreFile());
22
+ return files;
23
+ }
24
+
25
+ /** A member leaves out what it inherits; writing it down would only shadow it. */
26
+ function manifestFor(request: ScaffoldRequest): string {
27
+ if (request.kind === "workspace") return workspaceManifest();
28
+ const version = request.insideWorkspace ? [] : ['version = "0.1.0"'];
29
+ return ["[package]", `name = "${request.name}"`, ...version, "", "[dependencies]", ""].join("\n");
30
+ }
31
+
32
+ function workspaceManifest(): string {
33
+ return [
34
+ "[workspace]",
35
+ 'members = ["packages/*"]',
36
+ "",
37
+ "# Metadata a member inherits by leaving it out.",
38
+ "[workspace.package]",
39
+ 'version = "0.1.0"',
40
+ "",
41
+ "# Versions a member takes with `dep = { workspace = true }`.",
42
+ "[workspace.dependencies]",
43
+ "",
44
+ ].join("\n");
45
+ }
46
+
47
+ /** A library exports; a program runs. Each starts as the smallest of itself. */
48
+ function sourceFor(request: ScaffoldRequest): ScaffoldFile {
49
+ if (request.kind === "lib") {
50
+ return {
51
+ path: "src/lib.vn",
52
+ content: ["pub fn greet(name: string) -> string {", ' "Hello, ${name}"', "}", ""].join("\n"),
53
+ };
54
+ }
55
+ return { path: "src/main.vn", content: `print "Hello from ${request.name}"\n` };
56
+ }
57
+
58
+ /** Everything derived lives in one directory, so ignoring it is one line. */
59
+ function ignoreFile(): ScaffoldFile {
60
+ return { path: ".gitignore", content: `${TARGET_DIR}/\n` };
61
+ }
@@ -0,0 +1,23 @@
1
+ /** What kind of thing is being started. */
2
+ export type ScaffoldKind = "lib" | "bin" | "workspace";
3
+
4
+ /** What to start, and what the surroundings already provide. */
5
+ export interface ScaffoldRequest {
6
+ kind: ScaffoldKind;
7
+ /** The package name, which is also the directory when one is created. */
8
+ name: string;
9
+ /**
10
+ * Whether a workspace above will claim this package as a member.
11
+ *
12
+ * Changes what is written rather than only where: a member leaves out what it
13
+ * would inherit and carries no ignore file, because the root already owns the
14
+ * one `target/` there is.
15
+ */
16
+ insideWorkspace?: boolean;
17
+ }
18
+
19
+ /** One file to write, its path relative to the directory being started. */
20
+ export interface ScaffoldFile {
21
+ path: string;
22
+ content: string;
23
+ }
@@ -0,0 +1,45 @@
1
+ import type { FileSystem } from "@venn-lang/contracts";
2
+ import { join } from "../paths/index.js";
3
+ import { outputDir, type ProfileName } from "./layout.js";
4
+
5
+ /** One target a build covered, and where it starts. */
6
+ export interface BuiltTarget {
7
+ package: string;
8
+ kind: string;
9
+ name: string;
10
+ /** The entry file, relative to the workspace root. */
11
+ path: string;
12
+ }
13
+
14
+ /** What one build covered, and how it went. */
15
+ export interface BuildRecord {
16
+ profile: ProfileName;
17
+ targets: readonly BuiltTarget[];
18
+ /** How many `.vn` files were read, and how many problems were found. */
19
+ files: number;
20
+ problems: number;
21
+ }
22
+
23
+ /** The record's name, inside the profile's output directory. */
24
+ export const RECORD_FILE = "build.json";
25
+
26
+ /**
27
+ * Writes what a build produced, where that build's outputs go.
28
+ *
29
+ * It answers "what does this project build, and did it hold together", and it
30
+ * is what an incremental build compares against once there is generated code to
31
+ * skip.
32
+ *
33
+ * @returns The path written.
34
+ * @throws Whatever the file system raises when the write fails.
35
+ */
36
+ export async function writeBuildRecord(args: {
37
+ fs: FileSystem;
38
+ root: string;
39
+ record: BuildRecord;
40
+ }): Promise<string> {
41
+ const path = join(outputDir({ root: args.root, profile: args.record.profile }), RECORD_FILE);
42
+ const text = `${JSON.stringify(args.record, null, 2)}\n`;
43
+ await args.fs.write(path, new TextEncoder().encode(text));
44
+ return path;
45
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ type BuildRecord,
3
+ type BuiltTarget,
4
+ RECORD_FILE,
5
+ writeBuildRecord,
6
+ } from "./build-record.js";
7
+ export {
8
+ modulesDir,
9
+ nativeModulesDir,
10
+ outputDir,
11
+ type ProfileName,
12
+ TARGET_DIR,
13
+ TARGET_LAYOUT,
14
+ targetDir,
15
+ } from "./layout.js";
@@ -0,0 +1,50 @@
1
+ import { join } from "../paths/index.js";
2
+
3
+ /**
4
+ * Where everything a build produces or fetches goes.
5
+ *
6
+ * One directory at the workspace root holds every derived thing, the way
7
+ * Cargo's `target/` does, so deleting it costs time and nothing else.
8
+ *
9
+ * The dependencies live inside it, and that placement is load-bearing rather
10
+ * than tidiness: Node resolves a package by walking up from the importing file
11
+ * looking for `node_modules`, and built output sits in `target/debug` or
12
+ * `target/release`, one level below `target/node_modules`. The ordinary
13
+ * resolver finds them with no loader and no symlink. Moving the output out of
14
+ * `target/` breaks that quietly.
15
+ */
16
+ export const TARGET_DIR = "target";
17
+
18
+ /** The names inside `target/`. */
19
+ export const TARGET_LAYOUT = {
20
+ /** What the package manager installs, and what Node's resolver will find. */
21
+ modules: "node_modules",
22
+ /** Compiled addons, kept apart because they are per-platform, not per-project. */
23
+ native: "native_modules",
24
+ /** Built output, one directory per profile. */
25
+ debug: "debug",
26
+ release: "release",
27
+ } as const;
28
+
29
+ /** Which build profile an output directory belongs to. */
30
+ export type ProfileName = "debug" | "release";
31
+
32
+ /** The `target/` of a project, given its root. */
33
+ export function targetDir(root: string): string {
34
+ return join(root, TARGET_DIR);
35
+ }
36
+
37
+ /** Where the package manager installs, and where Node's resolver will look. */
38
+ export function modulesDir(root: string): string {
39
+ return join(targetDir(root), TARGET_LAYOUT.modules);
40
+ }
41
+
42
+ /** Where compiled addons go, kept apart because they are per-platform. */
43
+ export function nativeModulesDir(root: string): string {
44
+ return join(targetDir(root), TARGET_LAYOUT.native);
45
+ }
46
+
47
+ /** Where a build of this profile is written. */
48
+ export function outputDir(args: { root: string; profile: ProfileName }): string {
49
+ return join(targetDir(args.root), TARGET_LAYOUT[args.profile]);
50
+ }
@@ -0,0 +1,2 @@
1
+ export { inherit } from "./inherit.js";
2
+ export { memberDirs } from "./member-dirs.js";
@@ -0,0 +1,60 @@
1
+ import type { Dependency, Manifest, PackageInfo } from "@venn-lang/contracts";
2
+
3
+ /**
4
+ * A member manifest with what the workspace root supplies filled in.
5
+ *
6
+ * Two things are inherited and they work differently. `[workspace.package]` is
7
+ * a default: the member's own value wins wherever it wrote one. A dependency
8
+ * marked `{ workspace = true }` is a request, so the root's answer replaces
9
+ * whatever was there. Writing both a version and `workspace = true` is a
10
+ * contradiction, and the request is the deliberate half.
11
+ *
12
+ * @param args.from The workspace root's manifest. A root with no `[workspace]`
13
+ * table supplies nothing, and the member is returned untouched.
14
+ * @returns The merged manifest. The member's own `name` always survives.
15
+ */
16
+ export function inherit(args: { manifest: Manifest; from: Manifest }): Manifest {
17
+ const shared = args.from.workspace;
18
+ if (!shared) return args.manifest;
19
+ const pkg = withDefaults(args.manifest.package, shared.package);
20
+ return {
21
+ ...args.manifest,
22
+ name: pkg.name,
23
+ version: pkg.version ?? args.manifest.version,
24
+ package: pkg,
25
+ dependencies: pinned(args.manifest.dependencies, shared.dependencies),
26
+ devDependencies: pinned(args.manifest.devDependencies, shared.dependencies),
27
+ ...settings(args.manifest, args.from),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * The environments and path aliases a member gets from its root.
33
+ *
34
+ * A workspace almost always wants `[env.staging]` and `#shared` written once.
35
+ * The alternative, every member repeating every variable, drifts silently until
36
+ * two members disagree about a URL. A member that writes its own key wins, per
37
+ * key rather than per table.
38
+ */
39
+ function settings(own: Manifest, root: Manifest): Pick<Manifest, "env" | "paths"> {
40
+ const env: Record<string, Record<string, string>> = { ...root.env };
41
+ for (const [name, vars] of Object.entries(own.env)) env[name] = { ...env[name], ...vars };
42
+ return { env, paths: { ...root.paths, ...own.paths } };
43
+ }
44
+
45
+ /** The member's own value wins; the root fills only what was left unsaid. */
46
+ function withDefaults(own: PackageInfo, shared: Partial<PackageInfo>): PackageInfo {
47
+ const merged = { ...own };
48
+ for (const [key, value] of Object.entries(shared) as [keyof PackageInfo, never][]) {
49
+ if (merged[key] === undefined || merged[key] === "") merged[key] = value;
50
+ }
51
+ return { ...merged, name: own.name };
52
+ }
53
+
54
+ function pinned(deps: readonly Dependency[], shared: readonly Dependency[]): readonly Dependency[] {
55
+ return deps.map((dep) => {
56
+ if (!dep.fromWorkspace) return dep;
57
+ const found = shared.find((one) => one.name === dep.name);
58
+ return found ? { ...found, optional: dep.optional, fromWorkspace: true } : dep;
59
+ });
60
+ }
@@ -0,0 +1,34 @@
1
+ import type { FileSystem, WorkspaceSettings } from "@venn-lang/contracts";
2
+ import { expandMembers } from "../glob/index.js";
3
+ import { isInside, join, normalise } from "../paths/index.js";
4
+
5
+ /**
6
+ * The directories a workspace's members occupy, exclusions already applied.
7
+ *
8
+ * @returns The member directories, normalised. One holding no `venn.toml` is
9
+ * dropped rather than reported: a glob describes a shape, and `packages/*`
10
+ * catching a `dist` folder on the way past is the glob working, not the
11
+ * workspace being wrong.
12
+ */
13
+ export async function memberDirs(args: {
14
+ fs: FileSystem;
15
+ root: string;
16
+ workspace: WorkspaceSettings;
17
+ }): Promise<string[]> {
18
+ const matched = await expandMembers({
19
+ fs: args.fs,
20
+ root: args.root,
21
+ patterns: args.workspace.members,
22
+ });
23
+ const excluded = args.workspace.exclude.map((path) => normalise(join(args.root, path)));
24
+ const kept = matched.filter((dir) => !excluded.some((one) => isInside(dir, one)));
25
+ return withManifest(args.fs, kept);
26
+ }
27
+
28
+ async function withManifest(fs: FileSystem, dirs: readonly string[]): Promise<string[]> {
29
+ const found: string[] = [];
30
+ for (const dir of dirs) {
31
+ if (await fs.exists(join(dir, "venn.toml"))) found.push(normalise(dir));
32
+ }
33
+ return found;
34
+ }