@zyrab/domo-ssg 0.5.3 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zyrab/domo-ssg",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "A Static Site Generator (SSG) for Domo-based projects.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -17,16 +17,21 @@
17
17
  "author": "Zyrab",
18
18
  "license": "MIT",
19
19
  "peerDependencies": {
20
- "@zyrab/domo": "^1.3.0",
21
- "@zyrab/domo-router": "^0.3.1",
22
- "@zyrab/domo-og": "0.1.5"
20
+ "@zyrab/domo": "^1.4.0",
21
+ "@zyrab/domo-og": "0.2.1",
22
+ "@zyrab/domo-router": "^0.3.1"
23
+ },
24
+ "devDependencies": {
25
+ "esbuild": "^0.20.0",
26
+ "@zyrab/domo": "^1.4.0"
23
27
  },
24
28
  "publishConfig": {
25
29
  "access": "public"
26
30
  },
27
31
  "files": [
28
32
  "src/",
29
- "README"
33
+ "templates",
34
+ "README.md"
30
35
  ],
31
36
  "scripts": {
32
37
  "build": "node src/index.js",
package/src/config.js CHANGED
@@ -8,13 +8,12 @@ export async function loadConfig() {
8
8
  if (mergedConfig) return mergedConfig;
9
9
  const userConfigPath = path.resolve(process.cwd(), "domo.config.js");
10
10
 
11
- // Default configuration values
12
11
  const defaultConfig = {
13
12
  outDir: "./dist",
14
13
  routesFile: "./routes.js",
15
14
  layout: "./layout.js",
16
15
  lang: "en",
17
- author: "Domo",
16
+ author: "Zyrab",
18
17
  exclude: ["css", "js", "assets", "robots.txt", "admin"],
19
18
  baseUrl: "http://localhost:3000",
20
19
  };
@@ -24,7 +23,7 @@ export async function loadConfig() {
24
23
  const importedConfig = await import(pathToFileURL(userConfigPath).href);
25
24
  userConfig = importedConfig.default || importedConfig;
26
25
  } catch (error) {
27
- console.warn(`⚠️ No custom config file found at ${configFilePath}. Using default settings.`);
26
+ console.warn(`[Domo-SSG] No custom config file found at ${configFilePath}. Using default settings.`);
28
27
  }
29
28
  mergedConfig = {
30
29
  ...defaultConfig,
@@ -38,7 +37,7 @@ export async function loadConfig() {
38
37
 
39
38
  export function getConfig() {
40
39
  if (!mergedConfig) {
41
- throw new Error("Interal Error: Config has not been loaded yet. Call loadConfig() first.");
40
+ throw new Error("[Domo-SSG] Interal Error: Config has not been loaded yet. Call loadConfig() first.");
42
41
  }
43
42
  return mergedConfig;
44
43
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @file event_extraction.js
3
+ * @description Logic for extracting events, state, and references for SSG/SPA bundling.
4
+ */
5
+
6
+ import { createHash } from "crypto";
7
+
8
+ /**
9
+ * Normalizes a function for the client.
10
+ * Handlers will use 'state' and 'target' identifiers which are
11
+ * consistently minified by esbuild alongside the listener scope.
12
+ */
13
+ function transformHandler(handlerInfo) {
14
+ const { type, selector, handler, name } = handlerInfo;
15
+ let body = "";
16
+ let fnSource = handler.toString();
17
+
18
+ const isExternal = handlerInfo.path !== null;
19
+
20
+ if (isExternal) {
21
+ // If it's a named function from an external file, we just call it.
22
+ body = `${name}(e${type !== "direct" ? ", target" : ""});`;
23
+ } else {
24
+ // Extract body from anonymous/inline function
25
+ const match =
26
+ fnSource.match(/^(?:async\s+)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>\s*\{?([\s\S]*?)\}?$/) ||
27
+ fnSource.match(/^(?:async\s+)?function\s*[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
28
+ body = match ? match[1].trim() : fnSource;
29
+ }
30
+
31
+ // Wrap in blocks to isolate 'target'.
32
+ if (type === "closest") {
33
+ return `{\n const target = e.target.closest("${selector}");\n if (target) {\n ${body}\n }\n }`;
34
+ }
35
+
36
+ if (type === "match") {
37
+ return `{\n if (e.target.matches("${selector}")) {\n const target = e.target;\n ${body}\n }\n }`;
38
+ }
39
+
40
+ return body;
41
+ }
42
+ function transformRef(refInfo) {
43
+ const { handler, name } = refInfo;
44
+ let fnSource = handler.toString();
45
+
46
+ // Extract the body of the ref callback
47
+ const match =
48
+ fnSource.match(/^(?:async\s+)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>\s*\{?([\s\S]*?)\}?$/) ||
49
+ fnSource.match(/^(?:async\s+)?function\s*[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
50
+ const body = match ? match[1].trim() : fnSource;
51
+
52
+ return `{\n const el = document.getElementById("${refInfo.id}");\n if (el) {\n const callback = (el) => { ${body} };\n callback(el);\n }\n }`;
53
+ }
54
+
55
+ /**
56
+ * Generates the ESM content for a specific element's events.
57
+ */
58
+ export function generateElementScript(id, events, states, refs) {
59
+ const imports = new Map();
60
+ const listeners = [];
61
+ const refLogics = [];
62
+
63
+ events.forEach(({ event, handlers }) => {
64
+ const logicBlocks = handlers
65
+ .map((h) => {
66
+ if (h.path) {
67
+ if (!imports.has(h.path)) imports.set(h.path, new Set());
68
+ imports.get(h.path).add(h.name);
69
+ }
70
+ return transformHandler(h);
71
+ })
72
+ .join("\n ");
73
+
74
+ // We inject the state declaration as a plain string at the top of the listener.
75
+ // Because esbuild parses this whole block, it will minify the identifier 'state'
76
+ // to the same name used in the 'logicBlocks' (e.g., const n = ...; n.toggled = ...).
77
+ const stateInclusion =
78
+ states && Object.keys(states).length
79
+ ? Object.entries(states)
80
+ .map(([key, val]) => `let ${key} = ${JSON.stringify(val)};`)
81
+ .join("\n")
82
+ : "";
83
+
84
+ listeners.push(
85
+ `${stateInclusion}\n document.getElementById("${id}").addEventListener("${event}", async (e) => {${logicBlocks}\n });`,
86
+ );
87
+ });
88
+ refs.forEach((r) => {
89
+ refLogics.push(transformRef({ ...r, id }));
90
+ });
91
+
92
+ let importStr = "";
93
+
94
+ for (const [path, names] of imports) {
95
+ importStr += `import { ${[...names].join(", ")} } from "${path}";\n`;
96
+ }
97
+ const combinedLogic = [...refLogics, ...listeners].join("\n");
98
+
99
+ return `${importStr}\n(function() {\n${combinedLogic}\n})();`;
100
+ }
101
+
102
+ /**
103
+ * Hash helper for caching
104
+ */
105
+ export function getHash(content) {
106
+ return createHash("sha1").update(content).digest("hex").slice(0, 8);
107
+ }
@@ -1,177 +1,208 @@
1
- // src/event-utils.js
2
-
3
- import { writeFileSync, mkdirSync, existsSync } from "fs";
4
- import { join } from "path";
5
- import { createHash } from "crypto";
6
-
7
- export function generateScriptContent(events) {
8
- return events
9
- .map(({ id, event, handlers }) => {
10
- const varSet = new Set();
11
- const logicLines = [];
12
- const closureFunctions = [];
13
- let matchCounter = 0;
14
-
15
- for (const { type, selector, handler } of handlers) {
16
- const fnSource = handler.toString();
17
- const { name, body } = destructureFunction(fnSource);
18
- const vars = extractExposedVariables(body);
19
- vars.forEach((v) => varSet.add(v));
20
-
21
- if (type === "closest") {
22
- const matchVar = `match${++matchCounter}`;
23
- logicLines.push(`const ${matchVar} = e.target.closest("${selector}");`);
24
-
25
- if (name) {
26
- logicLines.push(`if (${matchVar}) ${name}(e, ${matchVar});`);
27
- closureFunctions.push(fnSource);
28
- } else {
29
- const adjustedBody = body.replace(/\btarget\b/g, matchVar);
30
- logicLines.push(`if (${matchVar}) {\n${indent(adjustedBody, 2)}\n}`);
31
- }
32
- } else if (type === "match") {
33
- const matchExpr = `e.target.matches("${selector}")`;
34
-
35
- if (name) {
36
- logicLines.push(`if (${matchExpr}) ${name}(e, e.target);`);
37
- closureFunctions.push(fnSource);
38
- } else {
39
- const adjustedBody = body.replace(/\btarget\b/g, "e.target");
40
- logicLines.push(`if (${matchExpr}) {\n${indent(adjustedBody, 2)}\n}`);
41
- }
42
- } else if (type === "direct") {
43
- logicLines.push(body);
44
- }
45
- }
46
-
47
- const varsStr = [...varSet].join("\n");
48
- const handlerBody = `function(e) {\n${indent(logicLines.join("\n"), 1)}\n}`;
49
- const closures = closureFunctions.length ? `\n\n${closureFunctions.join("\n\n")}` : "";
50
-
51
- return `${
52
- varsStr ? varsStr + "\n\n" : ""
53
- }document.getElementById("${id}").addEventListener("${event}", ${handlerBody});${closures}`;
54
- })
55
- .join("\n\n\n");
1
+ import { writeFileSync, mkdirSync, existsSync, rmSync, readFileSync } from "fs";
2
+ import { join, dirname, relative } from "path";
3
+ import { fileURLToPath, pathToFileURL } from "url";
4
+ import { build } from "esbuild";
5
+ import { generateElementScript, getHash } from "./event-extraction.js";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+
10
+ const DOMO_CLIENT_PACKAGE = "@zyrab/domo/client";
11
+ let DOMO_CLIENT_SOURCE;
12
+
13
+ try {
14
+ // This works across different package managers
15
+ DOMO_CLIENT_SOURCE = require.resolve(DOMO_CLIENT_PACKAGE);
16
+ } catch (e) {
17
+ // Fallback for local dev if not linked
18
+ DOMO_CLIENT_SOURCE = join(__dirname, "../../domo/src/client/domo.client.js");
56
19
  }
57
-
58
- export function collectEvents(node, out = []) {
20
+ const cache = {
21
+ runtime: null,
22
+ events: new Map(), // hash -> file
23
+ islands: new Map(), // hash -> file
24
+ };
25
+
26
+ /**
27
+ * Plugin to convert imports of 'domo' into the global 'Domo' variable
28
+ */
29
+ const makeDomoExternalPlugin = {
30
+ name: "domo-external",
31
+ setup(build) {
32
+ // Intercept any import relating to domo
33
+ build.onResolve({ filter: /^domo$|^@zyrab\/domo/ }, (args) => {
34
+ return { path: args.path, external: true };
35
+ });
36
+ },
37
+ };
38
+
39
+ /**
40
+ * Traverses the Domo tree to find all elements with events or islands.
41
+ */
42
+ export function collectMetadata(node, out = { events: [], islands: [] }) {
59
43
  if (!node || typeof node !== "object") return out;
44
+
60
45
  const el = node.element;
61
- if (Array.isArray(el._events) && el._events.length > 0) {
62
- out.push(...el._events);
46
+
47
+ if ((el?._events?.length > 0 || el?._refs?.length > 0) && !el?._island) {
48
+ out.events.push({
49
+ id: el._attr["data-domo-id"] || el._attr["id"],
50
+ events: el._events || [],
51
+ states: el._state || {},
52
+ refs: el._refs || [],
53
+ });
63
54
  }
64
55
 
65
- if (Array.isArray(el._child)) {
56
+ if (el?._island) {
57
+ out.islands.push({
58
+ id: el._attr["data-domo-id"] || el._attr["id"],
59
+ path: el.__file,
60
+ });
61
+ }
62
+
63
+ if (Array.isArray(el?._child)) {
66
64
  for (const child of el._child) {
67
- collectEvents(child, out);
65
+ collectMetadata(child, out);
68
66
  }
69
67
  }
70
68
 
71
69
  return out;
72
70
  }
73
71
 
74
- function extractExposedVariables(source) {
75
- const lines = source.split("\n");
76
- const injected = [];
72
+ /**
73
+ * Bundle runtime (once)
74
+ */
75
+ async function bundleRuntime(outputDir) {
76
+ if (cache.runtime) return cache.runtime;
77
+
78
+ const file = "domo.runtime.js";
79
+ const out = join(outputDir, "js", file);
80
+
81
+ await build({
82
+ entryPoints: [DOMO_CLIENT_SOURCE],
83
+ bundle: true,
84
+ minify: false,
85
+ format: "iife",
86
+ globalName: "Domo",
87
+ outfile: out,
88
+ platform: "browser",
89
+ });
90
+
91
+ cache.runtime = file;
92
+ return file;
93
+ }
77
94
 
78
- for (const line of lines) {
79
- const trimmed = line.trim();
95
+ /**
96
+ * Bundle event logic
97
+ */
98
+ async function bundleEvents(metadata, jsDir, tempDir) {
99
+ if (metadata.events.length === 0) return null;
80
100
 
81
- if (trimmed === "") continue;
101
+ const raw = metadata.events
102
+ .map(({ id, events, states, refs }) => generateElementScript(id, events, states, refs))
103
+ .join("\n\n");
82
104
 
83
- if (trimmed.startsWith("// @ssg-let")) {
84
- const decl = trimmed.replace("// @ssg-let", "").replace(/;$/, "").trim();
85
- injected.push(`let ${decl};`);
86
- } else if (trimmed.startsWith("// @ssg-const")) {
87
- const decl = trimmed.replace("// @ssg-const", "").replace(/;$/, "").trim();
88
- injected.push(`const ${decl};`);
89
- } else if (!trimmed.startsWith("//")) {
90
- break; // stop at first non-comment code line
91
- }
92
- }
105
+ const hash = getHash(raw);
106
+ if (cache.events.has(hash)) return cache.events.get(hash);
93
107
 
94
- return injected;
95
- }
108
+ const file = `${hash}.events.js`;
109
+ const entry = join(tempDir, `${hash}.entry.js`);
96
110
 
97
- function destructureFunction(fnSource) {
98
- const funcMatch = fnSource.match(/^function\s*([a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{([\s\S]*)\}$/);
99
- if (funcMatch) {
100
- const [, name, body] = funcMatch;
101
- return { name: name.trim(), body: body.trim() };
102
- }
111
+ writeFileSync(entry, raw, "utf8");
103
112
 
104
- const arrowMatch = fnSource.match(/^\(?[a-zA-Z0-9_,\s]*\)?\s*=>\s*\{([\s\S]*)\}$/);
105
- if (arrowMatch) {
106
- return { name: "", body: arrowMatch[1].trim() };
107
- }
113
+ await build({
114
+ entryPoints: [entry],
115
+ bundle: true,
116
+ minify: false,
117
+ format: "iife",
118
+ outfile: join(jsDir, file),
119
+ platform: "browser",
120
+ });
108
121
 
109
- return { name: "", body: "" };
110
- }
122
+ rmSync(entry);
111
123
 
112
- function indent(str, level = 1) {
113
- const pad = " ".repeat(level);
114
- return str
115
- .split("\n")
116
- .map((line) => pad + line)
117
- .join("\n");
124
+ cache.events.set(hash, file);
125
+ return file;
118
126
  }
119
127
 
120
- function normalizeEventLogic(events) {
121
- const blocks = [];
122
-
123
- for (const { handlers } of events) {
124
- for (const { handler } of handlers) {
125
- const fnSource = handler.toString();
126
- const { body } = destructureFunction(fnSource);
127
- const injected = extractExposedVariables(body);
128
+ /**
129
+ * Bundle islands (deduped by CONTENT, not path)
130
+ */
131
+ async function bundleIslands(metadata, jsDir, tempDir) {
132
+ const results = [];
133
+ const islandsToBundle = metadata.islands.filter((i) => i.path);
134
+
135
+ await Promise.all(
136
+ islandsToBundle.map(async (island) => {
137
+ const { path: filePath, id } = island;
138
+ const content = readFileSync(filePath, "utf8");
139
+ const hash = getHash(content);
140
+
141
+ if (cache.islands.has(hash)) {
142
+ results.push({ path: filePath, file: cache.islands.get(hash) });
143
+ return;
144
+ }
128
145
 
129
- blocks.push({
130
- injected: injected.join("\n"),
131
- body: normalizeCode(body),
146
+ const file = `${hash}.island.js`;
147
+ const entryPath = join(tempDir, `${hash}.island.entry.js`);
148
+
149
+ // wrapper: hydrate island at the correct element
150
+ const wrapper = `
151
+ import Island from "${filePath.replace(/\\/g, "/")}";
152
+
153
+ (function() {
154
+ const el = document.querySelector('[data-domo-id="${id}"]');
155
+ if (el) {
156
+ const instance = Island();
157
+ if (instance && instance._isDomo) {
158
+ const built = instance.build();
159
+ el.replaceWith(built);
160
+ }
161
+ }
162
+ })();
163
+ `;
164
+
165
+ writeFileSync(entryPath, wrapper, "utf8");
166
+
167
+ await build({
168
+ entryPoints: [entryPath],
169
+ bundle: true,
170
+ minify: false,
171
+ format: "iife",
172
+ outfile: join(jsDir, file),
173
+ platform: "browser",
174
+ plugins: [makeDomoExternalPlugin],
132
175
  });
133
- }
134
- }
135
176
 
136
- // Sort to make sure logic is order-independent
137
- blocks.sort((a, b) => (a.body + a.injected).localeCompare(b.body + b.injected));
177
+ rmSync(entryPath);
178
+ cache.islands.set(hash, file);
179
+ results.push({ path: filePath, file });
180
+ }),
181
+ );
138
182
 
139
- return blocks.map(({ injected, body }) => `${injected}\n${body}`).join("\n");
183
+ return results;
140
184
  }
185
+ /**
186
+ * Main orchestrator
187
+ */
188
+ export async function writeJs(content, outputDir) {
189
+ const metadata = collectMetadata(content);
141
190
 
142
- function normalizeCode(code) {
143
- return code
144
- .split("\n")
145
- .map((line) => line.trim())
146
- .filter((line) => line && !line.startsWith("//"))
147
- .join("\n");
148
- }
191
+ const hasInteractivity = metadata.events.length > 0 || metadata.islands.length > 0;
149
192
 
150
- function hashContent(content) {
151
- return createHash("sha1").update(content).digest("hex").slice(0, 8); // short hash like '2fa4b1ab'
152
- }
193
+ if (!hasInteractivity) return null;
153
194
 
154
- const generatedCache = new Map(); // hash → filename
195
+ const jsDir = join(outputDir, "js");
196
+ const tempDir = join(outputDir, ".domo_temp");
155
197
 
156
- export function writeJs(constent, outputDir) {
157
- const events = collectEvents(constent);
158
- if (events.length <= 0) return;
198
+ if (!existsSync(jsDir)) mkdirSync(jsDir, { recursive: true });
199
+ if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
159
200
 
160
- const jsDir = join(outputDir, "js");
161
- if (!existsSync(jsDir)) mkdirSync(jsDir);
162
-
163
- const normalized = normalizeEventLogic(events);
164
- const hash = hashContent(normalized);
165
-
166
- let fileName;
167
- if (generatedCache.has(hash)) {
168
- fileName = generatedCache.get(hash);
169
- } else {
170
- fileName = `${hash}.js`;
171
- const jsContent = generateScriptContent(events);
172
- writeFileSync(join(jsDir, fileName), jsContent, "utf8");
173
- generatedCache.set(hash, fileName);
174
- }
201
+ const [runtime, events, islands] = await Promise.all([
202
+ bundleRuntime(outputDir),
203
+ bundleEvents(metadata, jsDir, tempDir),
204
+ bundleIslands(metadata, jsDir, tempDir),
205
+ ]);
175
206
 
176
- return join(fileName);
207
+ return [runtime, events, ...islands.map((i) => i.file)].filter(Boolean);
177
208
  }
package/src/file-utils.js CHANGED
@@ -42,9 +42,7 @@ export function cleanOutputDir(outputDir, exclude) {
42
42
  const entryPath = path.join(outputDir, entry);
43
43
  fs.rmSync(entryPath, { recursive: true, force: true });
44
44
  }
45
- console.log(`📁 Cleaned output directory: ${outputDir}`);
46
45
  } else {
47
46
  fs.mkdirSync(outputDir, { recursive: true });
48
- console.log(`📁 Created output directory: ${outputDir}`);
49
47
  }
50
48
  }
package/src/index.js CHANGED
@@ -4,35 +4,25 @@ import { loadConfig } from "./config.js";
4
4
  import { cleanOutputDir } from "./file-utils.js";
5
5
  import { generateSitemap } from "./sitemap.js";
6
6
  import { buildRoutes } from "./route-traversal.js";
7
- import "dotenv/config";
8
7
 
9
8
  async function main() {
10
- process.env.DOMO_SSG = "true";
11
9
  const config = await loadConfig();
12
10
 
13
- // Import layout and route tree using pathToFileURL and .href for dynamic imports
14
11
  const { routes } = await import(pathToFileURL(config.routesFile).href);
15
12
  const { renderLayout } = await import(pathToFileURL(config.layout).href);
16
13
 
17
- console.log("🚀 Starting Domo SSG build...");
18
- console.log(`📁 Output directory: ${config.outDir}`);
19
- console.log(`🗺️ Routes file: ${config.routesFile}`);
20
- console.log(`🧩 Layout file: ${config.layout}`);
14
+ console.log("[Domo-SSG] Starting Domo SSG build...");
21
15
 
22
- // 1. Clean the output directory
23
16
  cleanOutputDir(config.outDir, config.exclude);
24
17
 
25
- // 2. Build all routes recursively
26
18
  await buildRoutes(routes, renderLayout);
27
19
 
28
- // 3. Generate sitemap
29
20
  generateSitemap(config.outDir, config.baseUrl, config.exclude);
30
21
 
31
- console.log("Domo SSG build complete!");
22
+ console.log("[Domo-SSG] build complete!");
32
23
  }
33
24
 
34
- // Execute the build process
35
25
  main().catch((error) => {
36
- console.error("Domo SSG build failed:", error);
37
- process.exit(1); // Exit with a non-zero code to indicate failure
26
+ console.error("[Domo-SSG] build failed:", error);
27
+ process.exit(1);
38
28
  });
@@ -39,13 +39,12 @@ export async function handleRoute(params, renderLayout) {
39
39
  // Render the component content
40
40
  const content = await component(props);
41
41
 
42
- const embededScript = writeJs(content, outDir, path);
43
-
42
+ const bundlePath = await writeJs(content, outDir);
44
43
  const ogImage = await tryGenerateOgImage(meta, outDir, path);
45
44
 
46
45
  const fontPaths = normalizeAssets([fonts, assets.fonts]);
47
46
  const stylePaths = normalizeAssets([styles, assets.styles]);
48
- const scriptPaths = normalizeAssets([embededScript, scripts, assets.scripts]);
47
+ const scriptPaths = normalizeAssets([bundlePath, scripts, assets.scripts]);
49
48
 
50
49
  const html = await renderLayout(content, {
51
50
  scripts: scriptPaths,
@@ -63,6 +62,6 @@ export async function handleRoute(params, renderLayout) {
63
62
  // Write the generated HTML to a file
64
63
  writeHTML(outDir, path, html);
65
64
  } catch (e) {
66
- console.warn(`⚠️ Error rendering ${path}:\n${e.stack}`);
65
+ console.warn(`[Domo-SSG] Error rendering ${path}:\n${e.stack}`);
67
66
  }
68
67
  }
@@ -25,14 +25,14 @@ export async function buildRoutes(routes, renderLayout, parentPath = "", props =
25
25
  const resolvedParams = await routeNode.routeParams(parentRouteName);
26
26
 
27
27
  if (!Array.isArray(resolvedParams) || resolvedParams.length === 0) {
28
- console.warn(`⚠️ No items returned for dynamic route at ${currentRoute}`);
28
+ console.warn(`[Domo-SSG] No items returned for dynamic route at ${currentRoute}`);
29
29
  continue;
30
30
  }
31
31
 
32
32
  for (const item of resolvedParams) {
33
33
  const segment = item[paramName];
34
34
  if (!segment) {
35
- console.warn(`⚠️ Missing parameter:'${paramName}' in item for dynamic route at ${currentRoute}`);
35
+ console.warn(`[Domo-SSG] Missing parameter:'${paramName}' in item for dynamic route at ${currentRoute}`);
36
36
  continue;
37
37
  }
38
38
 
@@ -46,7 +46,7 @@ export async function buildRoutes(routes, renderLayout, parentPath = "", props =
46
46
  }
47
47
  }
48
48
  } catch (e) {
49
- console.warn(`⚠️ Skipped dynamic route generation for ${currentRoute}: ${e.message}`);
49
+ console.warn(`[Domo-SSG] Skipped dynamic route generation for ${currentRoute}: ${e.message}`);
50
50
  }
51
51
  continue;
52
52
  }
@@ -56,7 +56,7 @@ export async function buildRoutes(routes, renderLayout, parentPath = "", props =
56
56
  await buildRoutes(routeNode, renderLayout, currentRoute, { ...props });
57
57
  }
58
58
  if (!routeNode.component && Object.keys(routeNode).length > 0) {
59
- console.warn(`⚠️ Route "${currentRoute}" has no component but contains other data.`);
59
+ console.warn(`[Domo-SSG] Route "${currentRoute}" has no component but contains other data.`);
60
60
  }
61
61
  }
62
62
  }
package/src/sitemap.js CHANGED
@@ -39,9 +39,7 @@ export function generateSitemap(outputDir, baseUrl, exclude = []) {
39
39
  let urlPath = "/" + relative.replace(/\\/g, "/");
40
40
 
41
41
  // Root correction
42
- if (urlPath === "//" || urlPath === "/.") {
43
- urlPath = "/";
44
- }
42
+ if (urlPath === "//" || urlPath === "/.") urlPath = "/";
45
43
 
46
44
  const stats = fs.statSync(indexPath);
47
45
  const lastmod = stats.mtime.toISOString();
@@ -61,20 +59,21 @@ export function generateSitemap(outputDir, baseUrl, exclude = []) {
61
59
 
62
60
  walk(outputDir);
63
61
 
64
- const xml = `<?xml version="1.0" encoding="UTF-8"?>
65
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
66
- ${urls
67
- .map(
68
- ({ loc, lastmod, priority, changefreq }) => ` <url>
69
- <loc>${loc}</loc>
70
- <lastmod>${lastmod}</lastmod>
71
- <changefreq>${changefreq}</changefreq>
72
- <priority>${priority}</priority>
73
- </url>`
74
- )
75
- .join("\n")}
76
- </urlset>`;
62
+ const xml = `
63
+ <?xml version="1.0" encoding="UTF-8"?>
64
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
65
+ ${urls
66
+ .map(
67
+ ({ loc, lastmod, priority, changefreq }) => `
68
+ <url>
69
+ <loc>${loc}</loc>
70
+ <lastmod>${lastmod}</lastmod>
71
+ <changefreq>${changefreq}</changefreq>
72
+ <priority>${priority}</priority>
73
+ </url>`,
74
+ )
75
+ .join("\n")}
76
+ </urlset>`;
77
77
 
78
78
  fs.writeFileSync(path.join(outputDir, "sitemap.xml"), xml, "utf8");
79
- console.log(`🧭 Generated sitemap.xml at: ${path.join(outputDir, "sitemap.xml")}`);
80
79
  }
package/src/utils.js CHANGED
@@ -1,17 +1,15 @@
1
1
  import Router from "@zyrab/domo-router";
2
+
2
3
  export function normalizeAssets(arr) {
3
- if (!arr) return []; // Handle null/undefined safely
4
- const flatArray = Array.isArray(arr) ? arr.flat() : [arr]; // Support single string/object
4
+ if (!arr) return [];
5
+ const flatArray = Array.isArray(arr) ? arr.flat() : [arr];
5
6
  const result = [];
6
7
 
7
8
  for (const item of flatArray) {
8
- if (!item) continue; // Skip null/undefined/false
9
- if (typeof item === "string") {
10
- result.push({ href: item });
11
- } else if (typeof item === "object" && item.href) {
12
- result.push(item);
13
- } else if (typeof item === "object" && !item.href) {
14
- // For objects without href, attempt to normalize keys like { src: "..." }
9
+ if (!item) continue;
10
+ if (typeof item === "string") result.push({ href: item });
11
+ else if (typeof item === "object" && item.href) result.push(item);
12
+ else if (typeof item === "object" && !item.href) {
15
13
  if (item.src) result.push({ ...item, href: item.src });
16
14
  }
17
15
  }
@@ -19,7 +17,7 @@ export function normalizeAssets(arr) {
19
17
  return result;
20
18
  }
21
19
 
22
- export async function tryGenerateOgImage(routeMeta, outputDir, path) {
20
+ export async function tryGenerateOgImage(routeMeta, ogOutputPath, path) {
23
21
  if (!routeMeta.generateOgImage) return;
24
22
  const slug = Router.info().segments.at(-1).slice(1);
25
23
 
@@ -28,7 +26,7 @@ export async function tryGenerateOgImage(routeMeta, outputDir, path) {
28
26
 
29
27
  const ogPath = generate({
30
28
  ...routeMeta,
31
- outputDir,
29
+ ogOutputPath,
32
30
  slug,
33
31
  routeKey: path,
34
32
  });
@@ -36,9 +34,9 @@ export async function tryGenerateOgImage(routeMeta, outputDir, path) {
36
34
  return ogPath;
37
35
  } catch (err) {
38
36
  if (err.code === "ERR_MODULE_NOT_FOUND" || err.message.includes("Cannot find module")) {
39
- console.warn(`⚠️ OG image generation skipped for "${slug}" — install 'domo-og' to enable this feature.`);
37
+ console.warn(`[Domo-SSG] OG image generation skipped for "${slug}" — install 'domo-og' to enable this feature.`);
40
38
  } else {
41
- console.warn(`⚠️ OG image generation failed for "${slug}":\n${err.stack}`);
39
+ console.warn(`[Domo-SSG] OG image generation failed for "${slug}":\n${err.stack}`);
42
40
  }
43
41
  }
44
42
  }
@@ -0,0 +1,17 @@
1
+ // domo.config.js
2
+ export default {
3
+ outDir: "./dist",
4
+ routesFile: "./routes.js",
5
+ layout: "./layout.js",
6
+ author: "Domo SSG",
7
+ baseUrl: "https://www.domo.zyrab.dev",
8
+ lang: "en",
9
+ theme: "auto",
10
+ exclude: ["js", "css", "app-ads.txt", "assets", "data"],
11
+ assets: {
12
+ scripts: ["test.js", "global.js", { href: "theme-toggle.js", preload: true }],
13
+ styles: [{ href: "main.css", prefetch: false }, "hoora.css"],
14
+ fonts: [{ href: "wohaha.woff2", preload: true }, "yasWemadeIt.woff2"],
15
+ favicon: "path/to/fivicon.ico",
16
+ },
17
+ };
@@ -0,0 +1,110 @@
1
+ import Router from "../../../packages/domo-router/src/core.js";
2
+ import createHeader from "./header.js";
3
+ export async function renderLayout(content, data) {
4
+ const {
5
+ title,
6
+ description,
7
+ descriptionOG,
8
+ scripts,
9
+ styles,
10
+ fonts,
11
+ favicon,
12
+ baseUrl,
13
+ canonical,
14
+ lang,
15
+ author,
16
+ type,
17
+ ogImage,
18
+ theme,
19
+ } = data;
20
+
21
+ const canonicalUrl = baseUrl + (canonical || Router.path());
22
+
23
+ const scriptTags = scripts
24
+ .map((file) =>
25
+ file.preload
26
+ ? `<link rel="preload" as="script" href="/js/${file.href}">
27
+ <script defer src="/js/${file.href}"></script>`
28
+ : `<script defer src="/js/${file.href || file}"></script>`
29
+ )
30
+ .join("\n");
31
+
32
+ const styleTags = styles
33
+ .map((style) =>
34
+ style.preload
35
+ ? `<link rel="preload" href="/css/${style.href}" as="style" onload="this.rel='stylesheet'">`
36
+ : `<link rel="stylesheet" href="/css/${style.href || style}">`
37
+ )
38
+ .join("\n");
39
+
40
+ const fontTags = fonts
41
+ .map((font) =>
42
+ font.preload
43
+ ? `<link rel="preload" href="/assets/fonts/${font.href}" as="font" type="font/woff2" crossorigin="anonymous">`
44
+ : `<link rel="stylesheet" href="assets/fonts/${font.href || font}">`
45
+ )
46
+ .join("\n");
47
+
48
+ const themeColor =
49
+ theme === "auto" || theme === "toggle"
50
+ ? ` <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
51
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
52
+ <meta name="color-scheme" content="light dark">`
53
+ : ` <meta name="theme-color" content="${theme === "dark" ? "#000000" : "#ffffff"}">
54
+ <meta name="color-scheme" content="${theme}">`;
55
+
56
+ return `<!DOCTYPE html>
57
+ <html lang="${lang}">
58
+ <head>
59
+ <meta charset="UTF-8">
60
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
61
+ <title>${title}</title>
62
+ <meta name="description" content="${description}">
63
+ <meta name="author" content="${author}">
64
+ <meta name="robots" content="index, follow">
65
+
66
+ <!-- Canonical -->
67
+ <link rel="canonical" href="${canonicalUrl}">
68
+
69
+ <!-- Social: OpenGraph -->
70
+ <meta property="og:title" content="${title}">
71
+ <meta property="og:description" content="${descriptionOG || description}">
72
+ <meta property="og:image" content="${ogImage}">
73
+ <meta property="og:url" content="${baseUrl}${Router.path()}">
74
+ <meta property="og:type" content="${type || "website"}">
75
+
76
+ <!-- Social: Twitter -->
77
+ <meta name="twitter:card" content="summary_large_image">
78
+ <meta name="twitter:title" content="${title}">
79
+ <meta name="twitter:description" content="${descriptionOG || description}">
80
+ <meta name="twitter:image" content="${ogImage}">
81
+
82
+ <!-- Favicon and Touch Icon -->
83
+ <link rel="icon" href="/assets/${favicon}" type="image/x-icon">
84
+ <link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
85
+
86
+ <!-- Theme Colors -->
87
+ ${themeColor}
88
+
89
+ <!-- Performance -->
90
+ ${fontTags}
91
+ ${styleTags}
92
+
93
+ <!-- Privacy & Security -->
94
+ <meta name="referrer" content="strict-origin-when-cross-origin">
95
+ <meta http-equiv="Permissions-Policy" content="interest-cohort=()">
96
+ <meta http-equiv="Content-Security-Policy" content="script-src 'self'">
97
+
98
+
99
+ <!-- Scripts: preload or noraml injection -->
100
+ ${scriptTags}
101
+
102
+ </head>
103
+ <body>
104
+ ${createHeader()}
105
+ <main>
106
+ ${content.build()}
107
+ </main>
108
+ </body>
109
+ </html>`;
110
+ }
@@ -0,0 +1,45 @@
1
+ import Home from "./pages/home.js";
2
+ import About from "./pages/about.js";
3
+ import Contacts from "./pages/contacts.js";
4
+ import createProjects from "./pages/projects.js";
5
+ import createProjectPage from "./components/project-page.js";
6
+ import Error from "./pages/errot.js";
7
+ import { loadJson } from "./load-json.js";
8
+
9
+ export const routes = {
10
+ "/": {
11
+ component: Home,
12
+ meta: { title: "Home" },
13
+ },
14
+ "/about": {
15
+ component: About,
16
+ script: ["test.js", { href: "js-with-props.js", preload: true }],
17
+ style: ["test.css", { href: "css-with-props.css", preload: true }],
18
+ font: ["test.woff2", { href: "font-with-props.woff2", preload: true }],
19
+ meta: { title: "About" },
20
+ },
21
+ "/contacts": {
22
+ component: Contacts,
23
+ meta: {
24
+ title: "contacts",
25
+ description: "page description",
26
+ descriptionOG: "Learn more about our mission and values.",
27
+ canonical: "/test/canonical", //u will need this if u have highly duplicated pages. good for SEO,
28
+ ogImage: "/test/image.png",
29
+ type: "product", // this is for twitter cards to define what type of content is on page, default is webste
30
+ },
31
+ },
32
+ "/projects": {
33
+ component: createProjects,
34
+ meta: { title: "contacts" },
35
+ "/:id": {
36
+ routeParams: async (parentRouteName) => await loadJson(parentRouteName), // parentRouteName = projects, this is usfull in deeply nested dinmic routs
37
+ component: createProjectPage,
38
+ meta: { title: "test page" },
39
+ },
40
+ },
41
+ "*": {
42
+ component: Error,
43
+ meta: { title: "error" },
44
+ },
45
+ };
File without changes