defuss-ssg 0.0.1

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.
@@ -0,0 +1,366 @@
1
+ import chokidar from 'chokidar';
2
+ import express from 'express';
3
+ import serveStatic from 'serve-static';
4
+ import { join, resolve, dirname, sep } from 'node:path';
5
+ import { existsSync, rmdirSync, mkdirSync } from 'node:fs';
6
+ import esbuild from 'esbuild';
7
+ import remarkFrontmatter from 'remark-frontmatter';
8
+ import rehypeKatex from 'rehype-katex';
9
+ import remarkMath from 'remark-math';
10
+ import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
11
+ import { t as tailwindPlugin } from './tailwind-DV23JSh-.mjs';
12
+ import mdx from '@mdx-js/esbuild';
13
+ import glob from 'fast-glob';
14
+ import { getBrowserGlobals, getDocument, renderSync, renderToString } from 'defuss/server';
15
+ import { cp, readFile, writeFile } from 'node:fs/promises';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const remarkPlugins = [
19
+ // Parse both YAML and TOML (or omit options to default to YAML)
20
+ [remarkFrontmatter, ["yaml", "toml"]],
21
+ // Export each key as an ESM binding: export const title = "…"
22
+ [remarkMdxFrontmatter, { name: "meta" }],
23
+ // Convert $…$ and $$…$$ into math nodes for KaTeX
24
+ remarkMath
25
+ ];
26
+ const rehypePlugins = [
27
+ //rehypeMdxTitle,
28
+ rehypeKatex
29
+ ];
30
+
31
+ const readConfig = async (projectDir, debug) => {
32
+ const configPath = join(projectDir, "config.ts");
33
+ let config = {};
34
+ if (existsSync(configPath)) {
35
+ if (debug) {
36
+ console.log(`Using config from ${configPath}`);
37
+ }
38
+ const result = await esbuild.build({
39
+ entryPoints: [configPath],
40
+ format: "esm",
41
+ bundle: true,
42
+ target: ["esnext"],
43
+ write: false
44
+ });
45
+ const code = result.outputFiles[0].text;
46
+ const encoded = Buffer.from(code).toString("base64");
47
+ const dataUrl = `data:text/javascript;base64,${encoded}`;
48
+ const module = await import(dataUrl);
49
+ config = module.default;
50
+ }
51
+ config.pages = config.pages || configDefaults.pages;
52
+ config.output = config.output || configDefaults.output;
53
+ config.components = config.components || configDefaults.components;
54
+ config.assets = config.assets || configDefaults.assets;
55
+ config.plugins = config.plugins || configDefaults.plugins;
56
+ config.tmp = config.tmp || configDefaults.tmp;
57
+ config.remarkPlugins = config.remarkPlugins || configDefaults.remarkPlugins;
58
+ config.rehypePlugins = config.rehypePlugins || configDefaults.rehypePlugins;
59
+ return config;
60
+ };
61
+ const configDefaults = {
62
+ pages: "pages",
63
+ output: "dist",
64
+ components: "components",
65
+ assets: "assets",
66
+ tmp: ".ssg-temp",
67
+ plugins: [tailwindPlugin],
68
+ remarkPlugins,
69
+ rehypePlugins
70
+ };
71
+
72
+ const __filename = fileURLToPath(import.meta.url);
73
+ const __dirname = dirname(__filename);
74
+ const build = async ({
75
+ projectDir,
76
+ debug = false,
77
+ mode = "build"
78
+ }) => {
79
+ const startTime = performance.now();
80
+ const config = await readConfig(projectDir, debug);
81
+ if (debug) {
82
+ console.log("PRE config", config);
83
+ }
84
+ if (debug) {
85
+ console.log("Using config:", config);
86
+ }
87
+ const inputPagesDir = join(projectDir, config.pages);
88
+ const inputComponentsDir = join(projectDir, config.components);
89
+ const inputAssetsDir = join(projectDir, config.assets);
90
+ const tmpPagesDir = join(config.tmp, config.pages);
91
+ const tmpComponentsDir = join(config.tmp, config.components);
92
+ const outputProjectDir = join(projectDir, config.output);
93
+ const outputPagesDir = join(projectDir, config.output, config.pages);
94
+ const outputComponentsDir = join(
95
+ projectDir,
96
+ config.output,
97
+ config.components
98
+ );
99
+ const outputAssetsDir = join(projectDir, config.output, config.assets);
100
+ if (debug) {
101
+ console.log("Input pages dir:", inputPagesDir);
102
+ console.log("Input components dir:", inputComponentsDir);
103
+ console.log("Input assets dir:", inputAssetsDir);
104
+ console.log("Temp pages dir:", tmpPagesDir);
105
+ console.log("Temp components dir:", tmpComponentsDir);
106
+ console.log("Output pages dir:", outputPagesDir);
107
+ console.log("Output components dir:", outputComponentsDir);
108
+ console.log("Output assets dir:", outputAssetsDir);
109
+ }
110
+ if (!existsSync(inputPagesDir)) {
111
+ throw new Error(`Input pages directory does not exist: ${inputPagesDir}`);
112
+ } else if (debug) {
113
+ console.log(`Input pages directory exists: ${inputPagesDir}`);
114
+ }
115
+ if (!existsSync(inputComponentsDir)) {
116
+ console.warn(
117
+ `There is no components directory: ${inputComponentsDir}. You may not be able to use any custom components.`
118
+ );
119
+ }
120
+ if (!existsSync(inputAssetsDir)) {
121
+ console.warn(
122
+ `There is no assets directory: ${inputAssetsDir}. You may not be able to serve any custom assets.`
123
+ );
124
+ }
125
+ for (const plugin of config.plugins || []) {
126
+ if (plugin.phase === "pre" && (plugin.mode === mode || plugin.mode === "both")) {
127
+ if (debug) {
128
+ console.log(`Running pre-plugin: ${plugin.name}`);
129
+ }
130
+ await plugin.fn(projectDir, config);
131
+ }
132
+ }
133
+ if (existsSync(config.tmp)) {
134
+ if (debug) {
135
+ console.log(`Removing existing temp folder: ${config.tmp}`);
136
+ }
137
+ rmdirSync(config.tmp, { recursive: true });
138
+ }
139
+ await cp(projectDir, config.tmp, {
140
+ recursive: true,
141
+ filter: (src) => {
142
+ const relative = src.replace(join(projectDir, ""), "");
143
+ if (relative.startsWith(join("assets", "")) || relative.startsWith(join("node_modules", ""))) {
144
+ return false;
145
+ }
146
+ return true;
147
+ }
148
+ });
149
+ await cp(
150
+ // because of this the packaging of defuss-ssg must be done with "files" including the "components" folder
151
+ // in a built situation, __dirname is the dist folder of defuss-ssg
152
+ resolve(join(__dirname, "components", "index.mjs")),
153
+ // dist folder
154
+ join(tmpComponentsDir, "hydrate.tsx")
155
+ // a valid JS is always a valid TS file
156
+ );
157
+ await cp(
158
+ // because of this the packaging of defuss-ssg must be done with "files" including the "runtime" file
159
+ // in a built situation, __dirname is the dist folder of defuss-ssg
160
+ resolve(join(__dirname, "runtime.mjs")),
161
+ // dist folder
162
+ join(tmpComponentsDir, "runtime.ts")
163
+ // a valid JS is always a valid TS file
164
+ );
165
+ await esbuild.build({
166
+ entryPoints: [join(tmpPagesDir, "**/*.mdx")],
167
+ format: "esm",
168
+ bundle: true,
169
+ sourcemap: true,
170
+ target: ["esnext"],
171
+ outdir: tmpPagesDir,
172
+ plugins: [
173
+ mdx({
174
+ // using the defuss jsxImportSource so that the output code contains JSX runtime calls
175
+ // and can be rendered to HTML here on the server (in Node.js).
176
+ jsxImportSource: "defuss",
177
+ // We also use any remark/rehype plugins specified in the config file.
178
+ remarkPlugins: config.remarkPlugins,
179
+ rehypePlugins: config.rehypePlugins
180
+ })
181
+ ]
182
+ });
183
+ await esbuild.build({
184
+ entryPoints: [
185
+ join(tmpComponentsDir, "**/*.tsx"),
186
+ join(tmpComponentsDir, "**/*.ts")
187
+ ],
188
+ format: "esm",
189
+ bundle: true,
190
+ // making sure we can do code splitting for shared dependencies (e.g. defuss lib)
191
+ splitting: true,
192
+ target: ["esnext"],
193
+ outdir: tmpComponentsDir
194
+ });
195
+ const outputFiles = await glob.async(join(tmpPagesDir, "**/*.js"));
196
+ if (!existsSync(outputProjectDir)) {
197
+ mkdirSync(outputProjectDir, { recursive: true });
198
+ }
199
+ for (const outputFile of outputFiles) {
200
+ const outputHtmlFilePath = outputFile.replace(".js", ".html");
201
+ const relativeOutputHtmlFilePath = outputHtmlFilePath.replace(
202
+ `${tmpPagesDir}${sep}`,
203
+ ""
204
+ );
205
+ if (debug) {
206
+ console.log("Processing output file (JS):", outputFile);
207
+ console.log("Output HTML file path:", outputHtmlFilePath);
208
+ console.log("Relative output HTML path:", relativeOutputHtmlFilePath);
209
+ }
210
+ const code = await readFile(outputFile, "utf-8");
211
+ const encoded = Buffer.from(code).toString("base64");
212
+ const dataUrl = `data:text/javascript;base64,${encoded}`;
213
+ const exports = await import(dataUrl);
214
+ if (debug) {
215
+ console.log("exports", exports);
216
+ }
217
+ let vdom = exports.default(exports);
218
+ for (const plugin of config.plugins || []) {
219
+ if (plugin.phase === "page-vdom" && (plugin.mode === mode || plugin.mode === "both")) {
220
+ if (debug) {
221
+ console.log(`Running page-vdom plugin: ${plugin.name}`);
222
+ }
223
+ vdom = await plugin.fn(
224
+ vdom,
225
+ relativeOutputHtmlFilePath,
226
+ projectDir,
227
+ config
228
+ );
229
+ }
230
+ }
231
+ const browserGlobals = getBrowserGlobals();
232
+ const document = getDocument(false, browserGlobals);
233
+ browserGlobals.document = document;
234
+ let el = renderSync(vdom, document.documentElement, {
235
+ browserGlobals
236
+ });
237
+ for (const plugin of config.plugins || []) {
238
+ if (plugin.phase === "page-dom" && (plugin.mode === mode || plugin.mode === "both")) {
239
+ if (debug) {
240
+ console.log(`Running page-dom plugin: ${plugin.name}`);
241
+ }
242
+ el = await plugin.fn(
243
+ el,
244
+ relativeOutputHtmlFilePath,
245
+ projectDir,
246
+ config
247
+ );
248
+ }
249
+ }
250
+ let html = renderToString(el);
251
+ for (const plugin of config.plugins || []) {
252
+ if (plugin.phase === "page-html" && (plugin.mode === mode || plugin.mode === "both")) {
253
+ if (debug) {
254
+ console.log(`Running page-html plugin: ${plugin.name}`);
255
+ }
256
+ html = await plugin.fn(
257
+ html,
258
+ relativeOutputHtmlFilePath,
259
+ projectDir,
260
+ config
261
+ );
262
+ }
263
+ }
264
+ if (debug) {
265
+ console.log("Writing HTML file", outputHtmlFilePath);
266
+ }
267
+ if (debug) {
268
+ console.log("Relative HTML path:", relativeOutputHtmlFilePath);
269
+ }
270
+ const finalOutputFile = join(
271
+ projectDir,
272
+ config.output,
273
+ relativeOutputHtmlFilePath
274
+ );
275
+ if (debug) {
276
+ console.log("Full HTML output path:", finalOutputFile);
277
+ }
278
+ const finalOutputDir = dirname(finalOutputFile);
279
+ if (!existsSync(finalOutputDir)) {
280
+ mkdirSync(finalOutputDir, { recursive: true });
281
+ }
282
+ await writeFile(finalOutputFile, html);
283
+ }
284
+ await cp(tmpComponentsDir, outputComponentsDir, { recursive: true });
285
+ await cp(inputAssetsDir, outputAssetsDir, { recursive: true });
286
+ for (const plugin of config.plugins || []) {
287
+ if (plugin.phase === "post" && (plugin.mode === mode || plugin.mode === "both")) {
288
+ if (debug) {
289
+ console.log(`Running post-plugin: ${plugin.name}`);
290
+ }
291
+ await plugin.fn(projectDir, config);
292
+ }
293
+ }
294
+ if (!debug) {
295
+ rmdirSync(config.tmp, { recursive: true });
296
+ }
297
+ const endTime = performance.now();
298
+ const totalTime = (endTime - startTime) / 1e3;
299
+ console.log(`Build completed in ${totalTime.toFixed(2)} seconds.`);
300
+ };
301
+
302
+ const serve = async ({ projectDir, debug = false }) => {
303
+ const config = await readConfig(projectDir, debug);
304
+ const outputDir = join(projectDir, config.output);
305
+ const pagesDir = join(projectDir, config.pages);
306
+ const componentsDir = join(projectDir, config.components);
307
+ const assetsDir = join(projectDir, config.assets);
308
+ await build({ projectDir, debug, mode: "serve" });
309
+ const app = express();
310
+ const port = 3e3;
311
+ app.use(serveStatic(outputDir));
312
+ app.listen(port, () => {
313
+ console.log(`Server running at http://localhost:${port}`);
314
+ });
315
+ let isBuilding = false;
316
+ let pendingBuild = false;
317
+ const triggerBuild = async () => {
318
+ if (isBuilding) {
319
+ pendingBuild = true;
320
+ if (debug) {
321
+ console.log("Build scheduled after current one completes");
322
+ }
323
+ return;
324
+ }
325
+ isBuilding = true;
326
+ try {
327
+ await build({ projectDir, debug, mode: "serve" });
328
+ } finally {
329
+ isBuilding = false;
330
+ if (pendingBuild) {
331
+ pendingBuild = false;
332
+ if (debug) {
333
+ console.log("Running pending build");
334
+ }
335
+ await triggerBuild();
336
+ }
337
+ }
338
+ };
339
+ const watcher = chokidar.watch([pagesDir, componentsDir, assetsDir], {
340
+ ignored: /(^|[\/\\])\../,
341
+ // Ignore dotfiles
342
+ persistent: true,
343
+ ignoreInitial: true
344
+ // Ignore initial add events
345
+ });
346
+ watcher.on("change", async (path) => {
347
+ if (debug) {
348
+ console.log(`File changed: ${path}`);
349
+ }
350
+ await triggerBuild();
351
+ });
352
+ watcher.on("add", async (path) => {
353
+ if (debug) {
354
+ console.log(`File added: ${path}`);
355
+ }
356
+ await triggerBuild();
357
+ });
358
+ watcher.on("unlink", async (path) => {
359
+ if (debug) {
360
+ console.log(`File removed: ${path}`);
361
+ }
362
+ await triggerBuild();
363
+ });
364
+ };
365
+
366
+ export { rehypePlugins as a, readConfig as b, configDefaults as c, build as d, remarkPlugins as r, serve as s };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var node_path = require('node:path');
4
+
5
+ const tailwindPlugin = {
6
+ name: "tailwind",
7
+ mode: "both",
8
+ phase: "post",
9
+ fn: async (projectDir, { tmp }) => {
10
+ console.log("Tailwind CSS plugin running... 1 DIR:", node_path.join(projectDir));
11
+ }
12
+ };
13
+
14
+ exports.tailwindPlugin = tailwindPlugin;
@@ -0,0 +1,12 @@
1
+ import { join } from 'node:path';
2
+
3
+ const tailwindPlugin = {
4
+ name: "tailwind",
5
+ mode: "both",
6
+ phase: "post",
7
+ fn: async (projectDir, { tmp }) => {
8
+ console.log("Tailwind CSS plugin running... 1 DIR:", join(projectDir));
9
+ }
10
+ };
11
+
12
+ export { tailwindPlugin as t };
@@ -0,0 +1,101 @@
1
+ import { Options } from '@mdx-js/esbuild';
2
+ import { VNode } from 'defuss/server';
3
+
4
+ type RemarkPlugins = Options["remarkPlugins"];
5
+ type RehypePlugins = Options["rehypePlugins"];
6
+ type PluginFnPageHtml = (html: string, relativeOutputHtmlFilePath: string, projectDir: string, config: SsgConfig) => Promise<string> | string;
7
+ type PluginFnPageVdom = (vdom: VNode, relativeOutputHtmlFilePath: string, projectDir: string, config: SsgConfig) => Promise<VNode> | VNode;
8
+ type PluginFnPageDom = (dom: HTMLElement, relativeOutputHtmlFilePath: string, projectDir: string, config: SsgConfig) => Promise<HTMLElement> | HTMLElement;
9
+ type PluginFnPrePost = (projectDir: string, config: SsgConfig) => Promise<void> | void;
10
+ type BuildMode = "serve" | "build" | "both";
11
+ interface BuildOptions {
12
+ /**
13
+ * Enable debug logging during the build process
14
+ * Defaults to false
15
+ */
16
+ debug?: boolean;
17
+ /**
18
+ * The root directory of the project to build
19
+ * No default - this must be provided
20
+ */
21
+ projectDir: string;
22
+ /**
23
+ * The mode in which to run the build: "serve" for development server mode,
24
+ * "build" for static site generation mode;
25
+ * Defaults to "build"
26
+ */
27
+ mode: Omit<BuildMode, "both">;
28
+ }
29
+ type PluginFn = PluginFnPageHtml | PluginFnPageVdom | PluginFnPageDom | PluginFnPrePost;
30
+ /**
31
+ * Plugin interface for extending the SSG build process
32
+ */
33
+ interface SsgPlugin<T = PluginFn> {
34
+ /**
35
+ * The name of the plugin
36
+ */
37
+ name: string;
38
+ /**
39
+ * When to run the plugin: "pre" before the build starts, "post" after the build completes
40
+ * "page-vdom" after the VDOM for each page is created, before rendering to DOM
41
+ * "page-dom" after the DOM for each page is created, before serializing to HTML
42
+ * "page-html" after the HTML for each page is created, before writing to disk
43
+ */
44
+ phase: "pre" | "page-vdom" | "page-dom" | "page-html" | "post";
45
+ /**
46
+ * The mode(s) in which the plugin should run: "serve" for development server mode,
47
+ * "build" for static site generation mode, or "both" for both modes
48
+ * Defaults to "both"
49
+ */
50
+ mode: BuildMode;
51
+ /**
52
+ * The plugin function to execute
53
+ * @param config The current SsgConfig object
54
+ */
55
+ fn: T;
56
+ }
57
+ interface SsgConfig {
58
+ /**
59
+ * Input directory containing page files (e.g., MDX files)
60
+ * Defaults to "pages"
61
+ */
62
+ pages: string;
63
+ /**
64
+ * Output directory for generated static site files
65
+ * Defaults to "dist"
66
+ */
67
+ output: string;
68
+ /**
69
+ * Directory containing reusable components (e.g., defuss components)
70
+ * Defaults to "components"
71
+ */
72
+ components: string;
73
+ /**
74
+ * Directory containing static assets (e.g., images, fonts)
75
+ * Defaults to "assets"
76
+ */
77
+ assets: string;
78
+ /**
79
+ * Temporary working directory for build process
80
+ * Defaults to ".ssg-temp"
81
+ */
82
+ tmp: string;
83
+ /**
84
+ * Optional list of plugins to extend the build process
85
+ */
86
+ plugins: Array<SsgPlugin>;
87
+ /**
88
+ * Remark plugins to use for MDX processing
89
+ * You can import the default set from "defuss-ssg" and extend it
90
+ * like this: import { remarkPlugins as defaultRemarkPlugins } from "defuss-ssg"
91
+ */
92
+ remarkPlugins: Options["remarkPlugins"];
93
+ /**
94
+ * Rehype plugins to use for MDX processing
95
+ * You can import the default set from "defuss-ssg" and extend it
96
+ * like this: import { rehypePlugins as defaultRehypePlugins } from "defuss-ssg"
97
+ */
98
+ rehypePlugins: Options["rehypePlugins"];
99
+ }
100
+
101
+ export type { BuildOptions as B, PluginFnPageHtml as P, RemarkPlugins as R, SsgConfig as S, RehypePlugins as a, PluginFnPageVdom as b, PluginFnPageDom as c, PluginFnPrePost as d, BuildMode as e, PluginFn as f, SsgPlugin as g };
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "defuss-ssg",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "bin": "./dist/cli.mjs",
9
+ "license": "MIT",
10
+ "description": "A simple static site generator (SSG) built with defuss.",
11
+ "keywords": ["ssg", "static", "site", "generator", "defuss"],
12
+ "repository": {
13
+ "url": "git+https://github.com/kyr0/defuss.git",
14
+ "type": "git"
15
+ },
16
+ "scripts": {
17
+ "clean": "rm -rf ./dist && rm -rf ./node_modules/.pnpm",
18
+ "pretest": "pnpm run build",
19
+ "prebuild": "pnpm run clean",
20
+ "build": "pkgroll",
21
+ "cli-build": "node ./dist/cli.mjs build",
22
+ "cli-serve": "node ./dist/cli.mjs serve",
23
+ "test": "vitest --run --coverage"
24
+ },
25
+ "author": "Aron Homberg <info@aron-homberg.de>",
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "require": {
30
+ "types": "./dist/index.d.cts",
31
+ "default": "./dist/index.cjs"
32
+ },
33
+ "import": {
34
+ "types": "./dist/index.d.mts",
35
+ "default": "./dist/index.mjs"
36
+ }
37
+ },
38
+ "./components": {
39
+ "require": {
40
+ "types": "./dist/components/index.d.cts",
41
+ "default": "./dist/components/index.cjs"
42
+ },
43
+ "import": {
44
+ "types": "./dist/components/index.d.mts",
45
+ "default": "./dist/components/index.mjs"
46
+ }
47
+ },
48
+ "./runtime": {
49
+ "require": {
50
+ "types": "./dist/runtime.d.cts",
51
+ "default": "./dist/runtime.cjs"
52
+ },
53
+ "import": {
54
+ "types": "./dist/runtime.d.mts",
55
+ "default": "./dist/runtime.mjs"
56
+ }
57
+ },
58
+ "./plugins": {
59
+ "require": {
60
+ "types": "./dist/plugins/index.d.cts",
61
+ "default": "./dist/plugins/index.cjs"
62
+ },
63
+ "import": {
64
+ "types": "./dist/plugins/index.d.mts",
65
+ "default": "./dist/plugins/index.mjs"
66
+ }
67
+ }
68
+ },
69
+ "main": "./dist/index.cjs",
70
+ "module": "./dist/index.mjs",
71
+ "types": "./dist/index.d.cts",
72
+ "files": ["dist"],
73
+ "engines": {
74
+ "node": "^18.17.1 || ^20.3.0 || >=21.0.0"
75
+ },
76
+ "dependencies": {
77
+ "express": "^5.1.0",
78
+ "serve-static": "^2.2.0",
79
+ "chokidar": "^4.0.3",
80
+ "@mdx-js/esbuild": "^3.1.1",
81
+ "esbuild": "^0.25.9",
82
+ "rehype-mdx-title": "^3.2.0",
83
+ "rehype-katex": "^7.0.1",
84
+ "remark-math": "^6.0.0",
85
+ "remark-frontmatter": "^5.0.0",
86
+ "remark-mdx-frontmatter": "^5.2.0",
87
+ "fast-glob": "^3.3.3",
88
+ "@types/express": "^5.0.3",
89
+ "@types/serve-static": "^1.15.8",
90
+ "defuss": "^2.1.1",
91
+ "defuss-runtime": "^1.2.0"
92
+ },
93
+ "devDependencies": {
94
+ "pkgroll": "^2.5.1",
95
+ "typescript": "^5.6.3",
96
+ "happy-dom": "^15.11.7"
97
+ }
98
+ }