Skip to content

Commit

Permalink
Add Project.add helper (#400)
Browse files Browse the repository at this point in the history
Co-authored-by: Sönke Hahn <[email protected]>
  • Loading branch information
alexdavid and soenkehahn authored Nov 14, 2023
1 parent 09db1b6 commit f075a8a
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion examples/frontend-create-react-app/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@
);
apps = forAllSystems (system:
let
pkgs = import "${nixpkgs}" { inherit system; };
pkgs = import "${nixpkgs}" {
config.allowUnfree = true;
inherit system;
};
in
{
"main/start" = {
Expand Down
5 changes: 4 additions & 1 deletion examples/frontend-yarn-webpack/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@
);
apps = forAllSystems (system:
let
pkgs = import "${nixpkgs}" { inherit system; };
pkgs = import "${nixpkgs}" {
config.allowUnfree = true;
inherit system;
};
in
{
"frontend" = {
Expand Down
5 changes: 4 additions & 1 deletion examples/go-http-backend/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@
);
apps = forAllSystems (system:
let
pkgs = import "${nixpkgs}" { inherit system; };
pkgs = import "${nixpkgs}" {
config.allowUnfree = true;
inherit system;
};
in
{
"server/migrate" = {
Expand Down
5 changes: 4 additions & 1 deletion examples/haskell/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@
);
apps = forAllSystems (system:
let
pkgs = import "${nixpkgs}" { inherit system; };
pkgs = import "${nixpkgs}" {
config.allowUnfree = true;
inherit system;
};
in
{
"helloFromHaskell" = {
Expand Down
5 changes: 4 additions & 1 deletion examples/monorepo/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,10 @@
);
apps = forAllSystems (system:
let
pkgs = import "${nixpkgs}" { inherit system; };
pkgs = import "${nixpkgs}" {
config.allowUnfree = true;
inherit system;
};
in
{
"backend/run" = {
Expand Down
5 changes: 4 additions & 1 deletion examples/npm-project/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@
);
apps = forAllSystems (system:
let
pkgs = import "${nixpkgs}" { inherit system; };
pkgs = import "${nixpkgs}" {
config.allowUnfree = true;
inherit system;
};
in
{
"run" = {
Expand Down
4 changes: 2 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
5 changes: 4 additions & 1 deletion ts/internal/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
);
Expand Down
156 changes: 155 additions & 1 deletion ts/project.test.ts
Original file line number Diff line number Diff line change
@@ -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/[email protected]/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
Expand Down Expand Up @@ -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 extends garn.Project>(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 =
<P extends garn.Project>(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);
});
});
36 changes: 33 additions & 3 deletions ts/project.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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<Additions, Dependencies = object> = (
project: Dependencies & ProjectData,
) => Additions;

type ProjectHelpers = {
/**
* Returns a new Project with the provided devtools added to the default
Expand Down Expand Up @@ -67,6 +71,22 @@ type ProjectHelpers = {
..._args: Array<NixStrLitInterpolatable>
): 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<T extends ProjectData, Additions>(
this: T,
fn: Plugin<Additions, T>,
): Omit<T, keyof Additions> & Additions;

/**
* Adds an `Executable` with the given name to the Project
*
Expand Down Expand Up @@ -199,6 +219,16 @@ const proxyEnvironmentHelpers = (): ProjectHelpers => ({
return mkShellPackage(defaultEnvironment, s, ...args);
},

add<T extends ProjectData, Additions>(
this: T,
fn: Plugin<Additions, T>,
): Omit<T, keyof Additions> & Additions {
return {
...this,
...fn(this),
};
},

addExecutable<T extends ProjectData, Name extends string>(
this: T,
name: Name,
Expand Down
Loading

0 comments on commit f075a8a

Please sign in to comment.