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,59 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, extname, join, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/astro/admin.ts
|
|
7
|
+
const TYPES = {
|
|
8
|
+
".html": "text/html; charset=utf-8",
|
|
9
|
+
".js": "text/javascript; charset=utf-8",
|
|
10
|
+
".css": "text/css; charset=utf-8",
|
|
11
|
+
".map": "application/json",
|
|
12
|
+
".svg": "image/svg+xml",
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".ico": "image/x-icon",
|
|
15
|
+
".woff2": "font/woff2"
|
|
16
|
+
};
|
|
17
|
+
const CSP = "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'";
|
|
18
|
+
const notFound = () => new Response("Not found", { status: 404 });
|
|
19
|
+
/** Serves the built admin SPA from `dist`; extensionless paths fall back to index.html (client-side routing). */
|
|
20
|
+
async function serveAdmin(dist, rel) {
|
|
21
|
+
let path;
|
|
22
|
+
try {
|
|
23
|
+
path = decodeURIComponent(rel);
|
|
24
|
+
} catch {
|
|
25
|
+
return notFound();
|
|
26
|
+
}
|
|
27
|
+
const root = resolve(dist);
|
|
28
|
+
const ext = extname(path);
|
|
29
|
+
const target = resolve(root, ext ? path : "index.html");
|
|
30
|
+
if (!target.startsWith(root + sep)) return notFound();
|
|
31
|
+
try {
|
|
32
|
+
const body = await readFile(target);
|
|
33
|
+
const html = ext === "" || ext === ".html";
|
|
34
|
+
return new Response(body, { headers: {
|
|
35
|
+
"content-type": TYPES[html ? ".html" : ext] ?? "application/octet-stream",
|
|
36
|
+
"x-content-type-options": "nosniff",
|
|
37
|
+
"cache-control": html ? "no-cache" : "public, max-age=31536000, immutable",
|
|
38
|
+
...html ? { "content-security-policy": CSP } : {}
|
|
39
|
+
} });
|
|
40
|
+
} catch {
|
|
41
|
+
return notFound();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const findAdminDist = () => {
|
|
45
|
+
const req = createRequire(import.meta.url);
|
|
46
|
+
const candidates = [];
|
|
47
|
+
try {
|
|
48
|
+
const dir = dirname(req.resolve("wuppycms/package.json"));
|
|
49
|
+
candidates.push(join(dir, "node_modules", "@wuppycms", "admin", "dist"), join(dir, "..", "admin", "dist"), join(dir, "..", "..", "packages", "admin", "dist"));
|
|
50
|
+
} catch {}
|
|
51
|
+
candidates.push(join(process.cwd(), "node_modules", "@wuppycms", "admin", "dist"), join(process.cwd(), "packages", "admin", "dist"));
|
|
52
|
+
const found = candidates.find((dir) => existsSync(join(dir, "index.html")));
|
|
53
|
+
if (!found) throw new Error("Cannot locate @wuppycms/admin/dist");
|
|
54
|
+
return found;
|
|
55
|
+
};
|
|
56
|
+
const adminDist = (_resolveFrom) => findAdminDist();
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { serveAdmin as n, adminDist as t };
|
package/dist/astro.d.mts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AstroIntegration } from "astro";
|
|
2
|
+
|
|
3
|
+
//#region src/astro/integration.d.ts
|
|
4
|
+
/** Turn off a route the site already serves itself (e.g. `sitemap: false` next to @astrojs/sitemap). */
|
|
5
|
+
interface WuppyIntegrationOptions {
|
|
6
|
+
sitemap?: boolean;
|
|
7
|
+
robots?: boolean;
|
|
8
|
+
rss?: boolean;
|
|
9
|
+
/** Read Wuppy collections from disk on each request so edits show without a rebuild (default true). */
|
|
10
|
+
live?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare function wuppycms(options?: WuppyIntegrationOptions): AstroIntegration;
|
|
13
|
+
/**
|
|
14
|
+
* A page that renders one of the project's Astro components by name, without props, for the visual editor's canvas.
|
|
15
|
+
* It is generated (one static import per component) so the project's own Astro compiles it, styles included. It only
|
|
16
|
+
* renders design code the site already ships, and takes no input but a name from that list, so it needs no sign-in.
|
|
17
|
+
*/
|
|
18
|
+
declare function componentPreview(root: string): Promise<string | undefined>;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { WuppyIntegrationOptions, componentPreview, wuppycms as default };
|
package/dist/astro.mjs
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import "./frontmatter-DGaz9--t.mjs";
|
|
2
|
+
import { a as themeComponentsPlugin, i as mapSource, n as themePageRel, o as quietHmrPlugin, r as themePagesPlugin } from "./theme-pages-CRsBm1EX.mjs";
|
|
3
|
+
import { t as listThemeComponents } from "./components-BrGAEPly.mjs";
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
//#region src/astro/live-content.ts
|
|
10
|
+
/**
|
|
11
|
+
* Build-time half of live content (see live-runtime.ts). A Vite plugin swaps `astro:content` for a module that reads
|
|
12
|
+
* Wuppy collections from disk on each request, and pages that read collections are rendered on request instead of
|
|
13
|
+
* prerendered, so edits made in the admin show at once on the public site.
|
|
14
|
+
*/
|
|
15
|
+
const SHIM = "\0wuppy:live-content";
|
|
16
|
+
const CONTENT_CONFIGS = [
|
|
17
|
+
"src/content.config.ts",
|
|
18
|
+
"src/content.config.mts",
|
|
19
|
+
"src/content.config.js",
|
|
20
|
+
"src/content.config.mjs",
|
|
21
|
+
"src/content/config.ts",
|
|
22
|
+
"src/content/config.js",
|
|
23
|
+
"src/content/config.mjs"
|
|
24
|
+
];
|
|
25
|
+
const posix = (p) => p.split(sep).join("/");
|
|
26
|
+
const isConfig = (importer) => /[\\/](content\.config|content[\\/]config|live\.config)\.[mc]?[jt]s$/.test(importer);
|
|
27
|
+
/** Whether a page route should render on request: it reads content collections and did not opt into prerendering. */
|
|
28
|
+
function isLiveRoute(root, component) {
|
|
29
|
+
if (!component.startsWith("src/pages/") || !/\.(astro|ts|js|mjs)$/.test(component)) return false;
|
|
30
|
+
let source;
|
|
31
|
+
try {
|
|
32
|
+
source = readFileSync(join(root, component), "utf8");
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (!source.includes("astro:content") || /export\s+const\s+prerender\s*=\s*true/.test(source)) return false;
|
|
37
|
+
return component.endsWith(".astro") || !source.includes("getStaticPaths");
|
|
38
|
+
}
|
|
39
|
+
/** `src/pages/posts/[slug].astro` -> `/posts/[slug]`. */
|
|
40
|
+
const routePattern = (component) => `/${component.replace(/^src\/pages\//, "").replace(/\.[^.]+$/, "").replace(/(^|\/)index$/, "")}`;
|
|
41
|
+
/** Wraps the compiled page's default export so it gets its props from getStaticPaths() at request time. */
|
|
42
|
+
function wrapPage(code, runtime, pattern) {
|
|
43
|
+
if (!/\bgetStaticPaths\b/.test(code)) return null;
|
|
44
|
+
let name;
|
|
45
|
+
const out = code.replace(/export\s+default\s+(\$\$\w+)\s*;?/, (_, n) => (name = n, "")).replace(/(\$\$\w+)\s+as\s+default\b/, (_, n) => (name = n, `${n} as __wuppyPage`));
|
|
46
|
+
if (!name) return null;
|
|
47
|
+
return `import { livePage as __wuppyLivePage } from ${JSON.stringify(runtime)};\n${out}\nexport default __wuppyLivePage(${name}, getStaticPaths, ${JSON.stringify(pattern)});\n`;
|
|
48
|
+
}
|
|
49
|
+
function liveContentPlugin(o) {
|
|
50
|
+
const contentConfig = CONTENT_CONFIGS.map((f) => join(o.root, f)).find((f) => existsSync(f));
|
|
51
|
+
return {
|
|
52
|
+
name: "wuppy:live-content",
|
|
53
|
+
enforce: "pre",
|
|
54
|
+
resolveId: {
|
|
55
|
+
order: "pre",
|
|
56
|
+
handler(id, importer) {
|
|
57
|
+
if (id !== "astro:content" || !importer || importer === SHIM || isConfig(importer)) return null;
|
|
58
|
+
if (this.environment?.config?.consumer === "client") return null;
|
|
59
|
+
return SHIM;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async load(id) {
|
|
63
|
+
if (id !== SHIM) return null;
|
|
64
|
+
const astro = realpathSync(join(o.root, "node_modules/astro/package.json"));
|
|
65
|
+
const from = async (spec, importer = astro) => {
|
|
66
|
+
const r = await this.resolve(spec, importer, { skipSelf: true });
|
|
67
|
+
if (!r) throw new Error(`[wuppy] live content: cannot resolve ${spec}`);
|
|
68
|
+
return JSON.stringify(r.id);
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
code: `import * as real from "astro:content";
|
|
72
|
+
${contentConfig ? `import * as site from ${JSON.stringify(contentConfig)};` : "const site = { collections: {} };"}
|
|
73
|
+
import { z } from ${await from("astro/zod", join(o.root, "package.json"))};
|
|
74
|
+
import { parseFrontmatter } from ${await from("@astrojs/internal-helpers/frontmatter")};
|
|
75
|
+
import xxhash from ${await from("xxhash-wasm")};
|
|
76
|
+
import { pathToFileURL } from "node:url";
|
|
77
|
+
import { createRequire } from "node:module";
|
|
78
|
+
import { join } from "node:path";
|
|
79
|
+
import { createLive, markdownRenderer, mdxContent } from ${JSON.stringify(o.runtime)};
|
|
80
|
+
export * from "astro:content";
|
|
81
|
+
|
|
82
|
+
const settings = ${JSON.stringify(o.markdown())};
|
|
83
|
+
const hasher = xxhash();
|
|
84
|
+
const schemas = new Map();
|
|
85
|
+
const live = createLive({
|
|
86
|
+
root: process.cwd(),
|
|
87
|
+
built: (name) => real.getCollection(name),
|
|
88
|
+
schema(name) {
|
|
89
|
+
if (!schemas.has(name)) {
|
|
90
|
+
let schema = site.collections?.[name]?.schema;
|
|
91
|
+
// shortcut: image() fields stay plain strings for edited entries; unchanged ones keep Astro's optimised image.
|
|
92
|
+
if (typeof schema === "function") schema = schema({ image: () => z.string() });
|
|
93
|
+
schemas.set(name, schema);
|
|
94
|
+
}
|
|
95
|
+
return schemas.get(name);
|
|
96
|
+
},
|
|
97
|
+
parseFrontmatter: (source) => parseFrontmatter(source),
|
|
98
|
+
digest: async (source) => (await hasher).h64ToString(source),
|
|
99
|
+
async renderMarkdown(body, data, filePath) {
|
|
100
|
+
const renderer = await markdownRenderer(process.cwd(), settings);
|
|
101
|
+
const result = await renderer.render(body, { frontmatter: data, fileURL: pathToFileURL(join(process.cwd(), filePath)) });
|
|
102
|
+
return { html: result.code, metadata: { ...result.metadata, imagePaths: [] } };
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export async function getCollection(name, filter) {
|
|
107
|
+
return (await live.isLive(name)) ? live.getCollection(name, filter) : real.getCollection(name, filter);
|
|
108
|
+
}
|
|
109
|
+
export async function getEntry(collectionOrRef, id) {
|
|
110
|
+
const name = typeof collectionOrRef === "string" ? collectionOrRef : collectionOrRef?.collection;
|
|
111
|
+
if (!(await live.isLive(name))) return real.getEntry(collectionOrRef, id);
|
|
112
|
+
return live.getEntry(name, typeof collectionOrRef === "string" ? id : (collectionOrRef.id ?? collectionOrRef.slug));
|
|
113
|
+
}
|
|
114
|
+
// An MDX entry changed since the build has no compiled module: compile it now, with the site's Markdown settings.
|
|
115
|
+
export async function render(entry, ...rest) {
|
|
116
|
+
if (!live.rendersAtRuntime(entry)) return real.render(entry, ...rest);
|
|
117
|
+
try {
|
|
118
|
+
const { Content, headings } = await mdxContent(entry.body ?? "", { markdown: settings });
|
|
119
|
+
return { Content, headings, remarkPluginFrontmatter: {} };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// An entry this theme cannot render (a component it lacks, an import) must not take every page listing it down.
|
|
122
|
+
if (!String(e?.message).startsWith("[wuppy] MDX not allowed")) throw e;
|
|
123
|
+
console.warn(\`\${e.message} in \${entry.filePath}; its body is left out\`);
|
|
124
|
+
const { Content } = await mdxContent("", { markdown: settings });
|
|
125
|
+
return { Content, headings: [], remarkPluginFrontmatter: {} };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function getEntries(refs) {
|
|
129
|
+
return Promise.all(refs.map((ref) => getEntry(ref)));
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
meta: { astro: {
|
|
133
|
+
hydratedComponents: [],
|
|
134
|
+
clientOnlyComponents: [],
|
|
135
|
+
serverComponents: [],
|
|
136
|
+
scripts: [],
|
|
137
|
+
propagation: "in-tree",
|
|
138
|
+
containsHead: false,
|
|
139
|
+
pageOptions: {}
|
|
140
|
+
} }
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
transform: {
|
|
144
|
+
order: "post",
|
|
145
|
+
handler(code, id) {
|
|
146
|
+
if (!id.endsWith(".astro")) return null;
|
|
147
|
+
const component = posix(relative(o.root, id));
|
|
148
|
+
if (!o.liveRoutes.has(component)) return null;
|
|
149
|
+
const out = wrapPage(code, o.runtime, routePattern(component));
|
|
150
|
+
return out === null ? null : {
|
|
151
|
+
code: out,
|
|
152
|
+
map: null
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/astro/integration.ts
|
|
161
|
+
const ext = import.meta.url.endsWith(".ts") ? "ts" : "mjs";
|
|
162
|
+
const CONFIG_FILES = [
|
|
163
|
+
"wuppy.config.ts",
|
|
164
|
+
"wuppy.config.mjs",
|
|
165
|
+
"wuppy.config.js"
|
|
166
|
+
];
|
|
167
|
+
const entry = (path) => new URL(`./${path}.${ext}`, import.meta.url);
|
|
168
|
+
function wuppycms(options = {}) {
|
|
169
|
+
let root = process.cwd();
|
|
170
|
+
let markdown;
|
|
171
|
+
let image;
|
|
172
|
+
const liveRoutes = /* @__PURE__ */ new Set();
|
|
173
|
+
let live = false;
|
|
174
|
+
let site;
|
|
175
|
+
const settings = () => JSON.parse(JSON.stringify({
|
|
176
|
+
features: (markdown?.processor?.options)?.features ?? {},
|
|
177
|
+
shared: {
|
|
178
|
+
image,
|
|
179
|
+
syntaxHighlight: markdown?.syntaxHighlight,
|
|
180
|
+
shikiConfig: markdown?.shikiConfig,
|
|
181
|
+
gfm: markdown?.gfm,
|
|
182
|
+
smartypants: markdown?.smartypants
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
return {
|
|
186
|
+
name: "wuppycms",
|
|
187
|
+
hooks: {
|
|
188
|
+
"astro:config:setup": async ({ config, injectRoute, addMiddleware, updateConfig }) => {
|
|
189
|
+
root = fileURLToPath(config.root);
|
|
190
|
+
live = options.live !== false && CONFIG_FILES.some((f) => existsSync(join(root, f)));
|
|
191
|
+
const runtime = fileURLToPath(entry("live-runtime"));
|
|
192
|
+
site = process.env.WUPPY_BASE_ROOT && resolve(process.env.WUPPY_BASE_ROOT) !== resolve(root) ? process.env.WUPPY_BASE_ROOT : void 0;
|
|
193
|
+
if (live) updateConfig({ vite: { plugins: [liveContentPlugin({
|
|
194
|
+
root,
|
|
195
|
+
runtime,
|
|
196
|
+
liveRoutes,
|
|
197
|
+
markdown: settings
|
|
198
|
+
}), ...site ? [themePagesPlugin({
|
|
199
|
+
root,
|
|
200
|
+
runtime,
|
|
201
|
+
markdown: settings
|
|
202
|
+
})] : []] } });
|
|
203
|
+
if (site) updateConfig({ devToolbar: { enabled: true } });
|
|
204
|
+
updateConfig({ vite: { plugins: [themeComponentsPlugin(root), quietHmrPlugin()] } });
|
|
205
|
+
updateConfig({ vite: { server: { watch: { ignored: [`${join(root, ".wuppy")}/**`] } } } });
|
|
206
|
+
if (ext === "ts") updateConfig({ vite: { ssr: { noExternal: [/^@wuppycms\//] } } });
|
|
207
|
+
addMiddleware({
|
|
208
|
+
entrypoint: entry("redirect-middleware"),
|
|
209
|
+
order: "pre"
|
|
210
|
+
});
|
|
211
|
+
addMiddleware({
|
|
212
|
+
entrypoint: entry("page-cache-middleware"),
|
|
213
|
+
order: "post"
|
|
214
|
+
});
|
|
215
|
+
addMiddleware({
|
|
216
|
+
entrypoint: entry("visual-middleware"),
|
|
217
|
+
order: "post"
|
|
218
|
+
});
|
|
219
|
+
if (site) addMiddleware({
|
|
220
|
+
entrypoint: entry("theme-seo-middleware"),
|
|
221
|
+
order: "post"
|
|
222
|
+
});
|
|
223
|
+
if (options.sitemap !== false) injectRoute({
|
|
224
|
+
pattern: "/sitemap.xml",
|
|
225
|
+
entrypoint: entry("routes/sitemap")
|
|
226
|
+
});
|
|
227
|
+
if (options.robots !== false) injectRoute({
|
|
228
|
+
pattern: "/robots.txt",
|
|
229
|
+
entrypoint: entry("routes/robots")
|
|
230
|
+
});
|
|
231
|
+
if (options.rss !== false) injectRoute({
|
|
232
|
+
pattern: "/rss.xml",
|
|
233
|
+
entrypoint: entry("routes/rss")
|
|
234
|
+
});
|
|
235
|
+
injectRoute({
|
|
236
|
+
pattern: "/api/[...path]",
|
|
237
|
+
entrypoint: entry("routes/api")
|
|
238
|
+
});
|
|
239
|
+
injectRoute({
|
|
240
|
+
pattern: "/uploads/[...path]",
|
|
241
|
+
entrypoint: entry("routes/uploads")
|
|
242
|
+
});
|
|
243
|
+
injectRoute({
|
|
244
|
+
pattern: "/admin/[...path]",
|
|
245
|
+
entrypoint: entry("routes/admin")
|
|
246
|
+
});
|
|
247
|
+
injectRoute({
|
|
248
|
+
pattern: "/icons.svg",
|
|
249
|
+
entrypoint: entry("routes/icons")
|
|
250
|
+
});
|
|
251
|
+
const preview = await componentPreview(root);
|
|
252
|
+
if (preview) injectRoute({
|
|
253
|
+
pattern: "/_wuppy/component/[name]",
|
|
254
|
+
entrypoint: preview
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
"astro:route:setup": ({ route }) => {
|
|
258
|
+
if (!live) return;
|
|
259
|
+
if (site && themePageRel(root, join(root, route.component))) route.prerender = false;
|
|
260
|
+
if (!isLiveRoute(root, route.component)) return;
|
|
261
|
+
route.prerender = false;
|
|
262
|
+
liveRoutes.add(route.component);
|
|
263
|
+
},
|
|
264
|
+
"astro:config:done": ({ config, logger }) => {
|
|
265
|
+
markdown = config.markdown;
|
|
266
|
+
image = config.image;
|
|
267
|
+
if (live && config.markdown.processor?.name !== "satteri") logger.warn(`Live content renders edited Markdown with Astro's default processor, not "${config.markdown.processor?.name}"; plugins from it apply after the next build.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A page that renders one of the project's Astro components by name, without props, for the visual editor's canvas.
|
|
274
|
+
* It is generated (one static import per component) so the project's own Astro compiles it, styles included. It only
|
|
275
|
+
* renders design code the site already ships, and takes no input but a name from that list, so it needs no sign-in.
|
|
276
|
+
*/
|
|
277
|
+
async function componentPreview(root) {
|
|
278
|
+
const list = await listThemeComponents(root);
|
|
279
|
+
if (!list.length) return void 0;
|
|
280
|
+
const file = join(root, "node_modules", ".wuppy", "component-preview.astro");
|
|
281
|
+
await mkdir(join(file, ".."), { recursive: true });
|
|
282
|
+
await writeFile(file, [
|
|
283
|
+
"---",
|
|
284
|
+
mapSource(list.map((c) => ({
|
|
285
|
+
...c,
|
|
286
|
+
path: join(root, c.path)
|
|
287
|
+
}))),
|
|
288
|
+
"export const prerender = false;",
|
|
289
|
+
"const map = __wuppyComponents;",
|
|
290
|
+
"const name = Astro.params.name ?? \"\";",
|
|
291
|
+
"const Component = Object.hasOwn(map, name) ? map[name] : undefined;",
|
|
292
|
+
"if (!Component) return new Response(null, { status: 404 });",
|
|
293
|
+
"Astro.response.headers.set(\"x-robots-tag\", \"noindex\");",
|
|
294
|
+
"---",
|
|
295
|
+
"<Component />",
|
|
296
|
+
""
|
|
297
|
+
].join("\n"));
|
|
298
|
+
return file;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
//#endregion
|
|
302
|
+
export { componentPreview, wuppycms as default };
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { b as findDisallowed, i as capabilityHash$1, n as loadConfig } from "./config-CM8_-x_W.mjs";
|
|
3
|
+
import "./frontmatter-DGaz9--t.mjs";
|
|
4
|
+
import { o as importRepo, s as init, t as generateTypes } from "./generate-iX4B_2U2.mjs";
|
|
5
|
+
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { isFilter, manifestSchema } from "@wuppycms/plugin-types";
|
|
10
|
+
|
|
11
|
+
//#region src/cli/mdx-to-md.ts
|
|
12
|
+
/** Code (fenced and inline) is copied as is, so it is left out of the checks. */
|
|
13
|
+
const prose = (body) => body.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, "").replace(/`[^`\n]*`/g, "");
|
|
14
|
+
/**
|
|
15
|
+
* One `.mdx` body as Markdown: drops `import`/`export` lines, turns `{/* comments *\/}` into HTML comments and
|
|
16
|
+
* `className` into `class`. Returns the reasons it cannot be converted (components, expressions) instead.
|
|
17
|
+
*/
|
|
18
|
+
async function mdxToMarkdown(source) {
|
|
19
|
+
const front = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(source)?.[0] ?? "";
|
|
20
|
+
const raw = source.slice(front.length).replace(/^(import|export)\s[^\n]*\n?/gm, "");
|
|
21
|
+
const body = raw.replace(/\{\/\*([\s\S]*?)\*\/\}/g, "<!--$1-->").replace(/(<[a-z][\w-]*\b[^>]*?\s)className=/g, "$1class=").replace(/^\n+/, "");
|
|
22
|
+
const problems = [];
|
|
23
|
+
if (/\{[^}\n]*\}/.test(prose(body).replace(/<!--[\s\S]*?-->/g, ""))) problems.push("JSX expression {…}");
|
|
24
|
+
problems.push(...await findDisallowed(raw, [], "mdx"));
|
|
25
|
+
return problems.length ? { problems } : { markdown: front + body };
|
|
26
|
+
}
|
|
27
|
+
/** `wuppycms mdx-to-md [dir]`: converts the `.mdx` entries of every collection (or one folder) to `.md`, once. */
|
|
28
|
+
async function convertMdx(cwd, dir) {
|
|
29
|
+
let dirs;
|
|
30
|
+
try {
|
|
31
|
+
dirs = dir ? [dir] : (await loadConfig(cwd)).collections.map((c) => c.dir);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return {
|
|
34
|
+
code: 1,
|
|
35
|
+
stdout: "",
|
|
36
|
+
stderr: e.message
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const lines = [];
|
|
40
|
+
let kept = 0;
|
|
41
|
+
for (const d of dirs) {
|
|
42
|
+
const names = await readdir(join(cwd, d), { recursive: true }).catch(() => []);
|
|
43
|
+
for (const name of names.filter((n) => n.endsWith(".mdx"))) {
|
|
44
|
+
const file = join(cwd, d, name);
|
|
45
|
+
const target = file.replace(/\.mdx$/, ".md");
|
|
46
|
+
const rel = relative(cwd, file);
|
|
47
|
+
const out = await access(target).then(() => true, () => false) ? { problems: [`${relative(cwd, target)} already exists`] } : await mdxToMarkdown(await readFile(file, "utf8"));
|
|
48
|
+
if ("problems" in out) {
|
|
49
|
+
kept++;
|
|
50
|
+
lines.push(`kept ${rel}: ${out.problems.join(", ")}`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
await writeFile(target, out.markdown);
|
|
54
|
+
await rm(file);
|
|
55
|
+
lines.push(`converted ${rel}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!lines.length) lines.push("No .mdx files found.");
|
|
59
|
+
else if (kept) lines.push("", `${kept} file(s) kept as .mdx: move their components into .astro files, then run this again.`);
|
|
60
|
+
return {
|
|
61
|
+
code: 0,
|
|
62
|
+
stdout: lines.join("\n") + "\n",
|
|
63
|
+
stderr: ""
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/cli/upgrade.ts
|
|
69
|
+
const git = promisify(execFile);
|
|
70
|
+
/** Data files are parsed with defaults, so no release has needed a migration yet; add one here when a format changes. */
|
|
71
|
+
const MIGRATIONS = [];
|
|
72
|
+
const parts = (v) => v.split(/[.-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
|
|
73
|
+
const compareVersions = (a, b) => {
|
|
74
|
+
const [x, y] = [parts(a), parts(b)];
|
|
75
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return (x[i] ?? 0) - (y[i] ?? 0);
|
|
76
|
+
return 0;
|
|
77
|
+
};
|
|
78
|
+
const readJson = async (path) => readFile(path, "utf8").then((text) => JSON.parse(text), () => null);
|
|
79
|
+
/**
|
|
80
|
+
* Brings project data up to the installed `wuppycms` version. Install the new version first (`pnpm up wuppycms`);
|
|
81
|
+
* the previous state is whatever Git has, so a dirty tree is refused: rolling back is `git checkout`.
|
|
82
|
+
*/
|
|
83
|
+
async function upgrade(cwd, { force = false } = {}) {
|
|
84
|
+
const installed = (await readJson(join(cwd, "node_modules", "wuppycms", "package.json")))?.version;
|
|
85
|
+
if (!installed) return {
|
|
86
|
+
code: 1,
|
|
87
|
+
message: "wuppy is not installed here. Run your package manager's install first."
|
|
88
|
+
};
|
|
89
|
+
const stateFile = join(cwd, ".wuppy", "version.json");
|
|
90
|
+
const from = (await readJson(stateFile))?.version ?? "0.0.0";
|
|
91
|
+
if (compareVersions(installed, from) < 0) return {
|
|
92
|
+
code: 1,
|
|
93
|
+
message: `Installed ${installed} is older than the data (${from}). Downgrades are not supported; restore from Git or a backup.`
|
|
94
|
+
};
|
|
95
|
+
const pending = MIGRATIONS.filter((m) => compareVersions(m.to, from) > 0 && compareVersions(m.to, installed) <= 0);
|
|
96
|
+
const isRepo = await access(join(cwd, ".git")).then(() => true, () => false);
|
|
97
|
+
if (pending.length > 0 && isRepo && !force) {
|
|
98
|
+
const { stdout } = await git("git", ["status", "--porcelain"], { cwd });
|
|
99
|
+
if (stdout.trim()) return {
|
|
100
|
+
code: 1,
|
|
101
|
+
message: "Commit or stash your changes first, so the upgrade can be undone with `git checkout .` (or pass --force)."
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
for (const m of pending) await m.run(cwd);
|
|
105
|
+
await mkdir(join(cwd, ".wuppy"), { recursive: true });
|
|
106
|
+
await writeFile(stateFile, JSON.stringify({ version: installed }) + "\n");
|
|
107
|
+
return {
|
|
108
|
+
code: 0,
|
|
109
|
+
message: `Upgraded ${from} -> ${installed}\n${pending.length ? pending.map((m) => ` - ${m.title}`).join("\n") : " (no data changes needed)"}\nRun \`wuppycms types\` to refresh generated types. To undo data changes: git checkout . && git clean -fd .wuppy/data`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/cli/index.ts
|
|
115
|
+
const USAGE = "Usage: wuppycms init [--force] | wuppycms import [<git-url> <dir>] | wuppycms plugin hash <plugin-dir> | wuppycms doctor | wuppycms types | wuppycms upgrade [--force] | wuppycms mdx-to-md [<dir>]";
|
|
116
|
+
const exists = (p) => access(p).then(() => true, () => false);
|
|
117
|
+
/** Checks the things that break a fresh install: config, secret, content dirs, Git. */
|
|
118
|
+
async function doctor(cwd, env) {
|
|
119
|
+
const lines = [];
|
|
120
|
+
let failed = false;
|
|
121
|
+
const check = (ok, label, hint = "") => {
|
|
122
|
+
if (!ok) failed = true;
|
|
123
|
+
lines.push(`${ok ? "ok " : "FAIL"} ${label}${ok ? "" : ` - ${hint}`}`);
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
const config = await loadConfig(cwd);
|
|
127
|
+
check(true, "wuppy.config loads");
|
|
128
|
+
for (const c of config.collections) check(await exists(join(cwd, c.dir)), `content dir ${c.dir}`, "directory is missing");
|
|
129
|
+
check(!config.git || await exists(join(cwd, ".git")), "Git repository", "run `git init` (created on first sign-up otherwise)");
|
|
130
|
+
} catch (e) {
|
|
131
|
+
check(false, "wuppy.config loads", e.message);
|
|
132
|
+
}
|
|
133
|
+
check((env.WUPPY_SECRET ?? "").length >= 32, "WUPPY_SECRET", "set 32+ random characters in .env");
|
|
134
|
+
return {
|
|
135
|
+
code: failed ? 1 : 0,
|
|
136
|
+
stdout: lines.join("\n") + "\n",
|
|
137
|
+
stderr: ""
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function run(argv, cwd, env = process.env) {
|
|
141
|
+
const [cmd, sub, dir] = argv;
|
|
142
|
+
if (cmd === "doctor") return doctor(cwd, env);
|
|
143
|
+
if (cmd === "init") return init(cwd, { force: argv.includes("--force") });
|
|
144
|
+
if (cmd === "mdx-to-md") return convertMdx(cwd, sub);
|
|
145
|
+
if (cmd === "import") return importRepo(sub ?? "", dir ?? "", cwd);
|
|
146
|
+
if (cmd === "upgrade") {
|
|
147
|
+
const r = await upgrade(cwd, { force: argv.includes("--force") });
|
|
148
|
+
return r.code === 0 ? {
|
|
149
|
+
code: 0,
|
|
150
|
+
stdout: r.message + "\n",
|
|
151
|
+
stderr: ""
|
|
152
|
+
} : {
|
|
153
|
+
code: r.code,
|
|
154
|
+
stdout: "",
|
|
155
|
+
stderr: r.message
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (cmd === "types") try {
|
|
159
|
+
const { collections } = await loadConfig(cwd);
|
|
160
|
+
await writeFile(join(cwd, "wuppy-env.d.ts"), generateTypes(collections));
|
|
161
|
+
return {
|
|
162
|
+
code: 0,
|
|
163
|
+
stdout: `Wrote wuppy-env.d.ts (${collections.length} collections)\n`,
|
|
164
|
+
stderr: ""
|
|
165
|
+
};
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return {
|
|
168
|
+
code: 1,
|
|
169
|
+
stdout: "",
|
|
170
|
+
stderr: e.message
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (cmd === "plugin" && sub === "hash") {
|
|
174
|
+
if (!dir) return {
|
|
175
|
+
code: 1,
|
|
176
|
+
stdout: "",
|
|
177
|
+
stderr: USAGE
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
const manifest = manifestSchema.parse(JSON.parse(await readFile(join(cwd, dir, "manifest.json"), "utf8")));
|
|
181
|
+
const filters = manifest.hooks.filter(isFilter);
|
|
182
|
+
return {
|
|
183
|
+
code: 0,
|
|
184
|
+
stderr: "",
|
|
185
|
+
stdout: `Capabilities requested by "${manifest.id}":\n${JSON.stringify(manifest.capabilities, null, 2)}\n${filters.length ? `\nBefore-hooks (can refuse, and some can change, changes before they are made): ${filters.join(", ")}\n` : ""}\nIf you accept them, add to wuppy.config:\n { dir: "${dir}", approve: "${capabilityHash$1(manifest)}" }\n`
|
|
186
|
+
};
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return {
|
|
189
|
+
code: 1,
|
|
190
|
+
stdout: "",
|
|
191
|
+
stderr: `Cannot read ${dir}/manifest.json: ${e.message}`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
code: 1,
|
|
197
|
+
stdout: "",
|
|
198
|
+
stderr: USAGE
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/cli/main.ts
|
|
204
|
+
const { code, stdout, stderr } = await run(process.argv.slice(2), process.cwd());
|
|
205
|
+
if (stdout) process.stdout.write(stdout);
|
|
206
|
+
if (stderr) process.stderr.write(stderr + "\n");
|
|
207
|
+
process.exit(code);
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
export { };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/themes/components.ts
|
|
5
|
+
const NAME = /^[A-Z][A-Za-z0-9]*$/;
|
|
6
|
+
/**
|
|
7
|
+
* Components under `src/components` (recursively). They are design code, not content: the active theme's components are
|
|
8
|
+
* trusted, so they are added to the MDX allowlist automatically. Nested folders are flattened by filename; a duplicate
|
|
9
|
+
* name keeps the first path so two components can never silently shadow each other in the map.
|
|
10
|
+
*/
|
|
11
|
+
async function listThemeComponents(root) {
|
|
12
|
+
const found = (await readdir(join(root, "src", "components"), { recursive: true }).catch(() => [])).map((name) => name.split("\\").join("/")).filter((name) => name.endsWith(".astro") && !name.split("/").some((part) => part.startsWith("_"))).toSorted();
|
|
13
|
+
const out = [];
|
|
14
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15
|
+
for (const relative of found) {
|
|
16
|
+
const name = relative.split("/").pop().replace(/\.astro$/, "");
|
|
17
|
+
if (!NAME.test(name) || seen.has(name)) continue;
|
|
18
|
+
seen.add(name);
|
|
19
|
+
out.push({
|
|
20
|
+
name,
|
|
21
|
+
path: `src/components/${relative}`
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** The component tag names from one or more roots, in the order the roots are given. */
|
|
27
|
+
async function themeComponentNames(...roots) {
|
|
28
|
+
const names = [];
|
|
29
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30
|
+
for (const root of roots) for (const component of await listThemeComponents(root)) {
|
|
31
|
+
if (seen.has(component.name)) continue;
|
|
32
|
+
seen.add(component.name);
|
|
33
|
+
names.push(component.name);
|
|
34
|
+
}
|
|
35
|
+
return names;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { themeComponentNames as n, listThemeComponents as t };
|