@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 +3 -3
- package/src/Registry.js +17 -0
- package/src/config.js +5 -0
- package/src/event-extraction.js +131 -76
- package/src/event-utils.js +99 -68
- package/src/file-utils.js +40 -0
- package/src/index.js +58 -1
- package/src/utils.js +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zyrab/domo-ssg",
|
|
3
|
-
"version": "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.
|
|
26
|
+
"@zyrab/domo": "^1.5.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
package/src/Registry.js
ADDED
|
@@ -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
|
};
|
package/src/event-extraction.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
}
|
|
42
|
-
function transformRef(refInfo) {
|
|
43
|
-
const { handler, name } = refInfo;
|
|
44
|
-
let fnSource = handler.toString();
|
|
44
|
+
let resolvedPath = providedPath;
|
|
45
45
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
60
|
+
let matchCounter = 0;
|
|
62
61
|
|
|
62
|
+
// 1. Process Events
|
|
63
63
|
events.forEach(({ event, handlers }) => {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/event-utils.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, existsSync, rmSync, readFileSync } from "fs";
|
|
2
|
-
import { join, dirname,
|
|
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
|
-
*
|
|
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
|
|
30
|
-
name: "domo
|
|
34
|
+
const rewriteDomoPlugin = {
|
|
35
|
+
name: "rewrite-domo",
|
|
31
36
|
setup(build) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { path:
|
|
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
|
-
|
|
58
|
-
|
|
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:
|
|
85
|
-
format: "
|
|
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:
|
|
117
|
-
format: "
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
176
|
+
// Outputs to: dist/js/islands/hash.js
|
|
177
|
+
entryPoints[`islands/${hash}`] = entryPath;
|
|
178
|
+
}
|
|
145
179
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|