@stacksjs/actions 0.70.87 → 0.70.90

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 (159) hide show
  1. package/dist/action.js +78 -2
  2. package/dist/actions.js +2 -2
  3. package/dist/add.js +20 -2
  4. package/dist/ai/request-model-access.js +10 -2
  5. package/dist/auth/client.js +44 -3
  6. package/dist/auth/prune.js +35 -2
  7. package/dist/auth/setup.js +152 -36
  8. package/dist/auth/token.js +10 -2
  9. package/dist/blog-admin.d.ts +55 -0
  10. package/dist/blog-admin.js +156 -0
  11. package/dist/blog.d.ts +11 -0
  12. package/dist/blog.js +306 -56
  13. package/dist/build/cli.js +6 -2
  14. package/dist/build/component-libs.js +7 -2
  15. package/dist/build/core.js +55 -2
  16. package/dist/build/desktop.js +5 -2
  17. package/dist/build/docs.js +5 -2
  18. package/dist/build/frontend-static.js +123 -8
  19. package/dist/build/server.js +6 -2
  20. package/dist/build/stacks.js +8 -2
  21. package/dist/build/views.js +11 -2
  22. package/dist/build.js +64 -2
  23. package/dist/bump.js +162 -7
  24. package/dist/cdn/invalidate-cache.js +0 -1
  25. package/dist/changelog.js +27 -3
  26. package/dist/check/ports.js +16 -2
  27. package/dist/clean.js +4 -2
  28. package/dist/commit.js +12 -2
  29. package/dist/copy-types.js +10 -3
  30. package/dist/database/seed.js +26 -2
  31. package/dist/deploy/create-receipt-rule.js +36 -2
  32. package/dist/deploy/index.js +1318 -44
  33. package/dist/dev/api.js +94 -6
  34. package/dist/dev/components.js +7 -2
  35. package/dist/dev/dashboard-globals.js +89 -3
  36. package/dist/dev/dashboard-utils.js +380 -2
  37. package/dist/dev/dashboard.js +435 -8
  38. package/dist/dev/desktop.js +5 -2
  39. package/dist/dev/docs.js +24 -2
  40. package/dist/dev/index.js +29 -2
  41. package/dist/dev/migrate-watcher.js +74 -2
  42. package/dist/dev/system-tray.js +5 -2
  43. package/dist/dev/views.js +147 -2
  44. package/dist/discover-packages.js +32 -2
  45. package/dist/domains/add.js +61 -2
  46. package/dist/domains/purchase.js +66 -2
  47. package/dist/domains/remove.js +24 -2
  48. package/dist/examples.js +34 -2
  49. package/dist/find-projects.js +16 -2
  50. package/dist/fresh.js +17 -2
  51. package/dist/generate/action-types.js +9 -4
  52. package/dist/generate/component-meta.js +9 -3
  53. package/dist/generate/custom-cli-file.js +20 -3
  54. package/dist/generate/env-files.js +46 -6
  55. package/dist/generate/ide-helpers.js +8 -3
  56. package/dist/generate/index.js +158 -2
  57. package/dist/generate/lib-entries.js +16 -3
  58. package/dist/generate/pantry-file.js +46 -2
  59. package/dist/generate/symlink-core.js +2 -2
  60. package/dist/generate/types.js +3 -2
  61. package/dist/generate/vscode-custom-data.js +9 -3
  62. package/dist/generate/web-types.js +0 -1
  63. package/dist/helpers/component-meta.js +295 -2
  64. package/dist/helpers/index.js +1 -2
  65. package/dist/helpers/lib-entries.js +73 -2
  66. package/dist/helpers/package-json.js +45 -14
  67. package/dist/helpers/utils.js +105 -2
  68. package/dist/helpers/vscode-custom-data.js +30 -2
  69. package/dist/index.d.ts +13 -0
  70. package/dist/index.js +50 -2
  71. package/dist/key-generate.js +24 -2
  72. package/dist/lint/fix.js +4 -3
  73. package/dist/lint/index.js +4 -3
  74. package/dist/lint/lint.js +34 -2
  75. package/dist/make-command.js +64 -14
  76. package/dist/make-job.js +41 -16
  77. package/dist/make-policy.js +74 -26
  78. package/dist/make-resource.js +50 -21
  79. package/dist/make.js +328 -2
  80. package/dist/migrate/database.js +16 -2
  81. package/dist/migrate/fresh.js +31 -2
  82. package/dist/orm/base.js +0 -2
  83. package/dist/orm/test.js +0 -1
  84. package/dist/prepublish.js +16 -2
  85. package/dist/queue/clear.js +42 -2
  86. package/dist/queue/dlq-purge.js +28 -2
  87. package/dist/queue/dlq-retry.js +35 -2
  88. package/dist/queue/dlq.js +55 -5
  89. package/dist/queue/failed.js +69 -5
  90. package/dist/queue/flush.js +49 -3
  91. package/dist/queue/inspect.js +154 -8
  92. package/dist/queue/list.js +69 -5
  93. package/dist/queue/monitor.js +94 -6
  94. package/dist/queue/pause.js +28 -2
  95. package/dist/queue/quarantine.js +42 -3
  96. package/dist/queue/resume.js +27 -2
  97. package/dist/queue/retry.js +41 -2
  98. package/dist/queue/schedule-list.js +33 -5
  99. package/dist/queue/schedule.js +12 -2
  100. package/dist/queue/status.js +68 -5
  101. package/dist/queue/table.js +10 -2
  102. package/dist/queue/unquarantine.js +27 -2
  103. package/dist/queue/work.js +57 -2
  104. package/dist/release.js +21 -3
  105. package/dist/route/list.js +78 -3
  106. package/dist/run-action.js +1 -2
  107. package/dist/saas/setup.js +9 -2
  108. package/dist/scaffold-crud.js +164 -40
  109. package/dist/schedule/run.js +7 -2
  110. package/dist/search/flush.js +10 -2
  111. package/dist/search/import.js +10 -2
  112. package/dist/search/settings-list.js +10 -2
  113. package/dist/search/settings.js +9 -2
  114. package/dist/serve/api.js +35 -2
  115. package/dist/setup/index.js +1 -2
  116. package/dist/setup/ssl.js +271 -2
  117. package/dist/stacks.js +265 -2
  118. package/dist/templates.js +301 -2
  119. package/dist/test/feature.js +11 -2
  120. package/dist/test/index.js +11 -2
  121. package/dist/test/ui.js +4 -2
  122. package/dist/test/unit.js +11 -2
  123. package/dist/typecheck.js +4 -2
  124. package/dist/types.js +16 -2
  125. package/dist/upgrade/binary.js +29 -2
  126. package/dist/upgrade/bun.js +0 -1
  127. package/dist/upgrade/dependencies.js +0 -2
  128. package/dist/upgrade/framework-utils.js +193 -2
  129. package/dist/upgrade/framework.js +310 -7
  130. package/dist/upgrade/index.js +17 -2
  131. package/dist/upgrade/packages.js +99 -12
  132. package/dist/upgrade/shell.js +40 -4
  133. package/dist/upgrade.js +38 -2
  134. package/package.json +16 -16
  135. package/dist/chunk-0c8sd1t6.js +0 -2
  136. package/dist/chunk-0g2nzhx9.js +0 -240
  137. package/dist/chunk-0gadhvn7.js +0 -3
  138. package/dist/chunk-0h27aa4r.js +0 -93
  139. package/dist/chunk-3peh8kqs.js +0 -851
  140. package/dist/chunk-62gdfav8.js +0 -3
  141. package/dist/chunk-6bk53y02.js +0 -3
  142. package/dist/chunk-78c5n3s2.js +0 -3
  143. package/dist/chunk-c74qv38e.js +0 -17
  144. package/dist/chunk-cdk2r2jd.js +0 -1
  145. package/dist/chunk-dbz9dp2h.js +0 -3
  146. package/dist/chunk-e142dheq.js +0 -3
  147. package/dist/chunk-ec4gky40.js +0 -3
  148. package/dist/chunk-h3d81wrg.js +0 -3
  149. package/dist/chunk-m3q0x4h0.js +0 -688
  150. package/dist/chunk-mqgbyhph.js +0 -5
  151. package/dist/chunk-n24m8prb.js +0 -3
  152. package/dist/chunk-n5eqwhck.js +0 -7
  153. package/dist/chunk-nsvnqv8y.js +0 -5
  154. package/dist/chunk-nty6mmx4.js +0 -283
  155. package/dist/chunk-s735m9mg.js +0 -1
  156. package/dist/chunk-tpb8jgsk.js +0 -7
  157. package/dist/chunk-tsbk5vne.js +0 -3
  158. package/dist/chunk-xg9xscqz.js +0 -1
  159. package/dist/chunk-y6e448he.js +0 -7
@@ -0,0 +1,156 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const CONTENT_DIR = join(process.cwd(), "content/blog"), FIELD_ORDER = ["title", "description", "date", "author", "authorBio", "poster", "featured", "draft"];
4
+
5
+ export class BlogAdminError extends Error {
6
+ status;
7
+ constructor(message, status = 422) {
8
+ super(message);
9
+ this.status = status;
10
+ this.name = "BlogAdminError";
11
+ }
12
+ }
13
+ export function slugify(input) {
14
+ return String(input).normalize("NFKD").replace(/[\u0300-\u036F]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
15
+ }
16
+ function assertSafeSlug(slug) {
17
+ if (!slug)
18
+ throw new BlogAdminError("A slug is required.");
19
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug))
20
+ throw new BlogAdminError(`Invalid slug "${slug}". Use lowercase letters, numbers, and hyphens.`);
21
+ return slug;
22
+ }
23
+ function parseFrontmatter(md) {
24
+ const m = md.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
25
+ if (!m)
26
+ return { data: {}, body: md };
27
+ const data = {};
28
+ for (const line of (m[1] ?? "").split(`
29
+ `)) {
30
+ const i = line.indexOf(":");
31
+ if (i === -1)
32
+ continue;
33
+ const key = line.slice(0, i).trim();
34
+ let val = line.slice(i + 1).trim();
35
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'"))
36
+ val = val.slice(1, -1);
37
+ data[key] = val;
38
+ }
39
+ return { data, body: m[2] ?? "" };
40
+ }
41
+ function serialiseValue(key, value) {
42
+ if (/[\r\n]/.test(value))
43
+ throw new BlogAdminError(`"${key}" cannot contain line breaks.`);
44
+ if (!(/^["']|["']$|^\s|\s$|:\s/.test(value) || value === ""))
45
+ return value;
46
+ if (value.includes('"'))
47
+ return `'${value}'`;
48
+ return `"${value}"`;
49
+ }
50
+ function toFileContents(post) {
51
+ const fm = [];
52
+ for (const key of FIELD_ORDER) {
53
+ const raw = post[key];
54
+ if (typeof raw === "boolean") {
55
+ if (raw)
56
+ fm.push(`${key}: true`);
57
+ continue;
58
+ }
59
+ const value = String(raw ?? "");
60
+ if (!value)
61
+ continue;
62
+ fm.push(`${key}: ${serialiseValue(key, value)}`);
63
+ }
64
+ const body = post.body.replace(/\s+$/, "");
65
+ return `---
66
+ ${fm.join(`
67
+ `)}
68
+ ---
69
+
70
+ ${body}
71
+ `;
72
+ }
73
+ function readingTimeFor(body) {
74
+ const wordCount = body.split(/\s+/).filter(Boolean).length;
75
+ return { wordCount, readingTime: Math.max(1, Math.round(wordCount / 200)) };
76
+ }
77
+ function hydrate(slug, raw, updatedAt) {
78
+ const { data, body } = parseFrontmatter(raw), { wordCount, readingTime } = readingTimeFor(body);
79
+ return {
80
+ slug,
81
+ title: data.title ?? "",
82
+ description: data.description ?? "",
83
+ date: data.date ?? "",
84
+ author: data.author ?? "",
85
+ authorBio: data.authorBio ?? "",
86
+ poster: data.poster ?? "",
87
+ featured: data.featured === "true",
88
+ draft: data.draft === "true",
89
+ body: body.trim(),
90
+ wordCount,
91
+ readingTime,
92
+ updatedAt
93
+ };
94
+ }
95
+ function fileFor(slug) {
96
+ return join(CONTENT_DIR, `${assertSafeSlug(slug)}.md`);
97
+ }
98
+ export function listBlogPosts() {
99
+ if (!existsSync(CONTENT_DIR))
100
+ return [];
101
+ return readdirSync(CONTENT_DIR).filter((f) => f.endsWith(".md") && !f.startsWith(".")).map((f) => {
102
+ const path = join(CONTENT_DIR, f);
103
+ return hydrate(f.replace(/\.md$/, ""), readFileSync(path, "utf-8"), statSync(path).mtime.toISOString());
104
+ }).filter((p) => !!p.title).sort((a, b) => {
105
+ if (a.featured !== b.featured)
106
+ return a.featured ? -1 : 1;
107
+ return (b.date || "").localeCompare(a.date || "");
108
+ });
109
+ }
110
+ export function getBlogPost(slug) {
111
+ const file = fileFor(slug);
112
+ if (!existsSync(file))
113
+ return null;
114
+ return hydrate(slug, readFileSync(file, "utf-8"), statSync(file).mtime.toISOString());
115
+ }
116
+ export function saveBlogPost(input) {
117
+ const title = String(input.title ?? "").trim();
118
+ if (!title)
119
+ throw new BlogAdminError("A title is required.");
120
+ const slug = assertSafeSlug(String(input.slug ?? "").trim() || slugify(title)), originalSlug = input.originalSlug ? assertSafeSlug(input.originalSlug) : "", isRename = !!originalSlug && originalSlug !== slug;
121
+ if (existsSync(fileFor(slug)) && (!originalSlug || isRename))
122
+ throw new BlogAdminError(`A post with the slug "${slug}" already exists.`, 409);
123
+ if (originalSlug && !existsSync(fileFor(originalSlug)))
124
+ throw new BlogAdminError(`No post found with the slug "${originalSlug}".`, 404);
125
+ const date = String(input.date ?? "").trim() || new Date().toISOString().slice(0, 10);
126
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
127
+ throw new BlogAdminError("Date must be in YYYY-MM-DD format.");
128
+ const body = String(input.body ?? "").trim(), { wordCount, readingTime } = readingTimeFor(body), post = {
129
+ slug,
130
+ title,
131
+ description: String(input.description ?? "").trim(),
132
+ date,
133
+ author: String(input.author ?? "").trim(),
134
+ authorBio: String(input.authorBio ?? "").trim(),
135
+ poster: String(input.poster ?? "").trim(),
136
+ featured: input.featured === !0,
137
+ draft: input.draft === !0,
138
+ body,
139
+ wordCount,
140
+ readingTime,
141
+ updatedAt: new Date().toISOString()
142
+ };
143
+ if (!existsSync(CONTENT_DIR))
144
+ mkdirSync(CONTENT_DIR, { recursive: !0 });
145
+ writeFileSync(fileFor(slug), toFileContents(post), "utf-8");
146
+ if (isRename)
147
+ unlinkSync(fileFor(originalSlug));
148
+ return post;
149
+ }
150
+ export function deleteBlogPost(slug) {
151
+ const file = fileFor(slug);
152
+ if (!existsSync(file))
153
+ return !1;
154
+ unlinkSync(file);
155
+ return !0;
156
+ }
package/dist/blog.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * An origin safe to bake into absolute links, or '' when it is not usable.
3
+ *
4
+ * The deploy-time build takes its baseUrl from `APP_URL`, which is not always
5
+ * plaintext: a release that loads an env file it cannot decrypt leaves the
6
+ * literal ciphertext there, and prefixing it with `https://` produced feed and
7
+ * sitemap URLs like `https://encrypted:tLq7…==/blog/introducing-stacks` on the
8
+ * live site. Anything that is not a parseable http(s) origin with a dotted
9
+ * hostname is rejected here so callers fall back to the configured site url.
10
+ */
11
+ export declare function usableOrigin(value?: string): string;
1
12
  /**
2
13
  * Render a blog request via BunPress, or return null to let the caller handle
3
14
  * non-blog paths. Handles the listing, posts, the RSS feed, and the sitemap.
package/dist/blog.js CHANGED
@@ -1,30 +1,155 @@
1
- // @bun
2
- import"./chunk-h3d81wrg.js";import{existsSync as R,mkdirSync as C,readdirSync as b,readFileSync as X,writeFileSync as o}from"fs";import{homedir as i}from"os";import{join as B}from"path";var M=B(process.cwd(),"content/blog"),E=B(M,".theme.css"),y={title:"The Stacks Blog",description:"Notes from building a full-stack TypeScript framework whose only dependencies are TypeScript and Bun.",siteTitle:"Stacks Blog",author:"The Stacks Team",url:"https://stacksjs.com",nav:[{text:"Blog",link:"/blog"},{text:"Docs",link:"/docs"},{text:"GitHub",link:"https://github.com/stacksjs/stacks"}],themes:["colored","light","dark"],defaultTheme:"colored",colophon:'Built with Stacks \xB7 TypeScript &amp; Bun \xB7 <a href="/blog/feed.xml">RSS</a>',emptyTitle:"No posts yet",emptyText:"The first one is in the works. Leave your email and it will land in your inbox the moment it ships."},q=null;function w(){if(!q)q=(async()=>{try{let z=(await import(B(process.cwd(),"config/blog.ts"))).default??{},J={...y,...z};if(!z.siteTitle&&z.title)J.siteTitle=z.title;return J}catch{return y}})();return q}var O=null;async function U(){if(!O)O=(async()=>{let G=[B(i(),"Code/Tools/bunpress/packages/bunpress/dist/src/index.js"),B(process.cwd(),"pantry/@stacksjs/bunpress/dist/src/index.js"),"@stacksjs/bunpress"];for(let z of G)try{if(z.startsWith("@")||R(z))return await import(z)}catch{}return null})();return O}function $(G){return String(G).replace(/[&<>"]/g,(z)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[z])}function L(G){return String(G).replace(/[&<>"']/g,(z)=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&apos;"})[z])}function S(G,z){return`${G.replace(/\/$/,"")}/blog/${z}`}function r(G){if(!G)return"";let z=new Date(G);return Number.isNaN(z.getTime())?G:z.toUTCString()}async function H(G){let z=await w(),J=G||z.url,W=x().slice(0,50).map((Z)=>{let V=S(J,Z.slug);return` <item>
3
- <title>${L(Z.fm.title||Z.slug)}</title>
4
- <link>${L(V)}</link>
5
- <guid>${L(V)}</guid>
6
- <pubDate>${L(r(Z.fm.date))}</pubDate>
7
- <description>${L(Z.fm.description||"")}</description>
8
- </item>`}).join(`
9
- `);return`<?xml version="1.0" encoding="UTF-8"?>
10
- <rss version="2.0">
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const CONTENT_DIR = join(process.cwd(), "content/blog"), THEME_FILE = join(CONTENT_DIR, ".theme.css"), STACKS_DEFAULTS = {
5
+ title: "The Stacks Blog",
6
+ description: "Notes from building a full-stack TypeScript framework whose only dependencies are TypeScript and Bun.",
7
+ siteTitle: "Stacks Blog",
8
+ author: "The Stacks Team",
9
+ url: "https://stacksjs.com",
10
+ nav: [
11
+ { text: "Blog", link: "/blog" },
12
+ { text: "Docs", link: "/docs" },
13
+ { text: "GitHub", link: "https://github.com/stacksjs/stacks" }
14
+ ],
15
+ themes: ["colored", "light", "dark"],
16
+ defaultTheme: "colored",
17
+ colophon: 'Built with Stacks \xB7 TypeScript &amp; Bun \xB7 <a href="/blog/feed.xml">RSS</a>',
18
+ emptyTitle: "No posts yet",
19
+ emptyText: "The first one is in the works. Leave your email and it will land in your inbox the moment it ships."
20
+ };
21
+ let sitePromise = null;
22
+ function site() {
23
+ if (!sitePromise)
24
+ sitePromise = (async () => {
25
+ try {
26
+ const cfg = (await import(join(process.cwd(), "config/blog.ts"))).default ?? {}, merged = { ...STACKS_DEFAULTS, ...cfg };
27
+ if (!cfg.siteTitle && cfg.title)
28
+ merged.siteTitle = cfg.title;
29
+ return merged;
30
+ } catch {
31
+ return STACKS_DEFAULTS;
32
+ }
33
+ })();
34
+ return sitePromise;
35
+ }
36
+ let bunpressPromise = null;
37
+ async function loadBunPress() {
38
+ if (!bunpressPromise)
39
+ bunpressPromise = (async () => {
40
+ const candidates = [
41
+ join(homedir(), "Code/Tools/bunpress/packages/bunpress/dist/src/index.js"),
42
+ join(process.cwd(), "pantry/@stacksjs/bunpress/dist/src/index.js"),
43
+ "@stacksjs/bunpress"
44
+ ];
45
+ for (const entry of candidates)
46
+ try {
47
+ if (entry.startsWith("@") || existsSync(entry))
48
+ return await import(entry);
49
+ } catch {}
50
+ return null;
51
+ })();
52
+ return bunpressPromise;
53
+ }
54
+ function escapeHtml(s) {
55
+ return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
56
+ }
57
+ function escapeXml(s) {
58
+ return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
59
+ }
60
+ export function usableOrigin(value) {
61
+ if (!value)
62
+ return "";
63
+ try {
64
+ const url = new URL(/^https?:\/\//.test(value) ? value : `https://${value}`);
65
+ if (url.protocol !== "http:" && url.protocol !== "https:")
66
+ return "";
67
+ if (!url.hostname.includes(".") || !/^[a-z0-9.-]+$/i.test(url.hostname))
68
+ return "";
69
+ return url.origin;
70
+ } catch {
71
+ return "";
72
+ }
73
+ }
74
+ function postUrl(baseUrl, slug) {
75
+ return `${baseUrl.replace(/\/$/, "")}/blog/${slug}`;
76
+ }
77
+ function rssDate(date) {
78
+ if (!date)
79
+ return "";
80
+ const parsed = new Date(date);
81
+ return Number.isNaN(parsed.getTime()) ? date : parsed.toUTCString();
82
+ }
83
+ async function fallbackFeedXml(baseUrl) {
84
+ const cfg = await site(), origin = usableOrigin(baseUrl) || usableOrigin(cfg.url) || cfg.url, items = listPosts().slice(0, 50).map((p) => {
85
+ const url = postUrl(origin, p.slug);
86
+ return ` <item>
87
+ <title>${escapeXml(p.fm.title || p.slug)}</title>
88
+ <link>${escapeXml(url)}</link>
89
+ <guid>${escapeXml(url)}</guid>
90
+ <pubDate>${escapeXml(rssDate(p.fm.date))}</pubDate>
91
+ <description>${escapeXml(p.fm.description || "")}</description>
92
+ <dc:creator>${escapeXml(p.fm.author || cfg.author)}</dc:creator>
93
+ </item>`;
94
+ }).join(`
95
+ `);
96
+ return `<?xml version="1.0" encoding="UTF-8"?>
97
+ <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
11
98
  <channel>
12
- <title>${L(z.title)}</title>
13
- <link>${L(`${J}/blog`)}</link>
14
- <description>${L(z.description)}</description>
15
- ${W}
99
+ <title>${escapeXml(cfg.title)}</title>
100
+ <link>${escapeXml(`${origin}/blog`)}</link>
101
+ <description>${escapeXml(cfg.description)}</description>
102
+ ${items}
16
103
  </channel>
17
- </rss>`}async function h(G){let z=await w(),J=G||z.url;return`<?xml version="1.0" encoding="UTF-8"?>
104
+ </rss>`;
105
+ }
106
+ async function fallbackSitemapXml(baseUrl) {
107
+ const cfg = await site(), origin = usableOrigin(baseUrl) || usableOrigin(cfg.url) || cfg.url;
108
+ return `<?xml version="1.0" encoding="UTF-8"?>
18
109
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
19
- ${[`${J}/blog`,...x().map((W)=>S(J,W.slug))].map((W)=>` <url><loc>${L(W)}</loc></url>`).join(`
110
+ ${[`${origin}/blog`, ...listPosts().map((p) => postUrl(origin, p.slug))].map((url) => ` <url><loc>${escapeXml(url)}</loc></url>`).join(`
20
111
  `)}
21
- </urlset>`}function m(G){let z=G.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!z)return{data:{},body:G};let J={},Y=z[1]??"";for(let W of Y.split(`
22
- `)){let Z=W.indexOf(":");if(Z===-1)continue;let V=W.slice(0,Z).trim(),Q=W.slice(Z+1).trim();if(Q.startsWith('"')&&Q.endsWith('"')||Q.startsWith("'")&&Q.endsWith("'"))Q=Q.slice(1,-1);J[V]=Q}return{data:J,body:z[2]??""}}function T(G){if(!G)return"";let z=new Date(G);if(Number.isNaN(z.getTime()))return G;return`${["January","February","March","April","May","June","July","August","September","October","November","December"][z.getUTCMonth()]} ${z.getUTCDate()}, ${z.getUTCFullYear()}`}var P={colored:{label:"Park theme",glyph:"\uD83C\uDF32"},light:{label:"Light theme",glyph:"\u2600"},dark:{label:"Dark theme",glyph:"\uD83C\uDF19"}};async function D(){let G=await w(),z=G.themes.length?G.themes:y.themes,J=z.includes(G.defaultTheme)?G.defaultTheme:z[0],Y=z.map((Z)=>`<button type="button" data-t="${Z}" title="${P[Z].label}" aria-label="${P[Z].label}" onclick="stxBlogTheme('${Z}')">${P[Z].glyph}</button>`).join(`
23
- `),W=z.length>1?`<div class="blog-theme-toggle" role="group" aria-label="Theme">
24
- ${Y}
25
- </div>`:"";return`<script>
112
+ </urlset>`;
113
+ }
114
+ function parseFrontmatter(md) {
115
+ const m = md.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
116
+ if (!m)
117
+ return { data: {}, body: md };
118
+ const data = {}, frontmatter = m[1] ?? "";
119
+ for (const line of frontmatter.split(`
120
+ `)) {
121
+ const i = line.indexOf(":");
122
+ if (i === -1)
123
+ continue;
124
+ const key = line.slice(0, i).trim();
125
+ let val = line.slice(i + 1).trim();
126
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'"))
127
+ val = val.slice(1, -1);
128
+ data[key] = val;
129
+ }
130
+ return { data, body: m[2] ?? "" };
131
+ }
132
+ function formatDate(d) {
133
+ if (!d)
134
+ return "";
135
+ const date = new Date(d);
136
+ if (Number.isNaN(date.getTime()))
137
+ return d;
138
+ return `${["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}`;
139
+ }
140
+ const THEME_BUTTONS = {
141
+ colored: { label: "Park theme", glyph: "\uD83C\uDF32" },
142
+ light: { label: "Light theme", glyph: "\u2600" },
143
+ dark: { label: "Dark theme", glyph: "\uD83C\uDF19" }
144
+ };
145
+ async function blogChrome() {
146
+ const cfg = await site(), modes = cfg.themes.length ? cfg.themes : STACKS_DEFAULTS.themes, fallback = modes.includes(cfg.defaultTheme) ? cfg.defaultTheme : modes[0], buttons = modes.map((m) => `<button type="button" data-t="${m}" title="${THEME_BUTTONS[m].label}" aria-label="${THEME_BUTTONS[m].label}" onclick="stxBlogTheme('${m}')">${THEME_BUTTONS[m].glyph}</button>`).join(`
147
+ `), toggle = modes.length > 1 ? `<div class="blog-theme-toggle" role="group" aria-label="Theme">
148
+ ${buttons}
149
+ </div>` : "";
150
+ return `<script>
26
151
  (function () {
27
- var modes = ${JSON.stringify(z)}
152
+ var modes = ${JSON.stringify(modes)}
28
153
  function ok(theme) {
29
154
  return modes.indexOf(theme) !== -1
30
155
  }
@@ -35,68 +160,132 @@ ${[`${J}/blog`,...x().map((W)=>S(J,W.slug))].map((W)=>` <url><loc>${L(W)}</loc>
35
160
  }
36
161
  catch {}
37
162
 
163
+ // 'stacks-theme' is the shared key; the marketing page writes it too, so a
164
+ // choice made on / carries to /blog. 'stacks-blog-theme' is the old
165
+ // blog-only key, read once so an existing choice is not thrown away.
38
166
  var savedTheme = ''
39
167
  try {
40
- savedTheme = localStorage.getItem('stacks-blog-theme') || ''
168
+ savedTheme = localStorage.getItem('stacks-theme') || localStorage.getItem('stacks-blog-theme') || ''
169
+ }
170
+ catch {}
171
+
172
+ // A visitor who has never chosen gets their OS preference rather than a
173
+ // hard-coded default, which is what prefers-color-scheme is for. The
174
+ // configured default still answers for everyone who prefers light.
175
+ var prefersDark = false
176
+ try {
177
+ prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
41
178
  }
42
179
  catch {}
43
180
 
44
- var theme = ok(queryTheme) ? queryTheme : (ok(savedTheme) ? savedTheme : ${JSON.stringify(J)})
181
+ var systemTheme = (prefersDark && ok('dark')) ? 'dark' : ${JSON.stringify(fallback)}
182
+ var theme = ok(queryTheme) ? queryTheme : (ok(savedTheme) ? savedTheme : systemTheme)
45
183
  document.documentElement.setAttribute('data-theme', theme)
46
184
  window.stxBlogTheme = function (nextTheme) {
47
185
  try {
48
- localStorage.setItem('stacks-blog-theme', nextTheme)
186
+ localStorage.setItem('stacks-theme', nextTheme)
49
187
  }
50
188
  catch {}
51
189
  document.documentElement.setAttribute('data-theme', nextTheme)
52
190
  }
53
191
  })()
54
192
  </script>
55
- ${W}`}async function F(){return`<div class="blog-landscape" aria-hidden="true">
193
+ ${toggle}`;
194
+ }
195
+ async function blogFooter() {
196
+ return `<div class="blog-landscape" aria-hidden="true">
56
197
  <span class="ridge"></span>
57
198
  <span class="river"></span>
58
- <p class="colophon">${(await w()).colophon}</p>
59
- </div>`}async function v(G,z){let J=await w(),Y=R(E)?X(E,"utf-8"):"",W=G.defaultConfig.markdown?.css||"";return{...G.defaultConfig,title:z.title||J.title,description:z.description||J.description,docsDir:M,theme:"vitepress",markdown:{...G.defaultConfig.markdown,css:`${W}
60
- ${Y}`},themeConfig:{siteTitle:J.siteTitle,nav:J.nav}}}function x(){if(!R(M))return[];return b(M).filter((G)=>G.endsWith(".md")).map((G)=>{let{data:z}=m(X(B(M,G),"utf-8"));return{slug:G.replace(/\.md$/,""),fm:z}}).filter((G)=>!!G.fm.title&&!!G.fm.date).sort((G,z)=>{let J=G.fm.featured==="true"?1:0,Y=z.fm.featured==="true"?1:0;if(J!==Y)return Y-J;return(z.fm.date||"").localeCompare(G.fm.date||"")})}async function l(G){return G.wrapInLayout(await D()+`
199
+ <p class="colophon">${(await site()).colophon}</p>
200
+ </div>`;
201
+ }
202
+ async function blogConfig(bp, fm) {
203
+ const cfg = await site(), themeCss = existsSync(THEME_FILE) ? readFileSync(THEME_FILE, "utf-8") : "", baseCss = bp.defaultConfig.markdown?.css || "";
204
+ return {
205
+ ...bp.defaultConfig,
206
+ title: fm.title || cfg.title,
207
+ description: fm.description || cfg.description,
208
+ docsDir: CONTENT_DIR,
209
+ theme: "vitepress",
210
+ markdown: { ...bp.defaultConfig.markdown, css: `${baseCss}
211
+ ${themeCss}` },
212
+ themeConfig: {
213
+ siteTitle: cfg.siteTitle,
214
+ nav: cfg.nav
215
+ }
216
+ };
217
+ }
218
+ function listPosts() {
219
+ if (!existsSync(CONTENT_DIR))
220
+ return [];
221
+ return readdirSync(CONTENT_DIR).filter((f) => f.endsWith(".md")).map((f) => {
222
+ const { data } = parseFrontmatter(readFileSync(join(CONTENT_DIR, f), "utf-8"));
223
+ return { slug: f.replace(/\.md$/, ""), fm: data };
224
+ }).filter((p) => !!p.fm.title && !!p.fm.date && p.fm.draft !== "true").sort((a, b) => {
225
+ const fa = a.fm.featured === "true" ? 1 : 0, fb = b.fm.featured === "true" ? 1 : 0;
226
+ if (fa !== fb)
227
+ return fb - fa;
228
+ return (b.fm.date || "").localeCompare(a.fm.date || "");
229
+ });
230
+ }
231
+ async function notFoundHtml(bp) {
232
+ const body = `
61
233
  <a class="blog-back" href="/blog">\u2190 All posts</a>
62
234
  <div class="blog-404">
63
235
  <span class="blog-404-art" aria-hidden="true"></span>
64
236
  <h1>Nothing here</h1>
65
237
  <p>This post doesn't exist (or it moved). Head back to <a href="/blog">all posts</a>.</p>
66
- </div>`+await F(),await v(G,{title:"Not found"}),"/blog","page")}async function u(G,z,J){let Y=await w(),W=B(M,`${z}.md`);if(!R(W))return null;let Z=X(W,"utf-8"),{html:V,frontmatter:Q}=await G.markdownToHtml(Z,M);if(!Q.title||!Q.date)return null;let K=Q.author||Y.author,k=$(K.charAt(0).toUpperCase()),j=`
238
+ </div>`;
239
+ return bp.wrapInLayout(await blogChrome() + body + await blogFooter(), await blogConfig(bp, { title: "Not found" }), "/blog", "page");
240
+ }
241
+ async function postHtml(bp, slug, origin) {
242
+ const cfg = await site(), file = join(CONTENT_DIR, `${slug}.md`);
243
+ if (!existsSync(file))
244
+ return null;
245
+ const raw = readFileSync(file, "utf-8"), { html, frontmatter: fm } = await bp.markdownToHtml(raw, CONTENT_DIR);
246
+ if (!fm.title || !fm.date || fm.draft === !0 || fm.draft === "true")
247
+ return null;
248
+ const author = fm.author || cfg.author, initial = escapeHtml(author.charAt(0).toUpperCase()), header = `
67
249
  <a class="blog-back" href="/blog">\u2190 All posts</a>
68
250
  <div class="blog-post-head">
69
- <h1>${$(Q.title||z)}</h1>
251
+ <h1>${escapeHtml(fm.title || slug)}</h1>
70
252
  <p class="blog-post-meta">
71
- <span class="author">${$(K)}</span>
253
+ <span class="author">${escapeHtml(author)}</span>
72
254
  <span class="dot">\xB7</span>
73
- <time>${$(T(Q.date))}</time>
255
+ <time>${escapeHtml(formatDate(fm.date))}</time>
74
256
  </p>
75
- ${Q.poster?`<figure class="blog-post-poster"><img src="${$(Q.poster)}" alt="${$(Q.title||"")}"></figure>`:""}
76
- </div>`,I=Q.authorBio?`<aside class="blog-author-card">
77
- <div class="avatar">${k}</div>
78
- <div><span class="name">${$(K)}</span><p class="bio">${$(Q.authorBio)}</p></div>
79
- </aside>`:"",_=`${J}/blog/${z}`,n=`
257
+ ${fm.poster ? `<figure class="blog-post-poster"><img src="${escapeHtml(fm.poster)}" alt="${escapeHtml(fm.title || "")}"></figure>` : ""}
258
+ </div>`, authorCard = fm.authorBio ? `<aside class="blog-author-card">
259
+ <div class="avatar">${initial}</div>
260
+ <div><span class="name">${escapeHtml(author)}</span><p class="bio">${escapeHtml(fm.authorBio)}</p></div>
261
+ </aside>` : "", shareUrl = `${origin}/blog/${slug}`, share = `
80
262
  <div class="blog-share">
81
263
  <span class="blog-share-label">Share</span>
82
- <a href="https://twitter.com/intent/tweet?url=${encodeURIComponent(_)}&text=${encodeURIComponent(Q.title||"")}" target="_blank" rel="noreferrer">X / Twitter</a>
83
- <a href="https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(_)}" target="_blank" rel="noreferrer">LinkedIn</a>
84
- </div>`,N=x().filter((A)=>A.slug!==z).slice(0,3),c=N.length?`<nav class="blog-more">
264
+ <a href="https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(fm.title || "")}" target="_blank" rel="noreferrer">X / Twitter</a>
265
+ <a href="https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}" target="_blank" rel="noreferrer">LinkedIn</a>
266
+ </div>`, others = listPosts().filter((p) => p.slug !== slug).slice(0, 3), more = others.length ? `<nav class="blog-more">
85
267
  <h2>More from the blog</h2>
86
- <ul>${N.map((A)=>`<li><a href="/blog/${$(A.slug)}">${$(A.fm.title||A.slug)}</a></li>`).join("")}</ul>
87
- </nav>`:"";return G.wrapInLayout(await D()+j+V+I+n+c+await F(),await v(G,Q),`/blog/${z}`,"page")}async function d(G){let z=await w(),Y=x().map((V)=>{let Q=V.fm.featured==="true",K=V.fm.poster?`<div class="blog-card-media"><img src="${$(V.fm.poster)}" alt="" loading="lazy"></div>`:"";return`<a class="blog-card${Q?" is-featured":""}" href="/blog/${$(V.slug)}">
88
- ${K}
268
+ <ul>${others.map((p) => `<li><a href="/blog/${escapeHtml(p.slug)}">${escapeHtml(p.fm.title || p.slug)}</a></li>`).join("")}</ul>
269
+ </nav>` : "";
270
+ return bp.wrapInLayout(await blogChrome() + header + html + authorCard + share + more + await blogFooter(), await blogConfig(bp, fm), `/blog/${slug}`, "page");
271
+ }
272
+ async function indexHtml(bp) {
273
+ const cfg = await site(), cards = listPosts().map((p) => {
274
+ const featured = p.fm.featured === "true", media = p.fm.poster ? `<div class="blog-card-media"><img src="${escapeHtml(p.fm.poster)}" alt="" loading="lazy"></div>` : "";
275
+ return `<a class="blog-card${featured ? " is-featured" : ""}" href="/blog/${escapeHtml(p.slug)}">
276
+ ${media}
89
277
  <div>
90
- ${Q?'<span class="blog-card-flag">Featured</span>':""}
91
- <h2 class="blog-card-title">${$(V.fm.title||V.slug)}</h2>
92
- <p class="blog-card-excerpt">${$((V.fm.description||"").slice(0,180))}</p>
93
- <div class="blog-card-meta">${$(V.fm.author||z.author)} \xB7 ${$(T(V.fm.date))}</div>
278
+ ${featured ? '<span class="blog-card-flag">Featured</span>' : ""}
279
+ <h2 class="blog-card-title">${escapeHtml(p.fm.title || p.slug)}</h2>
280
+ <p class="blog-card-excerpt">${escapeHtml((p.fm.description || "").slice(0, 180))}</p>
281
+ <div class="blog-card-meta">${escapeHtml(p.fm.author || cfg.author)} \xB7 ${escapeHtml(formatDate(p.fm.date))}</div>
94
282
  </div>
95
- </a>`}).join(`
96
- `),W=`
283
+ </a>`;
284
+ }).join(`
285
+ `), emptyState = `
97
286
  <div class="blog-empty">
98
- <h2>${$(z.emptyTitle)}</h2>
99
- <p>${$(z.emptyText)}</p>
287
+ <h2>${escapeHtml(cfg.emptyTitle)}</h2>
288
+ <p>${escapeHtml(cfg.emptyText)}</p>
100
289
  <form class="blog-subscribe" method="POST" action="/api/email/subscribe">
101
290
  <input type="email" name="email" placeholder="you@example.com" autocomplete="email" aria-label="Email address" required>
102
291
  <input type="hidden" name="source" value="blog-empty">
@@ -139,10 +328,71 @@ ${Y}`},themeConfig:{siteTitle:J.siteTitle,nav:J.nav}}}function x(){if(!R(M))retu
139
328
  .finally(function () { if (button) button.disabled = false })
140
329
  })
141
330
  })()
142
- </script>`,Z=`
331
+ </script>`, body = `
143
332
  <div class="blog-listing-head">
144
333
  <span class="sign" aria-hidden="true"></span>
145
- <h1>${$(z.title)}</h1>
146
- <p>${$(z.description)}</p>
334
+ <h1>${escapeHtml(cfg.title)}</h1>
335
+ <p>${escapeHtml(cfg.description)}</p>
147
336
  </div>
148
- <div class="blog-cards">${Y||W}</div>`;return G.wrapInLayout(await D()+Z+await F(),await v(G,{title:z.title,description:z.description}),"/blog","page")}async function g(G,z){let J=await w(),Y={...await v(G,{title:J.title}),sitemap:{enabled:!0,baseUrl:`${z}/blog`}};return typeof G.buildRssFeed==="function"?G.buildRssFeed(M,Y,{enabled:!0,title:J.title,description:J.description,maxItems:50}):H(z)}async function f(G,z){let J={...await v(G,{title:"Blog"}),sitemap:{enabled:!0,baseUrl:`${z}/blog`}};return typeof G.buildSitemap==="function"?G.buildSitemap(M,J):h(z)}async function s(G){if(G.method!=="GET"&&G.method!=="HEAD")return null;let z=new URL(G.url),{pathname:J}=z,Y=J==="/blog/feed.xml",W=J==="/blog/sitemap.xml",Z=J==="/blog"||J==="/blog/"||/^\/blog\/[^/.]+$/.test(J);if(!Y&&!W&&!Z)return null;let V=await U();if(!V)return null;let Q=(j,I=200)=>new Response(j,{status:I,headers:{"Content-Type":"text/html; charset=utf-8"}});if(Y)return new Response(await g(V,z.origin),{headers:{"Content-Type":"application/rss+xml; charset=utf-8"}});if(W)return new Response(await f(V,z.origin),{headers:{"Content-Type":"application/xml; charset=utf-8"}});if(J==="/blog"||J==="/blog/")return Q(await d(V));let K=J.replace(/^\/blog\//,"").replace(/\/$/,""),k=await u(V,K,z.origin);return k?Q(k):Q(await l(V),404)}async function e(G){if(G.method!=="GET"&&G.method!=="HEAD")return null;let{pathname:z}=new URL(G.url);if(z==="/blog/feed.xml")return new Response(await H(""),{headers:{"Content-Type":"application/rss+xml; charset=utf-8"}});if(z==="/blog/sitemap.xml")return new Response(await h(""),{headers:{"Content-Type":"application/xml; charset=utf-8"}});return null}async function zz(G={outDir:"dist/blog"}){let z=G.outDir||"dist/blog",J=(G.baseUrl||(await w()).url).replace(/\/$/,""),Y=await U();if(!Y)throw Error("[blog] BunPress not found \u2014 cannot build the blog.");C(z,{recursive:!0});let W=(V,Q)=>{let K=B(z,V);C(B(K,".."),{recursive:!0}),o(K,Q,"utf-8")};W("index.html",await d(Y));let Z=x();for(let V of Z){let Q=await u(Y,V.slug,J);if(Q)W(B(V.slug,"index.html"),Q)}W("feed.xml",await g(Y,J)),W("sitemap.xml",await f(Y,J)),console.log(`[blog] built ${Z.length} post(s) + listing, feed.xml, sitemap.xml \u2192 ${z}`)}export{e as renderBlogFeed,s as renderBlog,zz as buildBlog};
337
+ <div class="blog-cards">${cards || emptyState}</div>`;
338
+ return bp.wrapInLayout(await blogChrome() + body + await blogFooter(), await blogConfig(bp, { title: cfg.title, description: cfg.description }), "/blog", "page");
339
+ }
340
+ async function feedXml(baseUrl) {
341
+ return fallbackFeedXml(baseUrl);
342
+ }
343
+ async function sitemapXml(baseUrl) {
344
+ return fallbackSitemapXml(baseUrl);
345
+ }
346
+ export async function renderBlog(req) {
347
+ if (req.method !== "GET" && req.method !== "HEAD")
348
+ return null;
349
+ const url = new URL(req.url), { pathname } = url, isFeed = pathname === "/blog/feed.xml", isSitemap = pathname === "/blog/sitemap.xml", isPage = pathname === "/blog" || pathname === "/blog/" || /^\/blog\/[^/.]+$/.test(pathname);
350
+ if (!isFeed && !isSitemap && !isPage)
351
+ return null;
352
+ const bp = await loadBunPress();
353
+ if (!bp)
354
+ return null;
355
+ const html = (s, status = 200) => new Response(s, { status, headers: { "Content-Type": "text/html; charset=utf-8" } });
356
+ if (isFeed)
357
+ return new Response(await feedXml(url.origin), { headers: { "Content-Type": "application/rss+xml; charset=utf-8" } });
358
+ if (isSitemap)
359
+ return new Response(await sitemapXml(url.origin), { headers: { "Content-Type": "application/xml; charset=utf-8" } });
360
+ if (pathname === "/blog" || pathname === "/blog/")
361
+ return html(await indexHtml(bp));
362
+ const slug = pathname.replace(/^\/blog\//, "").replace(/\/$/, ""), post = await postHtml(bp, slug, url.origin);
363
+ return post ? html(post) : html(await notFoundHtml(bp), 404);
364
+ }
365
+ export async function renderBlogFeed(req) {
366
+ if (req.method !== "GET" && req.method !== "HEAD")
367
+ return null;
368
+ const { pathname } = new URL(req.url);
369
+ if (pathname === "/blog/feed.xml")
370
+ return new Response(await fallbackFeedXml(""), { headers: { "Content-Type": "application/rss+xml; charset=utf-8" } });
371
+ if (pathname === "/blog/sitemap.xml")
372
+ return new Response(await fallbackSitemapXml(""), { headers: { "Content-Type": "application/xml; charset=utf-8" } });
373
+ return null;
374
+ }
375
+ export async function buildBlog(options = { outDir: "dist/blog" }) {
376
+ const outDir = options.outDir || "dist/blog", siteUrl = (await site()).url, baseUrl = usableOrigin(options.baseUrl) || usableOrigin(siteUrl) || siteUrl.replace(/\/$/, "");
377
+ if (options.baseUrl && !usableOrigin(options.baseUrl))
378
+ console.warn(`[blog] ignoring unusable baseUrl ${JSON.stringify(options.baseUrl)} \u2014 falling back to ${baseUrl}`);
379
+ const bp = await loadBunPress();
380
+ if (!bp)
381
+ throw Error("[blog] BunPress not found \u2014 cannot build the blog.");
382
+ mkdirSync(outDir, { recursive: !0 });
383
+ const write = (rel, content) => {
384
+ const target = join(outDir, rel);
385
+ mkdirSync(join(target, ".."), { recursive: !0 });
386
+ writeFileSync(target, content, "utf-8");
387
+ };
388
+ write("index.html", await indexHtml(bp));
389
+ const posts = listPosts();
390
+ for (const p of posts) {
391
+ const html = await postHtml(bp, p.slug, baseUrl);
392
+ if (html)
393
+ write(join(p.slug, "index.html"), html);
394
+ }
395
+ write("feed.xml", await feedXml(baseUrl));
396
+ write("sitemap.xml", await sitemapXml(baseUrl));
397
+ console.log(`[blog] built ${posts.length} post(s) + listing, feed.xml, sitemap.xml \u2192 ${outDir}`);
398
+ }
package/dist/build/cli.js CHANGED
@@ -1,2 +1,6 @@
1
- // @bun
2
- var{$:i}=globalThis.Bun;import{log as m}from"@stacksjs/cli";import{buddyPath as o}from"@stacksjs/path";m.info("Building CLI...");i.cwd(o());await i`bun ${o("build.ts")}`;
1
+ import { $ } from "bun";
2
+ import { log } from "@stacksjs/cli";
3
+ import { buddyPath } from "@stacksjs/path";
4
+ log.info("Building CLI...");
5
+ $.cwd(buddyPath());
6
+ await $`bun ${buddyPath("build.ts")}`;
@@ -1,2 +1,7 @@
1
- // @bun
2
- import{runCommand as r}from"@stacksjs/cli";import{NpmScript as m}from"@stacksjs/enums";import{frameworkPath as o}from"@stacksjs/path";await r(m.BuildComponents,{cwd:o(),verbose:!0});
1
+ import { runCommand } from "@stacksjs/cli";
2
+ import { NpmScript } from "@stacksjs/enums";
3
+ import { frameworkPath } from "@stacksjs/path";
4
+ await runCommand(NpmScript.BuildComponents, {
5
+ cwd: frameworkPath(),
6
+ verbose: !0
7
+ });