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