@emdash-cms/plugin-test 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +25 -0
- package/dist/config.d.mts +14 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +37 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.d.mts +55 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +118 -0
- package/dist/index.mjs.map +1 -0
- package/dist/worker.d.mts +9 -0
- package/dist/worker.d.mts.map +1 -0
- package/dist/worker.mjs +10 -0
- package/dist/worker.mjs.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Cloudflare Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @emdash-cms/plugin-test
|
|
2
|
+
|
|
3
|
+
Workerd-backed Vitest utilities for sandboxed EmDash plugins.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// vitest.config.ts
|
|
7
|
+
import { emdashPluginTest } from "@emdash-cms/plugin-test/config";
|
|
8
|
+
import { defineConfig } from "vitest/config";
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [emdashPluginTest()],
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createPluginTestHost } from "@emdash-cms/plugin-test";
|
|
17
|
+
|
|
18
|
+
const host = await createPluginTestHost();
|
|
19
|
+
await host.invokeRoute("health");
|
|
20
|
+
await host.dispose();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The configuration builds the plugin and supplies local D1 and Worker Loader bindings through `@cloudflare/vitest-plugin`. The host loads the built code through EmDash's production Cloudflare sandbox runner and `PluginBridge`.
|
|
24
|
+
|
|
25
|
+
Read [Test sandboxed plugins](https://docs.emdashcms.com/plugins/creating-plugins/testing/) for hooks, content fixtures, storage assertions, and test boundaries.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/config.d.ts
|
|
4
|
+
interface EmDashPluginTestOptions {
|
|
5
|
+
/** Plugin source directory. Defaults to the current working directory. */
|
|
6
|
+
dir?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Configure Vitest to build a sandboxed plugin and run tests inside workerd.
|
|
10
|
+
*/
|
|
11
|
+
declare function emdashPluginTest(options?: EmDashPluginTestOptions): Plugin;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { EmDashPluginTestOptions, emdashPluginTest };
|
|
14
|
+
//# sourceMappingURL=config.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.mts","names":[],"sources":["../src/config.ts"],"mappings":";;;UAQiB,uBAAA;;EAEhB,GAAA;AAAA;;;;iBAMe,gBAAA,CAAiB,OAAA,GAAS,uBAAA,GAA+B,MAAA"}
|
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { cloudflareTest } from "@cloudflare/vitest-plugin";
|
|
5
|
+
import { buildPlugin } from "@emdash-cms/plugin-cli";
|
|
6
|
+
|
|
7
|
+
//#region src/config.ts
|
|
8
|
+
/**
|
|
9
|
+
* Configure Vitest to build a sandboxed plugin and run tests inside workerd.
|
|
10
|
+
*/
|
|
11
|
+
function emdashPluginTest(options = {}) {
|
|
12
|
+
const pluginDir = resolve(options.dir ?? process.cwd());
|
|
13
|
+
const workerEntry = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.mjs", import.meta.url));
|
|
14
|
+
return cloudflareTest(async () => {
|
|
15
|
+
const build = await buildPlugin({ dir: pluginDir });
|
|
16
|
+
const [code, manifest] = await Promise.all([readFile(build.files.runtime, "utf8"), readFile(build.files.manifestJson, "utf8")]);
|
|
17
|
+
return {
|
|
18
|
+
main: workerEntry,
|
|
19
|
+
remoteBindings: false,
|
|
20
|
+
additionalExports: { PluginBridge: "WorkerEntrypoint" },
|
|
21
|
+
miniflare: {
|
|
22
|
+
compatibilityDate: "2026-08-20",
|
|
23
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
24
|
+
d1Databases: ["DB"],
|
|
25
|
+
workerLoaders: { LOADER: {} },
|
|
26
|
+
bindings: {
|
|
27
|
+
EMDASH_PLUGIN_CODE: code,
|
|
28
|
+
EMDASH_PLUGIN_MANIFEST: manifest
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { emdashPluginTest };
|
|
37
|
+
//# sourceMappingURL=config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { cloudflareTest } from \"@cloudflare/vitest-plugin\";\nimport { buildPlugin } from \"@emdash-cms/plugin-cli\";\nimport type { Plugin } from \"vite\";\n\nexport interface EmDashPluginTestOptions {\n\t/** Plugin source directory. Defaults to the current working directory. */\n\tdir?: string;\n}\n\n/**\n * Configure Vitest to build a sandboxed plugin and run tests inside workerd.\n */\nexport function emdashPluginTest(options: EmDashPluginTestOptions = {}): Plugin {\n\tconst pluginDir = resolve(options.dir ?? process.cwd());\n\tconst workerEntry = fileURLToPath(\n\t\tnew URL(import.meta.url.endsWith(\".ts\") ? \"./worker.ts\" : \"./worker.mjs\", import.meta.url),\n\t);\n\n\treturn cloudflareTest(async () => {\n\t\tconst build = await buildPlugin({ dir: pluginDir });\n\t\tconst [code, manifest] = await Promise.all([\n\t\t\treadFile(build.files.runtime, \"utf8\"),\n\t\t\treadFile(build.files.manifestJson, \"utf8\"),\n\t\t]);\n\n\t\treturn {\n\t\t\tmain: workerEntry,\n\t\t\tremoteBindings: false,\n\t\t\tadditionalExports: { PluginBridge: \"WorkerEntrypoint\" },\n\t\t\tminiflare: {\n\t\t\t\tcompatibilityDate: \"2026-08-20\",\n\t\t\t\tcompatibilityFlags: [\"nodejs_compat\"],\n\t\t\t\td1Databases: [\"DB\"],\n\t\t\t\tworkerLoaders: { LOADER: {} },\n\t\t\t\tbindings: {\n\t\t\t\t\tEMDASH_PLUGIN_CODE: code,\n\t\t\t\t\tEMDASH_PLUGIN_MANIFEST: manifest,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t});\n}\n"],"mappings":";;;;;;;;;;AAgBA,SAAgB,iBAAiB,UAAmC,EAAE,EAAU;CAC/E,MAAM,YAAY,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;CACvD,MAAM,cAAc,cACnB,IAAI,IAAI,OAAO,KAAK,IAAI,SAAS,MAAM,GAAG,gBAAgB,gBAAgB,OAAO,KAAK,IAAI,CAC1F;AAED,QAAO,eAAe,YAAY;EACjC,MAAM,QAAQ,MAAM,YAAY,EAAE,KAAK,WAAW,CAAC;EACnD,MAAM,CAAC,MAAM,YAAY,MAAM,QAAQ,IAAI,CAC1C,SAAS,MAAM,MAAM,SAAS,OAAO,EACrC,SAAS,MAAM,MAAM,cAAc,OAAO,CAC1C,CAAC;AAEF,SAAO;GACN,MAAM;GACN,gBAAgB;GAChB,mBAAmB,EAAE,cAAc,oBAAoB;GACvD,WAAW;IACV,mBAAmB;IACnB,oBAAoB,CAAC,gBAAgB;IACrC,aAAa,CAAC,KAAK;IACnB,eAAe,EAAE,QAAQ,EAAE,EAAE;IAC7B,UAAU;KACT,oBAAoB;KACpB,wBAAwB;KACxB;IACD;GACD;GACA"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CreateCollectionInput, CreateFieldInput, PluginManifest } from "emdash";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
interface PluginTestRequest {
|
|
5
|
+
url?: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
meta?: {
|
|
9
|
+
ip: string | null;
|
|
10
|
+
userAgent: string | null;
|
|
11
|
+
referer: string | null;
|
|
12
|
+
geo: {
|
|
13
|
+
country: string | null;
|
|
14
|
+
region: string | null;
|
|
15
|
+
city: string | null;
|
|
16
|
+
} | null;
|
|
17
|
+
};
|
|
18
|
+
user?: {
|
|
19
|
+
id: string;
|
|
20
|
+
email: string;
|
|
21
|
+
name: string | null;
|
|
22
|
+
role: number;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface PluginTestCollection extends CreateCollectionInput {
|
|
27
|
+
fields?: CreateFieldInput[];
|
|
28
|
+
}
|
|
29
|
+
interface PluginStorageTestEntry<T = unknown> {
|
|
30
|
+
id: string;
|
|
31
|
+
data: T;
|
|
32
|
+
}
|
|
33
|
+
interface PluginTestHost {
|
|
34
|
+
readonly manifest: PluginManifest;
|
|
35
|
+
invokeHook(name: string, event: unknown): Promise<unknown>;
|
|
36
|
+
invokeRoute(name: string, input?: unknown, request?: PluginTestRequest): Promise<unknown>;
|
|
37
|
+
createCollection(input: PluginTestCollection): Promise<void>;
|
|
38
|
+
seedContent(collection: string, items: Array<Record<string, unknown>>): Promise<void>;
|
|
39
|
+
storage<T = unknown>(collection: string): {
|
|
40
|
+
get(id: string): Promise<T | null>;
|
|
41
|
+
list(): Promise<Array<PluginStorageTestEntry<T>>>;
|
|
42
|
+
};
|
|
43
|
+
kv: {
|
|
44
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
45
|
+
list(): Promise<Array<PluginStorageTestEntry>>;
|
|
46
|
+
};
|
|
47
|
+
dispose(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Load the configured plugin through EmDash's production Cloudflare sandbox runner.
|
|
51
|
+
*/
|
|
52
|
+
declare function createPluginTestHost(): Promise<PluginTestHost>;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { PluginStorageTestEntry, PluginTestCollection, PluginTestHost, PluginTestRequest, createPluginTestHost };
|
|
55
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;UAsBiB,iBAAA;EAChB,GAAA;EACA,MAAA;EACA,OAAA,GAAU,MAAA;EACV,IAAA;IACC,EAAA;IACA,SAAA;IACA,OAAA;IACA,GAAA;MAAO,OAAA;MAAwB,MAAA;MAAuB,IAAA;IAAA;EAAA;EAEvD,IAAA;IACC,EAAA;IACA,KAAA;IACA,IAAA;IACA,IAAA;IACA,SAAA;EAAA;AAAA;AAAA,UAIe,oBAAA,SAA6B,qBAAA;EAC7C,MAAA,GAAS,gBAAA;AAAA;AAAA,UAGO,sBAAA;EAChB,EAAA;EACA,IAAA,EAAM,CAAA;AAAA;AAAA,UAGU,cAAA;EAAA,SACP,QAAA,EAAU,cAAA;EACnB,UAAA,CAAW,IAAA,UAAc,KAAA,YAAiB,OAAA;EAC1C,WAAA,CAAY,IAAA,UAAc,KAAA,YAAiB,OAAA,GAAU,iBAAA,GAAoB,OAAA;EACzE,gBAAA,CAAiB,KAAA,EAAO,oBAAA,GAAuB,OAAA;EAC/C,WAAA,CAAY,UAAA,UAAoB,KAAA,EAAO,KAAA,CAAM,MAAA,qBAA2B,OAAA;EACxE,OAAA,cACC,UAAA;IAEA,GAAA,CAAI,EAAA,WAAa,OAAA,CAAQ,CAAA;IACzB,IAAA,IAAQ,OAAA,CAAQ,KAAA,CAAM,sBAAA,CAAuB,CAAA;EAAA;EAE9C,EAAA;IACC,GAAA,cAAiB,GAAA,WAAc,OAAA,CAAQ,CAAA;IACvC,IAAA,IAAQ,OAAA,CAAQ,KAAA,CAAM,sBAAA;EAAA;EAEvB,OAAA,IAAW,OAAA;AAAA;AAhBZ;;;AAAA,iBA0CsB,oBAAA,CAAA,GAAwB,OAAA,CAAQ,cAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createDialect } from "@emdash-cms/cloudflare/db/d1";
|
|
2
|
+
import { CloudflareSandboxRunner } from "@emdash-cms/cloudflare/sandbox";
|
|
3
|
+
import { pluginManifestSchema } from "@emdash-cms/plugin-types";
|
|
4
|
+
import { reset } from "cloudflare:test";
|
|
5
|
+
import { env } from "cloudflare:workers";
|
|
6
|
+
import { ContentRepository, SchemaRegistry } from "emdash";
|
|
7
|
+
import { runMigrations } from "emdash/db";
|
|
8
|
+
import { Kysely } from "kysely";
|
|
9
|
+
|
|
10
|
+
//#region src/index.ts
|
|
11
|
+
const DEFAULT_META = {
|
|
12
|
+
ip: null,
|
|
13
|
+
userAgent: null,
|
|
14
|
+
referer: null,
|
|
15
|
+
geo: null
|
|
16
|
+
};
|
|
17
|
+
function isPluginTestBindings(value) {
|
|
18
|
+
if (typeof value !== "object" || value === null) return false;
|
|
19
|
+
if (!("DB" in value) || typeof value.DB !== "object" || value.DB === null) return false;
|
|
20
|
+
return "prepare" in value.DB && typeof value.DB.prepare === "function" && "EMDASH_PLUGIN_CODE" in value && typeof value.EMDASH_PLUGIN_CODE === "string" && "EMDASH_PLUGIN_MANIFEST" in value && typeof value.EMDASH_PLUGIN_MANIFEST === "string";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load the configured plugin through EmDash's production Cloudflare sandbox runner.
|
|
24
|
+
*/
|
|
25
|
+
async function createPluginTestHost() {
|
|
26
|
+
const bindings = env;
|
|
27
|
+
if (!isPluginTestBindings(bindings)) throw new Error("EmDash plugin test bindings are unavailable; add emdashPluginTest() to Vitest");
|
|
28
|
+
const manifestResult = pluginManifestSchema.safeParse(JSON.parse(bindings.EMDASH_PLUGIN_MANIFEST));
|
|
29
|
+
if (!manifestResult.success) throw new Error("EmDash plugin test manifest is invalid");
|
|
30
|
+
const manifest = manifestResult.data;
|
|
31
|
+
const db = new Kysely({ dialect: createDialect({
|
|
32
|
+
binding: "DB",
|
|
33
|
+
session: "disabled"
|
|
34
|
+
}) });
|
|
35
|
+
await runMigrations(db);
|
|
36
|
+
const runner = new CloudflareSandboxRunner({
|
|
37
|
+
db,
|
|
38
|
+
siteInfo: {
|
|
39
|
+
name: "EmDash plugin test site",
|
|
40
|
+
url: "https://plugin.test",
|
|
41
|
+
locale: "en"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
if (!runner.isAvailable()) {
|
|
45
|
+
await db.destroy();
|
|
46
|
+
throw new Error(`Plugin sandbox unavailable: ${runner.unavailableReason()}`);
|
|
47
|
+
}
|
|
48
|
+
const plugin = await runner.load(manifest, bindings.EMDASH_PLUGIN_CODE);
|
|
49
|
+
const registry = new SchemaRegistry(db);
|
|
50
|
+
const content = new ContentRepository(db);
|
|
51
|
+
let disposed = false;
|
|
52
|
+
const assertActive = () => {
|
|
53
|
+
if (disposed) throw new Error("Plugin test host has been disposed");
|
|
54
|
+
};
|
|
55
|
+
const readStorage = async (collection, id) => {
|
|
56
|
+
assertActive();
|
|
57
|
+
let query = bindings.DB.prepare("SELECT id, data FROM _plugin_storage WHERE plugin_id = ? AND collection = ?").bind(manifest.id, collection);
|
|
58
|
+
if (id !== void 0) query = bindings.DB.prepare("SELECT id, data FROM _plugin_storage WHERE plugin_id = ? AND collection = ? AND id = ?").bind(manifest.id, collection, id);
|
|
59
|
+
return ((await query.all()).results ?? []).map((row) => ({
|
|
60
|
+
id: row.id,
|
|
61
|
+
data: JSON.parse(row.data)
|
|
62
|
+
}));
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
manifest,
|
|
66
|
+
async invokeHook(name, event) {
|
|
67
|
+
assertActive();
|
|
68
|
+
return plugin.invokeHook(name, event);
|
|
69
|
+
},
|
|
70
|
+
async invokeRoute(name, input = {}, request = {}) {
|
|
71
|
+
assertActive();
|
|
72
|
+
return plugin.invokeRoute(name, input, {
|
|
73
|
+
url: request.url ?? `https://plugin.test/_emdash/api/plugins/${manifest.id}/${name}`,
|
|
74
|
+
method: request.method ?? "POST",
|
|
75
|
+
headers: request.headers ?? {},
|
|
76
|
+
meta: request.meta ?? DEFAULT_META,
|
|
77
|
+
user: request.user
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
async createCollection({ fields = [], ...collection }) {
|
|
81
|
+
assertActive();
|
|
82
|
+
await registry.createCollection(collection);
|
|
83
|
+
for (const field of fields) await registry.createField(collection.slug, field);
|
|
84
|
+
},
|
|
85
|
+
async seedContent(collection, items) {
|
|
86
|
+
assertActive();
|
|
87
|
+
for (const data of items) await content.create({
|
|
88
|
+
type: collection,
|
|
89
|
+
data
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
storage(collection) {
|
|
93
|
+
return {
|
|
94
|
+
async get(id) {
|
|
95
|
+
return (await readStorage(collection, id))[0]?.data ?? null;
|
|
96
|
+
},
|
|
97
|
+
list: () => readStorage(collection)
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
kv: {
|
|
101
|
+
async get(key) {
|
|
102
|
+
return (await readStorage("__kv", key))[0]?.data ?? null;
|
|
103
|
+
},
|
|
104
|
+
list: () => readStorage("__kv")
|
|
105
|
+
},
|
|
106
|
+
async dispose() {
|
|
107
|
+
if (disposed) return;
|
|
108
|
+
disposed = true;
|
|
109
|
+
await runner.terminateAll();
|
|
110
|
+
await db.destroy();
|
|
111
|
+
await reset();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { createPluginTestHost };
|
|
118
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { createDialect } from \"@emdash-cms/cloudflare/db/d1\";\nimport { CloudflareSandboxRunner } from \"@emdash-cms/cloudflare/sandbox\";\nimport { pluginManifestSchema } from \"@emdash-cms/plugin-types\";\nimport { reset } from \"cloudflare:test\";\nimport { env } from \"cloudflare:workers\";\nimport {\n\tContentRepository,\n\tSchemaRegistry,\n\ttype CreateCollectionInput,\n\ttype CreateFieldInput,\n\ttype Database,\n\ttype PluginManifest,\n} from \"emdash\";\nimport { runMigrations } from \"emdash/db\";\nimport { Kysely } from \"kysely\";\n\ninterface PluginTestBindings {\n\tDB: D1Database;\n\tEMDASH_PLUGIN_CODE: string;\n\tEMDASH_PLUGIN_MANIFEST: string;\n}\n\nexport interface PluginTestRequest {\n\turl?: string;\n\tmethod?: string;\n\theaders?: Record<string, string>;\n\tmeta?: {\n\t\tip: string | null;\n\t\tuserAgent: string | null;\n\t\treferer: string | null;\n\t\tgeo: { country: string | null; region: string | null; city: string | null } | null;\n\t};\n\tuser?: {\n\t\tid: string;\n\t\temail: string;\n\t\tname: string | null;\n\t\trole: number;\n\t\tcreatedAt: string;\n\t};\n}\n\nexport interface PluginTestCollection extends CreateCollectionInput {\n\tfields?: CreateFieldInput[];\n}\n\nexport interface PluginStorageTestEntry<T = unknown> {\n\tid: string;\n\tdata: T;\n}\n\nexport interface PluginTestHost {\n\treadonly manifest: PluginManifest;\n\tinvokeHook(name: string, event: unknown): Promise<unknown>;\n\tinvokeRoute(name: string, input?: unknown, request?: PluginTestRequest): Promise<unknown>;\n\tcreateCollection(input: PluginTestCollection): Promise<void>;\n\tseedContent(collection: string, items: Array<Record<string, unknown>>): Promise<void>;\n\tstorage<T = unknown>(\n\t\tcollection: string,\n\t): {\n\t\tget(id: string): Promise<T | null>;\n\t\tlist(): Promise<Array<PluginStorageTestEntry<T>>>;\n\t};\n\tkv: {\n\t\tget<T = unknown>(key: string): Promise<T | null>;\n\t\tlist(): Promise<Array<PluginStorageTestEntry>>;\n\t};\n\tdispose(): Promise<void>;\n}\n\nconst DEFAULT_META = {\n\tip: null,\n\tuserAgent: null,\n\treferer: null,\n\tgeo: null,\n} as const;\n\nfunction isPluginTestBindings(value: unknown): value is PluginTestBindings {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"DB\" in value) || typeof value.DB !== \"object\" || value.DB === null) return false;\n\treturn (\n\t\t\"prepare\" in value.DB &&\n\t\ttypeof value.DB.prepare === \"function\" &&\n\t\t\"EMDASH_PLUGIN_CODE\" in value &&\n\t\ttypeof value.EMDASH_PLUGIN_CODE === \"string\" &&\n\t\t\"EMDASH_PLUGIN_MANIFEST\" in value &&\n\t\ttypeof value.EMDASH_PLUGIN_MANIFEST === \"string\"\n\t);\n}\n\n/**\n * Load the configured plugin through EmDash's production Cloudflare sandbox runner.\n */\nexport async function createPluginTestHost(): Promise<PluginTestHost> {\n\tconst bindings: unknown = env;\n\tif (!isPluginTestBindings(bindings)) {\n\t\tthrow new Error(\n\t\t\t\"EmDash plugin test bindings are unavailable; add emdashPluginTest() to Vitest\",\n\t\t);\n\t}\n\tconst manifestResult = pluginManifestSchema.safeParse(\n\t\tJSON.parse(bindings.EMDASH_PLUGIN_MANIFEST),\n\t);\n\tif (!manifestResult.success) throw new Error(\"EmDash plugin test manifest is invalid\");\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- the shared runtime schema validates the wire manifest before it enters core's equivalent runtime type\n\tconst manifest = manifestResult.data as unknown as PluginManifest;\n\tconst db = new Kysely<Database>({\n\t\tdialect: createDialect({ binding: \"DB\", session: \"disabled\" }),\n\t});\n\tawait runMigrations(db);\n\n\tconst runner = new CloudflareSandboxRunner({\n\t\tdb,\n\t\tsiteInfo: {\n\t\t\tname: \"EmDash plugin test site\",\n\t\t\turl: \"https://plugin.test\",\n\t\t\tlocale: \"en\",\n\t\t},\n\t});\n\tif (!runner.isAvailable()) {\n\t\tawait db.destroy();\n\t\tthrow new Error(`Plugin sandbox unavailable: ${runner.unavailableReason()}`);\n\t}\n\tconst plugin = await runner.load(manifest, bindings.EMDASH_PLUGIN_CODE);\n\tconst registry = new SchemaRegistry(db);\n\tconst content = new ContentRepository(db);\n\tlet disposed = false;\n\n\tconst assertActive = () => {\n\t\tif (disposed) throw new Error(\"Plugin test host has been disposed\");\n\t};\n\tconst readStorage = async <T>(collection: string, id?: string) => {\n\t\tassertActive();\n\t\tlet query = bindings.DB.prepare(\n\t\t\t\"SELECT id, data FROM _plugin_storage WHERE plugin_id = ? AND collection = ?\",\n\t\t).bind(manifest.id, collection);\n\t\tif (id !== undefined) {\n\t\t\tquery = bindings.DB.prepare(\n\t\t\t\t\"SELECT id, data FROM _plugin_storage WHERE plugin_id = ? AND collection = ? AND id = ?\",\n\t\t\t).bind(manifest.id, collection, id);\n\t\t}\n\t\tconst result = await query.all<{ id: string; data: string }>();\n\t\treturn (result.results ?? []).map((row) => ({\n\t\t\tid: row.id,\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- the caller supplies the expected stored JSON type\n\t\t\tdata: JSON.parse(row.data) as T,\n\t\t}));\n\t};\n\n\treturn {\n\t\tmanifest,\n\t\tasync invokeHook(name, event) {\n\t\t\tassertActive();\n\t\t\treturn plugin.invokeHook(name, event);\n\t\t},\n\t\tasync invokeRoute(name, input = {}, request = {}) {\n\t\t\tassertActive();\n\t\t\treturn plugin.invokeRoute(name, input, {\n\t\t\t\turl: request.url ?? `https://plugin.test/_emdash/api/plugins/${manifest.id}/${name}`,\n\t\t\t\tmethod: request.method ?? \"POST\",\n\t\t\t\theaders: request.headers ?? {},\n\t\t\t\tmeta: request.meta ?? DEFAULT_META,\n\t\t\t\tuser: request.user,\n\t\t\t});\n\t\t},\n\t\tasync createCollection({ fields = [], ...collection }) {\n\t\t\tassertActive();\n\t\t\tawait registry.createCollection(collection);\n\t\t\tfor (const field of fields) await registry.createField(collection.slug, field);\n\t\t},\n\t\tasync seedContent(collection, items) {\n\t\t\tassertActive();\n\t\t\tfor (const data of items) await content.create({ type: collection, data });\n\t\t},\n\t\tstorage<T>(collection: string) {\n\t\t\treturn {\n\t\t\t\tasync get(id: string) {\n\t\t\t\t\tconst rows = await readStorage<T>(collection, id);\n\t\t\t\t\treturn rows[0]?.data ?? null;\n\t\t\t\t},\n\t\t\t\tlist: () => readStorage<T>(collection),\n\t\t\t};\n\t\t},\n\t\tkv: {\n\t\t\tasync get<T>(key: string) {\n\t\t\t\tconst rows = await readStorage<T>(\"__kv\", key);\n\t\t\t\treturn rows[0]?.data ?? null;\n\t\t\t},\n\t\t\tlist: () => readStorage(\"__kv\"),\n\t\t},\n\t\tasync dispose() {\n\t\t\tif (disposed) return;\n\t\t\tdisposed = true;\n\t\t\tawait runner.terminateAll();\n\t\t\tawait db.destroy();\n\t\t\tawait reset();\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.mts","names":[],"sources":["../src/worker.ts"],"mappings":";;;cAA8D,QAAA"}
|
package/dist/worker.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PluginBridge } from "@emdash-cms/cloudflare/sandbox";
|
|
2
|
+
|
|
3
|
+
//#region src/worker.ts
|
|
4
|
+
var worker_default = { fetch() {
|
|
5
|
+
return new Response("EmDash plugin test host");
|
|
6
|
+
} };
|
|
7
|
+
|
|
8
|
+
//#endregion
|
|
9
|
+
export { PluginBridge, worker_default as default };
|
|
10
|
+
//# sourceMappingURL=worker.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.mjs","names":[],"sources":["../src/worker.ts"],"sourcesContent":["export { PluginBridge } from \"@emdash-cms/cloudflare/sandbox\";\n\nexport default {\n\tfetch() {\n\t\treturn new Response(\"EmDash plugin test host\");\n\t},\n} satisfies ExportedHandler;\n"],"mappings":";;;AAEA,qBAAe,EACd,QAAQ;AACP,QAAO,IAAI,SAAS,0BAA0B;GAE/C"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emdash-cms/plugin-test",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Workerd-backed Vitest host for sandboxed EmDash plugins",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"default": "./dist/index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./config": {
|
|
13
|
+
"types": "./dist/config.d.mts",
|
|
14
|
+
"default": "./dist/config.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./worker": {
|
|
17
|
+
"types": "./dist/worker.d.mts",
|
|
18
|
+
"default": "./dist/worker.mjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@cloudflare/vitest-plugin": "^1.0.0",
|
|
26
|
+
"kysely": "^0.29.0",
|
|
27
|
+
"@emdash-cms/plugin-types": "0.3.1",
|
|
28
|
+
"@emdash-cms/plugin-cli": "0.10.0",
|
|
29
|
+
"@emdash-cms/cloudflare": "0.37.0",
|
|
30
|
+
"emdash": "0.37.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"vite": "^8.0.0",
|
|
34
|
+
"vitest": "^4.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
38
|
+
"@cloudflare/workers-types": "^5.20260820.1",
|
|
39
|
+
"publint": "0.3.17",
|
|
40
|
+
"tsdown": "0.20.3",
|
|
41
|
+
"typescript": "^6.0.3",
|
|
42
|
+
"vite": "^8.0.11",
|
|
43
|
+
"vitest": "^4.1.5"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/emdash-cms/emdash.git",
|
|
48
|
+
"directory": "packages/plugin-test"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/emdash-cms/emdash",
|
|
51
|
+
"keywords": [
|
|
52
|
+
"emdash",
|
|
53
|
+
"plugin",
|
|
54
|
+
"vitest",
|
|
55
|
+
"workerd"
|
|
56
|
+
],
|
|
57
|
+
"author": "Matt Kane",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"typecheck": "tsgo --noEmit",
|
|
63
|
+
"check": "publint"
|
|
64
|
+
}
|
|
65
|
+
}
|