Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
208 changes: 208 additions & 0 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/add.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ 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"
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[] = []
Expand Down
3 changes: 2 additions & 1 deletion src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/commands/prune.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
3 changes: 2 additions & 1 deletion src/commands/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/sync.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
5 changes: 2 additions & 3 deletions src/lib/core/__tests__/packages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromise(effect)

Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/errors.ts
Original file line number Diff line number Diff line change
@@ -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")<{
Expand Down
34 changes: 34 additions & 0 deletions src/lib/core/identity.ts
Original file line number Diff line number Diff line change
@@ -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}`
Loading
Loading