diff --git a/ts/project.test.ts b/ts/project.test.ts index d9cd67d9..48712a65 100644 --- a/ts/project.test.ts +++ b/ts/project.test.ts @@ -1,13 +1,15 @@ import { Check } from "./check.ts"; import { Executable } from "./executable.ts"; -import { Project } from "./project.ts"; +import { Plugin, Project } from "./project.ts"; import { describe, it } from "https://deno.land/std@0.206.0/testing/bdd.ts"; import * as garn from "./mod.ts"; import * as nix from "./nix.ts"; import { assertStdout, assertSuccess, runExecutable } from "./testUtils.ts"; +import { Package } from "./mod.ts"; const assertTypeIsCheck = (_c: Check) => {}; const assertTypeIsExecutable = (_e: Executable) => {}; +const assertTypeIsPackage = (_p: Package) => {}; const _testTypeCheckingOfAddCheck = (project: Project) => { const p = project @@ -133,4 +135,78 @@ describe("Project.add", () => { assertSuccess(output); assertStdout(output, "bar\n"); }); + + it("provides a nice type synonym for plugins that add a field", () => { + const plugin: Plugin = (p) => ({ + ...p, + addedField: garn.build``, + }); + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + {}, + ) + .addExecutable("foo", "") + .add(plugin); + assertTypeIsExecutable(project.foo); + assertTypeIsPackage(project.addedField); + }); + + it("provides a nice type synonym for plugins that add multiple fields", () => { + const plugin: Plugin = ( + p, + ) => ({ + ...p, + one: garn.build``, + two: garn.check(""), + }); + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + {}, + ) + .addExecutable("foo", "") + .add(plugin); + assertTypeIsExecutable(project.foo); + assertTypeIsPackage(project.one); + assertTypeIsCheck(project.two); + }); + + it("provides a nice interface for plugins that depend on a non-standard field", () => { + const plugin: Plugin<{ dep: Package }, { addedField: Executable }> = ( + p, + ) => ({ + ...p, + addedField: garn.shell`${p.dep}/bin/whatever`, + }); + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + { dep: garn.build`` }, + ) + .addExecutable("foo", "") + .add(plugin); + assertTypeIsExecutable(project.foo); + assertTypeIsExecutable(project.addedField); + // @ts-expect-error - `dep` is missing + () => garn.mkProject({ description: "" }, {}).add(plugin); + }); + + it("allows to overwrite fields", () => { + const plugin: Plugin = (p) => ({ + ...p, + field: garn.build``, + }); + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + { field: garn.shell("") }, + ) + .addExecutable("foo", "") + .add(plugin); + assertTypeIsExecutable(project.foo); + // @ts-expect-error - should not be an `Executable` anymore + assertTypeIsExecutable(project.field); + assertTypeIsPackage(project.field); + }); }); diff --git a/ts/project.ts b/ts/project.ts index d24b20ca..301053d0 100644 --- a/ts/project.ts +++ b/ts/project.ts @@ -1,11 +1,11 @@ import "./internal/registerInternalLib.ts"; import { Check, mkCheck } from "./check.ts"; -import { Environment, emptyEnvironment } from "./environment.ts"; +import { Environment } from "./environment.ts"; import { Executable, mkShellExecutable } from "./executable.ts"; import { hasTag } from "./internal/utils.ts"; import { NixStrLitInterpolatable } from "./nix.ts"; -import { Package, mkShellPackage } from "./package.ts"; +import { mkShellPackage, Package } from "./package.ts"; import { markAsMayNotExport } from "./internal/may_not_export.ts"; /** @@ -15,13 +15,15 @@ import { markAsMayNotExport } from "./internal/may_not_export.ts"; */ export type Project = ProjectHelpers & ProjectData; -type ProjectData = { +export type ProjectData = { tag: "project"; description: string; defaultEnvironment?: Environment; defaultExecutable?: Executable; }; +export type Plugin = (project: Input) => Output; + type ProjectHelpers = { /** * Returns a new Project with the provided devtools added to the default @@ -78,10 +80,10 @@ type ProjectHelpers = { * .add(self => self.addExecutable("codegen")`${self.mainPackage}/bin/codegen`) * ``` */ - add( - this: T, - fn: (p: T) => U, - ): U; + add( + this: Input, + fn: Plugin, + ): Omit & Output; /** * Adds an `Executable` with the given name to the Project @@ -215,11 +217,11 @@ const proxyEnvironmentHelpers = (): ProjectHelpers => ({ return mkShellPackage(defaultEnvironment, s, ...args); }, - add( - this: T, - fn: (p: T) => U, - ): U { - return fn(this); + add( + this: Input, + fn: Plugin, + ): Omit & Output { + return { ...this, ...fn(this) }; }, addExecutable(