@zyrab/domo-ssg 0.6.0 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zyrab/domo-ssg",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "A Static Site Generator (SSG) for Domo-based projects.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -17,13 +17,13 @@
17
17
  "author": "Zyrab",
18
18
  "license": "MIT",
19
19
  "peerDependencies": {
20
- "@zyrab/domo": "^1.4.0",
21
20
  "@zyrab/domo-og": "0.2.1",
21
+ "@zyrab/domo": "^1.5.0",
22
22
  "@zyrab/domo-router": "^0.3.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "esbuild": "^0.20.0",
26
- "@zyrab/domo": "^1.4.0"
26
+ "@zyrab/domo": "^1.5.0"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
@@ -0,0 +1,17 @@
1
+ // src/registry.js
2
+ class BuildRegistry {
3
+ constructor() {
4
+ this.routePaths = {};
5
+ }
6
+
7
+ setRoutes(routes) {
8
+ this.routePaths = routes;
9
+ }
10
+
11
+ getRoute(name) {
12
+ return this.routePaths[name];
13
+ }
14
+ }
15
+
16
+ // Export a single instance to be shared across the build process
17
+ export const registry = new BuildRegistry();
package/src/config.js CHANGED
@@ -10,10 +10,15 @@ export async function loadConfig() {
10
10
 
11
11
  const defaultConfig = {
12
12
  outDir: "./dist",
13
+ scanDir: "./src",
13
14
  routesFile: "./routes.js",
14
15
  layout: "./layout.js",
15
16
  lang: "en",
16
17
  author: "Zyrab",
18
+ assetsDir: [
19
+ { current: "src/assets", final: "assets" },
20
+ { current: "src/styles", final: "styles" },
21
+ ],
17
22
  exclude: ["css", "js", "assets", "robots.txt", "admin"],
18
23
  baseUrl: "http://localhost:3000",
19
24
  };
@@ -1,107 +1,162 @@
1
1
  /**
2
2
  * @file event_extraction.js
3
- * @description Logic for extracting events, state, and references for SSG/SPA bundling.
4
3
  */
5
-
6
4
  import { createHash } from "crypto";
5
+ import { resolve } from "path";
6
+ import { registry } from "./Registry.js";
7
+ import { formatComponentName } from "./utils.js";
8
+
9
+ const indent = (code, spaces = 2) =>
10
+ code
11
+ .split("\n")
12
+ .map((line) => " ".repeat(spaces) + line)
13
+ .join("\n");
14
+
15
+ function destructureFunction(fnSource) {
16
+ const match =
17
+ fnSource.match(/^(?:async\s+)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>\s*\{?([\s\S]*?)\}?$/) ||
18
+ fnSource.match(/^(?:async\s+)?function\s*[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
19
+
20
+ const nameMatch = fnSource.match(/function\s+([a-zA-Z0-9_$]+)/);
21
+
22
+ return {
23
+ name: nameMatch ? nameMatch[1] : null,
24
+ body: match ? match[1].trim() : fnSource.trim(),
25
+ };
26
+ }
7
27
 
8
28
  /**
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.
29
+ * NEW HELPER: Analyzes ANY handler (event or ref) and figures out
30
+ * if it needs to be imported, inlined as a closure, or stringified.
12
31
  */
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
- }
32
+ function resolveDependency(handlerObj) {
33
+ const { handler, path: providedPath, name: providedName } = handlerObj;
34
+ const fnSource = handler.toString();
35
+ const { name: extractedName, body } = destructureFunction(fnSource);
30
36
 
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
- }
37
+ let funcName = providedName || handler.name || extractedName;
35
38
 
36
- if (type === "match") {
37
- return `{\n if (e.target.matches("${selector}")) {\n const target = e.target;\n ${body}\n }\n }`;
39
+ // Ignore auto-inferred object keys like "#box" or ".btn"
40
+ if (funcName && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(funcName)) {
41
+ funcName = null;
38
42
  }
39
43
 
40
- return body;
41
- }
42
- function transformRef(refInfo) {
43
- const { handler, name } = refInfo;
44
- let fnSource = handler.toString();
44
+ let resolvedPath = providedPath;
45
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;
46
+ // Registry lookup
47
+ if (!resolvedPath && funcName) {
48
+ const fileKey = formatComponentName(funcName);
49
+ const lookupPath = registry.getRoute(fileKey);
50
+ if (lookupPath) resolvedPath = lookupPath;
51
+ }
51
52
 
52
- return `{\n const el = document.getElementById("${refInfo.id}");\n if (el) {\n const callback = (el) => { ${body} };\n callback(el);\n }\n }`;
53
+ return { funcName, resolvedPath, fnSource, body };
53
54
  }
54
55
 
55
- /**
56
- * Generates the ESM content for a specific element's events.
57
- */
58
- export function generateElementScript(id, events, states, refs) {
56
+ export function generateElementScript(id, events = [], states = {}, refs = []) {
59
57
  const imports = new Map();
58
+ const closureFunctions = [];
60
59
  const listeners = [];
61
- const refLogics = [];
60
+ let matchCounter = 0;
62
61
 
62
+ // 1. Process Events
63
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);
64
+ const logicLines = [];
65
+
66
+ handlers.forEach((h) => {
67
+ const { type, selector } = h;
68
+
69
+ // Use our new smart helper!
70
+ const { funcName, resolvedPath, fnSource, body } = resolveDependency(h);
71
+
72
+ // Register imports or closures globally for this file BEFORE writing logic
73
+ if (resolvedPath) {
74
+ if (!imports.has(resolvedPath)) imports.set(resolvedPath, new Set());
75
+ imports.get(resolvedPath).add(funcName);
76
+ } else if (funcName) {
77
+ closureFunctions.push(fnSource);
78
+ }
79
+
80
+ // ---> BUILD THE LOGIC LINES <---
81
+ if (type === "closest") {
82
+ const matchVar = `match${++matchCounter}`;
83
+ logicLines.push(`const ${matchVar} = e.target.closest("${selector}");`);
84
+
85
+ if (resolvedPath || funcName) {
86
+ logicLines.push(`if (${matchVar}) ${funcName}(e, ${matchVar});`);
87
+ } else {
88
+ const adjustedBody = body.replace(/\btarget\b/g, matchVar);
89
+ logicLines.push(`if (${matchVar}) {\n${indent(adjustedBody, 2)}\n}`);
69
90
  }
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 }));
91
+ } else if (type === "match") {
92
+ const matchExpr = `e.target.matches("${selector}")`;
93
+
94
+ if (resolvedPath || funcName) {
95
+ logicLines.push(`if (${matchExpr}) ${funcName}(e, e.target);`);
96
+ } else {
97
+ const adjustedBody = body.replace(/\btarget\b/g, "e.target");
98
+ logicLines.push(`if (${matchExpr}) {\n${indent(adjustedBody, 2)}\n}`);
99
+ }
100
+ } else if (type === "direct") {
101
+ // THIS COVERS YOUR .on() METHODS
102
+ if (resolvedPath || funcName) {
103
+ logicLines.push(`${funcName}(e);`);
104
+ } else {
105
+ logicLines.push(body);
106
+ }
107
+ }
108
+ });
109
+
110
+ const handlerBody = `async function(e) { e.pre\n${indent(logicLines.join("\n"), 1)}\n}`;
111
+ listeners.push(`document.querySelector('[data-domo-id="${id}"]').addEventListener("${event}", ${handlerBody});`);
90
112
  });
91
113
 
114
+ // 2. Process Refs (Now using the exact same smart lookup!)
115
+ const refLogics = refs
116
+ .map((r) => {
117
+ const { funcName, resolvedPath, fnSource, body } = resolveDependency(r);
118
+
119
+ // Track imports/closures just like events
120
+ if (resolvedPath) {
121
+ if (!imports.has(resolvedPath)) imports.set(resolvedPath, new Set());
122
+ imports.get(resolvedPath).add(funcName);
123
+ } else if (funcName) {
124
+ closureFunctions.push(fnSource);
125
+ }
126
+
127
+ // Output the Ref logic
128
+ if (resolvedPath || funcName) {
129
+ // If it's a named/imported function, pass the element to it: myRefFunction(el)
130
+ return `{\n const el = document.querySelector('[data-domo-id="${id}"]');\n if (el) ${funcName}(el);\n}`;
131
+ } else {
132
+ // If anonymous, inline the body
133
+ return `{\n const el = document.querySelector('[data-domo-id="${id}"]');\n if (el) {\n${indent(body, 4)}\n }\n}`;
134
+ }
135
+ })
136
+ .join("\n");
137
+
138
+ // 3. Assemble State
139
+ const stateInclusion = Object.entries(states)
140
+ .map(([key, val]) => `let ${key} = ${JSON.stringify(val)};`)
141
+ .join("\n");
142
+
143
+ // 4. Assemble Imports
92
144
  let importStr = "";
93
-
94
145
  for (const [path, names] of imports) {
95
- importStr += `import { ${[...names].join(", ")} } from "${path}";\n`;
146
+ const absolutePath = resolve(process.cwd(), path).replace(/\\/g, "/");
147
+ importStr += `import { ${[...names].join(", ")} } from "${absolutePath}";\n`;
96
148
  }
97
- const combinedLogic = [...refLogics, ...listeners].join("\n");
98
149
 
99
- return `${importStr}\n(function() {\n${combinedLogic}\n})();`;
150
+ // 5. Final Assembly
151
+ const closures = closureFunctions.length
152
+ ? `\n\n// Inline Functions\n${indent(closureFunctions.join("\n\n"), 2)}`
153
+ : "";
154
+
155
+ const combinedLogic = [stateInclusion, refLogics, ...listeners].filter(Boolean).join("\n\n");
156
+
157
+ return `${importStr}{\n${indent(combinedLogic, 2)}${closures}\n}`.trim();
100
158
  }
101
159
 
102
- /**
103
- * Hash helper for caching
104
- */
105
160
  export function getHash(content) {
106
161
  return createHash("sha1").update(content).digest("hex").slice(0, 8);
107
162
  }
@@ -1,8 +1,11 @@
1
1
  import { writeFileSync, mkdirSync, existsSync, rmSync, readFileSync } from "fs";
2
- import { join, dirname, relative } from "path";
2
+ import { join, dirname, resolve } from "path";
3
3
  import { fileURLToPath, pathToFileURL } from "url";
4
4
  import { build } from "esbuild";
5
+ import { createRequire } from "module";
5
6
  import { generateElementScript, getHash } from "./event-extraction.js";
7
+ import { registry } from "./Registry.js";
8
+ import { formatComponentName } from "./utils.js";
6
9
 
7
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
11
  const require = createRequire(import.meta.url);
@@ -24,14 +27,16 @@ const cache = {
24
27
  };
25
28
 
26
29
  /**
27
- * Plugin to convert imports of 'domo' into the global 'Domo' variable
30
+ * Converts a function name to your file naming convention based on strict patterns.
31
+ * @param {string} funcName - e.g., "createHeader", "createPreviewPage", "copyCode"
32
+ * @returns {string} - e.g., "header", "preview-page", "handle-copy-code"
28
33
  */
29
- const makeDomoExternalPlugin = {
30
- name: "domo-external",
34
+ const rewriteDomoPlugin = {
35
+ name: "rewrite-domo",
31
36
  setup(build) {
32
- // Intercept any import relating to domo
33
- build.onResolve({ filter: /^domo$|^@zyrab\/domo/ }, (args) => {
34
- return { path: args.path, external: true };
37
+ build.onResolve({ filter: /^@zyrab\/domo$|^domo$/ }, (args) => {
38
+ // Points the browser to your pre-bundled runtime
39
+ return { path: "/js/domo.runtime.js", external: true };
35
40
  });
36
41
  },
37
42
  };
@@ -53,13 +58,22 @@ export function collectMetadata(node, out = { events: [], islands: [] }) {
53
58
  });
54
59
  }
55
60
 
56
- if (el?._island) {
57
- out.islands.push({
58
- id: el._attr["data-domo-id"] || el._attr["id"],
59
- path: el.__file,
60
- });
61
- }
61
+ if (el?._island && el?.__island) {
62
+ const rawName = el.__island.name; // e.g., "createPreviewPage"
63
+ const fileKey = formatComponentName(rawName); // "preview-page"
62
64
 
65
+ // Look up the exact file path from your singleton
66
+ const filePath = registry.getRoute(fileKey);
67
+
68
+ if (!filePath) {
69
+ console.warn(`[Domo-SSG] Could not find file for island component: ${rawName}`);
70
+ } else {
71
+ out.islands.push({
72
+ id: el._attr["data-domo-id"] || el._attr["id"],
73
+ path: filePath,
74
+ });
75
+ }
76
+ }
63
77
  if (Array.isArray(el?._child)) {
64
78
  for (const child of el._child) {
65
79
  collectMetadata(child, out);
@@ -81,9 +95,8 @@ async function bundleRuntime(outputDir) {
81
95
  await build({
82
96
  entryPoints: [DOMO_CLIENT_SOURCE],
83
97
  bundle: true,
84
- minify: false,
85
- format: "iife",
86
- globalName: "Domo",
98
+ minify: true,
99
+ format: "esm",
87
100
  outfile: out,
88
101
  platform: "browser",
89
102
  });
@@ -113,9 +126,13 @@ async function bundleEvents(metadata, jsDir, tempDir) {
113
126
  await build({
114
127
  entryPoints: [entry],
115
128
  bundle: true,
116
- minify: false,
117
- format: "iife",
129
+ minify: true,
130
+ format: "esm",
118
131
  outfile: join(jsDir, file),
132
+ packages: "external",
133
+
134
+ plugins: [rewriteDomoPlugin], // Injects our Domo rewrite
135
+
119
136
  platform: "browser",
120
137
  });
121
138
 
@@ -125,62 +142,76 @@ async function bundleEvents(metadata, jsDir, tempDir) {
125
142
  return file;
126
143
  }
127
144
 
128
- /**
129
- * Bundle islands (deduped by CONTENT, not path)
130
- */
131
145
  async function bundleIslands(metadata, jsDir, tempDir) {
132
- const results = [];
133
146
  const islandsToBundle = metadata.islands.filter((i) => i.path);
147
+ if (islandsToBundle.length === 0) return [];
148
+
149
+ const entryPoints = {};
150
+
151
+ // Create wrappers for the islands
152
+ for (const island of islandsToBundle) {
153
+ const { path: filePath, id } = island;
154
+ const content = readFileSync(filePath, "utf8");
155
+ const hash = getHash(content);
156
+
157
+ const entryPath = join(tempDir, `${hash}.entry.js`);
158
+ const absolutePath = resolve(process.cwd(), filePath).replace(/\\/g, "/");
159
+
160
+ const wrapper = `
161
+ import Island from "${absolutePath}";
162
+
163
+ const el = document.querySelector('[data-domo-id="${id}"]');
164
+ if (el) {
165
+ const instance = Island();
166
+ if (instance && instance._isDomo) {
167
+ el.appendChild(instance.build());
168
+ } else if (instance instanceof DocumentFragment || instance instanceof HTMLElement) {
169
+ el.appendChild(instance);
170
+ }
171
+ }
172
+ `;
134
173
 
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);
174
+ writeFileSync(entryPath, wrapper, "utf8");
140
175
 
141
- if (cache.islands.has(hash)) {
142
- results.push({ path: filePath, file: cache.islands.get(hash) });
143
- return;
144
- }
176
+ // Outputs to: dist/js/islands/hash.js
177
+ entryPoints[`islands/${hash}`] = entryPath;
178
+ }
145
179
 
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],
175
- });
180
+ // 2. The Modern esbuild Call
181
+ const result = await build({
182
+ entryPoints,
183
+ bundle: true,
184
+ splitting: true,
185
+ minify: true,
186
+ format: "esm",
187
+ outdir: jsDir,
188
+
189
+ // --> THE MAGIC BULLET FOR NPM PACKAGES <--
190
+ packages: "external",
191
+
192
+ plugins: [rewriteDomoPlugin], // Injects our Domo rewrite
176
193
 
177
- rmSync(entryPath);
178
- cache.islands.set(hash, file);
179
- results.push({ path: filePath, file });
180
- }),
181
- );
194
+ // Tells esbuild how to name the shared files so it looks like your project
195
+ // instead of random chunk strings
196
+ chunkNames: "components/[name]-[hash]",
182
197
 
183
- return results;
198
+ metafile: true,
199
+ platform: "browser",
200
+ });
201
+
202
+ // Clean up temp files
203
+ Object.values(entryPoints).forEach((entryPath) => rmSync(entryPath));
204
+
205
+ // Extract generated paths for injection
206
+ const allGeneratedPaths = Object.keys(result.metafile.outputs).map((filePath) => {
207
+ const relativeToDist = filePath.replace(/\\/g, "/").split("/").slice(1).join("/");
208
+ let path = `/${relativeToDist}`;
209
+
210
+ // 3. Remove "js/" specifically if it appears immediately after the leading slash
211
+ // This transforms "/js/main.js" -> "/main.js"
212
+ return path.replace(/^\/js\//, "/");
213
+ });
214
+ return allGeneratedPaths.filter((path) => path.endsWith(".js"));
184
215
  }
185
216
  /**
186
217
  * Main orchestrator
@@ -204,5 +235,5 @@ export async function writeJs(content, outputDir) {
204
235
  bundleIslands(metadata, jsDir, tempDir),
205
236
  ]);
206
237
 
207
- return [runtime, events, ...islands.map((i) => i.file)].filter(Boolean);
238
+ return [runtime, events, ...islands].filter(Boolean);
208
239
  }
package/src/file-utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/file-utils.js
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import { existsSync, cpSync } from "fs";
4
5
 
5
6
  /**
6
7
  * Ensures that the directory for a given file path exists.
@@ -46,3 +47,42 @@ export function cleanOutputDir(outputDir, exclude) {
46
47
  fs.mkdirSync(outputDir, { recursive: true });
47
48
  }
48
49
  }
50
+
51
+ /**
52
+ * Recursively copies a folder from source to destination.
53
+ */
54
+ export function copyStaticFolder(srcPath, destPath) {
55
+ if (!existsSync(srcPath)) {
56
+ return; // If the folder doesn't exist, just silently skip it
57
+ }
58
+
59
+ try {
60
+ // cpSync copies the whole folder and its contents synchronously
61
+ cpSync(srcPath, destPath, { recursive: true });
62
+ console.log(`[Domo-SSG] Copied static folder: ${srcPath} -> ${destPath}`);
63
+ } catch (error) {
64
+ console.error(`[Domo-SSG] Failed to copy ${srcPath}:`, error);
65
+ }
66
+ }
67
+
68
+ export function scanRoutes(dir) {
69
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
70
+
71
+ const routes = {};
72
+
73
+ for (const entry of entries) {
74
+ const fullPath = path.join(dir, entry.name);
75
+
76
+ if (entry.isDirectory()) {
77
+ Object.assign(routes, scanRoutes(fullPath));
78
+ } else {
79
+ if (!entry.name.endsWith(".js")) continue;
80
+
81
+ const fileName = path.basename(entry.name, ".js");
82
+
83
+ routes[fileName] = fullPath;
84
+ }
85
+ }
86
+
87
+ return routes;
88
+ }
package/src/index.js CHANGED
@@ -1,19 +1,24 @@
1
1
  // src/index.js
2
2
  import { pathToFileURL } from "url";
3
3
  import { loadConfig } from "./config.js";
4
- import { cleanOutputDir } from "./file-utils.js";
4
+ import { cleanOutputDir, copyStaticFolder, scanRoutes } from "./file-utils.js";
5
5
  import { generateSitemap } from "./sitemap.js";
6
6
  import { buildRoutes } from "./route-traversal.js";
7
+ import { registry } from "./Registry.js";
8
+ import { join } from "path";
7
9
 
8
10
  async function main() {
9
11
  const config = await loadConfig();
12
+ const routePaths = scanRoutes(config.scanDir);
10
13
 
14
+ registry.setRoutes(routePaths);
11
15
  const { routes } = await import(pathToFileURL(config.routesFile).href);
12
16
  const { renderLayout } = await import(pathToFileURL(config.layout).href);
13
17
 
14
18
  console.log("[Domo-SSG] Starting Domo SSG build...");
15
19
 
16
20
  cleanOutputDir(config.outDir, config.exclude);
21
+ config.assetsDir.forEach((f) => copyStaticFolder(join(process.cwd(), f.current), join(config.outDir, f.final)));
17
22
 
18
23
  await buildRoutes(routes, renderLayout);
19
24
 
@@ -26,3 +31,55 @@ main().catch((error) => {
26
31
  console.error("[Domo-SSG] build failed:", error);
27
32
  process.exit(1);
28
33
  });
34
+
35
+ // import { writeFileSync } from "fs";
36
+ // import { build } from "esbuild";
37
+
38
+ // // Your existing plugin to cleanly handle Domo
39
+ // const rewriteDomoPlugin = {
40
+ // name: "rewrite-domo",
41
+ // setup(build) {
42
+ // build.onResolve({ filter: /^@zyrab\/domo$|^domo$/ }, () => {
43
+ // return { path: "/js/domo.runtime.js", external: true };
44
+ // });
45
+ // },
46
+ // };
47
+
48
+ // export async function preBundleAssets(routePaths, outputDir) {
49
+ // // Grab all file paths from your registry/scanner
50
+ // const allSourceFiles = Object.values(routePaths);
51
+
52
+ // const result = await build({
53
+ // entryPoints: allSourceFiles,
54
+ // outdir: join(outputDir, "js"),
55
+ // entryNames: "[dir]/[name]-[hash]", // e.g., islands/header-A1B2C.js
56
+ // format: "esm",
57
+ // bundle: true,
58
+ // splitting: true,
59
+ // minify: true, // Minify everything!
60
+ // metafile: true,
61
+ // packages: "external",
62
+ // plugins: [rewriteDomoPlugin],
63
+ // });
64
+
65
+ // // --- GENERATE THE MANIFEST ---
66
+ // const manifest = {};
67
+ // const outputs = result.metafile.outputs;
68
+
69
+ // for (const [outputPath, info] of Object.entries(outputs)) {
70
+ // // If this output file was generated from an entry point, map it!
71
+ // if (info.entryPoint) {
72
+ // // Normalize paths for lookup
73
+ // const originalPath = info.entryPoint.replace(/\\/g, "/");
74
+ // const finalBrowserPath = `/${outputPath.replace(/\\/g, "/").split("/").slice(1).join("/")}`;
75
+
76
+ // manifest[originalPath] = finalBrowserPath;
77
+ // }
78
+ // }
79
+
80
+ // // Save manifest so the SSG can use it
81
+ // writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2));
82
+ // console.log("[Domo-SSG] Client assets pre-bundled. Manifest generated.");
83
+
84
+ // return manifest;
85
+ // }
package/src/utils.js CHANGED
@@ -40,3 +40,33 @@ export async function tryGenerateOgImage(routeMeta, ogOutputPath, path) {
40
40
  }
41
41
  }
42
42
  }
43
+
44
+ export function formatComponentName(funcName) {
45
+ if (!funcName) return "";
46
+
47
+ // Helper to turn camelCase or PascalCase into kebab-case
48
+ const toKebab = (str) => {
49
+ return str
50
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
51
+ .toLowerCase()
52
+ .replace(/^-/, ""); // Catch accidental leading dashes
53
+ };
54
+
55
+ // Rule 1: UI Components (Start with "create")
56
+ if (funcName.startsWith("create")) {
57
+ const strippedName = funcName.replace(/^create/, "");
58
+ return toKebab(strippedName);
59
+ }
60
+
61
+ // Rule 2: Handlers (Everything else gets a "handle-" prefix)
62
+ else {
63
+ const baseName = toKebab(funcName);
64
+
65
+ // Just a safety check in case you occasionally name the function "handleSomething"
66
+ if (baseName.startsWith("handle-")) {
67
+ return baseName;
68
+ }
69
+
70
+ return `handle-${baseName}`;
71
+ }
72
+ }