@zyrab/domo-ssg 0.5.4 → 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 +9 -4
- package/src/event-extraction.js +107 -0
- package/src/event-utils.js +174 -143
- package/src/route-handler.js +2 -3
- package/templates/domo.config.js +17 -0
- package/templates/layout.js +110 -0
- package/templates/routes.js +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zyrab/domo-ssg",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
21
|
-
"@zyrab/domo-og": "0.2.
|
|
20
|
+
"@zyrab/domo": "^1.4.0",
|
|
21
|
+
"@zyrab/domo-og": "0.2.1",
|
|
22
22
|
"@zyrab/domo-router": "^0.3.1"
|
|
23
23
|
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"esbuild": "^0.20.0",
|
|
26
|
+
"@zyrab/domo": "^1.4.0"
|
|
27
|
+
},
|
|
24
28
|
"publishConfig": {
|
|
25
29
|
"access": "public"
|
|
26
30
|
},
|
|
27
31
|
"files": [
|
|
28
32
|
"src/",
|
|
29
|
-
"
|
|
33
|
+
"templates",
|
|
34
|
+
"README.md"
|
|
30
35
|
],
|
|
31
36
|
"scripts": {
|
|
32
37
|
"build": "node src/index.js",
|
|
@@ -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
|
+
}
|
package/src/event-utils.js
CHANGED
|
@@ -1,177 +1,208 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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 (
|
|
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
|
-
|
|
65
|
+
collectMetadata(child, out);
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
return out;
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Bundle event logic
|
|
97
|
+
*/
|
|
98
|
+
async function bundleEvents(metadata, jsDir, tempDir) {
|
|
99
|
+
if (metadata.events.length === 0) return null;
|
|
80
100
|
|
|
81
|
-
|
|
101
|
+
const raw = metadata.events
|
|
102
|
+
.map(({ id, events, states, refs }) => generateElementScript(id, events, states, refs))
|
|
103
|
+
.join("\n\n");
|
|
82
104
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
95
|
-
}
|
|
108
|
+
const file = `${hash}.events.js`;
|
|
109
|
+
const entry = join(tempDir, `${hash}.entry.js`);
|
|
96
110
|
|
|
97
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
}
|
|
122
|
+
rmSync(entry);
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
|
|
177
|
+
rmSync(entryPath);
|
|
178
|
+
cache.islands.set(hash, file);
|
|
179
|
+
results.push({ path: filePath, file });
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
138
182
|
|
|
139
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
195
|
+
const jsDir = join(outputDir, "js");
|
|
196
|
+
const tempDir = join(outputDir, ".domo_temp");
|
|
155
197
|
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
207
|
+
return [runtime, events, ...islands.map((i) => i.file)].filter(Boolean);
|
|
177
208
|
}
|
package/src/route-handler.js
CHANGED
|
@@ -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
|
|
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([
|
|
47
|
+
const scriptPaths = normalizeAssets([bundlePath, scripts, assets.scripts]);
|
|
49
48
|
|
|
50
49
|
const html = await renderLayout(content, {
|
|
51
50
|
scripts: scriptPaths,
|
|
@@ -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
|
+
};
|