wuppycms 0.0.0-stage → 0.2.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/dist/admin-fo4l4lu2.mjs +59 -0
- package/dist/astro.d.mts +20 -0
- package/dist/astro.mjs +302 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +210 -0
- package/dist/components-BrGAEPly.mjs +39 -0
- package/dist/config-CM8_-x_W.mjs +1242 -0
- package/dist/dist-D7fysV_5.mjs +9048 -0
- package/dist/frontmatter-DGaz9--t.mjs +24 -0
- package/dist/generate-iX4B_2U2.mjs +473 -0
- package/dist/i18n-CgLAHd9Q.mjs +79 -0
- package/dist/index-B6AHmgP9.d.mts +34 -0
- package/dist/index.d.mts +8636 -0
- package/dist/index.mjs +83 -0
- package/dist/live-runtime.d.mts +120 -0
- package/dist/live-runtime.mjs +207 -0
- package/dist/markdown.mjs +4 -0
- package/dist/mdx.d.mts +68 -0
- package/dist/mdx.mjs +4 -0
- package/dist/media-DGfJXgBA.mjs +356 -0
- package/dist/page-cache-middleware.d.mts +8 -0
- package/dist/page-cache-middleware.mjs +101 -0
- package/dist/publishing.d.mts +17 -0
- package/dist/publishing.mjs +42 -0
- package/dist/read-Bh4-a3Ap.mjs +18 -0
- package/dist/redirect-middleware.d.mts +6 -0
- package/dist/redirect-middleware.mjs +37 -0
- package/dist/render-DzH7Wil3.mjs +116 -0
- package/dist/routes/admin.d.mts +7 -0
- package/dist/routes/admin.mjs +10 -0
- package/dist/routes/api.d.mts +7 -0
- package/dist/routes/api.mjs +97 -0
- package/dist/routes/icons.d.mts +12 -0
- package/dist/routes/icons.mjs +36 -0
- package/dist/routes/robots.d.mts +7 -0
- package/dist/routes/robots.mjs +15 -0
- package/dist/routes/rss.d.mts +7 -0
- package/dist/routes/rss.mjs +20 -0
- package/dist/routes/sitemap.d.mts +7 -0
- package/dist/routes/sitemap.mjs +23 -0
- package/dist/routes/uploads.d.mts +7 -0
- package/dist/routes/uploads.mjs +11 -0
- package/dist/schemas-dHRJ1HeG.mjs +275 -0
- package/dist/seo.d.mts +125 -0
- package/dist/seo.mjs +130 -0
- package/dist/server-D92ryW2y.mjs +5883 -0
- package/dist/service-CgcvCG7l.mjs +353 -0
- package/dist/site-dF-xVPqe.mjs +148 -0
- package/dist/site.d.mts +28 -0
- package/dist/site.mjs +44 -0
- package/dist/sitemap-Ba0vil6a.mjs +46 -0
- package/dist/theme-pages-CRsBm1EX.mjs +246 -0
- package/dist/theme-seo-middleware.d.mts +7 -0
- package/dist/theme-seo-middleware.mjs +62 -0
- package/dist/visual-middleware.d.mts +7 -0
- package/dist/visual-middleware.mjs +71 -0
- package/package.json +72 -3
- package/README.md +0 -3
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { t as parseDocument } from "./frontmatter-DGaz9--t.mjs";
|
|
2
|
+
import { t as listThemeComponents } from "./components-BrGAEPly.mjs";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/astro/quiet-hmr.ts
|
|
8
|
+
/**
|
|
9
|
+
* The visual editor changes a theme element on the canvas itself, then writes the same change to the `.astro` file.
|
|
10
|
+
* Astro would reload the whole page for that write (it looks like F5 while typing), so a write the editor marks as
|
|
11
|
+
* already shown skips the reload: the file's server modules are still invalidated, so the next request renders it.
|
|
12
|
+
*/
|
|
13
|
+
const KEY = "__wuppyQuietFiles";
|
|
14
|
+
const quiet = globalThis[KEY] ??= /* @__PURE__ */ new Map();
|
|
15
|
+
/** The next change of this file (within a few seconds, so a later edit outside the editor still reloads) is quiet. */
|
|
16
|
+
const quietNextChange = (file) => quiet.set(realpathSync(file), Date.now() + 3e3);
|
|
17
|
+
function quietHmrPlugin() {
|
|
18
|
+
return {
|
|
19
|
+
name: "wuppy:quiet-hmr",
|
|
20
|
+
enforce: "pre",
|
|
21
|
+
hotUpdate: {
|
|
22
|
+
order: "pre",
|
|
23
|
+
handler({ file, modules, timestamp }) {
|
|
24
|
+
const until = quiet.get(file);
|
|
25
|
+
if (!until) return;
|
|
26
|
+
if (until < Date.now()) {
|
|
27
|
+
quiet.delete(file);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const own = modules.filter((m) => m.file === file);
|
|
31
|
+
if (!own.length) return;
|
|
32
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const m of own) this.environment.moduleGraph.invalidateModule(m, seen, timestamp, true);
|
|
34
|
+
const runner = this.environment.runner?.evaluatedModules;
|
|
35
|
+
for (const m of seen) {
|
|
36
|
+
const cached = m.id && runner?.getModuleById(m.id);
|
|
37
|
+
if (cached) runner.invalidateModule(cached);
|
|
38
|
+
}
|
|
39
|
+
return modules.filter((m) => m.file !== file);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/astro/theme-components.ts
|
|
47
|
+
/**
|
|
48
|
+
* Makes the active theme's Astro components available to MDX content without asking the theme author to pass a
|
|
49
|
+
* components map in every page template. A tiny Vite transform adds `components={__wuppyComponents}` to `<Content />`
|
|
50
|
+
* (build and live rendering use the same page) and imports the components directly, so there is no virtual module for
|
|
51
|
+
* Vite's dev module runner to resolve.
|
|
52
|
+
*
|
|
53
|
+
* The map is generated from source files, so adding a component needs a dev-server restart/rebuild; that matches the
|
|
54
|
+
* theme source workflow (Code view already asks for a rebuild after source changes).
|
|
55
|
+
*/
|
|
56
|
+
function mapSource(list) {
|
|
57
|
+
return [
|
|
58
|
+
"import { markHTMLString as __wuppyHtml } from \"astro/runtime/server/index.js\";",
|
|
59
|
+
"const __wuppySafe = (name, C) => Object.assign((result, props, slots) => {",
|
|
60
|
+
" const failed = (e) => {",
|
|
61
|
+
" console.error(`[wuppy] <${name}> could not render: ${e?.message ?? e}`);",
|
|
62
|
+
" return __wuppyHtml(`<!-- wuppy: <${name}> could not render -->`);",
|
|
63
|
+
" };",
|
|
64
|
+
" try {",
|
|
65
|
+
" const out = C(result, props, slots);",
|
|
66
|
+
" return out && typeof out.then === \"function\" ? out.catch(failed) : out;",
|
|
67
|
+
" } catch (e) {",
|
|
68
|
+
" return failed(e);",
|
|
69
|
+
" }",
|
|
70
|
+
"}, C);",
|
|
71
|
+
...list.map((component, i) => `import __wuppyC${i} from ${JSON.stringify(component.path)};`),
|
|
72
|
+
`const __wuppyComponents = { ${list.map((component, i) => `${JSON.stringify(component.name)}: __wuppySafe(${JSON.stringify(component.name)}, __wuppyC${i})`).join(", ")} };`
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wraps the page's `<Content />` in `<!--wuppy:body-->` comments, so the visual editor can load the live page and edit
|
|
77
|
+
* the entry in place inside any theme's layout. Comments do not change layout or CSS.
|
|
78
|
+
*/
|
|
79
|
+
const BODY_SOURCE = ["import { createComponent as __wuppyCreate, renderComponent as __wuppyRenderComponent, render as __wuppyRender } from \"astro/runtime/server/index.js\";", "const __wuppyBody = (C) => __wuppyCreate((result, props, slots) => __wuppyRender`<!--wuppy:body-->${__wuppyRenderComponent(result, \"Content\", C, props, slots)}<!--/wuppy:body-->`);"].join("\n");
|
|
80
|
+
function themeComponentsPlugin(root) {
|
|
81
|
+
const roots = [resolve(root)];
|
|
82
|
+
let pending;
|
|
83
|
+
const components = () => pending ??= (async () => {
|
|
84
|
+
const out = [];
|
|
85
|
+
const seen = /* @__PURE__ */ new Set();
|
|
86
|
+
for (const base of roots) for (const component of await listThemeComponents(base)) {
|
|
87
|
+
if (seen.has(component.name)) continue;
|
|
88
|
+
seen.add(component.name);
|
|
89
|
+
out.push({
|
|
90
|
+
name: component.name,
|
|
91
|
+
path: join(base, component.path)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
})();
|
|
96
|
+
return {
|
|
97
|
+
name: "wuppy:theme-components",
|
|
98
|
+
transform: {
|
|
99
|
+
order: "post",
|
|
100
|
+
async handler(code, id) {
|
|
101
|
+
if (!id.endsWith(".astro") || id.includes("/node_modules/")) return null;
|
|
102
|
+
if (code.includes("const __wuppyBody")) return null;
|
|
103
|
+
const list = await components();
|
|
104
|
+
if (code.includes(`renderComponent($$result, "Content", Content`)) {
|
|
105
|
+
const map = list.length > 0 && !/\bcomponents\s*[:,]/.test(code);
|
|
106
|
+
const patched = code.replace(/renderComponent\(\$\$result, "Content", Content, \{/g, `renderComponent($$$$result, "Content", __wuppyBody(Content), {${map ? " components: __wuppyComponents," : ""}`);
|
|
107
|
+
return {
|
|
108
|
+
code: `${BODY_SOURCE}\n${map ? `${mapSource(list)}\n` : ""}${patched}`,
|
|
109
|
+
map: null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (!list.length || code.includes("const __wuppyComponents")) return null;
|
|
113
|
+
if (!code.includes("<Content")) return null;
|
|
114
|
+
let changed = false;
|
|
115
|
+
const body = code.replace(/<Content\b([^>]*)>/g, (full, attrs) => {
|
|
116
|
+
if (/\bcomponents\s*=/.test(attrs)) return full;
|
|
117
|
+
changed = true;
|
|
118
|
+
return `<Content components={__wuppyComponents}${attrs}>`;
|
|
119
|
+
});
|
|
120
|
+
if (!changed) return null;
|
|
121
|
+
const at = /^---\r?\n/.exec(body)?.[0].length ?? 0;
|
|
122
|
+
const out = `${body.slice(0, at)}${mapSource(list)}\n${body.slice(at)}`;
|
|
123
|
+
return {
|
|
124
|
+
code: at ? out : `---\n${mapSource(list)}\n---\n${body}`,
|
|
125
|
+
map: null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/astro/theme-pages.ts
|
|
134
|
+
/** `<theme>/src/pages/docs/intro.md` -> `docs/intro`, or undefined when it is not a static Markdown or Astro page. */
|
|
135
|
+
function themePageRel(root, id) {
|
|
136
|
+
const rel = relative(join(root, "src/pages"), id).split(sep).join("/");
|
|
137
|
+
if (rel.startsWith("..") || !/\.(mdx?|astro)$/.test(rel) || rel.includes("[") || rel.split("/").some((p) => p.startsWith("_"))) return void 0;
|
|
138
|
+
return rel.replace(/\.(mdx?|astro)$/, "");
|
|
139
|
+
}
|
|
140
|
+
const BODY_ID = "virtual:wuppy-theme-body/";
|
|
141
|
+
/**
|
|
142
|
+
* A theme `.astro` page keeps its own markup; the blocks added to it in the admin (its Pages entry's body) render at the
|
|
143
|
+
* end of its `<main>`, or inside its last element when it has none, through a generated `<WuppyThemeBody />`.
|
|
144
|
+
*/
|
|
145
|
+
function withThemeBody(source, rel) {
|
|
146
|
+
const front = /^\s*---\r?\n/.exec(source);
|
|
147
|
+
const importLine = `import WuppyThemeBody from ${JSON.stringify(BODY_ID + rel)};`;
|
|
148
|
+
const head = front ? source.slice(0, front[0].length) + importLine + (themeBodyShift(source) ? "\n" : " ") : `---\n${importLine}\n---\n`;
|
|
149
|
+
const rest = front ? source.slice(front[0].length) : source;
|
|
150
|
+
const end = front ? rest.search(/^---\s*$/m) : 0;
|
|
151
|
+
const template = rest.slice(end);
|
|
152
|
+
let at = template.lastIndexOf("</main>");
|
|
153
|
+
if (at === -1) {
|
|
154
|
+
for (const m of template.matchAll(/<\/([A-Za-z][\w.:-]*)\s*>/g)) if (!/^(style|script)$/i.test(m[1])) at = m.index;
|
|
155
|
+
}
|
|
156
|
+
if (at === -1) return source;
|
|
157
|
+
return head + rest.slice(0, end) + template.slice(0, at) + "<WuppyThemeBody />" + template.slice(at);
|
|
158
|
+
}
|
|
159
|
+
/** Lines withThemeBody adds before the template: none, unless the page has no frontmatter or an empty one. */
|
|
160
|
+
function themeBodyShift(source) {
|
|
161
|
+
const front = /^\s*---\r?\n/.exec(source);
|
|
162
|
+
if (!front) return 3;
|
|
163
|
+
return /^---\s*$/m.test(source.slice(front[0].length).split("\n")[0] ?? "") ? 1 : 0;
|
|
164
|
+
}
|
|
165
|
+
/** The `<WuppyThemeBody />` module for one page: the entry's body between the visual editor's markers. */
|
|
166
|
+
const bodyModule = (rel, runtime, components, settings) => `import { createComponent, render, renderComponent, unescapeHTML } from "astro/runtime/server/index.js";
|
|
167
|
+
import { themeBody } from ${JSON.stringify(runtime)};
|
|
168
|
+
${components || "const __wuppyComponents = {};"}
|
|
169
|
+
export default createComponent(async (result) => {
|
|
170
|
+
const page = await themeBody(${JSON.stringify(rel)}, ${JSON.stringify(settings)});
|
|
171
|
+
if (!page) return render\`\`;
|
|
172
|
+
return page.Content
|
|
173
|
+
? render\`<!--wuppy:body-->\${renderComponent(result, "Content", page.Content, { components: __wuppyComponents })}<!--/wuppy:body-->\`
|
|
174
|
+
: render\`<!--wuppy:body-->\${unescapeHTML(page.html)}<!--/wuppy:body-->\`;
|
|
175
|
+
});
|
|
176
|
+
`;
|
|
177
|
+
function pageModule(o) {
|
|
178
|
+
return `import { createComponent, render, renderComponent, unescapeHTML } from "astro/runtime/server/index.js";
|
|
179
|
+
import { themePage } from ${JSON.stringify(o.runtime)};
|
|
180
|
+
${o.layout ? `import Layout from ${JSON.stringify(o.layout)};` : ""}
|
|
181
|
+
${o.components || "const __wuppyComponents = {};"}
|
|
182
|
+
const file = ${JSON.stringify(o.file)};
|
|
183
|
+
const url = ${JSON.stringify(o.url)};
|
|
184
|
+
export default createComponent(async (result) => {
|
|
185
|
+
const page = await themePage(${JSON.stringify(o.rel)}, file, ${JSON.stringify(o.settings)});
|
|
186
|
+
const content = { ...page.data, file, url };
|
|
187
|
+
const body = () => page.Content
|
|
188
|
+
? render\`<!--wuppy:body-->\${renderComponent(result, "Content", page.Content, { components: __wuppyComponents })}<!--/wuppy:body-->\`
|
|
189
|
+
: render\`<!--wuppy:body-->\${unescapeHTML(page.html)}<!--/wuppy:body-->\`;
|
|
190
|
+
return ${o.layout ? `render\`\${renderComponent(result, "Layout", Layout, { file, url, content, frontmatter: content, headings: page.headings, "server:root": true }, { default: body })}\`` : "body()"};
|
|
191
|
+
});
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
function themePagesPlugin(o) {
|
|
195
|
+
return {
|
|
196
|
+
name: "wuppy:theme-pages",
|
|
197
|
+
enforce: "pre",
|
|
198
|
+
resolveId: (id) => id.startsWith(BODY_ID) ? `\0${id}` : null,
|
|
199
|
+
transform: {
|
|
200
|
+
order: "pre",
|
|
201
|
+
handler(code, id) {
|
|
202
|
+
if (id.includes("?") || !id.endsWith(".astro")) return null;
|
|
203
|
+
const rel = themePageRel(o.root, id);
|
|
204
|
+
return rel ? {
|
|
205
|
+
code: withThemeBody(code, rel),
|
|
206
|
+
map: null
|
|
207
|
+
} : null;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
load: {
|
|
211
|
+
order: "pre",
|
|
212
|
+
async handler(id) {
|
|
213
|
+
const map = async () => {
|
|
214
|
+
const all = await listThemeComponents(o.root);
|
|
215
|
+
return all.length ? mapSource(all.map((c) => ({
|
|
216
|
+
...c,
|
|
217
|
+
path: join(o.root, c.path)
|
|
218
|
+
}))) : "";
|
|
219
|
+
};
|
|
220
|
+
if (id.startsWith(`\0${BODY_ID}`)) return {
|
|
221
|
+
code: bodyModule(id.slice(26), o.runtime, await map(), o.markdown()),
|
|
222
|
+
map: null
|
|
223
|
+
};
|
|
224
|
+
const file = id.split("?")[0];
|
|
225
|
+
const rel = themePageRel(o.root, file);
|
|
226
|
+
if (!rel || id.includes("?") || file.endsWith(".astro")) return null;
|
|
227
|
+
const { layout } = parseDocument(await readFile(file, "utf8")).data;
|
|
228
|
+
return {
|
|
229
|
+
code: pageModule({
|
|
230
|
+
rel,
|
|
231
|
+
file,
|
|
232
|
+
runtime: o.runtime,
|
|
233
|
+
layout: typeof layout === "string" ? layout : void 0,
|
|
234
|
+
url: `/${rel.replace(/(^|\/)index$/, "")}`,
|
|
235
|
+
components: await map(),
|
|
236
|
+
settings: o.markdown()
|
|
237
|
+
}),
|
|
238
|
+
map: null
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
export { themeComponentsPlugin as a, mapSource as i, themePageRel as n, quietHmrPlugin as o, themePagesPlugin as r, quietNextChange as s, themeBodyShift as t };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import "./config-CM8_-x_W.mjs";
|
|
2
|
+
import { t as parseDocument } from "./frontmatter-DGaz9--t.mjs";
|
|
3
|
+
import { a as pagesDir, c as templateUrl, i as loadSiteConfig } from "./site-dF-xVPqe.mjs";
|
|
4
|
+
import { esc } from "./seo.mjs";
|
|
5
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
//#region src/astro/theme-seo.ts
|
|
9
|
+
/** Puts a Pages entry's SEO fields into the `<head>` a theme page rendered. */
|
|
10
|
+
function withSeo(html, data) {
|
|
11
|
+
const str = (k) => typeof data[k] === "string" && data[k] ? data[k] : void 0;
|
|
12
|
+
const title = str("seoTitle");
|
|
13
|
+
const description = str("seoDescription");
|
|
14
|
+
let out = html;
|
|
15
|
+
if (title) out = out.replace(/<title>[\s\S]*?<\/title>/i, `<title>${esc(title)}</title>`);
|
|
16
|
+
const tags = [];
|
|
17
|
+
if (description) {
|
|
18
|
+
const meta = /<meta\s+name=["']description["'][^>]*>/i;
|
|
19
|
+
const tag = `<meta name="description" content="${esc(description)}">`;
|
|
20
|
+
if (meta.test(out)) out = out.replace(meta, tag);
|
|
21
|
+
else tags.push(tag);
|
|
22
|
+
}
|
|
23
|
+
if (data.noindex === true) tags.push("<meta name=\"robots\" content=\"noindex\">");
|
|
24
|
+
const at = out.search(/<\/head>/i);
|
|
25
|
+
return tags.length && at !== -1 ? out.slice(0, at) + tags.join("") + out.slice(at) : out;
|
|
26
|
+
}
|
|
27
|
+
/** The site's Pages entries that stand for a theme `.astro` page, by the URL that page serves. */
|
|
28
|
+
async function templatePages(site, dir) {
|
|
29
|
+
const map = /* @__PURE__ */ new Map();
|
|
30
|
+
if (!dir) return map;
|
|
31
|
+
const names = await readdir(join(site, dir), { recursive: true }).catch(() => []);
|
|
32
|
+
for (const name of names.filter((n) => /\.mdx?$/.test(n))) {
|
|
33
|
+
const { data } = parseDocument(await readFile(join(site, dir, name), "utf8"));
|
|
34
|
+
if (typeof data.template === "string") map.set(templateUrl(data.template), data);
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
/** A theme run for a site: the theme's `.astro` pages get the SEO title, description and noindex set in the admin. */
|
|
39
|
+
function createThemeSeoMiddleware(site, theme) {
|
|
40
|
+
let dir;
|
|
41
|
+
return async (ctx, next) => {
|
|
42
|
+
if (ctx.request.method !== "GET") return next();
|
|
43
|
+
const res = await next();
|
|
44
|
+
if (res.status !== 200 || !(res.headers.get("content-type") ?? "").includes("text/html")) return res;
|
|
45
|
+
const path = ctx.url.pathname.replace(/\/$/, "") || "/";
|
|
46
|
+
const data = (await templatePages(site, await (dir ??= loadSiteConfig(site, theme).then((c) => pagesDir(c.collections))))).get(path);
|
|
47
|
+
if (!data) return res;
|
|
48
|
+
const headers = new Headers(res.headers);
|
|
49
|
+
headers.delete("content-length");
|
|
50
|
+
return new Response(withSeo(await res.text(), data), {
|
|
51
|
+
status: res.status,
|
|
52
|
+
headers
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/astro/theme-seo-middleware.ts
|
|
59
|
+
const onRequest = createThemeSeoMiddleware(process.env.WUPPY_BASE_ROOT ?? process.cwd(), process.cwd());
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { onRequest };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import "./config-CM8_-x_W.mjs";
|
|
2
|
+
import "./frontmatter-DGaz9--t.mjs";
|
|
3
|
+
import { s as siteRoot } from "./site-dF-xVPqe.mjs";
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { fontLinks, pageCss, visualCss, visualDataSchema } from "@wuppycms/visual-editor";
|
|
8
|
+
|
|
9
|
+
//#region src/astro/visual.ts
|
|
10
|
+
const SKIP_PATH = /^\/(admin|api|uploads|_astro)(\/|$)/;
|
|
11
|
+
let loaded;
|
|
12
|
+
const defaults = visualDataSchema.parse({});
|
|
13
|
+
/** The visual editor's site data, re-read when the file changes. Null before the editor ever saved any. */
|
|
14
|
+
async function visualData(root) {
|
|
15
|
+
const path = join(root, ".wuppy", "data", "visual.json");
|
|
16
|
+
const info = await stat(path).catch(() => null);
|
|
17
|
+
if (!info) return null;
|
|
18
|
+
if (loaded?.mtime === info.mtimeMs) return loaded;
|
|
19
|
+
const raw = await readFile(path, "utf8").then(JSON.parse).catch(() => null);
|
|
20
|
+
const parsed = visualDataSchema.safeParse(raw);
|
|
21
|
+
if (!parsed.success) return null;
|
|
22
|
+
const css = visualCss(parsed.data);
|
|
23
|
+
loaded = {
|
|
24
|
+
mtime: info.mtimeMs,
|
|
25
|
+
data: parsed.data,
|
|
26
|
+
version: css ? createHash("sha1").update(css).digest("hex").slice(0, 10) : ""
|
|
27
|
+
};
|
|
28
|
+
return loaded;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Adds the visual editor's CSS to every HTML page: a link to the site-wide sheet (framework, selectors, user CSS),
|
|
32
|
+
* the font stylesheets, and a `<style>` with the responsive/state styles of the visual nodes on this page.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Astro's dev server marks each element with the `.astro` file and spot it comes from, and its dev toolbar removes the
|
|
36
|
+
* marks once the page loads. The visual editor edits theme markup by them, so it gets a copy the toolbar leaves alone.
|
|
37
|
+
*/
|
|
38
|
+
const withSourceCopy = (html) => html.includes("data-astro-source-file") ? html.replace(/ data-astro-source-(file|loc)="([^"]*)"/g, " data-astro-source-$1=\"$2\" data-wuppy-$1=\"$2\"") : html;
|
|
39
|
+
function createVisualMiddleware(root) {
|
|
40
|
+
return async (ctx, next) => {
|
|
41
|
+
if (ctx.request.method !== "GET" || SKIP_PATH.test(ctx.url.pathname)) return next();
|
|
42
|
+
const res = await next();
|
|
43
|
+
if (res.status !== 200 || !(res.headers.get("content-type") ?? "").includes("text/html")) return res;
|
|
44
|
+
const html = withSourceCopy(await res.text());
|
|
45
|
+
const visual = await visualData(root);
|
|
46
|
+
const tags = [];
|
|
47
|
+
if (visual?.version) tags.push(`<link rel="stylesheet" href="/api/public/visual.css?v=${visual.version}">`);
|
|
48
|
+
for (const href of visual ? fontLinks(visual.data.framework) : []) tags.push(`<link rel="stylesheet" href="${href.replace(/&/g, "&")}">`);
|
|
49
|
+
const page = pageCss(html, visual?.data ?? defaults);
|
|
50
|
+
if (page) tags.push(`<style data-wuppy-visual>${page}</style>`);
|
|
51
|
+
const headers = new Headers(res.headers);
|
|
52
|
+
headers.delete("content-length");
|
|
53
|
+
if (!tags.length) return new Response(html, {
|
|
54
|
+
status: res.status,
|
|
55
|
+
headers
|
|
56
|
+
});
|
|
57
|
+
const at = html.search(/<\/head>/i);
|
|
58
|
+
const out = at === -1 ? tags.join("") + html : html.slice(0, at) + tags.join("") + html.slice(at);
|
|
59
|
+
return new Response(out, {
|
|
60
|
+
status: res.status,
|
|
61
|
+
headers
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/astro/visual-middleware.ts
|
|
68
|
+
const onRequest = createVisualMiddleware(siteRoot());
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { onRequest };
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wuppycms",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"import": "./dist/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"./astro": {
|
|
11
|
+
"types": "./dist/astro.d.mts",
|
|
12
|
+
"import": "./dist/astro.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./seo": {
|
|
15
|
+
"types": "./dist/seo.d.mts",
|
|
16
|
+
"import": "./dist/seo.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./publishing": {
|
|
19
|
+
"types": "./dist/publishing.d.mts",
|
|
20
|
+
"import": "./dist/publishing.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./site": {
|
|
23
|
+
"types": "./dist/site.d.mts",
|
|
24
|
+
"import": "./dist/site.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./markdown": {
|
|
27
|
+
"types": "./dist/markdown.d.mts",
|
|
28
|
+
"import": "./dist/markdown.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./mdx": {
|
|
31
|
+
"types": "./dist/mdx.d.mts",
|
|
32
|
+
"import": "./dist/mdx.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@astrojs/compiler-rs": "^0.5.2",
|
|
37
|
+
"hono": "4.13.13",
|
|
38
|
+
"nodemailer": "^10.1.7",
|
|
39
|
+
"quickjs-emscripten": "0.32.0",
|
|
40
|
+
"resend": "^6.34.1",
|
|
41
|
+
"yaml": "^2.8.0",
|
|
42
|
+
"zod": "4.5.4",
|
|
43
|
+
"@wuppycms/admin": "0.2.0",
|
|
44
|
+
"@wuppycms/auth": "0.2.0",
|
|
45
|
+
"@wuppycms/git": "0.2.0",
|
|
46
|
+
"@wuppycms/plugin-types": "0.2.0",
|
|
47
|
+
"@wuppycms/visual-editor": "0.2.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.16.0",
|
|
51
|
+
"@types/nodemailer": "^8.0.2",
|
|
52
|
+
"astro": "^7.3.2",
|
|
53
|
+
"tsdown": "^0.20.3",
|
|
54
|
+
"typescript": "^6.0.3",
|
|
55
|
+
"vitest": "4.1.12"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"astro": "^7.3.2"
|
|
59
|
+
},
|
|
60
|
+
"bin": {
|
|
61
|
+
"wuppycms": "./dist/cli.mjs"
|
|
62
|
+
},
|
|
63
|
+
"files": [
|
|
64
|
+
"dist"
|
|
65
|
+
],
|
|
66
|
+
"optionalDependencies": {
|
|
67
|
+
"sharp": "0.35.5"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
72
|
+
"build": "tsdown",
|
|
73
|
+
"smoke:dist": "node scripts/smoke-dist.mjs"
|
|
74
|
+
}
|
|
6
75
|
}
|
package/README.md
DELETED