Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Project.add helper #400

Merged
merged 9 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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