Skip to content

Commit

Permalink
feat(generate): Add transform option
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy351 committed Feb 9, 2024
1 parent a7474ce commit a375679
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-clocks-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kosko/generate": minor
---

Add `transform` option to `generate` and `resolve` function.
25 changes: 25 additions & 0 deletions packages/generate/src/__tests__/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,28 @@ describe("when concurrency < 1", () => {
).rejects.toThrow("Concurrency must be greater than 0");
});
});

describe("when transform is given", () => {
beforeEach(async () => {
await createTempFiles({
"a.js": "module.exports = {a: 1}"
});
});

test("should transform the value", async () => {
const result = await generate({
components: ["*"],
path: tmpDir.path,
transform: (manifest) => ({ ...(manifest.data as any), b: 2 })
});
expect(result).toEqual({
manifests: [
{
path: join(tmpDir.path, "a.js"),
index: [],
data: { a: 1, b: 2 }
}
]
});
});
});
66 changes: 66 additions & 0 deletions packages/generate/src/__tests__/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,69 @@ describe("when concurrency = 1", () => {
]);
});
});

test("transform a manifest", async () => {
const transform = jest.fn((manifest) => (manifest.data as number) + 1);

await expect(
resolve(1, { path: "foo", index: [5], transform })
).resolves.toEqual([{ path: "foo", index: [5], data: 2 }]);
expect(transform).toHaveBeenCalledWith({ path: "foo", index: [5], data: 1 });
expect(transform).toHaveBeenCalledOnce();
});

test("transform multiple manifests", async () => {
await expect(
resolve([1, 2, 3], {
transform: (manifest) => (manifest.data as number) + 1
})
).resolves.toEqual([
{ path: "", index: [0], data: 2 },
{ path: "", index: [1], data: 3 },
{ path: "", index: [2], data: 4 }
]);
});

test("transform returns a Promise", async () => {
await expect(
resolve(1, {
transform: async (manifest) => (manifest.data as number) + 1
})
).resolves.toEqual([{ path: "", index: [], data: 2 }]);
});

test("transform returns null", async () => {
await expect(
resolve(1, {
transform: () => null
})
).resolves.toEqual([]);
});

test("transform returns undefined", async () => {
await expect(
resolve(1, {
transform: () => undefined
})
).resolves.toEqual([]);
});

test("should throw ResolveError when the transform function throws an error", async () => {
const cause = new Error("err");
const err = await getRejectedValue(
resolve(1, {
path: "test",
index: [5],
transform: () => {
throw cause;
}
})
);

assert(err instanceof ResolveError);
expect(err.message).toEqual("Transform function thrown an error");
expect(err.path).toEqual("test");
expect(err.index).toEqual([5]);
expect(err.cause).toEqual(cause);
expect(err.value).toEqual(1);
});
8 changes: 7 additions & 1 deletion packages/generate/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "@kosko/require";
import type { Manifest, Result } from "./base";
import logger, { LogLevel } from "@kosko/log";
import { handleResolvePromises, resolve } from "./resolve";
import { ResolveOptions, handleResolvePromises, resolve } from "./resolve";
import { GenerateError } from "./error";
import { glob, GlobResult } from "./glob";
import pLimit from "p-limit";
Expand Down Expand Up @@ -56,6 +56,11 @@ export interface GenerateOptions {
* {@inheritDoc ResolveOptions.concurrency}
*/
concurrency?: number;

/**
* {@inheritdoc ResolveOptions.transform}
*/
transform?: ResolveOptions["transform"];
}

async function resolveComponentPath(
Expand Down Expand Up @@ -166,6 +171,7 @@ export async function generate(options: GenerateOptions): Promise<Result> {
validate: options.validate,
bail: options.bail,
concurrency: options.concurrency,
transform: options.transform,
index: [],
path
});
Expand Down
59 changes: 43 additions & 16 deletions packages/generate/src/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ export interface ResolveOptions {
* @defaultValue `10`
*/
concurrency?: number;

/**
* Transform a manifest. This function is called when a new manifest is found,
* and before the validation. The return value will override the data of the
* manifest. If the return value is `undefined` or `null`, the manifest will
* be removed from the result.
*/
transform?(manifest: Manifest): unknown | Promise<unknown>;
}

/**
Expand All @@ -122,7 +130,14 @@ export async function resolve(
value: unknown,
options: ResolveOptions = {}
): Promise<Manifest[]> {
const { validate = true, index = [], path = "", bail, concurrency } = options;
const {
validate = true,
index = [],
path = "",
bail,
concurrency,
transform
} = options;
const limit = pLimit(validateConcurrency(concurrency));

function createResolveError(message: string, err: unknown) {
Expand Down Expand Up @@ -199,25 +214,37 @@ export async function resolve(
return handleResolvePromises(promises, bail);
}

if (validate) {
if (isValidator(value)) {
try {
logger.log(
LogLevel.Debug,
`Validating manifests ${index.join(".")} in ${options.path}`
);
await value.validate();
} catch (err) {
throw createResolveError("Validation error", err);
}
}
}

const manifest: Manifest = {
let manifest: Manifest = {
path,
index,
data: value
};

if (typeof transform === "function") {
try {
const newValue = await transform(manifest);

// Remove the manifest if the return value is undefined or null
if (newValue == null) return [];

// Create a new object to avoid mutation
manifest = { ...manifest, data: newValue };
} catch (err) {
throw createResolveError("Transform function thrown an error", err);
}
}

if (validate && isValidator(manifest.data)) {
try {
logger.log(
LogLevel.Debug,
`Validating manifests ${index.join(".")} in ${options.path}`
);
await manifest.data.validate();
} catch (err) {
throw createResolveError("Validation error", err);
}
}

return [manifest];
}
9 changes: 3 additions & 6 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Manifest } from "@kosko/generate";
import type { ResolveOptions } from "@kosko/generate";

/**
* Plugin factory.
Expand Down Expand Up @@ -33,10 +33,7 @@ export interface PluginContext {
*/
export interface Plugin {
/**
* Transform a manifest. This function is called when a new manifest is
* resolved, and before validation. The return value will override the
* data of the manifest. If the return value is `undefined` or `null`, the
* manifest will be removed from the result.
* {@inheritDoc @kosko/generate#ResolveOptions.transform}
*/
transformManifest?(manifest: Manifest): unknown | Promise<unknown>;
transformManifest?: ResolveOptions["transform"];
}

0 comments on commit a375679

Please sign in to comment.