From 427c493c41a31eeff238c57f8050dbaeb137ebe8 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Thu, 5 Jan 2023 15:35:11 -0800 Subject: [PATCH] feat: Add store option for linked mode builds (#40) * feat: add store option for ssr-client state preservation * fix: add test case * fix: update readme * fix: add store unit tests * fix: remove non-supported entries option * chore: add changeset --- .changeset/dull-candles-brake.md | 5 ++ README.md | 20 ++++++ .../build.expected.loading.0.html | 9 +++ .../build.expected.loading.1.html | 9 +++ .../build.expected.step-0.0.html | 9 +++ .../__snapshots__/dev.expected.loading.0.html | 9 +++ .../__snapshots__/dev.expected.loading.1.html | 9 +++ .../__snapshots__/dev.expected.step-0.0.html | 9 +++ .../isomorphic-memory-store/dev-server.js | 33 ++++++++++ .../isomorphic-memory-store/server.js | 11 ++++ .../src/components/class-component.marko | 20 ++++++ .../src/components/implicit-component.marko | 9 +++ .../src/components/layout-component.marko | 15 +++++ .../isomorphic-memory-store/src/index.js | 9 +++ .../src/template.marko | 9 +++ .../isomorphic-memory-store/test.config.ts | 9 +++ src/__tests__/main.test.ts | 11 ++-- src/index.ts | 61 ++++++------------ src/store/__tests__/file-store.test.ts | 56 +++++++++++++++++ src/store/__tests__/memory-store.test.ts | 28 +++++++++ src/store/file-store.ts | 63 +++++++++++++++++++ src/store/index.ts | 3 + src/store/memory-store.ts | 17 +++++ src/store/types.ts | 5 ++ 24 files changed, 392 insertions(+), 46 deletions(-) create mode 100644 .changeset/dull-candles-brake.md create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.0.html create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.1.html create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.step-0.0.html create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.0.html create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.1.html create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.step-0.0.html create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/dev-server.js create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/server.js create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/src/components/class-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/src/components/implicit-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/src/components/layout-component.marko create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/src/index.js create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/src/template.marko create mode 100644 src/__tests__/fixtures/isomorphic-memory-store/test.config.ts create mode 100644 src/store/__tests__/file-store.test.ts create mode 100644 src/store/__tests__/memory-store.test.ts create mode 100644 src/store/file-store.ts create mode 100644 src/store/index.ts create mode 100644 src/store/memory-store.ts create mode 100644 src/store/types.ts diff --git a/.changeset/dull-candles-brake.md b/.changeset/dull-candles-brake.md new file mode 100644 index 0000000..85e4911 --- /dev/null +++ b/.changeset/dull-candles-brake.md @@ -0,0 +1,5 @@ +--- +"@marko/vite": patch +--- + +Add store option for linked mode builds diff --git a/README.md b/README.md index 0e66612..4ecd100 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,26 @@ marko({ runtimeId: "MY_MARKO_RUNTIME_ID" }); Set this to `false` to opt out of [linked mode](#linked-mode). When this is false, the plugin will only handle resolving and transforming `.marko` files. +### options.store + +Storage mechanism to preserve data between SSR and client builds when building in linked mode. Two implementations are available: + +- FileStore _(default)_ + + ```js + import { FileStore } from "@marko/vite"; + const store = new FileStore(); + ``` + + Reads/writes data to the file system. Use this when running the SSR and client builds in seperate processes such as when using Vite from the command line or npm scripts. + +- MemoryStore + ```js + import { MemoryStore } from "@marko/vite"; + const store = new MemoryStore(); + ``` + Reads/writes data to memory. This option can be used when building with Vite programatically. + ## Code of Conduct This project adheres to the [eBay Code of Conduct](./.github/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. diff --git a/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.0.html b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.0.html new file mode 100644 index 0000000..1d8a1c2 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.0.html @@ -0,0 +1,9 @@ +
+
+ Mounted: false Clicks: 0 +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.1.html b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.1.html new file mode 100644 index 0000000..3f9e475 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.loading.1.html @@ -0,0 +1,9 @@ +
+
+ Mounted: true Clicks: 0 +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.step-0.0.html b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.step-0.0.html new file mode 100644 index 0000000..0345f73 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/build.expected.step-0.0.html @@ -0,0 +1,9 @@ +
+
+ Mounted: true Clicks: 1 +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.0.html b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.0.html new file mode 100644 index 0000000..1d8a1c2 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.0.html @@ -0,0 +1,9 @@ +
+
+ Mounted: false Clicks: 0 +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.1.html b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.1.html new file mode 100644 index 0000000..3f9e475 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.loading.1.html @@ -0,0 +1,9 @@ +
+
+ Mounted: true Clicks: 0 +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.step-0.0.html b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.step-0.0.html new file mode 100644 index 0000000..0345f73 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/__snapshots__/dev.expected.step-0.0.html @@ -0,0 +1,9 @@ +
+
+ Mounted: true Clicks: 1 +
+
\ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/dev-server.js b/src/__tests__/fixtures/isomorphic-memory-store/dev-server.js new file mode 100644 index 0000000..ecab68d --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/dev-server.js @@ -0,0 +1,33 @@ +// In dev we'll start a Vite dev server in middleware mode, +// and forward requests to our http request handler. + +const { createServer } = require("vite"); +const { join } = require("path"); +const markoPlugin = require("../../..").default; + +module.exports = (async () => { + const devServer = await createServer({ + root: __dirname, + appType: "custom", + logLevel: "silent", + plugins: [markoPlugin()], + optimizeDeps: { force: true }, + server: { + middlewareMode: true, + watch: { + ignored: ["**/node_modules/**", "**/dist/**", "**/__snapshots__/**"], + }, + }, + }); + return devServer.middlewares.use(async (req, res, next) => { + try { + const { handler } = await devServer.ssrLoadModule( + join(__dirname, "./src/index.js") + ); + await handler(req, res, next); + } catch (err) { + devServer.ssrFixStacktrace(err); + return next(err); + } + }); +})(); diff --git a/src/__tests__/fixtures/isomorphic-memory-store/server.js b/src/__tests__/fixtures/isomorphic-memory-store/server.js new file mode 100644 index 0000000..ad89b47 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/server.js @@ -0,0 +1,11 @@ +// In production, simply start up the http server. +const path = require("path"); +const { createServer } = require("http"); +const serve = require("serve-handler"); +const { handler } = require("./dist/index.mjs"); +const serveOpts = { public: path.resolve(__dirname, "dist") }; +module.exports = createServer(async (req, res) => { + await handler(req, res); + if (res.headersSent) return; + await serve(req, res, serveOpts); +}); diff --git a/src/__tests__/fixtures/isomorphic-memory-store/src/components/class-component.marko b/src/__tests__/fixtures/isomorphic-memory-store/src/components/class-component.marko new file mode 100644 index 0000000..9579382 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/src/components/class-component.marko @@ -0,0 +1,20 @@ +class { + onCreate() { + this.state = { + clickCount: 0, + mounted: false + }; + } + onMount() { + this.state.mounted = true; + } + + handleClick() { + this.state.clickCount++; + } +} + + + Mounted: ${state.mounted} + Clicks: ${state.clickCount} + diff --git a/src/__tests__/fixtures/isomorphic-memory-store/src/components/implicit-component.marko b/src/__tests__/fixtures/isomorphic-memory-store/src/components/implicit-component.marko new file mode 100644 index 0000000..7d8310b --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/src/components/implicit-component.marko @@ -0,0 +1,9 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Implicit Component"); + } +} + + + + \ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/src/components/layout-component.marko b/src/__tests__/fixtures/isomorphic-memory-store/src/components/layout-component.marko new file mode 100644 index 0000000..8d60022 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/src/components/layout-component.marko @@ -0,0 +1,15 @@ +static { + if (typeof window === "object") { + document.body.firstElementChild.append("Loaded Layout Component"); + } +} + + + + + Hello World + + + <${input.renderBody}/> + + \ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/src/index.js b/src/__tests__/fixtures/isomorphic-memory-store/src/index.js new file mode 100644 index 0000000..d3f5422 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/src/index.js @@ -0,0 +1,9 @@ +import template from "./template.marko"; + +export function handler(req, res) { + if (req.url === "/") { + res.statusCode = 200; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + template.render({}, res); + } +} diff --git a/src/__tests__/fixtures/isomorphic-memory-store/src/template.marko b/src/__tests__/fixtures/isomorphic-memory-store/src/template.marko new file mode 100644 index 0000000..2c5af41 --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/src/template.marko @@ -0,0 +1,9 @@ +style { + div { color: green } +} + + + + + + \ No newline at end of file diff --git a/src/__tests__/fixtures/isomorphic-memory-store/test.config.ts b/src/__tests__/fixtures/isomorphic-memory-store/test.config.ts new file mode 100644 index 0000000..fe343ac --- /dev/null +++ b/src/__tests__/fixtures/isomorphic-memory-store/test.config.ts @@ -0,0 +1,9 @@ +import { MemoryStore, type Options } from "../../.."; + +export const ssr = true; +export async function steps() { + await page.click("#clickable"); +} +export const options: Options = { + store: new MemoryStore(), +}; diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index dfd1168..b06ba85 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -10,7 +10,7 @@ import { JSDOM } from "jsdom"; import { createRequire } from "module"; import * as playwright from "playwright"; import { defaultNormalizer, defaultSerializer } from "@marko/fixture-snapshots"; -import markoPlugin from ".."; +import markoPlugin, { type Options } from ".."; declare global { const page: playwright.Page; @@ -84,6 +84,7 @@ for (const fixture of fs.readdirSync(FIXTURES)) { const config = requireCwd(path.join(dir, "test.config.ts")) as { ssr: boolean; steps?: Step | Step[]; + options?: Options; }; const steps: Step[] = config.steps ? Array.isArray(config.steps) @@ -105,7 +106,7 @@ for (const fixture of fs.readdirSync(FIXTURES)) { root: dir, configFile: false, logLevel: "silent", - plugins: [markoPlugin()], + plugins: [markoPlugin(config.options)], build: { write: true, minify: false, @@ -118,7 +119,7 @@ for (const fixture of fs.readdirSync(FIXTURES)) { root: dir, configFile: false, logLevel: "silent", - plugins: [markoPlugin()], + plugins: [markoPlugin(config.options)], build: { write: true, minify: false, @@ -148,7 +149,7 @@ for (const fixture of fs.readdirSync(FIXTURES)) { }, logLevel: "silent", optimizeDeps: { force: true }, - plugins: [markoPlugin({ linked: false })], + plugins: [markoPlugin({ ...config.options, linked: false })], }); devServer.listen(await getAvailablePort()); @@ -161,7 +162,7 @@ for (const fixture of fs.readdirSync(FIXTURES)) { root: dir, configFile: false, logLevel: "silent", - plugins: [markoPlugin({ linked: false })], + plugins: [markoPlugin({ ...config.options, linked: false })], build: { write: true, minify: false, diff --git a/src/index.ts b/src/index.ts index 19fa111..d8a0b75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import type * as vite from "vite"; import type * as Compiler from "@marko/compiler"; -import os from "os"; import fs from "fs"; import path from "path"; import crypto from "crypto"; @@ -16,6 +15,10 @@ import { DocManifest, } from "./manifest-generator"; import esbuildPlugin from "./esbuild-plugin"; +import { BuildStore, FileStore } from "./store"; + +export * from "./store"; +export type { BuildStore } from "./store"; export interface Options { // Defaults to true, set to false to disable automatic component discovery and hydration. @@ -28,6 +31,8 @@ export interface Options { translator?: string; // Overrides the Babel config that Marko will use. babelConfig?: Compiler.Config["babelConfig"]; + // Store to use between SSR and client builds, defaults to file system + store?: BuildStore; } interface BrowserManifest { @@ -53,13 +58,13 @@ const queryReg = /\?marko-.+$/; const browserEntryQuery = "?marko-browser-entry"; const serverEntryQuery = "?marko-server-entry"; const virtualFileQuery = "?marko-virtual"; +const manifestFileName = "manifest.json"; const markoExt = ".marko"; const htmlExt = ".html"; const resolveOpts = { skipSelf: true }; const cache = new Map(); const thisFile = typeof __filename === "string" ? __filename : fileURLToPath(import.meta.url); -let tempDir: Promise | undefined; export default function markoPlugin(opts: Options = {}): vite.Plugin[] { let compiler: typeof Compiler; @@ -128,6 +133,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { let devServer: vite.ViteDevServer; let registeredTag: string | false = false; let serverManifest: ServerManifest | undefined; + let store: BuildStore; const entrySources = new Map(); const transformWatchFiles = new Map(); const transformOptionalFiles = new Map(); @@ -144,6 +150,11 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { devEntryFile = path.join(root, "index.html"); isBuild = env.command === "build"; isSSRBuild = isBuild && linked && Boolean(config.build!.ssr); + store = + opts.store || + new FileStore( + `marko-vite-${crypto.createHash("SHA1").update(root).digest("hex")}` + ); if (linked && !registeredTag) { // Here we inject either the watchMode vite tag, or the build one. @@ -237,12 +248,12 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { }, async buildStart(inputOptions) { if (isBuild && linked && !isSSRBuild) { - const serverMetaFile = await getServerManifestFile(root); - this.addWatchFile(serverMetaFile); + // Is this needed? + //this.addWatchFile(serverMetaFile); try { serverManifest = JSON.parse( - await fs.promises.readFile(serverMetaFile, "utf-8") + (await store.get(manifestFileName))! ) as ServerManifest; inputOptions.input = toHTMLEntries(root, serverManifest.entries); for (const entry in serverManifest.entrySources) { @@ -489,15 +500,12 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { } } - await fs.promises.writeFile( - await getServerManifestFile(root), - JSON.stringify(serverManifest) - ); + await store.set(manifestFileName, JSON.stringify(serverManifest)); } else { const browserManifest: BrowserManifest = {}; - for (const entryId in serverManifest.entries) { - const fileName = serverManifest.entries[entryId]; + for (const entryId in serverManifest!.entries) { + const fileName = serverManifest!.entries[entryId]; let chunkId = fileName + htmlExt; let chunk = bundle[chunkId]; @@ -525,7 +533,7 @@ export default function markoPlugin(opts: Options = {}): vite.Plugin[] { browserManifest )};\n`; - for (const fileName of serverManifest.chunksNeedingAssets) { + for (const fileName of serverManifest!.chunksNeedingAssets) { await fs.promises.appendFile(fileName, manifestStr); } } @@ -557,35 +565,6 @@ function toHTMLEntries(root: string, serverEntries: ServerManifest["entries"]) { return result; } -async function getServerManifestFile(root: string) { - return path.join(await getTempDir(root), "manifest.json"); -} - -function getTempDir(root: string) { - return ( - tempDir || - (tempDir = (async () => { - const dir = path.join( - os.tmpdir(), - `marko-vite-${crypto.createHash("SHA1").update(root).digest("hex")}` - ); - - try { - const stat = await fs.promises.stat(dir); - - if (stat.isDirectory()) { - return dir; - } - } catch { - await fs.promises.mkdir(dir); - return dir; - } - - throw new Error("Unable to create temp directory"); - })()) - ); -} - function toEntryId(id: string) { const lastSepIndex = id.lastIndexOf(path.sep); let name = id.slice(lastSepIndex + 1, id.indexOf(".", lastSepIndex)); diff --git a/src/store/__tests__/file-store.test.ts b/src/store/__tests__/file-store.test.ts new file mode 100644 index 0000000..eb14722 --- /dev/null +++ b/src/store/__tests__/file-store.test.ts @@ -0,0 +1,56 @@ +import fs from "fs"; +import assert from "assert"; +import FileStore from "../file-store"; + +let store: FileStore; +beforeEach(() => { + store = new FileStore("test"); +}); + +afterEach(async () => { + try { + const dir = await store._temp; + if (dir) { + await fs.promises.rm(dir, { recursive: true, force: true }); + } + } catch (err) { + // do nothing + } +}); + +describe("file store should", () => { + it("allow retrieving data", async () => { + const expectedKey = "hello"; + const expectedData = "world"; + + await store.set(expectedKey, expectedData); + + assert.equal(await store.get(expectedKey), expectedData); + assert.equal(await store.get("non-existing key"), undefined); + }); + + it("allow checking for existence of data", async () => { + const expectedKey = "hello"; + + await store.set(expectedKey, ""); + + assert.equal(await store.has(expectedKey), true); + assert.equal(await store.has("non-existing key"), false); + }); + + it("throw when unable to create a temp directory", async () => { + const mkdir = fs.promises.mkdir; + fs.promises.mkdir = () => { + throw new Error("Fake `fs.promises.mkdir` always throws"); + }; + + try { + await store.set("hello", "world"); + assert.fail(); + } catch (err) { + fs.promises.mkdir = mkdir; + } finally { + fs.promises.mkdir = mkdir; + } + }); +}); diff --git a/src/store/__tests__/memory-store.test.ts b/src/store/__tests__/memory-store.test.ts new file mode 100644 index 0000000..003ff9f --- /dev/null +++ b/src/store/__tests__/memory-store.test.ts @@ -0,0 +1,28 @@ +import assert from "assert"; +import MemoryStore from "../memory-store"; + +let store: MemoryStore; +beforeEach(() => { + store = new MemoryStore(); +}); + +describe("memory store should", () => { + it("allow retrieving data", async () => { + const expectedKey = "hello"; + const expectedData = "world"; + + await store.set(expectedKey, expectedData); + + assert.equal(await store.get(expectedKey), expectedData); + assert.equal(await store.get("non-existing key"), undefined); + }); + + it("allow checking for existence of data", async () => { + const expectedKey = "hello"; + + await store.set(expectedKey, ""); + + assert.equal(await store.has(expectedKey), true); + assert.equal(await store.has("non-existing key"), false); + }); +}); diff --git a/src/store/file-store.ts b/src/store/file-store.ts new file mode 100644 index 0000000..d686bf2 --- /dev/null +++ b/src/store/file-store.ts @@ -0,0 +1,63 @@ +import path from "path"; +import fs from "fs"; +import os from "os"; +import type { BuildStore } from "./types"; + +export default class FileStore implements BuildStore { + _id: string; + _temp: Promise | undefined; + _cache: Map; + constructor(id: string) { + this._id = id; + this._cache = new Map(); + } + async _getKeyPath(key: string) { + this._temp ??= getTempDir(this._id); + return path.join(await this._temp, key); + } + async has(key: string) { + if (!this._cache.has(key)) { + const path = await this._getKeyPath(key); + try { + await fs.promises.access(path); + } catch (e) { + return false; + } + } + return true; + } + async get(key: string) { + let value = this._cache.get(key); + if (value === undefined) { + const path = await this._getKeyPath(key); + try { + value = await fs.promises.readFile(path, "utf-8"); + } catch (e) { + return undefined; + } + this._cache.set(key, value); + } + return value; + } + async set(key: string, value: string) { + this._cache.set(key, value); + const path = await this._getKeyPath(key); + await fs.promises.writeFile(path, value, "utf-8"); + } +} + +async function getTempDir(id: string) { + const dir = path.join(os.tmpdir(), id); + + try { + const stat = await fs.promises.stat(dir); + if (stat.isDirectory()) { + return dir; + } + } catch { + await fs.promises.mkdir(dir); + return dir; + } + + throw new Error("Unable to create temp directory"); +} diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..18672da --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,3 @@ +export { default as FileStore } from "./file-store"; +export { default as MemoryStore } from "./memory-store"; +export type { BuildStore } from "./types"; diff --git a/src/store/memory-store.ts b/src/store/memory-store.ts new file mode 100644 index 0000000..c657fc5 --- /dev/null +++ b/src/store/memory-store.ts @@ -0,0 +1,17 @@ +import type { BuildStore } from "./types"; + +export default class MemoryStore implements BuildStore { + _store: Map; + constructor() { + this._store = new Map(); + } + async has(key: string) { + return Promise.resolve(this._store.has(key)); + } + async get(key: string) { + return Promise.resolve(this._store.get(key)); + } + async set(key: string, value: string) { + this._store.set(key, value); + } +} diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 0000000..85daac2 --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,5 @@ +export interface BuildStore { + has(key: string): Promise; + get(key: string): Promise; + set(key: string, value: string): Promise; +}