chore: generate
This commit is contained in:
@@ -26,9 +26,21 @@ export type Resolved =
|
|||||||
type Valid = Exclude<Resolved, { kind: "invalid" }>
|
type Valid = Exclude<Resolved, { kind: "invalid" }>
|
||||||
|
|
||||||
export type Mention =
|
export type Mention =
|
||||||
| { readonly name: string; readonly kind: "reference"; readonly reference: Valid; readonly target?: string; readonly path: string }
|
| {
|
||||||
|
readonly name: string
|
||||||
|
readonly kind: "reference"
|
||||||
|
readonly reference: Valid
|
||||||
|
readonly target?: string
|
||||||
|
readonly path: string
|
||||||
|
}
|
||||||
| { readonly name: string; readonly kind: "invalid"; readonly target?: string; readonly message: string }
|
| { readonly name: string; readonly kind: "invalid"; readonly target?: string; readonly message: string }
|
||||||
| { readonly name: string; readonly kind: "missing"; readonly target: string; readonly path: string; readonly message: string }
|
| {
|
||||||
|
readonly name: string
|
||||||
|
readonly kind: "missing"
|
||||||
|
readonly target: string
|
||||||
|
readonly path: string
|
||||||
|
readonly message: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly list: () => Effect.Effect<Resolved[]>
|
readonly list: () => Effect.Effect<Resolved[]>
|
||||||
@@ -73,7 +85,9 @@ export const layer = Layer.effect(
|
|||||||
repository: reference.repository,
|
repository: reference.repository,
|
||||||
path: reference.path,
|
path: reference.path,
|
||||||
run: yield* Effect.cached(
|
run: yield* Effect.cached(
|
||||||
cache.ensure({ reference: reference.reference, branch: reference.branch, refresh: true }).pipe(Effect.asVoid),
|
cache
|
||||||
|
.ensure({ reference: reference.reference, branch: reference.branch, refresh: true })
|
||||||
|
.pipe(Effect.asVoid),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -90,13 +104,12 @@ export const layer = Layer.effect(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
{ concurrency: 4, discard: true },
|
{ concurrency: 4, discard: true },
|
||||||
).pipe(
|
).pipe(Effect.forkScoped)
|
||||||
Effect.forkScoped,
|
|
||||||
)
|
|
||||||
|
|
||||||
const ensurePath = Effect.fn("ProjectReference.ensurePath")(function* (target?: string) {
|
const ensurePath = Effect.fn("ProjectReference.ensurePath")(function* (target?: string) {
|
||||||
const normalized = normalizePath(target)
|
const normalized = normalizePath(target)
|
||||||
if (!normalized) return yield* Effect.forEach(materializers, (materializer) => materializer.run, { discard: true })
|
if (!normalized)
|
||||||
|
return yield* Effect.forEach(materializers, (materializer) => materializer.run, { discard: true })
|
||||||
yield* materializers.find((materializer) => contains(materializer.path, normalized))?.run ?? Effect.void
|
yield* materializers.find((materializer) => contains(materializer.path, normalized))?.run ?? Effect.void
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -110,7 +123,9 @@ export const layer = Layer.effect(
|
|||||||
ensurePath,
|
ensurePath,
|
||||||
containsManagedPath: Effect.fn("ProjectReference.containsManagedPath")(function* (target?: string) {
|
containsManagedPath: Effect.fn("ProjectReference.containsManagedPath")(function* (target?: string) {
|
||||||
const normalized = normalizePath(target)
|
const normalized = normalizePath(target)
|
||||||
return normalized ? references.some((reference) => reference.kind === "git" && contains(reference.path, normalized)) : false
|
return normalized
|
||||||
|
? references.some((reference) => reference.kind === "git" && contains(reference.path, normalized))
|
||||||
|
: false
|
||||||
}),
|
}),
|
||||||
resolveMention: Effect.fn("ProjectReference.resolveMention")(function* (value: string) {
|
resolveMention: Effect.fn("ProjectReference.resolveMention")(function* (value: string) {
|
||||||
const [name, ...rest] = value.split("/")
|
const [name, ...rest] = value.split("/")
|
||||||
@@ -122,8 +137,10 @@ export const layer = Layer.effect(
|
|||||||
if (!target) return { name, kind: "reference", reference, path: reference.path }
|
if (!target) return { name, kind: "reference", reference, path: reference.path }
|
||||||
|
|
||||||
const resolved = path.resolve(reference.path, target)
|
const resolved = path.resolve(reference.path, target)
|
||||||
if (!AppFileSystem.contains(reference.path, resolved)) return { name, kind: "invalid", target, message: "Reference target escapes its root" }
|
if (!AppFileSystem.contains(reference.path, resolved))
|
||||||
if (!(yield* fs.existsSafe(resolved))) return { name, kind: "missing", target, path: resolved, message: "Reference target does not exist" }
|
return { name, kind: "invalid", target, message: "Reference target escapes its root" }
|
||||||
|
if (!(yield* fs.existsSafe(resolved)))
|
||||||
|
return { name, kind: "missing", target, path: resolved, message: "Reference target does not exist" }
|
||||||
return { name, kind: "reference", reference, target, path: resolved }
|
return { name, kind: "reference", reference, target, path: resolved }
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -140,7 +157,12 @@ const inert: Interface = {
|
|||||||
containsManagedPath: () => Effect.succeed(false),
|
containsManagedPath: () => Effect.succeed(false),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAll(input: { references: ConfigReference.NormalizedInfo; directory: string; home: string; repos: string }) {
|
export function resolveAll(input: {
|
||||||
|
references: ConfigReference.NormalizedInfo
|
||||||
|
directory: string
|
||||||
|
home: string
|
||||||
|
repos: string
|
||||||
|
}) {
|
||||||
const seen = new Map<string, { name: string; branch?: string }>()
|
const seen = new Map<string, { name: string; branch?: string }>()
|
||||||
return Object.entries(input.references).map(([name, reference]): Resolved => {
|
return Object.entries(input.references).map(([name, reference]): Resolved => {
|
||||||
const resolved = resolve({ name, reference, directory: input.directory, home: input.home, repos: input.repos })
|
const resolved = resolve({ name, reference, directory: input.directory, home: input.home, repos: input.repos })
|
||||||
@@ -160,7 +182,13 @@ export function resolveAll(input: { references: ConfigReference.NormalizedInfo;
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolve(input: { name: string; reference: ConfigReference.NormalizedEntry; directory: string; home: string; repos: string }): Resolved {
|
export function resolve(input: {
|
||||||
|
name: string
|
||||||
|
reference: ConfigReference.NormalizedEntry
|
||||||
|
directory: string
|
||||||
|
home: string
|
||||||
|
repos: string
|
||||||
|
}): Resolved {
|
||||||
if (input.reference.kind === "invalid") return { name: input.name, kind: "invalid", message: input.reference.message }
|
if (input.reference.kind === "invalid") return { name: input.name, kind: "invalid", message: input.reference.message }
|
||||||
if (input.reference.kind === "local") {
|
if (input.reference.kind === "local") {
|
||||||
return { name: input.name, kind: "local", path: localPath(input.directory, input.home, input.reference.path) }
|
return { name: input.name, kind: "local", path: localPath(input.directory, input.home, input.reference.path) }
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ import { it } from "./lib/effect"
|
|||||||
|
|
||||||
describe("ProjectReference", () => {
|
describe("ProjectReference", () => {
|
||||||
it.live("uses the broad experimental flag unless references are explicitly configured", () =>
|
it.live("uses the broad experimental flag unless references are explicitly configured", () =>
|
||||||
withEnv({ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: undefined },
|
withEnv(
|
||||||
|
{ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: undefined },
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(true)
|
expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(true)
|
||||||
}),
|
}),
|
||||||
).pipe(
|
).pipe(
|
||||||
Effect.flatMap(() =>
|
Effect.flatMap(() =>
|
||||||
withEnv({ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: "false" },
|
withEnv(
|
||||||
|
{ OPENCODE_EXPERIMENTAL: "true", OPENCODE_EXPERIMENTAL_REFERENCES: "false" },
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(false)
|
expect(Flag.OPENCODE_EXPERIMENTAL_REFERENCES).toBe(false)
|
||||||
}),
|
}),
|
||||||
@@ -85,84 +87,100 @@ describe("ProjectReference", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
it.live("merges config aliases and exposes mention and managed-path operations", () =>
|
it.live("merges config aliases and exposes mention and managed-path operations", () =>
|
||||||
withoutReferences(withTmp((tmp) => {
|
withoutReferences(
|
||||||
const calls: RepositoryCache.EnsureInput[] = []
|
withTmp((tmp) => {
|
||||||
const project = path.join(tmp.path, "project")
|
const calls: RepositoryCache.EnsureInput[] = []
|
||||||
const nested = path.join(project, "packages", "app")
|
const project = path.join(tmp.path, "project")
|
||||||
const docs = path.join(project, "docs")
|
const nested = path.join(project, "packages", "app")
|
||||||
const repos = path.join(tmp.path, "repos")
|
const docs = path.join(project, "docs")
|
||||||
return Effect.gen(function* () {
|
const repos = path.join(tmp.path, "repos")
|
||||||
yield* Effect.promise(async () => {
|
return Effect.gen(function* () {
|
||||||
await fs.mkdir(nested, { recursive: true })
|
yield* Effect.promise(async () => {
|
||||||
await fs.mkdir(docs)
|
await fs.mkdir(nested, { recursive: true })
|
||||||
await fs.writeFile(path.join(docs, "README.md"), "docs")
|
await fs.mkdir(docs)
|
||||||
})
|
await fs.writeFile(path.join(docs, "README.md"), "docs")
|
||||||
|
})
|
||||||
|
|
||||||
yield* withReferences(
|
yield* withReferences(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const references = yield* ProjectReference.Service
|
const references = yield* ProjectReference.Service
|
||||||
const git = path.join(repos, "github.com", "owner", "repo")
|
const git = path.join(repos, "github.com", "owner", "repo")
|
||||||
|
|
||||||
expect(yield* references.list()).toMatchObject([
|
expect(yield* references.list()).toMatchObject([
|
||||||
{ name: "docs", kind: "local", path: docs },
|
{ name: "docs", kind: "local", path: docs },
|
||||||
{ name: "sdk", kind: "git", path: git },
|
{ name: "sdk", kind: "git", path: git },
|
||||||
])
|
])
|
||||||
expect(yield* references.resolveMention("docs/README.md")).toMatchObject({
|
expect(yield* references.resolveMention("docs/README.md")).toMatchObject({
|
||||||
name: "docs",
|
name: "docs",
|
||||||
kind: "reference",
|
kind: "reference",
|
||||||
target: "README.md",
|
target: "README.md",
|
||||||
path: path.join(docs, "README.md"),
|
path: path.join(docs, "README.md"),
|
||||||
})
|
})
|
||||||
expect(yield* references.resolveMention("docs/missing.md")).toMatchObject({ name: "docs", kind: "missing" })
|
expect(yield* references.resolveMention("docs/missing.md")).toMatchObject({
|
||||||
expect(yield* references.resolveMention("docs/../outside.md")).toMatchObject({ name: "docs", kind: "invalid" })
|
name: "docs",
|
||||||
expect(yield* references.resolveMention("unknown")).toBeUndefined()
|
kind: "missing",
|
||||||
expect(yield* references.resolveMention("sdk")).toMatchObject({ name: "sdk", kind: "reference", path: git })
|
})
|
||||||
expect(yield* references.containsManagedPath(path.join(git, "README.md"))).toBe(true)
|
expect(yield* references.resolveMention("docs/../outside.md")).toMatchObject({
|
||||||
expect(yield* references.containsManagedPath(path.join(docs, "README.md"))).toBe(false)
|
name: "docs",
|
||||||
yield* references.ensurePath()
|
kind: "invalid",
|
||||||
expect(calls).toHaveLength(1)
|
})
|
||||||
}).pipe(
|
expect(yield* references.resolveMention("unknown")).toBeUndefined()
|
||||||
Effect.provide(
|
expect(yield* references.resolveMention("sdk")).toMatchObject({
|
||||||
testLayer({
|
name: "sdk",
|
||||||
directory: nested,
|
kind: "reference",
|
||||||
project,
|
path: git,
|
||||||
repos,
|
})
|
||||||
documents: [
|
expect(yield* references.containsManagedPath(path.join(git, "README.md"))).toBe(true)
|
||||||
document({ docs: { path: "./old-docs" }, sdk: "owner/old" }),
|
expect(yield* references.containsManagedPath(path.join(docs, "README.md"))).toBe(false)
|
||||||
document({ docs: { path: "./docs" }, sdk: { repository: "owner/repo", branch: "main" } }),
|
yield* references.ensurePath()
|
||||||
],
|
expect(calls).toHaveLength(1)
|
||||||
ensure: (input) => Effect.sync(() => result(repos, calls, input)),
|
}).pipe(
|
||||||
}),
|
Effect.provide(
|
||||||
|
testLayer({
|
||||||
|
directory: nested,
|
||||||
|
project,
|
||||||
|
repos,
|
||||||
|
documents: [
|
||||||
|
document({ docs: { path: "./old-docs" }, sdk: "owner/old" }),
|
||||||
|
document({ docs: { path: "./docs" }, sdk: { repository: "owner/repo", branch: "main" } }),
|
||||||
|
],
|
||||||
|
ensure: (input) => Effect.sync(() => result(repos, calls, input)),
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
})
|
||||||
})
|
}),
|
||||||
})),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("is inert while the runtime flag is disabled", () =>
|
it.live("is inert while the runtime flag is disabled", () =>
|
||||||
withoutReferences(withTmp((tmp) => {
|
withoutReferences(
|
||||||
const calls: RepositoryCache.EnsureInput[] = []
|
withTmp((tmp) => {
|
||||||
return Effect.gen(function* () {
|
const calls: RepositoryCache.EnsureInput[] = []
|
||||||
const references = yield* ProjectReference.Service
|
return Effect.gen(function* () {
|
||||||
expect(yield* references.list()).toEqual([])
|
const references = yield* ProjectReference.Service
|
||||||
expect(yield* references.get("sdk")).toBeUndefined()
|
expect(yield* references.list()).toEqual([])
|
||||||
expect(yield* references.resolveMention("sdk")).toBeUndefined()
|
expect(yield* references.get("sdk")).toBeUndefined()
|
||||||
expect(yield* references.containsManagedPath(path.join(tmp.path, "repos", "github.com", "owner", "repo"))).toBe(false)
|
expect(yield* references.resolveMention("sdk")).toBeUndefined()
|
||||||
yield* references.ensurePath()
|
expect(
|
||||||
expect(calls).toEqual([])
|
yield* references.containsManagedPath(path.join(tmp.path, "repos", "github.com", "owner", "repo")),
|
||||||
}).pipe(
|
).toBe(false)
|
||||||
Effect.provide(
|
yield* references.ensurePath()
|
||||||
testLayer({
|
expect(calls).toEqual([])
|
||||||
directory: tmp.path,
|
}).pipe(
|
||||||
project: tmp.path,
|
Effect.provide(
|
||||||
repos: path.join(tmp.path, "repos"),
|
testLayer({
|
||||||
documents: [document({ sdk: "owner/repo" })],
|
directory: tmp.path,
|
||||||
ensure: (input) => Effect.sync(() => result(path.join(tmp.path, "repos"), calls, input)),
|
project: tmp.path,
|
||||||
}),
|
repos: path.join(tmp.path, "repos"),
|
||||||
),
|
documents: [document({ sdk: "owner/repo" })],
|
||||||
)
|
ensure: (input) => Effect.sync(() => result(path.join(tmp.path, "repos"), calls, input)),
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("starts Git materialization in the background without blocking the location layer", () =>
|
it.live("starts Git materialization in the background without blocking the location layer", () =>
|
||||||
@@ -173,7 +191,10 @@ describe("ProjectReference", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
expect(yield* (yield* ProjectReference.Service).list()).toHaveLength(1)
|
expect(yield* (yield* ProjectReference.Service).list()).toHaveLength(1)
|
||||||
yield* Deferred.await(started).pipe(
|
yield* Deferred.await(started).pipe(
|
||||||
Effect.timeoutOrElse({ duration: "1 second", orElse: () => Effect.die(new Error("refresh did not start")) }),
|
Effect.timeoutOrElse({
|
||||||
|
duration: "1 second",
|
||||||
|
orElse: () => Effect.die(new Error("refresh did not start")),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
@@ -196,7 +217,11 @@ function document(references: ConfigReference.Info) {
|
|||||||
return new Config.Loaded({ source: { type: "memory" }, info: Schema.decodeUnknownSync(Config.Info)({ references }) })
|
return new Config.Loaded({ source: { type: "memory" }, info: Schema.decodeUnknownSync(Config.Info)({ references }) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function result(repos: string, calls: RepositoryCache.EnsureInput[], input: RepositoryCache.EnsureInput): RepositoryCache.Result {
|
function result(
|
||||||
|
repos: string,
|
||||||
|
calls: RepositoryCache.EnsureInput[],
|
||||||
|
input: RepositoryCache.EnsureInput,
|
||||||
|
): RepositoryCache.Result {
|
||||||
calls.push(input)
|
calls.push(input)
|
||||||
return {
|
return {
|
||||||
repository: input.reference.label,
|
repository: input.reference.label,
|
||||||
@@ -223,10 +248,16 @@ function testLayer(input: {
|
|||||||
Layer.succeed(
|
Layer.succeed(
|
||||||
Location.Service,
|
Location.Service,
|
||||||
Location.Service.of(
|
Location.Service.of(
|
||||||
location({ directory: AbsolutePath.make(input.directory) }, { projectDirectory: AbsolutePath.make(input.project) }),
|
location(
|
||||||
|
{ directory: AbsolutePath.make(input.directory) },
|
||||||
|
{ projectDirectory: AbsolutePath.make(input.project) },
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Layer.succeed(Config.Service, Config.Service.of({ directories: () => Effect.succeed([]), get: () => Effect.succeed(input.documents) })),
|
Layer.succeed(
|
||||||
|
Config.Service,
|
||||||
|
Config.Service.of({ directories: () => Effect.succeed([]), get: () => Effect.succeed(input.documents) }),
|
||||||
|
),
|
||||||
Layer.succeed(RepositoryCache.Service, RepositoryCache.Service.of({ ensure: input.ensure })),
|
Layer.succeed(RepositoryCache.Service, RepositoryCache.Service.of({ ensure: input.ensure })),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -234,7 +265,11 @@ function testLayer(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withTmp<A, E, R>(body: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>) {
|
function withTmp<A, E, R>(body: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>) {
|
||||||
return Effect.acquireUseRelease(Effect.promise(() => tmpdir()), body, (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()))
|
return Effect.acquireUseRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
body,
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function withReferences<A, E, R>(body: Effect.Effect<A, E, R>) {
|
function withReferences<A, E, R>(body: Effect.Effect<A, E, R>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user