@stackbox/cms 0.0.2
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.md +28 -0
- package/README.md +168 -0
- package/package.json +44 -0
- package/src/index.ts +66 -0
- package/src/modules/blog/module.ts +86 -0
- package/src/modules/blog/posts.ts +69 -0
- package/src/modules.ts +58 -0
- package/src/pages.ts +198 -0
- package/src/render-head.ts +32 -0
- package/src/render-page.ts +51 -0
- package/src/routing.ts +14 -0
- package/src/site.ts +153 -0
- package/src/slot-content.ts +77 -0
- package/src/slot-handle.ts +131 -0
- package/src/stackbox/context.ts +257 -0
- package/src/templates.ts +230 -0
- package/test/async-render.test.ts +103 -0
- package/test/blog.test.ts +93 -0
- package/test/fetch-routing.test.ts +76 -0
- package/test/stackbox-context.test.ts +44 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { html } from "@hyperspan/html";
|
|
4
|
+
import { createPage } from "../src/pages.js";
|
|
5
|
+
import { createSite, createSiteConfig } from "../src/site.js";
|
|
6
|
+
import { createTemplate } from "../src/templates.js";
|
|
7
|
+
|
|
8
|
+
function createFixtureSite() {
|
|
9
|
+
const siteConfig = createSiteConfig({ name: "Routing Test" });
|
|
10
|
+
|
|
11
|
+
const template = createTemplate({
|
|
12
|
+
siteConfig,
|
|
13
|
+
slots: [{ name: "content", options: { required: true, primary: true } }],
|
|
14
|
+
render({ slots }: { slots: { content: { render: () => unknown } } }): ReturnType<typeof html> {
|
|
15
|
+
return html`<body>${slots.content.render()}</body>`;
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const homePage = createPage(template, {
|
|
20
|
+
path: "/",
|
|
21
|
+
title: "Home",
|
|
22
|
+
slots: { content: ["<p>home</p>"] },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const aboutPage = createPage(template, {
|
|
26
|
+
path: "/about",
|
|
27
|
+
title: "About",
|
|
28
|
+
slots: { content: ["<p>about</p>"] },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return createSite(siteConfig, { pages: [homePage, aboutPage] });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("site.fetch routing", () => {
|
|
35
|
+
it("routes / and /about to correct pages", async () => {
|
|
36
|
+
const site = createFixtureSite();
|
|
37
|
+
|
|
38
|
+
const home = await site.fetch(new Request("https://example.com/"));
|
|
39
|
+
assert.strictEqual(home.status, 200);
|
|
40
|
+
assert.match(await home.text(), /<p>home<\/p>/);
|
|
41
|
+
|
|
42
|
+
const about = await site.fetch(new Request("https://example.com/about"));
|
|
43
|
+
assert.strictEqual(about.status, 200);
|
|
44
|
+
assert.match(await about.text(), /<p>about<\/p>/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("normalizes trailing slashes", async () => {
|
|
48
|
+
const site = createFixtureSite();
|
|
49
|
+
const res = await site.fetch(new Request("https://example.com/about/"));
|
|
50
|
+
assert.strictEqual(res.status, 200);
|
|
51
|
+
assert.match(await res.text(), /<p>about<\/p>/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns 404 for unknown pages", async () => {
|
|
55
|
+
const site = createFixtureSite();
|
|
56
|
+
const res = await site.fetch(new Request("https://example.com/missing"));
|
|
57
|
+
assert.strictEqual(res.status, 404);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns 405 for POST", async () => {
|
|
61
|
+
const site = createFixtureSite();
|
|
62
|
+
const res = await site.fetch(
|
|
63
|
+
new Request("https://example.com/", { method: "POST" }),
|
|
64
|
+
);
|
|
65
|
+
assert.strictEqual(res.status, 405);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns 200 with empty body for HEAD", async () => {
|
|
69
|
+
const site = createFixtureSite();
|
|
70
|
+
const res = await site.fetch(
|
|
71
|
+
new Request("https://example.com/", { method: "HEAD" }),
|
|
72
|
+
);
|
|
73
|
+
assert.strictEqual(res.status, 200);
|
|
74
|
+
assert.strictEqual(await res.text(), "");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createContext } from "../src/stackbox/context.js";
|
|
4
|
+
|
|
5
|
+
describe("Stackbox createContext", () => {
|
|
6
|
+
it("wraps request with uppercase method and query params", async () => {
|
|
7
|
+
const raw = new Request("https://example.com/path?foo=bar", {
|
|
8
|
+
method: "get",
|
|
9
|
+
headers: { Cookie: "session=abc123" },
|
|
10
|
+
});
|
|
11
|
+
const ctx = createContext(raw, { API_KEY: "secret" });
|
|
12
|
+
|
|
13
|
+
assert.strictEqual(ctx.req.method, "GET");
|
|
14
|
+
assert.strictEqual(ctx.req.query.get("foo"), "bar");
|
|
15
|
+
assert.strictEqual(ctx.req.cookies.get("session"), "abc123");
|
|
16
|
+
assert.strictEqual(ctx.env.API_KEY, "secret");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("ctx.res.html produces a valid Response", async () => {
|
|
20
|
+
const ctx = createContext(new Request("https://example.com/"), {});
|
|
21
|
+
const res = await ctx.res.html("<p>hello</p>");
|
|
22
|
+
|
|
23
|
+
assert.strictEqual(res.status, 200);
|
|
24
|
+
assert.match(await res.text(), /<p>hello<\/p>/);
|
|
25
|
+
assert.match(res.headers.get("Content-Type") ?? "", /text\/html/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("ctx.res.json produces a valid Response", async () => {
|
|
29
|
+
const ctx = createContext(new Request("https://example.com/"), {});
|
|
30
|
+
const res = await ctx.res.json({ ok: true });
|
|
31
|
+
|
|
32
|
+
assert.strictEqual(res.status, 200);
|
|
33
|
+
assert.deepEqual(await res.json(), { ok: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("sets cookies on outgoing response", async () => {
|
|
37
|
+
const ctx = createContext(new Request("https://example.com/"), {});
|
|
38
|
+
ctx.res.cookies.set("theme", "dark", { path: "/" });
|
|
39
|
+
const res = await ctx.res.text("ok");
|
|
40
|
+
|
|
41
|
+
const setCookie = res.headers.get("Set-Cookie") ?? res.headers.get("set-cookie");
|
|
42
|
+
assert.ok(setCookie?.includes("theme=dark"));
|
|
43
|
+
});
|
|
44
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"types": ["node"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"exclude": ["dist", "node_modules"]
|
|
19
|
+
}
|