diff --git a/docs/architecture.md b/docs/architecture.md index fbbded5..d78a9ba 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -25,8 +25,25 @@ executable `bin/packref` imports that output. | `manifests` | Detect project manifests and resolve declared dependencies to exact versions. JavaScript is the v1 adapter. | | `sources` | Normalize, select, and fetch repository or tarball source, including source-specific external adapters. | | `store` | Address and manage immutable global source snapshots by package identity. | -| `workspace` | Own the Packref lockfile, user-level state, project paths, integrations, and reference materialization adapters. | -| `core` | Define package identity, package specs, source types, registry contracts, and shared typed errors. | +| `workspace` | Own the Packref lockfile, user-level state, integrations, and reference materialization adapters. | +| `layout` | Define the on-disk paths of project state and user-level state, and the Packref home. | +| `core` | Define package identity, package specs, repository hosts, source types, registries, and shared typed errors. | + +### Layers + +Dependencies inside `src/lib` point in a single direction. A folder imports only folders in lower +layers, and folders in the same layer do not import each other: + +1. `core` and `shared` +2. `layout` +3. `registries`, `manifests`, `store`, and `workspace` +4. `sources` (imports `store` to write snapshots) +5. `references` + +`oxlint.config.ts` enforces these layers with one `no-restricted-imports` override for each folder. +`import/no-relative-parent-imports` stops relative imports from bypassing these overrides, and +`import/no-cycle` rejects import cycles between files. When you add a folder or a dependency, update +the overrides. Commands remain thin so the `references` interface is also the main behavior test seam. Registry and manifest adapters do not know filesystem layout, while source adapters do not know project manifests @@ -95,7 +112,8 @@ src/ commands/ CLI command definitions terminal/ prompts, logs, spinners, titles, and cancellation lib/ - core/ identities, sources, errors, registry contracts + core/ identities, specs, repository hosts, sources, registries, errors + layout/ on-disk paths and the Packref home manifests/ project dependency adapters references/ command-aligned domain workflows registries/ package registry adapters diff --git a/oxlint.config.ts b/oxlint.config.ts index b8dda9d..11104a1 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -10,7 +10,215 @@ export default defineConfig({ typeAware: true, typeCheck: true, }, + overrides: [ + // Each `src/lib` folder imports only the folders in lower layers. See the layers in + // `docs/architecture.md`. `references` is the top layer and may import every other folder. + { + files: ["src/lib/**/*.ts"], + rules: { + "import/no-relative-parent-imports": "error", + }, + }, + { + files: ["src/lib/core/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/layout/**", + "#lib/manifests/**", + "#lib/references/**", + "#lib/registries/**", + "#lib/shared/**", + "#lib/sources/**", + "#lib/store/**", + "#lib/workspace/**", + ], + message: "core must not import other lib folders.", + }, + ], + }, + ], + }, + }, + { + files: ["src/lib/shared/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/core/**", + "#lib/layout/**", + "#lib/manifests/**", + "#lib/references/**", + "#lib/registries/**", + "#lib/sources/**", + "#lib/store/**", + "#lib/workspace/**", + ], + message: "shared must not import other lib folders.", + }, + ], + }, + ], + }, + }, + { + files: ["src/lib/layout/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/manifests/**", + "#lib/references/**", + "#lib/registries/**", + "#lib/shared/**", + "#lib/sources/**", + "#lib/store/**", + "#lib/workspace/**", + ], + message: "layout may import only core.", + }, + ], + }, + ], + }, + }, + { + files: ["src/lib/manifests/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/layout/**", + "#lib/references/**", + "#lib/registries/**", + "#lib/shared/**", + "#lib/sources/**", + "#lib/store/**", + "#lib/workspace/**", + ], + message: "manifests may import only core.", + }, + ], + }, + ], + }, + }, + { + files: ["src/lib/registries/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/layout/**", + "#lib/manifests/**", + "#lib/references/**", + "#lib/shared/**", + "#lib/sources/**", + "#lib/store/**", + "#lib/workspace/**", + ], + message: "registries may import only core.", + }, + ], + }, + ], + }, + }, + { + files: ["src/lib/store/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/manifests/**", + "#lib/references/**", + "#lib/registries/**", + "#lib/sources/**", + "#lib/workspace/**", + ], + message: "store may import only core, layout, shared.", + }, + ], + }, + ], + }, + }, + { + files: ["src/lib/workspace/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/manifests/**", + "#lib/references/**", + "#lib/registries/**", + "#lib/sources/**", + "#lib/store/**", + ], + message: "workspace may import only core, layout, shared.", + }, + ], + }, + ], + }, + }, + { + files: ["src/lib/sources/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "#lib/layout/**", + "#lib/manifests/**", + "#lib/references/**", + "#lib/registries/**", + "#lib/workspace/**", + ], + message: "sources may import only core, shared, store.", + }, + ], + }, + ], + }, + }, + { + // Tests compose layers from several folders to build their fixtures. + files: ["src/lib/**/__tests__/**/*.ts"], + rules: { + "import/no-relative-parent-imports": "off", + "no-restricted-imports": "off", + }, + }, + ], rules: { + // Also report cycles that pass through type-only imports. + "import/no-cycle": ["error", { ignoreTypes: false }], // Effect combinators such as `Option.some(value)`, `Option.flatMap(fn)`, and // `Effect.map(effect, fn)` are indistinguishable from array iteration methods // to these array-specific rules. diff --git a/src/__tests__/add.integration.test.ts b/src/__tests__/add.integration.test.ts index 49316f5..b219bda 100644 --- a/src/__tests__/add.integration.test.ts +++ b/src/__tests__/add.integration.test.ts @@ -7,6 +7,7 @@ import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import { afterEach, describe, expect, it } from "vitest" import { parsePackageSpec } from "#lib/core/packages.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { ProjectDependencyReader } from "#lib/manifests/index.ts" import { PackageManagerResolver } from "#lib/manifests/javascript.ts" import { PackageManagerDetector } from "#lib/manifests/package-manager-detector.ts" @@ -14,7 +15,6 @@ import { addPackageReference } from "#lib/references/add.ts" import { NpmRegistryClient } from "#lib/registries/npm/client.ts" import { RepositoryDownloader } from "#lib/sources/repository/fetch.ts" import { RemoteTagReader } from "#lib/sources/repository/tags.ts" -import { PackrefHome } from "#lib/workspace/home.ts" import { Reflinker } from "#lib/workspace/reflinker.ts" const temporaryPaths: string[] = [] diff --git a/src/commands/add.ts b/src/commands/add.ts index a4afaf2..8b60b18 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -5,7 +5,8 @@ import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import type { ManifestDependency } from "#lib/manifests/manifest.ts" -import { formatPackageIdentity, parsePackageSpec } from "#lib/core/packages.ts" +import { formatPackageIdentity } from "#lib/core/identity.ts" +import { parsePackageSpec } from "#lib/core/packages.ts" import { addPackageReference, findPackageCandidates, diff --git a/src/commands/list.ts b/src/commands/list.ts index 1b991bb..33aa407 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,7 +1,7 @@ import * as Command from "effect/cli/Command" import * as Effect from "effect/Effect" import color from "picocolors" -import { formatPackageIdentity } from "#lib/core/packages.ts" +import { formatPackageIdentity } from "#lib/core/identity.ts" import { listPackageEntries, readProjectLockfile } from "#lib/workspace/lockfile.ts" import { requireInitializedProject } from "#lib/workspace/project.ts" import { Prompter } from "#terminal/prompter.ts" diff --git a/src/commands/prune.ts b/src/commands/prune.ts index 84519e3..faaf696 100644 --- a/src/commands/prune.ts +++ b/src/commands/prune.ts @@ -1,6 +1,6 @@ import * as Command from "effect/cli/Command" import * as Effect from "effect/Effect" -import { formatPackageIdentity } from "#lib/core/packages.ts" +import { formatPackageIdentity } from "#lib/core/identity.ts" import { applyPrunePlan, discoverPrunePlan } from "#lib/references/prune.ts" import { Prompter } from "#terminal/prompter.ts" import { printTitle } from "#terminal/title.ts" diff --git a/src/commands/remove.ts b/src/commands/remove.ts index fb0570a..296444b 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -4,7 +4,8 @@ import * as Command from "effect/cli/Command" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import type { PackageEntry } from "#lib/workspace/lockfile.ts" -import { formatPackageIdentity, parsePackageSpec } from "#lib/core/packages.ts" +import { formatPackageIdentity } from "#lib/core/identity.ts" +import { parsePackageSpec } from "#lib/core/packages.ts" import { findPackageReferenceMatches, listPackageReferences, diff --git a/src/commands/sync.ts b/src/commands/sync.ts index e497b90..7b03887 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -1,6 +1,6 @@ import * as Command from "effect/cli/Command" import * as Effect from "effect/Effect" -import { formatPackageIdentity } from "#lib/core/packages.ts" +import { formatPackageIdentity } from "#lib/core/identity.ts" import { preparePackageReferenceSync, syncPackageReferences, diff --git a/src/lib/core/__tests__/packages.test.ts b/src/lib/core/__tests__/packages.test.ts index 4e9ff6a..003710c 100644 --- a/src/lib/core/__tests__/packages.test.ts +++ b/src/lib/core/__tests__/packages.test.ts @@ -7,12 +7,11 @@ import { UnsupportedRepositoryHostError, } from "#lib/core/errors.ts" import { - getPackageIdentitySegments, packageCoordinatesEquivalence, packageCoordinatesOrder, - parsePackageSpec, type PackageIdentity, -} from "#lib/core/packages.ts" +} from "#lib/core/identity.ts" +import { getPackageIdentitySegments, parsePackageSpec } from "#lib/core/packages.ts" const runEffect = (effect: Effect.Effect) => Effect.runPromise(effect) diff --git a/src/lib/core/errors.ts b/src/lib/core/errors.ts index d6fe61e..badea59 100644 --- a/src/lib/core/errors.ts +++ b/src/lib/core/errors.ts @@ -1,5 +1,5 @@ import * as Data from "effect/Data" -import type { PackageIdentity } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" import { SUPPORTED_REGISTRIES } from "#lib/core/registry.ts" export class MissingPackageVersion extends Data.TaggedError("MissingPackageVersion")<{ diff --git a/src/lib/core/identity.ts b/src/lib/core/identity.ts new file mode 100644 index 0000000..b55ce6b --- /dev/null +++ b/src/lib/core/identity.ts @@ -0,0 +1,34 @@ +import * as Equivalence from "effect/Equivalence" +import * as Order from "effect/Order" + +export interface PackageCoordinates { + readonly name: string + readonly registry: string +} + +export interface PackageIdentity extends PackageCoordinates { + readonly version: string +} + +export const packageCoordinatesEquivalence = Equivalence.Struct({ + name: Equivalence.String, + registry: Equivalence.String, +}) + +export const packageCoordinatesOrder = Order.combineAll([ + Order.mapInput(Order.String, (coordinates: PackageCoordinates) => coordinates.registry), + Order.mapInput(Order.String, (coordinates: PackageCoordinates) => coordinates.name), +]) + +export const packageIdentityEquivalence = Equivalence.combine( + packageCoordinatesEquivalence, + Equivalence.mapInput(Equivalence.String, (identity: PackageIdentity) => identity.version) +) + +export const packageIdentityOrder = Order.combine( + packageCoordinatesOrder, + Order.mapInput(Order.String, (identity: PackageIdentity) => identity.version) +) + +export const formatPackageIdentity = (identity: PackageIdentity) => + `${identity.registry}:${identity.name}@${identity.version}` diff --git a/src/lib/core/packages.ts b/src/lib/core/packages.ts index 1aa4cb4..a600fe7 100644 --- a/src/lib/core/packages.ts +++ b/src/lib/core/packages.ts @@ -1,45 +1,18 @@ import type * as Types from "effect/Types" import * as Effect from "effect/Effect" -import * as Equivalence from "effect/Equivalence" -import * as Order from "effect/Order" +import type { PackageIdentity } from "#lib/core/identity.ts" import { InvalidPackageIdentity, UnsupportedRegistryError, UnsupportedRepositoryHostError, } from "#lib/core/errors.ts" import { DEFAULT_REGISTRY, checkIsRegistry, type Registry } from "#lib/core/registry.ts" - -export interface PackageCoordinates { - readonly name: string - readonly registry: string -} - -export interface PackageIdentity extends PackageCoordinates { - readonly version: string -} - -export const packageCoordinatesEquivalence = Equivalence.Struct({ - name: Equivalence.String, - registry: Equivalence.String, -}) - -export const packageCoordinatesOrder = Order.combineAll([ - Order.mapInput(Order.String, (coordinates: PackageCoordinates) => coordinates.registry), - Order.mapInput(Order.String, (coordinates: PackageCoordinates) => coordinates.name), -]) - -export const packageIdentityEquivalence = Equivalence.combine( - packageCoordinatesEquivalence, - Equivalence.mapInput(Equivalence.String, (identity: PackageIdentity) => identity.version) -) - -export const packageIdentityOrder = Order.combine( - packageCoordinatesOrder, - Order.mapInput(Order.String, (identity: PackageIdentity) => identity.version) -) - -export const formatPackageIdentity = (identity: PackageIdentity) => - `${identity.registry}:${identity.name}@${identity.version}` +import { + HOST_REPOSITORY_PROVIDERS, + checkIsRepositoryProvider, + REPOSITORY_PROVIDER_HOSTS, + type RepositoryProvider, +} from "#lib/core/repository.ts" export interface RegistryPackageSpec { readonly _tag: "registry" @@ -48,14 +21,6 @@ export interface RegistryPackageSpec { readonly specifier?: string } -export const SUPPORTED_REPOSITORY_PROVIDERS = [ - "bitbucket", - "github", - "gitlab", - "sourcehut", -] as const -export type RepositoryProvider = (typeof SUPPORTED_REPOSITORY_PROVIDERS)[number] - export interface RepositoryPackageSpec { readonly _tag: "repository" readonly name: string @@ -108,20 +73,6 @@ const validatePathSegment = Effect.fn("validatePathSegment")(function* ( } }) -export const REPOSITORY_PROVIDER_HOSTS = { - bitbucket: "bitbucket.org", - github: "github.com", - gitlab: "gitlab.com", - sourcehut: "git.sr.ht", -} satisfies Record - -const HOST_REPOSITORY_PROVIDERS = new Map( - SUPPORTED_REPOSITORY_PROVIDERS.map((provider) => [REPOSITORY_PROVIDER_HOSTS[provider], provider]) -) - -const checkIsRepositoryProvider = (value: string): value is RepositoryProvider => - SUPPORTED_REPOSITORY_PROVIDERS.some((provider) => provider === value) - const splitRepositoryRef = (value: string) => { const schemeIndex = value.indexOf("://") const scpPathIndex = schemeIndex === -1 ? value.indexOf(":") : -1 diff --git a/src/lib/core/repository.ts b/src/lib/core/repository.ts new file mode 100644 index 0000000..d852b82 --- /dev/null +++ b/src/lib/core/repository.ts @@ -0,0 +1,21 @@ +export const SUPPORTED_REPOSITORY_PROVIDERS = [ + "bitbucket", + "github", + "gitlab", + "sourcehut", +] as const +export type RepositoryProvider = (typeof SUPPORTED_REPOSITORY_PROVIDERS)[number] + +export const REPOSITORY_PROVIDER_HOSTS = { + bitbucket: "bitbucket.org", + github: "github.com", + gitlab: "gitlab.com", + sourcehut: "git.sr.ht", +} satisfies Record + +export const HOST_REPOSITORY_PROVIDERS = new Map( + SUPPORTED_REPOSITORY_PROVIDERS.map((provider) => [REPOSITORY_PROVIDER_HOSTS[provider], provider]) +) + +export const checkIsRepositoryProvider = (value: string): value is RepositoryProvider => + SUPPORTED_REPOSITORY_PROVIDERS.some((provider) => provider === value) diff --git a/src/lib/workspace/home.ts b/src/lib/layout/home.ts similarity index 100% rename from src/lib/workspace/home.ts rename to src/lib/layout/home.ts diff --git a/src/lib/workspace/paths.ts b/src/lib/layout/paths.ts similarity index 61% rename from src/lib/workspace/paths.ts rename to src/lib/layout/paths.ts index 8d39671..9c64e90 100644 --- a/src/lib/workspace/paths.ts +++ b/src/lib/layout/paths.ts @@ -1,4 +1,7 @@ -import type * as Path from "effect/Path" +import * as Effect from "effect/Effect" +import * as Path from "effect/Path" +import type { PackageIdentity } from "#lib/core/identity.ts" +import { getPackageIdentitySegments } from "#lib/core/packages.ts" export const PACKREF_DIRECTORY_NAME = ".packref" export const LOCKFILE_NAME = "packref-lock.json" @@ -16,3 +19,13 @@ export const getGlobalDirectoryPath = (path: Path.Path, home: string) => export const getGlobalConfigPath = (path: Path.Path, home: string) => path.join(getGlobalDirectoryPath(path, home), GLOBAL_CONFIG_NAME) + +export const getPackageIdentityPath = Effect.fn("getPackageIdentityPath")(function* ( + root: string, + identity: PackageIdentity +) { + const path = yield* Path.Path + const segments = yield* getPackageIdentitySegments(identity) + + return path.join(root, ...segments) +}) diff --git a/src/lib/references/__tests__/add.test.ts b/src/lib/references/__tests__/add.test.ts index 5a073ed..92e1b7d 100644 --- a/src/lib/references/__tests__/add.test.ts +++ b/src/lib/references/__tests__/add.test.ts @@ -15,6 +15,7 @@ import type { NpmPackageMetadata } from "#lib/registries/npm/metadata.ts" import type { PackageEntry } from "#lib/workspace/lockfile.ts" import { NetworkError, ReflinkError, SnapshotFetchError } from "#lib/core/errors.ts" import { parsePackageSpec } from "#lib/core/packages.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { ProjectDependencyReader } from "#lib/manifests/index.ts" import { PackageManagerResolver } from "#lib/manifests/javascript.ts" import { @@ -26,7 +27,6 @@ import { import { NpmRegistryClient } from "#lib/registries/npm/client.ts" import { RepositoryDownloader } from "#lib/sources/repository/fetch.ts" import { RemoteTagReader } from "#lib/sources/repository/tags.ts" -import { PackrefHome } from "#lib/workspace/home.ts" import { Reflinker } from "#lib/workspace/reflinker.ts" const temporaryPaths: string[] = [] diff --git a/src/lib/references/__tests__/install.test.ts b/src/lib/references/__tests__/install.test.ts index 7ef071b..03f1c99 100644 --- a/src/lib/references/__tests__/install.test.ts +++ b/src/lib/references/__tests__/install.test.ts @@ -17,10 +17,10 @@ import { StoreSourceMismatchError, TarballFetchError, } from "#lib/core/errors.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { installPackageReferences } from "#lib/references/install.ts" import { RepositoryDownloader } from "#lib/sources/repository/fetch.ts" import { RemoteTagReader } from "#lib/sources/repository/tags.ts" -import { PackrefHome } from "#lib/workspace/home.ts" import { Reflinker } from "#lib/workspace/reflinker.ts" type TestRepositorySource = Types.Mutable diff --git a/src/lib/references/__tests__/prune.test.ts b/src/lib/references/__tests__/prune.test.ts index 61df61e..2cf6e56 100644 --- a/src/lib/references/__tests__/prune.test.ts +++ b/src/lib/references/__tests__/prune.test.ts @@ -7,11 +7,11 @@ import * as NodeServices from "@effect/platform-node/NodeServices" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import { afterEach, describe, expect, it } from "vitest" -import type { PackageIdentity } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" import { initializeProject, repositoryEntry } from "#commands/__tests__/helpers.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { applyPrunePlan, discoverPrunePlan } from "#lib/references/prune.ts" import { getStoreEntryPath } from "#lib/store/index.ts" -import { PackrefHome } from "#lib/workspace/home.ts" const temporaryPaths: string[] = [] diff --git a/src/lib/references/__tests__/sync.test.ts b/src/lib/references/__tests__/sync.test.ts index 74d19ba..cbca28c 100644 --- a/src/lib/references/__tests__/sync.test.ts +++ b/src/lib/references/__tests__/sync.test.ts @@ -15,13 +15,13 @@ import { TarballFetchError, UnsupportedManifestError, } from "#lib/core/errors.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { ProjectDependencyReader } from "#lib/manifests/index.ts" import { PackageManagerResolver } from "#lib/manifests/javascript.ts" import { preparePackageReferenceSync, syncPackageReferences } from "#lib/references/sync.ts" import { NpmRegistryClient } from "#lib/registries/npm/client.ts" import { RepositoryDownloader } from "#lib/sources/repository/fetch.ts" import { RemoteTagReader } from "#lib/sources/repository/tags.ts" -import { PackrefHome } from "#lib/workspace/home.ts" import { Reflinker } from "#lib/workspace/reflinker.ts" const temporaryPaths: string[] = [] diff --git a/src/lib/references/add.ts b/src/lib/references/add.ts index 761f131..bbe2faa 100644 --- a/src/lib/references/add.ts +++ b/src/lib/references/add.ts @@ -5,6 +5,7 @@ import * as Option from "effect/Option" import * as Order from "effect/Order" import * as Path from "effect/Path" import * as Predicate from "effect/Predicate" +import type { ParsedPackageSpec, RepositoryPackageSpec } from "#lib/core/packages.ts" import type { RepositorySource } from "#lib/core/source.ts" import type { ManifestDependency } from "#lib/manifests/manifest.ts" import type { ResolvedPackageReference } from "#lib/registries/registry.ts" @@ -16,11 +17,9 @@ import { } from "#lib/core/errors.ts" import { packageCoordinatesEquivalence, - packageIdentityEquivalence, type PackageIdentity, - type ParsedPackageSpec, - type RepositoryPackageSpec, -} from "#lib/core/packages.ts" + packageIdentityEquivalence, +} from "#lib/core/identity.ts" import { ProjectDependencyReader } from "#lib/manifests/index.ts" import { resolvePackageReference } from "#lib/registries/index.ts" import { fetchRepositorySnapshot } from "#lib/sources/repository/fetch.ts" diff --git a/src/lib/references/clean.ts b/src/lib/references/clean.ts index 0163cc2..0413ff9 100644 --- a/src/lib/references/clean.ts +++ b/src/lib/references/clean.ts @@ -2,8 +2,8 @@ import * as Effect from "effect/Effect" import * as FileSystem from "effect/FileSystem" import * as Path from "effect/Path" import { PACKAGE_DIRECTORY_NAME } from "#lib/core/packages.ts" +import { getDirectoryPath, getProjectLockfilePath } from "#lib/layout/paths.ts" import { emptyLockfile, readProjectLockfile, writeLockfileAtPath } from "#lib/workspace/lockfile.ts" -import { getDirectoryPath, getProjectLockfilePath } from "#lib/workspace/paths.ts" import { requireInitializedProject } from "#lib/workspace/project.ts" export interface ProjectCleanPlan { diff --git a/src/lib/references/install.ts b/src/lib/references/install.ts index 6db27d8..a50ed7c 100644 --- a/src/lib/references/install.ts +++ b/src/lib/references/install.ts @@ -6,7 +6,7 @@ import { StoreSourceMismatchError, UnsupportedRepositoryHostError, } from "#lib/core/errors.ts" -import { SUPPORTED_REPOSITORY_PROVIDERS } from "#lib/core/packages.ts" +import { checkIsRepositoryProvider } from "#lib/core/repository.ts" import { packageSourceEquivalence } from "#lib/core/source.ts" import { fetchRepositorySnapshot } from "#lib/sources/repository/fetch.ts" import { @@ -69,9 +69,7 @@ const fetchLockedStoreEntry = Effect.fn("fetchLockedStoreEntry")(function* (entr return yield* ensureMatchingSource(entry, materialized) } - const isDirectRepository = SUPPORTED_REPOSITORY_PROVIDERS.some( - (provider) => provider === entry.registry - ) + const isDirectRepository = checkIsRepositoryProvider(entry.registry) const resolvedRepository = isDirectRepository ? yield* Effect.gen(function* () { const source = yield* normalizeRepositorySource(entry.source) diff --git a/src/lib/references/prune.ts b/src/lib/references/prune.ts index 0eac9d3..b7eb060 100644 --- a/src/lib/references/prune.ts +++ b/src/lib/references/prune.ts @@ -3,7 +3,7 @@ import * as FileSystem from "effect/FileSystem" import * as Filter from "effect/Filter" import * as Order from "effect/Order" import { ProjectFilesystemError } from "#lib/core/errors.ts" -import { packageIdentityOrder, type PackageIdentity } from "#lib/core/packages.ts" +import { type PackageIdentity, packageIdentityOrder } from "#lib/core/identity.ts" import { listStoreEntries, removeStoreEntry, type StoreEntry } from "#lib/store/index.ts" import { initializeGlobalConfig, unregisterProjects } from "#lib/workspace/config.ts" import { readProjectLockfile } from "#lib/workspace/lockfile.ts" diff --git a/src/lib/references/remove.ts b/src/lib/references/remove.ts index dd66da8..87ed490 100644 --- a/src/lib/references/remove.ts +++ b/src/lib/references/remove.ts @@ -2,7 +2,6 @@ import type * as Types from "effect/Types" import * as Array from "effect/Array" import * as Effect from "effect/Effect" import * as FileSystem from "effect/FileSystem" -import * as Path from "effect/Path" import * as Result from "effect/Result" import type { ParsedPackageSpec } from "#lib/core/packages.ts" import { @@ -10,7 +9,6 @@ import { PackageReferenceFilesystemError, RemovePackageReferencesError, } from "#lib/core/errors.ts" -import { getStorePackagePath } from "#lib/store/paths.ts" import { findPackageEntries, listPackageEntries, @@ -18,8 +16,7 @@ import { removePackageEntries, type PackageEntry, } from "#lib/workspace/lockfile.ts" -import { getDirectoryPath } from "#lib/workspace/paths.ts" -import { requireInitializedProject } from "#lib/workspace/project.ts" +import { getProjectReferencePath, requireInitializedProject } from "#lib/workspace/project.ts" const REMOVE_CONCURRENCY = 8 @@ -79,13 +76,11 @@ export const removePackageReferences = Effect.fn("removePackageReferences")(func entries: readonly PackageEntry[] ) { const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const projectDirectoryPath = getDirectoryPath(path, projectPath) const attempts = yield* Effect.forEach( entries, (entry) => Effect.gen(function* () { - const referencePath = yield* getStorePackagePath(projectDirectoryPath, entry) + const referencePath = yield* getProjectReferencePath(projectPath, entry) const referenceExists = yield* fs.exists(referencePath).pipe( Effect.mapError( (cause) => diff --git a/src/lib/references/sync.ts b/src/lib/references/sync.ts index e24b667..c65c6e5 100644 --- a/src/lib/references/sync.ts +++ b/src/lib/references/sync.ts @@ -2,16 +2,15 @@ import * as Array from "effect/Array" import * as Effect from "effect/Effect" import * as FileSystem from "effect/FileSystem" import * as Option from "effect/Option" -import * as Path from "effect/Path" import * as Result from "effect/Result" import type { ManifestDependency } from "#lib/manifests/manifest.ts" import { PackageReferenceFilesystemError, UnsupportedManifestError } from "#lib/core/errors.ts" import { + type PackageCoordinates, packageCoordinatesEquivalence, packageCoordinatesOrder, packageIdentityEquivalence, - type PackageCoordinates, -} from "#lib/core/packages.ts" +} from "#lib/core/identity.ts" import { ProjectDependencyReader } from "#lib/manifests/index.ts" import { materializePackageCandidateReference, @@ -19,15 +18,13 @@ import { type ResolvedPackageCandidateReference, } from "#lib/references/add.ts" import { removePackageReferences } from "#lib/references/remove.ts" -import { getStorePackagePath } from "#lib/store/paths.ts" import { registerProject } from "#lib/workspace/config.ts" import { listPackageEntries, readProjectLockfile, type PackageEntry, } from "#lib/workspace/lockfile.ts" -import { getDirectoryPath } from "#lib/workspace/paths.ts" -import { requireInitializedProject } from "#lib/workspace/project.ts" +import { getProjectReferencePath, requireInitializedProject } from "#lib/workspace/project.ts" export interface SyncPackageReferencesOptions { readonly projectPath?: string @@ -80,9 +77,7 @@ const packageReferenceExists = Effect.fn("sync.packageReferenceExists")(function entry: PackageEntry ) { const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const projectDirectoryPath = getDirectoryPath(path, projectPath) - const referencePath = yield* getStorePackagePath(projectDirectoryPath, entry) + const referencePath = yield* getProjectReferencePath(projectPath, entry) return yield* fs.exists(referencePath).pipe( Effect.mapError( diff --git a/src/lib/registries/registry.ts b/src/lib/registries/registry.ts index 399e3c4..b7a3d76 100644 --- a/src/lib/registries/registry.ts +++ b/src/lib/registries/registry.ts @@ -1,5 +1,6 @@ import type * as Effect from "effect/Effect" -import type { PackageIdentity, RegistryPackageSpec } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" +import type { RegistryPackageSpec } from "#lib/core/packages.ts" import type { Registry } from "#lib/core/registry.ts" import type { RepositorySourceCandidate } from "#lib/core/source.ts" diff --git a/src/lib/sources/repository/__tests__/fetch.test.ts b/src/lib/sources/repository/__tests__/fetch.test.ts index 52a6e32..f526740 100644 --- a/src/lib/sources/repository/__tests__/fetch.test.ts +++ b/src/lib/sources/repository/__tests__/fetch.test.ts @@ -8,8 +8,8 @@ import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import { afterEach, describe, expect, it } from "vitest" import { SnapshotFetchError } from "#lib/core/errors.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { fetchRepositorySnapshot, RepositoryDownloader } from "#lib/sources/repository/fetch.ts" -import { PackrefHome } from "#lib/workspace/home.ts" const temporaryPaths: string[] = [] diff --git a/src/lib/sources/repository/__tests__/tags.test.ts b/src/lib/sources/repository/__tests__/tags.test.ts index ef36c6b..c68d5e5 100644 --- a/src/lib/sources/repository/__tests__/tags.test.ts +++ b/src/lib/sources/repository/__tests__/tags.test.ts @@ -8,7 +8,8 @@ import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as PlatformError from "effect/PlatformError" import { afterEach, describe, expect, it } from "vitest" -import type { PackageIdentity, RepositoryPackageSpec } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" +import type { RepositoryPackageSpec } from "#lib/core/packages.ts" import type { NormalizedRepositorySource } from "#lib/core/source.ts" import { GitExecutableNotFoundError, diff --git a/src/lib/sources/repository/fetch.ts b/src/lib/sources/repository/fetch.ts index b8e1808..cda5d12 100644 --- a/src/lib/sources/repository/fetch.ts +++ b/src/lib/sources/repository/fetch.ts @@ -3,7 +3,7 @@ import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import { downloadTemplate } from "giget" -import type { PackageIdentity } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" import type { RepositorySource, ResolvedRepositoryRef } from "#lib/core/source.ts" import { SnapshotFetchError } from "#lib/core/errors.ts" import { materializeStoreEntry } from "#lib/store/index.ts" diff --git a/src/lib/sources/repository/normalize.ts b/src/lib/sources/repository/normalize.ts index c790675..1b6e768 100644 --- a/src/lib/sources/repository/normalize.ts +++ b/src/lib/sources/repository/normalize.ts @@ -2,6 +2,8 @@ import type * as Types from "effect/Types" import * as Effect from "effect/Effect" import * as Match from "effect/Match" import * as Option from "effect/Option" +import type { PackageIdentity } from "#lib/core/identity.ts" +import type { RepositoryPackageSpec } from "#lib/core/packages.ts" import type { NormalizedRepositorySource, RepositorySourceCandidate, @@ -13,29 +15,22 @@ import { UnsupportedRepositoryHostError, } from "#lib/core/errors.ts" import { + checkIsRepositoryProvider, + HOST_REPOSITORY_PROVIDERS, REPOSITORY_PROVIDER_HOSTS, SUPPORTED_REPOSITORY_PROVIDERS, - type PackageIdentity, - type RepositoryPackageSpec, type RepositoryProvider, -} from "#lib/core/packages.ts" +} from "#lib/core/repository.ts" import { matchRepositoryTag, RemoteTagReader } from "#lib/sources/repository/tags.ts" const COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/iu -const HOST_PROVIDERS = new Map( - SUPPORTED_REPOSITORY_PROVIDERS.map((provider) => [REPOSITORY_PROVIDER_HOSTS[provider], provider]) -) - const DEFAULT_SHORTHAND_PROVIDER = "github" satisfies RepositoryProvider const SHORTHAND_PATTERN = new RegExp( `^(?${SUPPORTED_REPOSITORY_PROVIDERS.join("|")}):(?.+)$`, "u" ) -const checkIsKnownProvider = (value: string): value is RepositoryProvider => - SUPPORTED_REPOSITORY_PROVIDERS.some((provider) => provider === value) - const cleanRepositoryPath = (repositoryPath: string) => repositoryPath .replace(/\/+$/u, "") @@ -56,7 +51,7 @@ const makeNormalizedSource = ( return Effect.fail(invalidRepositoryUrl(candidate, "repository path must not be empty")) } - const provider = HOST_PROVIDERS.get(host) + const provider = HOST_REPOSITORY_PROVIDERS.get(host) const fetchRepositoryPath = provider === "sourcehut" ? repositoryPath.replace(/^~/u, "") : repositoryPath @@ -130,7 +125,11 @@ export const normalizeRepositorySource = Effect.fn("normalizeRepositorySource")( const provider = Option.getOrUndefined(shorthandGroups)?.provider const shorthandPath = Option.getOrUndefined(shorthandGroups)?.repositoryPath - if (provider !== undefined && shorthandPath !== undefined && checkIsKnownProvider(provider)) { + if ( + provider !== undefined + && shorthandPath !== undefined + && checkIsRepositoryProvider(provider) + ) { return normalizeFromShorthandUrl(candidate, provider, shorthandPath) } diff --git a/src/lib/sources/repository/tags.ts b/src/lib/sources/repository/tags.ts index 57d0baa..4fa632b 100644 --- a/src/lib/sources/repository/tags.ts +++ b/src/lib/sources/repository/tags.ts @@ -12,7 +12,7 @@ import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" import * as Result from "effect/Result" import * as Schedule from "effect/Schedule" import * as Stream from "effect/Stream" -import type { PackageIdentity } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" import type { NormalizedRepositorySource } from "#lib/core/source.ts" import { GitExecutableNotFoundError, NetworkError } from "#lib/core/errors.ts" diff --git a/src/lib/sources/tarball/__tests__/fetch.test.ts b/src/lib/sources/tarball/__tests__/fetch.test.ts index 3a313f8..d7e2ad2 100644 --- a/src/lib/sources/tarball/__tests__/fetch.test.ts +++ b/src/lib/sources/tarball/__tests__/fetch.test.ts @@ -11,8 +11,8 @@ import * as Layer from "effect/Layer" import { createTarGzip } from "nanotar" import { afterEach, describe, expect, it } from "vitest" import { TarballFetchError } from "#lib/core/errors.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { fetchTarballSnapshot } from "#lib/sources/tarball/fetch.ts" -import { PackrefHome } from "#lib/workspace/home.ts" const temporaryPaths: string[] = [] diff --git a/src/lib/sources/tarball/fetch.ts b/src/lib/sources/tarball/fetch.ts index 8c6ab50..f9d6668 100644 --- a/src/lib/sources/tarball/fetch.ts +++ b/src/lib/sources/tarball/fetch.ts @@ -4,7 +4,7 @@ import * as HttpClient from "effect/http/HttpClient" import * as HttpClientResponse from "effect/http/HttpClientResponse" import * as Path from "effect/Path" import { parseTarGzip, type ParsedTarFileItem } from "nanotar" -import type { PackageIdentity } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" import type { TarballSource } from "#lib/core/source.ts" import { TarballFetchError } from "#lib/core/errors.ts" import { checkIsPathWithin } from "#lib/shared/path.ts" diff --git a/src/lib/store/__tests__/store.test.ts b/src/lib/store/__tests__/store.test.ts index eced956..2d58a17 100644 --- a/src/lib/store/__tests__/store.test.ts +++ b/src/lib/store/__tests__/store.test.ts @@ -7,13 +7,13 @@ import * as NodeServices from "@effect/platform-node/NodeServices" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import { afterEach, describe, expect, it } from "vitest" +import { PackrefHome } from "#lib/layout/home.ts" import { getStoreEntryPath, hasStoreEntry, listStoreEntries, removeStoreEntry, } from "#lib/store/index.ts" -import { PackrefHome } from "#lib/workspace/home.ts" const temporaryPaths: string[] = [] diff --git a/src/lib/store/index.ts b/src/lib/store/index.ts index 581ee49..cdf4297 100644 --- a/src/lib/store/index.ts +++ b/src/lib/store/index.ts @@ -8,19 +8,16 @@ import * as Schema from "effect/Schema" import * as Semaphore from "effect/Semaphore" import type { PackageSource } from "#lib/core/source.ts" import { GlobalStoreFilesystemError, StoreCorruptedError } from "#lib/core/errors.ts" -import { - packageIdentityOrder, - PACKAGE_DIRECTORY_NAME, - SUPPORTED_REPOSITORY_PROVIDERS, - type PackageIdentity, -} from "#lib/core/packages.ts" +import { type PackageIdentity, packageIdentityOrder } from "#lib/core/identity.ts" +import { PACKAGE_DIRECTORY_NAME } from "#lib/core/packages.ts" +import { checkIsRepositoryProvider } from "#lib/core/repository.ts" import { PackageSourceSchema } from "#lib/core/source.ts" +import { PackrefHome } from "#lib/layout/home.ts" import { formatJson } from "#lib/shared/json.ts" import { getGlobalStorePath, getStoreEntryPaths as getPathsForStoreEntry, } from "#lib/store/paths.ts" -import { PackrefHome } from "#lib/workspace/home.ts" export interface StoreEntry { readonly identity: PackageIdentity @@ -121,8 +118,7 @@ export const listStoreEntries = Effect.fn("listStoreEntries")(function* () { const packageSegmentPath = path.join(registryPath, packageSegment) const hasNestedPackageName = - packageSegment.startsWith("@") - || SUPPORTED_REPOSITORY_PROVIDERS.some((provider) => provider === registry) + packageSegment.startsWith("@") || checkIsRepositoryProvider(registry) if (hasNestedPackageName) { const scopedPackages = yield* listDirectoryOrEmpty(packageSegmentPath, semaphore) diff --git a/src/lib/store/paths.ts b/src/lib/store/paths.ts index 111b215..ee01043 100644 --- a/src/lib/store/paths.ts +++ b/src/lib/store/paths.ts @@ -1,7 +1,8 @@ import * as Effect from "effect/Effect" import * as Path from "effect/Path" -import { getPackageIdentitySegments, type PackageIdentity } from "#lib/core/packages.ts" -import { getGlobalDirectoryPath } from "#lib/workspace/paths.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" +import { getPackageIdentitySegments } from "#lib/core/packages.ts" +import { getGlobalDirectoryPath } from "#lib/layout/paths.ts" export const STORE_DIRECTORY_NAME = "store" export const STORE_METADATA_DIRECTORY_NAME = ".metadata" @@ -9,16 +10,6 @@ export const STORE_METADATA_DIRECTORY_NAME = ".metadata" export const getGlobalStorePath = (path: Path.Path, home: string) => path.join(getGlobalDirectoryPath(path, home), STORE_DIRECTORY_NAME) -export const getStorePackagePath = Effect.fn("getStorePackagePath")(function* ( - storeRoot: string, - identity: PackageIdentity -) { - const path = yield* Path.Path - const segments = yield* getPackageIdentitySegments(identity) - - return path.join(storeRoot, ...segments) -}) - export const getStoreEntryPaths = Effect.fn("getStoreEntryPaths")(function* ( storeRoot: string, identity: PackageIdentity diff --git a/src/lib/workspace/config.ts b/src/lib/workspace/config.ts index 7603830..2454d79 100644 --- a/src/lib/workspace/config.ts +++ b/src/lib/workspace/config.ts @@ -4,9 +4,9 @@ import * as Filter from "effect/Filter" import * as Path from "effect/Path" import * as Schema from "effect/Schema" import { ConfigParseError } from "#lib/core/errors.ts" +import { PackrefHome } from "#lib/layout/home.ts" +import { getGlobalConfigPath, getGlobalDirectoryPath } from "#lib/layout/paths.ts" import { formatJson } from "#lib/shared/json.ts" -import { PackrefHome } from "#lib/workspace/home.ts" -import { getGlobalConfigPath, getGlobalDirectoryPath } from "#lib/workspace/paths.ts" export const GlobalConfigSchema = Schema.Struct({ projects: Schema.Array(Schema.String), diff --git a/src/lib/workspace/lockfile.ts b/src/lib/workspace/lockfile.ts index 68a4a0e..8cb44d4 100644 --- a/src/lib/workspace/lockfile.ts +++ b/src/lib/workspace/lockfile.ts @@ -4,16 +4,16 @@ import * as FileSystem from "effect/FileSystem" import * as Filter from "effect/Filter" import * as Path from "effect/Path" import * as Schema from "effect/Schema" +import type { ParsedPackageSpec } from "#lib/core/packages.ts" import { LockfileParseError } from "#lib/core/errors.ts" import { + type PackageIdentity, packageIdentityEquivalence, packageIdentityOrder, - type PackageIdentity, - type ParsedPackageSpec, -} from "#lib/core/packages.ts" +} from "#lib/core/identity.ts" import { PackageSourceSchema } from "#lib/core/source.ts" +import { getProjectLockfilePath } from "#lib/layout/paths.ts" import { formatJson } from "#lib/shared/json.ts" -import { getProjectLockfilePath } from "#lib/workspace/paths.ts" export const PackageEntrySchema = Schema.Struct({ name: Schema.String, diff --git a/src/lib/workspace/project.ts b/src/lib/workspace/project.ts index a3df2a7..643d9b5 100644 --- a/src/lib/workspace/project.ts +++ b/src/lib/workspace/project.ts @@ -1,12 +1,11 @@ import * as Effect from "effect/Effect" import * as FileSystem from "effect/FileSystem" import * as Path from "effect/Path" -import type { PackageIdentity } from "#lib/core/packages.ts" +import type { PackageIdentity } from "#lib/core/identity.ts" import type { PackageSource } from "#lib/core/source.ts" import { NotInitializedError, ReflinkError } from "#lib/core/errors.ts" +import { getDirectoryPath, getPackageIdentityPath } from "#lib/layout/paths.ts" import { checkIsPathWithin } from "#lib/shared/path.ts" -import { getStorePackagePath } from "#lib/store/paths.ts" -import { getDirectoryPath } from "#lib/workspace/paths.ts" import { Reflinker } from "#lib/workspace/reflinker.ts" export const ensureDirectory = Effect.fn("ensureDirectory")(function* (projectPath: string) { @@ -43,7 +42,7 @@ export const getProjectReferencePath = Effect.fn("getProjectReferencePath")(func ) { const path = yield* Path.Path - return yield* getStorePackagePath(getDirectoryPath(path, projectPath), identity) + return yield* getPackageIdentityPath(getDirectoryPath(path, projectPath), identity) }) export const hasProjectReference = Effect.fn("hasProjectReference")(function* ( @@ -66,7 +65,7 @@ export const createProjectReference = Effect.fn("createProjectReference")(functi const path = yield* Path.Path const reflinker = yield* Reflinker const projectDirectoryPath = getDirectoryPath(path, projectPath) - const targetPath = yield* getStorePackagePath(projectDirectoryPath, identity) + const targetPath = yield* getPackageIdentityPath(projectDirectoryPath, identity) const referenceSourcePath = source.type === "repository" && source.directory !== undefined ? path.resolve(storePath, source.directory)