From 63e64f48e42859e9a9df33cbc0ef5068794d34c6 Mon Sep 17 00:00:00 2001 From: Jonas Winzen Date: Fri, 28 Jan 2022 17:55:34 +0100 Subject: [PATCH] feat(build-cache): add build-cache to support incremental builds --- src/cache.ts | 14 ++++++++++++ src/plugin.test.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++ src/plugin.ts | 23 ++++++++++++------- 3 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/cache.ts diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..5606df6 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,14 @@ +export const createBuildCache = ( + buildCallback: (key: string, input: string) => Promise | string +) => { + const cache = new Map(); + return async (key: string, input: string) => { + let cacheEntry = cache.get(key); + if (cacheEntry == null || cacheEntry?.input !== input) { + cacheEntry = { input, output: await buildCallback(key, input) }; + cache.set(key, cacheEntry); + return cacheEntry.output; + } + return cacheEntry.output; + }; +}; diff --git a/src/plugin.test.ts b/src/plugin.test.ts index b4ad149..50e74c0 100644 --- a/src/plugin.test.ts +++ b/src/plugin.test.ts @@ -2,8 +2,10 @@ import path from "path"; import { runInThisContext } from "vm"; import * as esbuild from "esbuild"; import getTransformKeywordDef from "ajv-keywords/dist/definitions/transform"; +import * as CodegenModule from "ajv/dist/standalone"; import type { Options } from "./plugin"; import { AjvPlugin } from "./plugin"; +import * as BuildCacheModule from "./cache"; const wrapInCjs = (source: string) => [ @@ -73,6 +75,34 @@ const evaluateCjs = (code: string): M => { return moduleObj.exports; }; +const makeIncrementalBuild = () => { + const ajvPlugin = AjvPlugin({ + ajvOptions: { coerceTypes: true }, + }); + return esbuild.build({ + stdin: { + contents: ` + import validator from "./testSchema.json?ajv"; + + export const validate = (data) => { + if (!validator(data)) return false; + return data; + };`, + loader: "js", + sourcefile: "test-entrypoint.js", + resolveDir: path.resolve(__dirname, "__fixtures__"), + }, + outfile: path.resolve(__dirname, "build", "main.js"), + target: "node16", + bundle: true, + format: "esm", + minify: false, + write: false, + plugins: [ajvPlugin], + incremental: true, + }); +}; + describe("AjvPlugin", () => { it("builds a a validation with external keywords", async () => { const { validate } = evaluateCjs<{ validate: (x: unknown) => unknown }>( @@ -124,4 +154,30 @@ describe("AjvPlugin", () => { printWidth: 120, }); }); + + it("caches build results", async () => { + const compileSpy = jest.spyOn(CodegenModule, "default"); + const createBuildCacheSpy = jest + .spyOn(BuildCacheModule, "createBuildCache") + .mockImplementation( + (cb) => + (...args) => + Promise.resolve(cb(...args)) + ); + + const result = await makeIncrementalBuild(); + expect(compileSpy).toHaveBeenCalledTimes(1); + await result.rebuild?.(); + expect(compileSpy).toHaveBeenCalledTimes(2); + result.rebuild?.dispose(); + + createBuildCacheSpy.mockRestore(); + compileSpy.mockClear(); + + const resultCached = await makeIncrementalBuild(); + expect(compileSpy).toHaveBeenCalledTimes(1); + await resultCached.rebuild?.(); + expect(compileSpy).toHaveBeenCalledTimes(1); + resultCached.rebuild?.dispose(); + }); }); diff --git a/src/plugin.ts b/src/plugin.ts index da93f14..e4651bc 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -10,6 +10,7 @@ import type { import Ajv from "ajv"; import standaloneCode from "ajv/dist/standalone"; import { mergeDeepLeft } from "ramda"; +import { createBuildCache } from "./cache"; export interface Options { extraKeywords?: CodeKeywordDefinition[]; @@ -47,6 +48,16 @@ export const AjvPlugin = ({ ajv.addKeyword(keywordDef); } + const compileWithCache = createBuildCache(async (filePath, fileContent) => { + ajv.removeSchema(); + const schema = JSON.parse(fileContent) as AnySchemaObject; + schema.$id = `http://example.com/${path.relative( + process.cwd(), + filePath + )}`; + return standaloneCode(ajv, await ajv.compileAsync(schema)); + }); + build.onResolve( { filter: /\.json\?ajv$/i }, async ({ path: rawPath, resolveDir }) => { @@ -61,15 +72,11 @@ export const AjvPlugin = ({ build.onLoad( { namespace: "ajv-validator", filter: /.*/ }, async ({ path: filePath }) => { - const schema = JSON.parse( - await fs.readFile(filePath, "utf8") - ) as AnySchemaObject; - schema.$id = `http://example.com/${path.relative( - process.cwd(), - filePath - )}`; return { - contents: standaloneCode(ajv, await ajv.compileAsync(schema)), + contents: await compileWithCache( + filePath, + await fs.readFile(filePath, "utf8") + ), loader: "js" as const, resolveDir: path.dirname(filePath), };