diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df62958..6dbc64fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ - Allow to build packages that are nested within projects with `garn build projectName.packageName`. - Allow to build top-level packages with `garn build packageName. - Allow adding packages to projects with `.addPackage("packageName", "{build script writing to $out}")`. +- Add `Project.add`, a function to apply so-called `Plugin`s to a project. This + provides a nice way to bundle up more complex project modifications into a + single declaration. It also allows to use `Plugin`s from other sources, + including third-party libraries. ## v0.0.15 diff --git a/examples/frontend-create-react-app/flake.nix b/examples/frontend-create-react-app/flake.nix index 0e504cd1..34a5186b 100644 --- a/examples/frontend-create-react-app/flake.nix +++ b/examples/frontend-create-react-app/flake.nix @@ -152,7 +152,10 @@ ); apps = forAllSystems (system: let - pkgs = import "${nixpkgs}" { inherit system; }; + pkgs = import "${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in { "main/start" = { diff --git a/examples/frontend-yarn-webpack/flake.nix b/examples/frontend-yarn-webpack/flake.nix index 42cc7eea..a35e38c5 100644 --- a/examples/frontend-yarn-webpack/flake.nix +++ b/examples/frontend-yarn-webpack/flake.nix @@ -125,7 +125,10 @@ ); apps = forAllSystems (system: let - pkgs = import "${nixpkgs}" { inherit system; }; + pkgs = import "${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in { "frontend" = { diff --git a/examples/go-http-backend/flake.nix b/examples/go-http-backend/flake.nix index 5a488f1f..2b289b1e 100644 --- a/examples/go-http-backend/flake.nix +++ b/examples/go-http-backend/flake.nix @@ -126,7 +126,10 @@ ); apps = forAllSystems (system: let - pkgs = import "${nixpkgs}" { inherit system; }; + pkgs = import "${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in { "server/migrate" = { diff --git a/examples/haskell/flake.nix b/examples/haskell/flake.nix index fa42c5a3..fe9c8958 100644 --- a/examples/haskell/flake.nix +++ b/examples/haskell/flake.nix @@ -186,7 +186,10 @@ ); apps = forAllSystems (system: let - pkgs = import "${nixpkgs}" { inherit system; }; + pkgs = import "${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in { "helloFromHaskell" = { diff --git a/examples/monorepo/flake.nix b/examples/monorepo/flake.nix index be0d43ad..4777eae3 100644 --- a/examples/monorepo/flake.nix +++ b/examples/monorepo/flake.nix @@ -232,7 +232,10 @@ ); apps = forAllSystems (system: let - pkgs = import "${nixpkgs}" { inherit system; }; + pkgs = import "${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in { "backend/run" = { diff --git a/examples/npm-project/flake.nix b/examples/npm-project/flake.nix index 2b812418..802cd0a5 100644 --- a/examples/npm-project/flake.nix +++ b/examples/npm-project/flake.nix @@ -224,7 +224,10 @@ ); apps = forAllSystems (system: let - pkgs = import "${nixpkgs}" { inherit system; }; + pkgs = import "${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in { "run" = { diff --git a/justfile b/justfile index 4037fa31..52ef5ee2 100644 --- a/justfile +++ b/justfile @@ -112,8 +112,8 @@ hpack-check: when (oldCabal /= newCabal) $ error "package.yaml has changed, please run hpack" -test-ts: - deno test --allow-write --allow-read --allow-run ts/*.test.ts ts/**/*.test.ts +test-ts *args="": + deno test --check --allow-write --allow-read --allow-run ts/*.test.ts ts/**/*.test.ts {{ args }} test *args="": hpack cabal run --test-show-details=streaming garn:spec -- {{ args }} diff --git a/ts/internal/runner.ts b/ts/internal/runner.ts index 3e01d5b7..211fba4f 100644 --- a/ts/internal/runner.ts +++ b/ts/internal/runner.ts @@ -173,7 +173,10 @@ const formatFlake = ( ${nixAttrSet(shells)} ); apps = forAllSystems (system: let - pkgs = import "\${nixpkgs}" { inherit system; }; + pkgs = import "\${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in ${nixAttrSet(apps)} ); diff --git a/ts/project.test.ts b/ts/project.test.ts index 8119f5b0..4b024ab4 100644 --- a/ts/project.test.ts +++ b/ts/project.test.ts @@ -1,9 +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 @@ -50,3 +56,151 @@ const _testTypeCheckingOfAddExecutableTemplate = (project: Project) => { // @ts-expect-error - shell should be the actual executable now, not the helper p.shell``; }; + +describe("Project.add", () => { + it("allows adding fields with .add", () => { + const project = garn + .mkProject({ description: "" }, {}) + .add((self) => ({ ...self, foo: garn.shell("echo foo") })); + const output = runExecutable(project.foo); + assertSuccess(output); + assertStdout(output, "foo\n"); + }); + + it("allows adding fields while referencing the Project", () => { + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + {}, + ) + .withDevTools([garn.mkPackage(nix.nixRaw`pkgs.hello`, "")]) + .add((self) => ({ ...self, foo: self.shell("hello") })); + const output = runExecutable(project.foo); + assertSuccess(output); + assertStdout(output, "Hello, world!\n"); + }); + + it("allows splicing in existing fields", () => { + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + { + package: garn.build` + mkdir -p $out/bin + echo 'echo main executable' > $out/bin/main + chmod +x $out/bin/main + `, + }, + ) + .add((self) => ({ ...self, foo: self.shell`${self.package}/bin/main` })); + const output = runExecutable(project.foo); + assertSuccess(output); + assertStdout(output, "main executable\n"); + }); + + it("allows bundling multiple changes into plugins", () => { + const plugin =

(p: P) => + p.addExecutable("foo", "echo foo").addExecutable("bar", "echo bar"); + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + {}, + ) + .add(plugin); + let output = runExecutable(project.foo); + assertSuccess(output); + assertStdout(output, "foo\n"); + output = runExecutable(project.bar); + assertSuccess(output); + assertStdout(output, "bar\n"); + }); + + it("allows writing plugins with parameters", () => { + const plugin = +

(config: { a: string; b: string }) => + (p: P) => + p + .addExecutable("a", `echo ${config.a}`) + .addExecutable("b", `echo ${config.b}`); + const project = garn + .mkProject( + { description: "", defaultEnvironment: garn.emptyEnvironment }, + {}, + ) + .add(plugin({ a: "foo", b: "bar" })); + let output = runExecutable(project.a); + assertSuccess(output); + assertStdout(output, "foo\n"); + output = runExecutable(project.b); + assertSuccess(output); + assertStdout(output, "bar\n"); + }); + + it("provides a nice type synonym for plugins that add a field", () => { + const plugin: Plugin<{ addedField: garn.Package }> = (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<{ one: garn.Package; two: garn.Check }> = (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<{ addedField: Executable }, { dep: Package }> = ( + 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 overwriting fields", () => { + const plugin: Plugin<{ field: Package }> = (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 6e15adb4..fd625f5a 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,17 @@ 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: Dependencies & ProjectData, +) => Additions; + type ProjectHelpers = { /** * Returns a new Project with the provided devtools added to the default @@ -67,6 +71,22 @@ type ProjectHelpers = { ..._args: Array ): Package; + /** + * Modify the given project. + * + * This can be useful for modifying a project in a method chaining style while + * being able to reference that project. For example: + * + * ```typescript + * export const myProject = garn.mkHaskellProject(...) + * .add(self => self.addExecutable("codegen")`${self.mainPackage}/bin/codegen`) + * ``` + */ + add( + this: T, + fn: Plugin, + ): Omit & Additions; + /** * Adds an `Executable` with the given name to the Project * @@ -199,6 +219,16 @@ const proxyEnvironmentHelpers = (): ProjectHelpers => ({ return mkShellPackage(defaultEnvironment, s, ...args); }, + add( + this: T, + fn: Plugin, + ): Omit & Additions { + return { + ...this, + ...fn(this), + }; + }, + addExecutable( this: T, name: Name, diff --git a/ts/testUtils.ts b/ts/testUtils.ts new file mode 100644 index 00000000..14a97f20 --- /dev/null +++ b/ts/testUtils.ts @@ -0,0 +1,81 @@ +import { assertEquals } from "https://deno.land/std@0.206.0/assert/mod.ts"; +import * as garn from "./mod.ts"; +import * as nix from "./nix.ts"; +import { nixAttrSet } from "./nix.ts"; + +type Output = { + exitCode: number; + stdout: string; + stderr: string; +}; + +const printOutput = (output: Output) => { + console.error(` + exitcode: ${output.exitCode} + stdout: + ${output.stdout} + stderr: + ${output.stderr} + `); +}; + +export const assertSuccess = (output: Output) => { + try { + assertEquals(output.exitCode, 0); + } catch (e) { + printOutput(output); + throw e; + } +}; + +export const assertStdout = (output: Output, expected: string) => { + try { + assertEquals(output.stdout, expected); + } catch (e) { + printOutput(output); + throw e; + } +}; + +export const assertStderr = (output: Output, expected: string) => { + try { + assertEquals(output.stderr, expected); + } catch (e) { + printOutput(output); + throw e; + } +}; + +export const runExecutable = (executable: garn.Executable): Output => { + const tempDir = Deno.makeTempDirSync({ prefix: "garn-test" }); + const nixpkgsInput = nix.nixFlakeDep("nixpkgs-repo", { + url: "github:NixOS/nixpkgs/6fc7203e423bbf1c8f84cccf1c4818d097612566", + }); + const flakeFile = nix.renderFlakeFile( + nixAttrSet({ + apps: nixAttrSet({ + "x86_64-linux": nixAttrSet({ + default: nixAttrSet({ + type: nix.nixStrLit`app`, + program: nix.nixRaw` + let pkgs = import ${nixpkgsInput} { + config.allowUnfree = true; + system = "x86_64-linux"; + }; + in ${executable.nixExpression} + `, + }), + }), + }), + }), + ); + Deno.writeTextFileSync(`${tempDir}/flake.nix`, flakeFile); + const output = new Deno.Command("nix", { + args: ["run", tempDir], + }).outputSync(); + return { + exitCode: output.code, + stdout: new TextDecoder().decode(output.stdout), + stderr: new TextDecoder().decode(output.stderr), + }; +}; diff --git a/website/flake.nix b/website/flake.nix index b6d66447..c43fdb3f 100644 --- a/website/flake.nix +++ b/website/flake.nix @@ -239,7 +239,10 @@ ); apps = forAllSystems (system: let - pkgs = import "${nixpkgs}" { inherit system; }; + pkgs = import "${nixpkgs}" { + config.allowUnfree = true; + inherit system; + }; in { "build" = {