@zyrab/domo-ssg 0.2.0 → 0.3.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 +1 -1
- package/src/config.js +26 -19
- package/src/index.js +3 -10
- package/src/route-handler.js +30 -17
- package/src/route-traversal.js +44 -89
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -1,37 +1,44 @@
|
|
|
1
1
|
// src/config.js
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
|
|
5
|
+
let mergedConfig = null;
|
|
6
|
+
|
|
7
|
+
export async function loadConfig() {
|
|
8
|
+
if (mergedConfig) return mergedConfig;
|
|
9
|
+
const userConfigPath = path.resolve(process.cwd(), "domo.config.js");
|
|
3
10
|
|
|
4
|
-
/**
|
|
5
|
-
* Loads and processes the SSG configuration.
|
|
6
|
-
* @param {string} configFilePath - Absolute path to the user's config file.
|
|
7
|
-
* @returns {Promise<object>} The resolved configuration object.
|
|
8
|
-
*/
|
|
9
|
-
export async function loadConfig(configFilePath) {
|
|
10
11
|
// Default configuration values
|
|
11
12
|
const defaultConfig = {
|
|
12
13
|
outDir: "./dist",
|
|
13
|
-
routesFile: "./routes.js",
|
|
14
|
-
layout: "./layout.js",
|
|
14
|
+
routesFile: "./routes.js",
|
|
15
|
+
layout: "./layout.js",
|
|
16
|
+
lang: "en",
|
|
17
|
+
author: "Domo",
|
|
15
18
|
exclude: ["css", "js", "assets", "robots.txt", "admin"],
|
|
16
|
-
baseUrl: "http://localhost:3000",
|
|
19
|
+
baseUrl: "http://localhost:3000",
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
let userConfig = {};
|
|
20
23
|
try {
|
|
21
|
-
const importedConfig = await import(
|
|
24
|
+
const importedConfig = await import(pathToFileURL(userConfigPath).href);
|
|
22
25
|
userConfig = importedConfig.default || importedConfig;
|
|
23
26
|
} catch (error) {
|
|
24
27
|
console.warn(`⚠️ No custom config file found at ${configFilePath}. Using default settings.`);
|
|
25
28
|
}
|
|
29
|
+
mergedConfig = {
|
|
30
|
+
...defaultConfig,
|
|
31
|
+
...userConfig,
|
|
32
|
+
outDir: path.resolve(process.cwd(), userConfig.outDir || defaultConfig.outDir),
|
|
33
|
+
routesFile: path.resolve(process.cwd(), userConfig.routesFile || defaultConfig.routesFile),
|
|
34
|
+
layout: path.resolve(process.cwd(), userConfig.layout || defaultConfig.layout),
|
|
35
|
+
};
|
|
36
|
+
return mergedConfig;
|
|
37
|
+
}
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// This assumes the config file itself specifies paths relative to its own location
|
|
32
|
-
mergedConfig.outDir = path.resolve(process.cwd(), mergedConfig.outDir);
|
|
33
|
-
mergedConfig.routesFile = path.resolve(process.cwd(), mergedConfig.routesFile);
|
|
34
|
-
mergedConfig.layout = path.resolve(process.cwd(), mergedConfig.layout);
|
|
35
|
-
|
|
39
|
+
export function getConfig() {
|
|
40
|
+
if (!mergedConfig) {
|
|
41
|
+
throw new Error("Config has not been loaded yet. Call loadConfig() first.");
|
|
42
|
+
}
|
|
36
43
|
return mergedConfig;
|
|
37
44
|
}
|
package/src/index.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
// src/index.js
|
|
1
|
+
// src/index.js
|
|
2
2
|
import { pathToFileURL } from "url";
|
|
3
|
-
import path from "path";
|
|
4
3
|
import { loadConfig } from "./config.js";
|
|
5
4
|
import { cleanOutputDir } from "./file-utils.js";
|
|
6
5
|
import { generateSitemap } from "./sitemap.js";
|
|
7
6
|
import { buildRoutes } from "./route-traversal.js";
|
|
8
7
|
|
|
9
|
-
// __filename and __dirname equivalents for ES Modules
|
|
10
|
-
|
|
11
8
|
async function main() {
|
|
12
|
-
|
|
13
|
-
// Assumes config is at the root of the project where the script is run
|
|
14
|
-
const userConfigPath = path.resolve(process.cwd(), "domo.config.js");
|
|
15
|
-
|
|
16
|
-
const config = await loadConfig(pathToFileURL(userConfigPath).href);
|
|
9
|
+
const config = await loadConfig();
|
|
17
10
|
|
|
18
11
|
// Import layout and route tree using pathToFileURL and .href for dynamic imports
|
|
19
12
|
const { routes } = await import(pathToFileURL(config.routesFile).href);
|
|
@@ -28,7 +21,7 @@ async function main() {
|
|
|
28
21
|
cleanOutputDir(config.outDir, config.exclude);
|
|
29
22
|
|
|
30
23
|
// 2. Build all routes recursively
|
|
31
|
-
await buildRoutes(routes,
|
|
24
|
+
await buildRoutes(routes, renderLayout);
|
|
32
25
|
|
|
33
26
|
// 3. Generate sitemap
|
|
34
27
|
generateSitemap(config.outDir, config.baseUrl, config.exclude);
|
package/src/route-handler.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/route-handler.js
|
|
2
2
|
import Router from "@zyrab/domo-router";
|
|
3
|
+
import { getConfig } from "./config.js";
|
|
3
4
|
import { writeHTML } from "./file-utils.js";
|
|
4
5
|
import { writeJs } from "./event-utils.js";
|
|
5
6
|
|
|
@@ -16,7 +17,6 @@ export function joinPaths(...segments) {
|
|
|
16
17
|
|
|
17
18
|
return "/" + pathStr.replace(/\/+/g, "/");
|
|
18
19
|
}
|
|
19
|
-
|
|
20
20
|
/**
|
|
21
21
|
* Handles the rendering and writing of a single route's HTML file.
|
|
22
22
|
* @param {object} routeConfig - Configuration for the current route.
|
|
@@ -26,42 +26,55 @@ export function joinPaths(...segments) {
|
|
|
26
26
|
* @param {string} [routeConfig.script] - Optional script to include in the layout.
|
|
27
27
|
* @param {object} [routeConfig.meta={}] - Optional metadata for the page (title, description).
|
|
28
28
|
* @param {Function} renderLayout - The layout rendering function from the user's config.
|
|
29
|
-
* @param {string} outputDir - The base output directory for generated files.
|
|
30
29
|
* @returns {Promise<void>}
|
|
31
30
|
*/
|
|
32
31
|
|
|
33
|
-
export async function handleRoute({ path, props, component,
|
|
32
|
+
export async function handleRoute({ path, props, component, scripts, styles, fonts, meta = {} }, renderLayout) {
|
|
33
|
+
const config = getConfig();
|
|
34
34
|
try {
|
|
35
35
|
// Set router info for server-side context
|
|
36
36
|
Router.setInfo(path, props);
|
|
37
37
|
|
|
38
|
-
// Calculate base depth for relative paths in layout if needed
|
|
39
|
-
const baseDepth = path === "/" ? 0 : path.split("/").filter(Boolean).length;
|
|
40
|
-
|
|
41
38
|
// Render the component content
|
|
42
39
|
const content = await component(props);
|
|
43
40
|
|
|
44
41
|
// --- Write JS file ---
|
|
45
|
-
const
|
|
46
|
-
let allScript = [];
|
|
47
|
-
if (Array.isArray(script) && script.length > 0) {
|
|
48
|
-
allScript.push(...script);
|
|
49
|
-
}
|
|
50
|
-
if (eScript && typeof eScript === "string" && eScript.trim() !== "") {
|
|
51
|
-
allScript.push(eScript);
|
|
52
|
-
}
|
|
42
|
+
const embededScript = writeJs(content, config.outDir, path);
|
|
53
43
|
|
|
44
|
+
const fontPaths = normalizeAssets([fonts, config.assets.fonts]);
|
|
45
|
+
const stylePaths = normalizeAssets([styles, config.assets.styles]);
|
|
46
|
+
const scriptPaths = normalizeAssets([embededScript, scripts, config.assets.scripts]);
|
|
54
47
|
// Render the full HTML layout
|
|
55
48
|
const html = await renderLayout(content, {
|
|
56
49
|
title: meta.title || "",
|
|
57
50
|
description: meta.description || "",
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
descriptionOG: meta.descriptionOG,
|
|
52
|
+
canonical: meta.canonical,
|
|
53
|
+
type: meta.type,
|
|
54
|
+
scripts: scriptPaths,
|
|
55
|
+
styles: stylePaths,
|
|
56
|
+
fonts: fontPaths,
|
|
57
|
+
favicon: config?.assets?.favicon,
|
|
58
|
+
baseUrl: config?.baseUrl,
|
|
59
|
+
lang: config?.lang,
|
|
60
|
+
author: config?.author,
|
|
61
|
+
theme: config?.theme,
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
// Write the generated HTML to a file
|
|
63
|
-
writeHTML(
|
|
65
|
+
writeHTML(config.outDir, path, html);
|
|
64
66
|
} catch (e) {
|
|
65
67
|
console.warn(`⚠️ Error rendering ${path}:\n${e.stack}`);
|
|
66
68
|
}
|
|
67
69
|
}
|
|
70
|
+
function normalizeAssets(arr) {
|
|
71
|
+
let result = [];
|
|
72
|
+
for (const el of arr) {
|
|
73
|
+
if (Array.isArray(el) && el.length > 0) {
|
|
74
|
+
result.push(...el);
|
|
75
|
+
} else if (typeof el === "string" && el.trim() !== "") {
|
|
76
|
+
result.push(el);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
package/src/route-traversal.js
CHANGED
|
@@ -4,77 +4,58 @@ import { handleRoute, joinPaths } from "./route-handler.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Recursively builds HTML files for all defined routes, including nested and dynamic routes.
|
|
6
6
|
* @param {object} routes - The route configuration object.
|
|
7
|
+
* @param {Function} renderLayout - The layout rendering function.
|
|
7
8
|
* @param {string} [parentPath=""] - The path accumulated from parent routes.
|
|
8
9
|
* @param {object} [props={}] - Accumulated properties from parent dynamic routes.
|
|
9
|
-
* @param {Function} renderLayout - The layout rendering function.
|
|
10
|
-
* @param {string} outputDir - The base output directory.
|
|
11
|
-
* @returns {Promise<void>}
|
|
12
10
|
*/
|
|
13
|
-
export async function buildRoutes(routes, parentPath = "", props = {}
|
|
14
|
-
for (const
|
|
15
|
-
const
|
|
11
|
+
export async function buildRoutes(routes, renderLayout, parentPath = "", props = {}) {
|
|
12
|
+
for (const routeSegment in routes) {
|
|
13
|
+
const routeNode = routes[routeSegment];
|
|
16
14
|
// Skip '/' if it's a child of another path (handled by parent's segment)
|
|
17
|
-
if (parentPath !== "" &&
|
|
15
|
+
if (parentPath !== "" && routeSegment === "/") continue;
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const currentRoute = joinPaths(parentPath, routeSegment);
|
|
18
|
+
// Handle dynamic routes with routeParams
|
|
19
|
+
if (routeNode.routeParams) {
|
|
21
20
|
try {
|
|
22
21
|
// Extract parameter name from dynamic segment (e.g., ":slug" -> "slug")
|
|
23
|
-
const
|
|
22
|
+
const paramName = routeSegment.split(":").filter(Boolean).pop();
|
|
24
23
|
// The last segment of the parent path is the slug for nested dynamic lists
|
|
25
|
-
const
|
|
24
|
+
const parentRouteName = parentPath.split("/").filter(Boolean).pop();
|
|
26
25
|
|
|
27
|
-
const
|
|
26
|
+
const resolvedParams = await routeNode.routeParams(parentRouteName);
|
|
28
27
|
|
|
29
|
-
if (!Array.isArray(
|
|
30
|
-
console.warn(`⚠️ No items returned for dynamic route at ${
|
|
28
|
+
if (!Array.isArray(resolvedParams) || resolvedParams.length === 0) {
|
|
29
|
+
console.warn(`⚠️ No items returned for dynamic route at ${currentRoute}`);
|
|
31
30
|
continue;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
for (const item of
|
|
35
|
-
if (!item[
|
|
36
|
-
console.warn(
|
|
37
|
-
`⚠️ Missing required parameter '${paramKey}' in item for dynamic route at ${joinPaths(
|
|
38
|
-
parentPath,
|
|
39
|
-
routeKey
|
|
40
|
-
)}`
|
|
41
|
-
);
|
|
33
|
+
for (const item of resolvedParams) {
|
|
34
|
+
if (!item[paramName]) {
|
|
35
|
+
console.warn(`⚠️ Missing required parameter '${paramName}' in item for dynamic route at ${currentRoute}`);
|
|
42
36
|
continue;
|
|
43
37
|
}
|
|
44
38
|
|
|
45
|
-
const segment = item[
|
|
46
|
-
const meta = {
|
|
39
|
+
const segment = item[paramName]; // Use the actual value from the item for the URL segment
|
|
40
|
+
const meta = {
|
|
41
|
+
title: item.title,
|
|
42
|
+
description: item.description,
|
|
43
|
+
type: item?.type,
|
|
44
|
+
canonical: item?.canonical,
|
|
45
|
+
ogImage: item?.ogImage,
|
|
46
|
+
descriptionOG: item?.descriptionOG,
|
|
47
|
+
}; // Merge item's meta with route's meta
|
|
47
48
|
const routePath = joinPaths(parentPath, segment); // Full path for this specific dynamic item
|
|
48
|
-
const childProps = { ...props, [
|
|
49
|
+
const childProps = { ...props, [paramName]: segment, itemData: item }; // Pass item data as prop
|
|
49
50
|
|
|
50
|
-
if (
|
|
51
|
-
await handleRoute(
|
|
52
|
-
|
|
53
|
-
path: routePath,
|
|
54
|
-
props: childProps,
|
|
55
|
-
script: r.script,
|
|
56
|
-
component: r.component,
|
|
57
|
-
meta,
|
|
58
|
-
},
|
|
59
|
-
renderLayout,
|
|
60
|
-
outputDir
|
|
61
|
-
);
|
|
62
|
-
} else if (r.children?.["/"]?.component) {
|
|
51
|
+
if (routeNode.component) {
|
|
52
|
+
await handleRoute({ ...routeNode, path: routePath, props: childProps, meta }, renderLayout);
|
|
53
|
+
} else if (routeNode.children?.["/"]?.component) {
|
|
63
54
|
// If dynamic route has children with a default component
|
|
64
|
-
await handleRoute(
|
|
65
|
-
{
|
|
66
|
-
path: routePath,
|
|
67
|
-
props: childProps,
|
|
68
|
-
script: r.children["/"].script,
|
|
69
|
-
component: r.children["/"].component,
|
|
70
|
-
meta: r.children["/"].meta || meta, // Children's meta takes precedence
|
|
71
|
-
},
|
|
72
|
-
renderLayout,
|
|
73
|
-
outputDir
|
|
74
|
-
);
|
|
55
|
+
await handleRoute({ ...routeNode.children["/"], path: routePath, props: childProps, meta }, renderLayout);
|
|
75
56
|
// Recursively build children of this dynamic item if they exist
|
|
76
|
-
if (
|
|
77
|
-
await buildRoutes(
|
|
57
|
+
if (routeNode.children) {
|
|
58
|
+
await buildRoutes(routeNode.children, renderLayout, routePath, childProps);
|
|
78
59
|
}
|
|
79
60
|
} else {
|
|
80
61
|
console.warn(
|
|
@@ -83,53 +64,27 @@ export async function buildRoutes(routes, parentPath = "", props = {}, renderLay
|
|
|
83
64
|
}
|
|
84
65
|
}
|
|
85
66
|
} catch (e) {
|
|
86
|
-
console.warn(`⚠️ Skipped dynamic route generation for ${
|
|
67
|
+
console.warn(`⚠️ Skipped dynamic route generation for ${currentRoute}: ${e.message}`);
|
|
87
68
|
}
|
|
88
69
|
continue; // Move to the next route key after handling dynamic list
|
|
89
70
|
}
|
|
90
71
|
|
|
91
72
|
// Handle static routes with a component
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
await handleRoute(
|
|
95
|
-
{
|
|
96
|
-
path: routePath,
|
|
97
|
-
props,
|
|
98
|
-
script: r.script,
|
|
99
|
-
component: r.component,
|
|
100
|
-
meta: r.meta,
|
|
101
|
-
},
|
|
102
|
-
renderLayout,
|
|
103
|
-
outputDir
|
|
104
|
-
);
|
|
73
|
+
if (routeNode.component) {
|
|
74
|
+
await handleRoute({ path: currentRoute, ...routeNode, props }, renderLayout);
|
|
105
75
|
// Continue to children if they exist for this static route
|
|
106
|
-
if (
|
|
107
|
-
await buildRoutes(
|
|
76
|
+
if (routeNode.children) {
|
|
77
|
+
await buildRoutes(routeNode.children, renderLayout, currentRoute, { ...props });
|
|
108
78
|
}
|
|
109
79
|
continue;
|
|
110
80
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (r.children["/"]?.component) {
|
|
117
|
-
await handleRoute(
|
|
118
|
-
{
|
|
119
|
-
path: routePath,
|
|
120
|
-
props,
|
|
121
|
-
script: r.children["/"].script,
|
|
122
|
-
component: r.children["/"].component,
|
|
123
|
-
meta: r.children["/"].meta,
|
|
124
|
-
},
|
|
125
|
-
renderLayout,
|
|
126
|
-
outputDir
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
console.warn(`⚠️ Route at ${routePath} has children but no default component ('/')`);
|
|
130
|
-
}
|
|
131
|
-
// Recursively build the children
|
|
132
|
-
await buildRoutes(r.children, routePath, { ...props }, renderLayout, outputDir);
|
|
81
|
+
// Render the default child component ('/') for this path if it exists
|
|
82
|
+
if (routeNode.children["/"]?.component) {
|
|
83
|
+
await handleRoute({ path: currentRoute, ...routeNode.children["/"], props }, renderLayout);
|
|
84
|
+
} else if (routeNode.children) {
|
|
85
|
+
console.warn(`⚠️ Route at ${currentRoute} has children but no default component ('/')`);
|
|
133
86
|
}
|
|
87
|
+
// Recursively build the children
|
|
88
|
+
await buildRoutes(routeNode.children, renderLayout, currentRoute, { ...props });
|
|
134
89
|
}
|
|
135
90
|
}
|