@stackbox/cms 0.0.4 → 0.1.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.
Files changed (122) hide show
  1. package/README.md +119 -7
  2. package/dist/AGENTS.md +215 -0
  3. package/dist/blocks.d.ts +11 -0
  4. package/dist/blocks.d.ts.map +1 -0
  5. package/dist/blocks.js +27 -0
  6. package/dist/blocks.js.map +1 -0
  7. package/dist/build.d.ts +14 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +145 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/cache.d.ts +14 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +93 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +16 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/index.d.ts +16 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +15 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/link.d.ts +6 -0
  24. package/dist/link.d.ts.map +1 -0
  25. package/dist/link.js +121 -0
  26. package/dist/link.js.map +1 -0
  27. package/dist/modules.d.ts +18 -0
  28. package/dist/modules.d.ts.map +1 -0
  29. package/dist/modules.js +25 -0
  30. package/dist/modules.js.map +1 -0
  31. package/dist/pages.d.ts +24 -0
  32. package/dist/pages.d.ts.map +1 -0
  33. package/dist/pages.js +87 -0
  34. package/dist/pages.js.map +1 -0
  35. package/dist/plugin.d.ts +22 -0
  36. package/dist/plugin.d.ts.map +1 -0
  37. package/dist/plugin.js +155 -0
  38. package/dist/plugin.js.map +1 -0
  39. package/dist/plugins/blog/index.d.ts +21 -0
  40. package/dist/plugins/blog/index.d.ts.map +1 -0
  41. package/dist/plugins/blog/index.js +68 -0
  42. package/dist/plugins/blog/index.js.map +1 -0
  43. package/dist/plugins/blog/posts.d.ts +15 -0
  44. package/dist/plugins/blog/posts.d.ts.map +1 -0
  45. package/dist/plugins/blog/posts.js +46 -0
  46. package/dist/plugins/blog/posts.js.map +1 -0
  47. package/dist/plugins/random-quote/assets/quotes.json +230 -0
  48. package/dist/plugins/random-quote/index.d.ts +14 -0
  49. package/dist/plugins/random-quote/index.d.ts.map +1 -0
  50. package/dist/plugins/random-quote/index.js +35 -0
  51. package/dist/plugins/random-quote/index.js.map +1 -0
  52. package/dist/plugins/sitemap/index.d.ts +14 -0
  53. package/dist/plugins/sitemap/index.d.ts.map +1 -0
  54. package/dist/plugins/sitemap/index.js +119 -0
  55. package/dist/plugins/sitemap/index.js.map +1 -0
  56. package/dist/render-head.d.ts +7 -0
  57. package/dist/render-head.d.ts.map +1 -0
  58. package/dist/render-head.js +27 -0
  59. package/dist/render-head.js.map +1 -0
  60. package/dist/render-page.d.ts +4 -0
  61. package/dist/render-page.d.ts.map +1 -0
  62. package/dist/render-page.js +34 -0
  63. package/dist/render-page.js.map +1 -0
  64. package/dist/routing.d.ts +3 -0
  65. package/dist/routing.d.ts.map +1 -0
  66. package/dist/routing.js +11 -0
  67. package/dist/routing.js.map +1 -0
  68. package/dist/site.d.ts +14 -0
  69. package/dist/site.d.ts.map +1 -0
  70. package/dist/site.js +233 -0
  71. package/dist/site.js.map +1 -0
  72. package/dist/slot-content.d.ts +14 -0
  73. package/dist/slot-content.d.ts.map +1 -0
  74. package/dist/slot-content.js +34 -0
  75. package/dist/slot-content.js.map +1 -0
  76. package/dist/slot-handle.d.ts +14 -0
  77. package/dist/slot-handle.d.ts.map +1 -0
  78. package/dist/slot-handle.js +83 -0
  79. package/dist/slot-handle.js.map +1 -0
  80. package/dist/stackbox/context.d.ts +3 -0
  81. package/dist/stackbox/context.d.ts.map +1 -0
  82. package/dist/stackbox/context.js +165 -0
  83. package/dist/stackbox/context.js.map +1 -0
  84. package/dist/templates.d.ts +13 -0
  85. package/dist/templates.d.ts.map +1 -0
  86. package/dist/templates.js +106 -0
  87. package/dist/templates.js.map +1 -0
  88. package/dist/types.d.ts +277 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +2 -0
  91. package/dist/types.js.map +1 -0
  92. package/package.json +18 -3
  93. package/src/plugins/blog/AGENTS.md +5 -1
  94. package/src/plugins/blog/index.ts +22 -5
  95. package/src/plugins/random-quote/AGENTS.md +30 -7
  96. package/src/plugins/random-quote/index.ts +17 -1
  97. package/src/plugins/random-quote/public_assets/widget.css +5 -0
  98. package/src/plugins/sitemap/AGENTS.md +82 -0
  99. package/src/plugins/sitemap/index.ts +156 -0
  100. package/.claude/settings.local.json +0 -8
  101. package/AGENTS.md +0 -87
  102. package/scripts/copy-plugin-assets.mjs +0 -30
  103. package/src/blocks.ts +0 -58
  104. package/src/index.ts +0 -67
  105. package/src/link.ts +0 -158
  106. package/src/pages.ts +0 -198
  107. package/src/render-head.ts +0 -32
  108. package/src/render-page.ts +0 -51
  109. package/src/routing.ts +0 -14
  110. package/src/site.ts +0 -160
  111. package/src/slot-content.ts +0 -77
  112. package/src/slot-handle.ts +0 -131
  113. package/src/stackbox/context.ts +0 -257
  114. package/src/templates.ts +0 -230
  115. package/test/async-render.test.ts +0 -103
  116. package/test/blog.test.ts +0 -93
  117. package/test/fetch-routing.test.ts +0 -76
  118. package/test/plugin-assets.test.ts +0 -80
  119. package/test/random-quote.test.ts +0 -55
  120. package/test/stackbox-context.test.ts +0 -44
  121. package/tsconfig.json +0 -20
  122. /package/src/plugins/random-quote/{public_assets → assets}/quotes.json +0 -0
@@ -1,76 +0,0 @@
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
- });
@@ -1,80 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { existsSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { describe, it } from "node:test";
5
- import {
6
- getPluginPublicRootDir,
7
- parsePluginAssetRequest,
8
- pluginAssetPath,
9
- servePluginAsset,
10
- } from "../src/link.js";
11
- import { createSite, createSiteConfig } from "../src/site.js";
12
- import { createPage } from "../src/pages.js";
13
- import { createTemplate } from "../src/templates.js";
14
- import { html } from "@hyperspan/html";
15
-
16
- describe("pluginAssetPath", () => {
17
- it("builds paths under /_sb/plugins", () => {
18
- assert.strictEqual(
19
- pluginAssetPath("random-quote", "photo.jpg"),
20
- "/_sb/plugins/random-quote/photo.jpg",
21
- );
22
- });
23
-
24
- it("rejects unsafe relative segments", () => {
25
- assert.throws(() => pluginAssetPath("random-quote", "../secret.txt"));
26
- });
27
- });
28
-
29
- describe("plugin public assets", () => {
30
- it("copies random-quote quotes.json to dist/public/_sb", () => {
31
- const file = join(
32
- getPluginPublicRootDir(),
33
- "random-quote",
34
- "quotes.json",
35
- );
36
- assert.ok(existsSync(file));
37
- });
38
-
39
- it("parsePluginAssetRequest rejects traversal", () => {
40
- assert.strictEqual(
41
- parsePluginAssetRequest("/_sb/plugins/random-quote/../quotes.json"),
42
- null,
43
- );
44
- });
45
-
46
- it("servePluginAsset returns JSON for bundled quotes", async () => {
47
- const res = servePluginAsset(
48
- "/_sb/plugins/random-quote/quotes.json",
49
- "GET",
50
- );
51
- assert.ok(res);
52
- assert.strictEqual(res!.status, 200);
53
- assert.match(res!.headers.get("content-type") ?? "", /json/);
54
- const body = await res!.text();
55
- assert.match(body, /Rumi/);
56
- });
57
-
58
- it("site.fetch serves plugin assets before page routes", async () => {
59
- const siteConfig = createSiteConfig({ name: "Asset Test" });
60
- const template = createTemplate({
61
- siteConfig,
62
- slots: [{ name: "content", options: { required: true, primary: true } }],
63
- render({ slots }: { slots: { content: { render: () => unknown } } }) {
64
- return html`<body>${slots.content.render()}</body>`;
65
- },
66
- });
67
- const homePage = createPage(template, {
68
- path: "/",
69
- title: "Home",
70
- slots: { content: ["<p>home</p>"] },
71
- });
72
- const site = createSite(siteConfig, { pages: [homePage] });
73
-
74
- const res = await site.fetch(
75
- new Request("https://example.com/_sb/plugins/random-quote/quotes.json"),
76
- );
77
- assert.strictEqual(res.status, 200);
78
- assert.match(await res.text(), /Rumi/);
79
- });
80
- });
@@ -1,55 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { renderAsync } from "@hyperspan/html";
4
- import { createSiteConfig } from "../src/site.js";
5
- import {
6
- defaultQuotes,
7
- pickRandomQuote,
8
- randomQuoteBlock,
9
- type Quote,
10
- } from "../src/plugins/random-quote/index.js";
11
-
12
- describe("random-quote plugin", () => {
13
- it("ships bundled default quotes", () => {
14
- assert.ok(defaultQuotes.length >= 50);
15
- assert.strictEqual(defaultQuotes[0]!.author, "Rumi");
16
- });
17
-
18
- it("pickRandomQuote returns a quote from the given list", () => {
19
- const custom: Quote[] = [
20
- { text: "Custom quote.", author: "Test Author" },
21
- ];
22
- const quote = pickRandomQuote(custom);
23
- assert.strictEqual(quote.author, "Test Author");
24
- });
25
-
26
- it("randomQuoteBlock uses bundled quotes by default", async () => {
27
- const siteConfig = createSiteConfig({ name: "Test" });
28
- const originalRandom = Math.random;
29
- Math.random = () => 0;
30
-
31
- try {
32
- const block = randomQuoteBlock();
33
- const htmlOut = await renderAsync(block.render({ siteConfig }));
34
-
35
- assert.match(htmlOut, /data-block="random-quote"/);
36
- assert.match(htmlOut, /<blockquote/);
37
- assert.match(htmlOut, /beat 40 scholars/);
38
- assert.match(htmlOut, /— Rumi/);
39
- } finally {
40
- Math.random = originalRandom;
41
- }
42
- });
43
-
44
- it("randomQuoteBlock accepts custom quotes", async () => {
45
- const siteConfig = createSiteConfig({ name: "Test" });
46
- const custom: Quote[] = [
47
- { text: "Ship your own quotes.", author: "Site Owner" },
48
- ];
49
- const block = randomQuoteBlock({ quotes: custom });
50
- const htmlOut = await renderAsync(block.render({ siteConfig }));
51
-
52
- assert.match(htmlOut, /Ship your own quotes/);
53
- assert.match(htmlOut, /— Site Owner/);
54
- });
55
- });
@@ -1,44 +0,0 @@
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 DELETED
@@ -1,20 +0,0 @@
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
- "resolveJsonModule": true,
16
- "types": ["node"]
17
- },
18
- "include": ["src/**/*.ts"],
19
- "exclude": ["dist", "node_modules"]
20
- }