diff --git a/.changeset/wide-snails-feel.md b/.changeset/wide-snails-feel.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/wide-snails-feel.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index 186f65780..91d26ddd3 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -82,6 +82,17 @@ module.exports = { pathNot: "^packages/(hunk-git|hunk-vcs)/src/|^packages/hunk/src/extension-api/", }, }, + { + name: "hunk-jj-stays-on-vcs-contract", + comment: + "@hunk/jj owns the Jujutsu provider and may reach only its local modules, the public extension contract, and explicit dependency-bottom @hunk/vcs leaves.", + severity: "error", + from: { path: "^packages/hunk-jj/src/" }, + to: { + path: "^packages/", + pathNot: "^packages/(hunk-jj|hunk-vcs)/src/|^packages/hunk/src/extension-api/", + }, + }, { name: "lib-is-a-leaf", comment: @@ -192,7 +203,7 @@ module.exports = { severity: "error", from: { path: "^packages/(?!hunk/)", - pathNot: "^packages/hunk-git/", + pathNot: "^packages/hunk-(git|jj)/", }, to: { path: "^packages/hunk/src/" }, }, diff --git a/bun.lock b/bun.lock index d3e7e8a38..17be3509e 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.7.0", "@hunk/git": "workspace:*", + "@hunk/jj": "workspace:*", "@hunk/session-broker": "workspace:*", "@hunk/session-broker-bun": "workspace:*", "@hunk/session-broker-core": "workspace:*", @@ -79,6 +80,14 @@ "hunkdiff": "workspace:*", }, }, + "packages/hunk-jj": { + "name": "@hunk/jj", + "version": "0.0.0", + "dependencies": { + "@hunk/vcs": "workspace:*", + "hunkdiff": "workspace:*", + }, + }, "packages/hunk-vcs": { "name": "@hunk/vcs", "version": "0.0.0", @@ -140,6 +149,8 @@ "@hunk/git": ["@hunk/git@workspace:packages/hunk-git"], + "@hunk/jj": ["@hunk/jj@workspace:packages/hunk-jj"], + "@hunk/session-broker": ["@hunk/session-broker@workspace:packages/session-broker"], "@hunk/session-broker-bun": ["@hunk/session-broker-bun@workspace:packages/session-broker-bun"], diff --git a/knip.json b/knip.json index 7e1dde9f4..d9c76b24c 100644 --- a/knip.json +++ b/knip.json @@ -42,6 +42,11 @@ "entry": ["src/**/*.test.ts"], "project": ["src/**/*.ts"] }, + "packages/hunk-jj": { + "entry": ["src/**/*.test.ts"], + "project": ["src/**/*.ts"], + "ignoreExportsUsedInFile": true + }, "packages/hunk-vcs": { "entry": ["src/**/*.test.ts"], "project": ["src/**/*.ts"] diff --git a/nix/bun.lock.nix b/nix/bun.lock.nix index 0a4348a83..f154ef058 100644 --- a/nix/bun.lock.nix +++ b/nix/bun.lock.nix @@ -54,6 +54,7 @@ hash = "sha512-vo/MwCnpJAVHBkGzWjCJ28wF45fYHAfbPZcH2rodZODHtch2GHA94KtMfusmVycTUtsLAsaNsHhtY6P8X3RQsA=="; }; "@hunk/git" = copyPathToStore ../packages/hunk-git; + "@hunk/jj" = copyPathToStore ../packages/hunk-jj; "@hunk/session-broker" = copyPathToStore ../packages/session-broker; "@hunk/session-broker-bun" = copyPathToStore ../packages/session-broker-bun; "@hunk/session-broker-core" = copyPathToStore ../packages/session-broker-core; diff --git a/package.json b/package.json index 0f946bde7..096ad3984 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.7.0", "@hunk/git": "workspace:*", + "@hunk/jj": "workspace:*", "@hunk/session-broker": "workspace:*", "@hunk/session-broker-bun": "workspace:*", "@hunk/session-broker-core": "workspace:*", diff --git a/packages/hunk-jj/package.json b/packages/hunk-jj/package.json new file mode 100644 index 000000000..8c0e540d5 --- /dev/null +++ b/packages/hunk-jj/package.json @@ -0,0 +1,26 @@ +{ + "name": "@hunk/jj", + "version": "0.0.0", + "private": true, + "description": "Private statically bundled Jujutsu provider for Hunk.", + "license": "MIT", + "files": [ + "src" + ], + "type": "module", + "sideEffects": false, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "@hunk/vcs": "workspace:*", + "hunkdiff": "workspace:*" + }, + "engines": { + "bun": ">=1.3.14", + "node": ">=22" + } +} diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/commands.test.ts b/packages/hunk-jj/src/commands.test.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/commands.test.ts rename to packages/hunk-jj/src/commands.test.ts diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/commands.ts b/packages/hunk-jj/src/commands.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/commands.ts rename to packages/hunk-jj/src/commands.ts diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/history.test.ts b/packages/hunk-jj/src/history.test.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/history.test.ts rename to packages/hunk-jj/src/history.test.ts diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/history.ts b/packages/hunk-jj/src/history.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/history.ts rename to packages/hunk-jj/src/history.ts diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/index.test.ts b/packages/hunk-jj/src/index.test.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/index.test.ts rename to packages/hunk-jj/src/index.test.ts diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/index.ts b/packages/hunk-jj/src/index.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/index.ts rename to packages/hunk-jj/src/index.ts diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/source.test.ts b/packages/hunk-jj/src/source.test.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/source.test.ts rename to packages/hunk-jj/src/source.test.ts diff --git a/packages/hunk/src/extensions/default/vcs/jujutsu/source.ts b/packages/hunk-jj/src/source.ts similarity index 100% rename from packages/hunk/src/extensions/default/vcs/jujutsu/source.ts rename to packages/hunk-jj/src/source.ts diff --git a/packages/hunk/src/extensions/default/vcs/index.ts b/packages/hunk/src/extensions/default/vcs/index.ts index 494032ca6..e213e6968 100644 --- a/packages/hunk/src/extensions/default/vcs/index.ts +++ b/packages/hunk/src/extensions/default/vcs/index.ts @@ -1,5 +1,5 @@ import gitExtension from "@hunk/git"; -import jjExtension from "./jujutsu"; +import jjExtension from "@hunk/jj"; import slExtension from "./sapling"; import { runExtensionFactory } from "../../runExtension"; import { diff --git a/scripts/hunk-jj-package.test.ts b/scripts/hunk-jj-package.test.ts new file mode 100644 index 000000000..3f4d6bfc9 --- /dev/null +++ b/scripts/hunk-jj-package.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test"; +import jjExtension, { JjVcsAdapter } from "@hunk/jj"; +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; + +const REPO_ROOT = resolve(import.meta.dir, ".."); +const PACKAGE_ROOT = join(REPO_ROOT, "packages", "hunk-jj"); + +describe("@hunk/jj package boundary", () => { + test("exports only the explicit Jujutsu provider entry", () => { + const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")) as { + name: string; + private: boolean; + files: string[]; + exports: Record; + dependencies: Record; + }; + + expect(manifest.name).toBe("@hunk/jj"); + expect(manifest.private).toBe(true); + expect(manifest.files).toEqual(["src"]); + expect(manifest.exports).toEqual({ + ".": { + types: "./src/index.ts", + import: "./src/index.ts", + }, + }); + expect(manifest.dependencies).toEqual({ + "@hunk/vcs": "workspace:*", + hunkdiff: "workspace:*", + }); + }); + + test("registers the private workspace without leaking it into hunkdiff", () => { + const rootManifest = JSON.parse(readFileSync(join(REPO_ROOT, "package.json"), "utf8")) as { + devDependencies: Record; + }; + const hunkManifest = readFileSync(join(REPO_ROOT, "packages", "hunk", "package.json"), "utf8"); + const bunLock = readFileSync(join(REPO_ROOT, "bun.lock"), "utf8"); + const nixLock = readFileSync(join(REPO_ROOT, "nix", "bun.lock.nix"), "utf8"); + + expect(rootManifest.devDependencies["@hunk/jj"]).toBe("workspace:*"); + expect(hunkManifest).not.toContain("@hunk/jj"); + expect(hunkManifest).not.toContain("workspace:"); + expect(bunLock).toContain('"packages/hunk-jj": {'); + expect(bunLock).toContain('"@hunk/jj@workspace:packages/hunk-jj"'); + expect(nixLock).toContain('"@hunk/jj" = copyPathToStore ../packages/hunk-jj;'); + }); + + test("loads its provider entrypoint through the workspace", () => { + expect(typeof jjExtension).toBe("function"); + expect(JjVcsAdapter.id).toBe("jj"); + }); +}); diff --git a/scripts/source-boundaries.test.ts b/scripts/source-boundaries.test.ts index 64ba69643..bb82c1751 100644 --- a/scripts/source-boundaries.test.ts +++ b/scripts/source-boundaries.test.ts @@ -8,6 +8,7 @@ const CORE_ROOT = join(SRC_ROOT, "core"); const EXTENSIONS_ROOT = join(SRC_ROOT, "extensions"); const BUNDLED_PROVIDER_ROOT = join(EXTENSIONS_ROOT, "default", "vcs"); const GIT_PACKAGE_ROOT = join(REPO_ROOT, "packages", "hunk-git", "src"); +const JJ_PACKAGE_ROOT = join(REPO_ROOT, "packages", "hunk-jj", "src"); const VCS_PACKAGE_ROOT = join(REPO_ROOT, "packages", "hunk-vcs", "src"); const REVIEW_MODEL_ROOT = join(CORE_ROOT, "review"); // The published extension contract, which the review model may name for the annotation shapes @@ -167,7 +168,7 @@ function unexpectedProviderImports() { "@hunk/vcs/path", "@hunk/vcs/source", ]); - return [BUNDLED_PROVIDER_ROOT, GIT_PACKAGE_ROOT].flatMap((providerRoot) => + return [BUNDLED_PROVIDER_ROOT, GIT_PACKAGE_ROOT, JJ_PACKAGE_ROOT].flatMap((providerRoot) => sourceFiles(providerRoot).flatMap((path) => importSpecifiers(path) // The lightweight scanner can match prose ending in `from "…"`; module specifiers @@ -182,7 +183,10 @@ function unexpectedProviderImports() { ) { return false; } - return !(path === join(BUNDLED_PROVIDER_ROOT, "index.ts") && specifier === "@hunk/git"); + return !( + path === join(BUNDLED_PROVIDER_ROOT, "index.ts") && + (specifier === "@hunk/git" || specifier === "@hunk/jj") + ); }) .map((specifier) => `${repoPath(path)} -> ${specifier}`), ), @@ -312,6 +316,7 @@ describe("source architecture boundaries", () => { test("keeps bundled providers on the public contract and explicit VCS helper leaves", () => { expect(forbiddenImports(BUNDLED_PROVIDER_ROOT, CORE_ROOT)).toEqual([]); expect(escapingImports(GIT_PACKAGE_ROOT, [GIT_PACKAGE_ROOT])).toEqual([]); + expect(escapingImports(JJ_PACKAGE_ROOT, [JJ_PACKAGE_ROOT])).toEqual([]); expect(unexpectedProviderImports()).toEqual([]); }); diff --git a/tsconfig.json b/tsconfig.json index f6abe2bba..d39e36a65 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "baseUrl": ".", "paths": { "@hunk/git": ["packages/hunk-git/src/index.ts"], + "@hunk/jj": ["packages/hunk-jj/src/index.ts"], "@hunk/session-broker": ["packages/session-broker/src/index.ts"], "@hunk/session-broker-bun": ["packages/session-broker-bun/src/index.ts"], "@hunk/session-broker-core": ["packages/session-broker-core/src/index.ts"],