@cocreate/server-side-render 1.12.7 → 1.14.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/.github/workflows/automated.yml +25 -38
- package/CHANGELOG.md +33 -0
- package/package.json +3 -3
- package/release.config.js +12 -4
- package/src/index.js +405 -266
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
name: Automated Workflow
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
4
|
push:
|
|
4
5
|
branches:
|
|
5
6
|
- master
|
|
7
|
+
|
|
6
8
|
jobs:
|
|
7
9
|
about:
|
|
8
10
|
runs-on: ubuntu-latest
|
|
@@ -18,52 +20,37 @@ jobs:
|
|
|
18
20
|
with:
|
|
19
21
|
direction: overwrite-github
|
|
20
22
|
githubToken: "${{ secrets.GITHUB }}"
|
|
23
|
+
|
|
21
24
|
release:
|
|
22
25
|
runs-on: ubuntu-latest
|
|
23
26
|
steps:
|
|
24
27
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
26
|
-
- name: Setup Node.js
|
|
27
|
-
uses: actions/setup-node@v3
|
|
28
|
+
uses: actions/checkout@v4
|
|
28
29
|
with:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
fetch-depth: 0 # Required so semantic-release can trace git tags/history
|
|
31
|
+
|
|
32
|
+
- name: Setup Node.js
|
|
33
|
+
uses: actions/setup-node@v4
|
|
33
34
|
with:
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
node-version: 22
|
|
36
|
+
|
|
37
|
+
- name: Install Semantic Release & Plugins
|
|
38
|
+
# Installs semantic-release and its plugins on the runner
|
|
39
|
+
run: |
|
|
40
|
+
npm install -g semantic-release \
|
|
41
|
+
@semantic-release/changelog \
|
|
42
|
+
@semantic-release/npm \
|
|
43
|
+
@semantic-release/github \
|
|
36
44
|
@semantic-release/git
|
|
37
|
-
|
|
45
|
+
|
|
46
|
+
- name: Run Semantic Release (Native)
|
|
47
|
+
id: semantic
|
|
48
|
+
# This will automatically pick up your export default config file in the repository root
|
|
49
|
+
run: npx semantic-release
|
|
38
50
|
env:
|
|
39
|
-
GITHUB_TOKEN: "${{ secrets.
|
|
51
|
+
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
40
52
|
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
|
53
|
+
|
|
41
54
|
outputs:
|
|
42
55
|
new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
|
|
43
|
-
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
|
44
|
-
upload:
|
|
45
|
-
runs-on: ubuntu-latest
|
|
46
|
-
needs: release
|
|
47
|
-
if: needs.release.outputs.new_release_published == 'true'
|
|
48
|
-
env:
|
|
49
|
-
VERSION: "${{ needs.release.outputs.new_release_version }}"
|
|
50
|
-
steps:
|
|
51
|
-
- name: Checkout
|
|
52
|
-
uses: actions/checkout@v3
|
|
53
|
-
- name: Setup Node.js
|
|
54
|
-
uses: actions/setup-node@v3
|
|
55
|
-
with:
|
|
56
|
-
node-version: 16
|
|
57
|
-
- name: Set npm registry auth
|
|
58
|
-
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
59
|
-
- name: Install dependencies
|
|
60
|
-
run: yarn install
|
|
61
|
-
|
|
62
|
-
- name: Set Environment Variables
|
|
63
|
-
run: |
|
|
64
|
-
echo "organization_id=${{ secrets.COCREATE_ORGANIZATION_ID }}" >> $GITHUB_ENV
|
|
65
|
-
echo "key=${{ secrets.COCREATE_KEY }}" >> $GITHUB_ENV
|
|
66
|
-
echo "host=${{ secrets.COCREATE_HOST }}" >> $GITHUB_ENV
|
|
67
|
-
- name: CoCreate Upload
|
|
68
|
-
run: coc upload
|
|
69
|
-
|
|
56
|
+
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
# [1.14.0](https://github.com/CoCreate-app/CoCreate-server-side-render/compare/v1.13.0...v1.14.0) (2026-07-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* update automated workflow and release configuration for improved semantic release handling ([30e01d7](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/30e01d75c47cb36dc20f83a136d7eb296887d504))
|
|
7
|
+
|
|
8
|
+
# [1.13.0](https://github.com/CoCreate-app/CoCreate-server-side-render/compare/v1.12.8...v1.13.0) (2026-07-14)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* handle error states for fetched files in server-side rendering ([8c492c3](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/8c492c39669082348bc920bb553479eb35ed35a1))
|
|
14
|
+
* removed post install ([2ce8447](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/2ce8447779ca84c2a887b7bfbac27a626aaaa044))
|
|
15
|
+
* semantic version handling. Reorganize .gitignore for improved clarity and structure ([15bf544](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/15bf5442decac9654746486d4d4cec8a1ad775b4))
|
|
16
|
+
* update .gitignore to exclude package-lock.json and pnpm-lock.yaml ([7605dff](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/7605dff02dd0b3071583e987d95ac723609515bc))
|
|
17
|
+
* update module export to ES6 syntax in release.config.js ([a29e40f](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/a29e40f1aa52b9113473ed27fbaffada015e03ff))
|
|
18
|
+
* update package.json to use ESM and restructure server/client entry points ([e9b4c83](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/e9b4c83720aa05dd9c45fc94fc070521357c32eb))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* optimize rendering logic and improve error handling in server-side rendering ([208aeba](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/208aebac9efe650b7ef5c30713d5c6a4cc5b0aac))
|
|
24
|
+
* update package.json and refactor index.js to use ES modules ([7ab7c9d](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/7ab7c9d225505ef368c3434278d695eb7ac6f8d7))
|
|
25
|
+
|
|
26
|
+
## [1.12.8](https://github.com/CoCreate-app/CoCreate-server-side-render/compare/v1.12.7...v1.12.8) (2026-04-04)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* enhance chunk rendering logic to support outerHTML insertion ([b605d4a](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/b605d4a775dd7dfb2493d8450f63022e553da5ee))
|
|
32
|
+
* svg images not rendering in server-side rendering ([01a6882](https://github.com/CoCreate-app/CoCreate-server-side-render/commit/01a688296c369bf5ffe32c6b0fb8c8f38e70b243))
|
|
33
|
+
|
|
1
34
|
## [1.12.7](https://github.com/CoCreate-app/CoCreate-server-side-render/compare/v1.12.6...v1.12.7) (2025-10-11)
|
|
2
35
|
|
|
3
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/server-side-render",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "A simple server-side-render component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"server-side-render",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "npx webpack --config webpack.config.js",
|
|
20
20
|
"build": "npx webpack --mode=production --config webpack.config.js",
|
|
21
|
-
"dev": "npx webpack --config webpack.config.js --watch"
|
|
22
|
-
"postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
|
|
21
|
+
"dev": "npx webpack --config webpack.config.js --watch"
|
|
23
22
|
},
|
|
24
23
|
"repository": {
|
|
25
24
|
"type": "git",
|
|
@@ -35,6 +34,7 @@
|
|
|
35
34
|
"type": "GitHub Sponsors ❤",
|
|
36
35
|
"url": "https://github.com/sponsors/CoCreate-app"
|
|
37
36
|
},
|
|
37
|
+
"type": "module",
|
|
38
38
|
"main": "./src/index.js",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@cocreate/utils": "^1.39.0",
|
package/release.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export default {
|
|
2
2
|
dryRun: false,
|
|
3
3
|
branches: ["master"],
|
|
4
4
|
plugins: [
|
|
@@ -10,12 +10,20 @@ module.exports = {
|
|
|
10
10
|
changelogFile: "CHANGELOG.md",
|
|
11
11
|
},
|
|
12
12
|
],
|
|
13
|
-
"@semantic-release/npm",
|
|
14
|
-
"@semantic-release/github",
|
|
13
|
+
"@semantic-release/npm",
|
|
15
14
|
[
|
|
16
|
-
"@semantic-release/
|
|
15
|
+
"@semantic-release/github",
|
|
17
16
|
{
|
|
17
|
+
successComment: false,
|
|
18
|
+
failTitle: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"@semantic-release/git",
|
|
23
|
+
{
|
|
24
|
+
// Only stage and commit the changelog and package.json
|
|
18
25
|
assets: ["CHANGELOG.md", "package.json"],
|
|
26
|
+
message: "chore(release): ${nextRelease.version} [skip ci]",
|
|
19
27
|
},
|
|
20
28
|
],
|
|
21
29
|
],
|
package/src/index.js
CHANGED
|
@@ -1,268 +1,407 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
1
|
+
import { parse } from "node-html-parser";
|
|
2
|
+
import { checkValue, ObjectId } from "@cocreate/utils";
|
|
3
|
+
|
|
4
|
+
// Using a Set for O(1) lookups is cleaner and faster than an object map
|
|
5
|
+
const IGNORE_ELEMENTS = new Set([
|
|
6
|
+
"FORM", "INPUT", "TEXTAREA", "SELECT", "LINK", "IFRAME", "COCREATE-SELECT"
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
// Prevent these tags from triggering the generic [src] chunk-fetching logic
|
|
10
|
+
const SKIP_SRC_ELEMENTS = new Set([
|
|
11
|
+
"SCRIPT", "IMG", "IFRAME", "AUDIO", "VIDEO", "SOURCE", "TRACK", "INPUT", "EMBED", "FRAME"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// CSS Selector for identifying target elements
|
|
15
|
+
const RENDER_SELECTOR = "[array], [src]:not(script, img, iframe, audio, video, source, track, input, embed, frame)";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Main functional entry point for Server-Side Rendering.
|
|
19
|
+
* Completely stateless—all required dependencies are passed directly as arguments.
|
|
20
|
+
* * @param {Object} options
|
|
21
|
+
* @param {Object} options.file - The template file object containing source markup
|
|
22
|
+
* @param {Object} options.organization - Organization configuration data
|
|
23
|
+
* @param {Object} options.urlObject - Request URL parsed object
|
|
24
|
+
* @param {string} options.langRegion - Target language region (e.g., 'en-US')
|
|
25
|
+
* @param {string} options.lang - Target language (e.g., 'en')
|
|
26
|
+
* @param {string} options.theme - Visual theme (e.g., 'dark')
|
|
27
|
+
* @param {Function} options.resolveFile - Injected custom file resolver callback
|
|
28
|
+
* @param {Object} options.crud - Crud database service instance
|
|
29
|
+
* @returns {Promise<string>} The fully rendered HTML string
|
|
30
|
+
*/
|
|
31
|
+
export async function HTML({
|
|
32
|
+
file,
|
|
33
|
+
organization,
|
|
34
|
+
urlObject,
|
|
35
|
+
langRegion,
|
|
36
|
+
lang,
|
|
37
|
+
theme,
|
|
38
|
+
resolveFile,
|
|
39
|
+
crud
|
|
40
|
+
}) {
|
|
41
|
+
let dep = [];
|
|
42
|
+
let dbCache = new Map();
|
|
43
|
+
|
|
44
|
+
const organization_id = file?.organization_id;
|
|
45
|
+
const host = urlObject?.hostname;
|
|
46
|
+
|
|
47
|
+
// Ensure file.path has a fallback to prevent runtime properties errors
|
|
48
|
+
const filePath = file?.path || file?.pathname || "/";
|
|
49
|
+
|
|
50
|
+
// Apply parsing directly on root file source without modifying any paths
|
|
51
|
+
let rootSrc = file?.src || "";
|
|
52
|
+
let dom = parse(rootSrc);
|
|
53
|
+
|
|
54
|
+
// Core Render Function (placed at the top for prominence)
|
|
55
|
+
async function render(currentDom, lastKey) {
|
|
56
|
+
// Find target elements and filter out nested targets to prevent redundant querying
|
|
57
|
+
const elements = currentDom.querySelectorAll(RENDER_SELECTOR)
|
|
58
|
+
|
|
59
|
+
for (let el of elements) {
|
|
60
|
+
let isData = el.hasAttribute("array");
|
|
61
|
+
let isSrc = el.hasAttribute("src") && !SKIP_SRC_ELEMENTS.has(el.tagName.toUpperCase());
|
|
62
|
+
|
|
63
|
+
let chunkDom = null;
|
|
64
|
+
|
|
65
|
+
// 1. Process File Source (Takes priority over Data Source)
|
|
66
|
+
if (isSrc) {
|
|
67
|
+
let src = el.getAttribute("src");
|
|
68
|
+
if (src) {
|
|
69
|
+
let pathname = filePath;
|
|
70
|
+
if (!pathname.endsWith("/")) pathname += "/";
|
|
71
|
+
|
|
72
|
+
pathname = new URL(src, `http://localhost${pathname}`).pathname;
|
|
73
|
+
if (pathname.endsWith("/")) pathname += "index.html";
|
|
74
|
+
|
|
75
|
+
// CIRCULAR DEPENDENCY CHECK: Track active src resolution paths in loop prevention list
|
|
76
|
+
let srcKey = `file:${pathname}`;
|
|
77
|
+
if (dep.includes(srcKey)) {
|
|
78
|
+
throw new Error(`Infinite loop detected: Parent context "${lastKey}" requested "${srcKey}", which is already in the active import chain.`);
|
|
79
|
+
}
|
|
80
|
+
dep.push(srcKey);
|
|
81
|
+
|
|
82
|
+
let fetchedFile = null;
|
|
83
|
+
|
|
84
|
+
// Use injected resolver callback if provided, otherwise fallback to CRUD query
|
|
85
|
+
if (typeof resolveFile === "function") {
|
|
86
|
+
fetchedFile = await resolveFile(pathname);
|
|
87
|
+
} else if (crud) {
|
|
88
|
+
let data = await crud.send({
|
|
89
|
+
method: "object.read",
|
|
90
|
+
host,
|
|
91
|
+
array: "files",
|
|
92
|
+
object: "",
|
|
93
|
+
$filter: { query: { pathname } },
|
|
94
|
+
organization_id
|
|
95
|
+
});
|
|
96
|
+
fetchedFile = data?.object?.[0] || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Intelligent Status Code Handling
|
|
100
|
+
let statusCode = fetchedFile?.status;
|
|
101
|
+
if (!statusCode) {
|
|
102
|
+
if (!fetchedFile || !fetchedFile.src) statusCode = 404;
|
|
103
|
+
else if (fetchedFile.public === false || String(fetchedFile.public) === "false") statusCode = 403;
|
|
104
|
+
else statusCode = 200;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (statusCode >= 400) {
|
|
108
|
+
el.setAttribute("error", statusCode.toString());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Render if status is successful OR if the element explicitly forces rendering errors via 'render-error'
|
|
112
|
+
if (statusCode < 400 || el.hasAttribute("render-error")) {
|
|
113
|
+
let chunkVal = fetchedFile?.src;
|
|
114
|
+
|
|
115
|
+
if (chunkVal) {
|
|
116
|
+
// Decode base64 SVG if necessary
|
|
117
|
+
if (typeof chunkVal === "string" && chunkVal.startsWith("data:image/svg+xml;base64,")) {
|
|
118
|
+
chunkVal = Buffer.from(chunkVal.split(",")[1], "base64").toString("utf-8");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Normalize non-string values safely to strings before parsing (failsafe)
|
|
122
|
+
let chunkStr = typeof chunkVal === 'string' ? chunkVal : String(chunkVal);
|
|
123
|
+
chunkDom = parse(chunkStr);
|
|
124
|
+
|
|
125
|
+
// Extract and merge any embedded <head> parameters before rendering nested items
|
|
126
|
+
extractAndMergeHead(chunkDom, dom);
|
|
127
|
+
|
|
128
|
+
// Passing dynamic srcKey instead of static "src-chunk" to preserve trace context
|
|
129
|
+
chunkDom = await render(chunkDom, srcKey);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
dep.pop(); // Complete tracking cycle for this source resolution
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// 2. Process Data Source (Only runs if element does not have a valid src being resolved)
|
|
136
|
+
else if (isData) {
|
|
137
|
+
let shouldProcessData = true;
|
|
138
|
+
|
|
139
|
+
// Exclude specific tags, templates, actions, and components from rendering
|
|
140
|
+
if (IGNORE_ELEMENTS.has(el.tagName.toUpperCase())) {
|
|
141
|
+
shouldProcessData = false;
|
|
142
|
+
} else if (el.closest(".template, [template], template, [render]")) {
|
|
143
|
+
shouldProcessData = false;
|
|
144
|
+
} else if (el.hasAttribute("render-query") || el.hasAttribute("component") || el.hasAttribute("plugin") || el.hasAttribute("actions")) {
|
|
145
|
+
shouldProcessData = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (shouldProcessData && crud) {
|
|
149
|
+
let array = el.getAttribute("array");
|
|
150
|
+
let key = el.getAttribute("key"); // Optional attribute
|
|
151
|
+
let _id = el.getAttribute("object") || "";
|
|
152
|
+
|
|
153
|
+
let $filter = {};
|
|
154
|
+
let filterAttr = el.getAttribute("filter");
|
|
155
|
+
if (filterAttr) {
|
|
156
|
+
try {
|
|
157
|
+
$filter = JSON.parse(filterAttr);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
$filter = {};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (array && checkValue(array) && (!key || checkValue(key))) {
|
|
164
|
+
let keyStr = key ? `-${key}` : '';
|
|
165
|
+
let filterKeyStr = filterAttr ? `-${filterAttr}` : '';
|
|
166
|
+
let crudKey = `${_id}${array}${keyStr}${filterKeyStr}`;
|
|
167
|
+
|
|
168
|
+
if (dep.includes(crudKey)) {
|
|
169
|
+
throw new Error(`Infinite loop detected: Parent context "${lastKey}" requested data key "${crudKey}", which has already been rendered.`);
|
|
170
|
+
}
|
|
171
|
+
dep.push(crudKey);
|
|
172
|
+
|
|
173
|
+
let cacheKey = `${_id}${array}${filterKeyStr}`;
|
|
174
|
+
let data;
|
|
175
|
+
|
|
176
|
+
if (dbCache.has(cacheKey)) {
|
|
177
|
+
data = dbCache.get(cacheKey);
|
|
178
|
+
} else {
|
|
179
|
+
data = await crud.send({
|
|
180
|
+
method: "object.read",
|
|
181
|
+
host,
|
|
182
|
+
array,
|
|
183
|
+
object: _id ? { _id } : "",
|
|
184
|
+
$filter,
|
|
185
|
+
organization_id
|
|
186
|
+
});
|
|
187
|
+
if (data?.object?.[0]) data = data.object[0];
|
|
188
|
+
dbCache.set(cacheKey, data);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (data) {
|
|
192
|
+
// If a key is defined, fetch that key's value; otherwise, render the entire object/document
|
|
193
|
+
let chunkVal = key ? data[key] : data;
|
|
194
|
+
|
|
195
|
+
if (chunkVal !== undefined && chunkVal !== null) {
|
|
196
|
+
// Normalize objects to JSON strings, and everything else safely to standard strings
|
|
197
|
+
let chunkStr = typeof chunkVal === 'object' ? JSON.stringify(chunkVal) : String(chunkVal);
|
|
198
|
+
chunkDom = parse(chunkStr);
|
|
199
|
+
|
|
200
|
+
// Extract and merge any embedded <head> parameters before rendering nested items
|
|
201
|
+
extractAndMergeHead(chunkDom, dom);
|
|
202
|
+
|
|
203
|
+
// Recursively resolve all nested requirements in the chunk BEFORE inserting it
|
|
204
|
+
chunkDom = await render(chunkDom, crudKey);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
dep.pop();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Apply DOM mutations only after everything in this sub-tree branch is completely resolved
|
|
213
|
+
if (chunkDom) {
|
|
214
|
+
if (el.getAttribute("value-type") === "outerHTML") {
|
|
215
|
+
if (chunkDom.childNodes?.length) {
|
|
216
|
+
el.replaceWith(...chunkDom.childNodes);
|
|
217
|
+
} else {
|
|
218
|
+
el.remove();
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
el.setAttribute("rendered", "");
|
|
222
|
+
el.innerHTML = "";
|
|
223
|
+
|
|
224
|
+
// Use node-html-parser compatible appendChild loop
|
|
225
|
+
if (chunkDom.childNodes) {
|
|
226
|
+
for (const child of [...chunkDom.childNodes]) {
|
|
227
|
+
el.appendChild(child);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
el.appendChild(chunkDom);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return currentDom;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Initialize recursion tracker with the primary file pathname to catch immediate self-reference imports
|
|
240
|
+
const rootNormalizedPath = filePath.endsWith("/") ? `${filePath}index.html` : filePath;
|
|
241
|
+
dep.push(`file:${rootNormalizedPath}`);
|
|
242
|
+
|
|
243
|
+
dom = await render(dom, "root");
|
|
244
|
+
|
|
245
|
+
if (langRegion || lang) {
|
|
246
|
+
dom = translate(dom, file, langRegion, lang);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Apply theme data-attributes/meta-tags if a head is available, or can be lazily resolved
|
|
250
|
+
if (theme) {
|
|
251
|
+
const htmlEl = dom.querySelector("html");
|
|
252
|
+
try {
|
|
253
|
+
if (htmlEl?.setAttribute) htmlEl.setAttribute("data-bs-theme", theme);
|
|
254
|
+
|
|
255
|
+
const currentHead = dom.querySelector("head");
|
|
256
|
+
if (currentHead) {
|
|
257
|
+
let meta = currentHead.querySelector('meta[name="color-scheme"]');
|
|
258
|
+
if (meta?.setAttribute) {
|
|
259
|
+
meta.setAttribute("content", theme);
|
|
260
|
+
} else {
|
|
261
|
+
const metaNode = parse(`<meta name="color-scheme" content="${theme}">`);
|
|
262
|
+
currentHead.appendChild(metaNode);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
// fail-safe
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Append localization link tags if head is present, or can be lazily resolved
|
|
271
|
+
if (organization?.languages?.length > 0) {
|
|
272
|
+
let langLinkTags = createLanguageLinkTags(file, organization, urlObject);
|
|
273
|
+
const currentHead = dom.querySelector("head");
|
|
274
|
+
if (currentHead && langLinkTags) {
|
|
275
|
+
const linksFragment = parse(langLinkTags);
|
|
276
|
+
for (const link of linksFragment.querySelectorAll("link")) {
|
|
277
|
+
currentHead.appendChild(link);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
dep = [];
|
|
283
|
+
dbCache.clear();
|
|
284
|
+
|
|
285
|
+
let finalHtml = dom.toString();
|
|
286
|
+
|
|
287
|
+
// Apply ObjectId generation globally across the fully rendered document
|
|
288
|
+
finalHtml = finalHtml.replaceAll("ObjectId()", () => ObjectId().toString());
|
|
289
|
+
|
|
290
|
+
return finalHtml;
|
|
266
291
|
}
|
|
267
292
|
|
|
268
|
-
|
|
293
|
+
// ==========================================
|
|
294
|
+
// Stateless Private Helper Functions
|
|
295
|
+
// ==========================================
|
|
296
|
+
|
|
297
|
+
function ensureMainHead(rootDom) {
|
|
298
|
+
let head = rootDom.querySelector("head");
|
|
299
|
+
if (!head) {
|
|
300
|
+
const htmlEl = rootDom.querySelector("html") || rootDom;
|
|
301
|
+
const headDom = parse("<head></head>");
|
|
302
|
+
head = headDom.querySelector("head");
|
|
303
|
+
if (htmlEl.childNodes && htmlEl.childNodes.length > 0) {
|
|
304
|
+
htmlEl.insertBefore(head, htmlEl.childNodes[0]);
|
|
305
|
+
} else {
|
|
306
|
+
htmlEl.appendChild(head);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return head;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function getMatchingSelector(newEl) {
|
|
313
|
+
const tagName = newEl.tagName.toUpperCase();
|
|
314
|
+
if (tagName === "TITLE") {
|
|
315
|
+
return "title";
|
|
316
|
+
}
|
|
317
|
+
if (tagName === "META") {
|
|
318
|
+
const name = newEl.getAttribute("name");
|
|
319
|
+
if (name) return `meta[name="${name}"]`;
|
|
320
|
+
const property = newEl.getAttribute("property");
|
|
321
|
+
if (property) return `meta[property="${property}"]`;
|
|
322
|
+
}
|
|
323
|
+
if (tagName === "SCRIPT") {
|
|
324
|
+
const type = newEl.getAttribute("type");
|
|
325
|
+
if (type === "application/ld+json") {
|
|
326
|
+
return 'script[type="application/ld+json"]';
|
|
327
|
+
}
|
|
328
|
+
const id = newEl.getAttribute("id");
|
|
329
|
+
if (id) return `script[id="${id}"]`;
|
|
330
|
+
}
|
|
331
|
+
if (tagName === "LINK") {
|
|
332
|
+
const rel = newEl.getAttribute("rel");
|
|
333
|
+
if (rel === "canonical") {
|
|
334
|
+
return 'link[rel="canonical"]';
|
|
335
|
+
}
|
|
336
|
+
const id = newEl.getAttribute("id");
|
|
337
|
+
if (id) return `link[id="${id}"]`;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function extractAndMergeHead(chunkDom, rootDom) {
|
|
343
|
+
const chunkHead = chunkDom.querySelector("head");
|
|
344
|
+
if (!chunkHead) return;
|
|
345
|
+
|
|
346
|
+
// Lazily ensure a main head exists on the root 'dom' since we found nested head assets to merge
|
|
347
|
+
const head = ensureMainHead(rootDom);
|
|
348
|
+
|
|
349
|
+
// Clone array of nodes to safely mutate during iteration
|
|
350
|
+
const children = [...chunkHead.childNodes];
|
|
351
|
+
|
|
352
|
+
for (const child of children) {
|
|
353
|
+
if (child.nodeType === 1) { // Element Node
|
|
354
|
+
const selector = getMatchingSelector(child);
|
|
355
|
+
let matched = false;
|
|
356
|
+
if (selector) {
|
|
357
|
+
const existing = head.querySelector(selector);
|
|
358
|
+
if (existing) {
|
|
359
|
+
existing.replaceWith(child);
|
|
360
|
+
matched = true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!matched) {
|
|
364
|
+
head.appendChild(child);
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
// For text, script content, or comment nodes, append straight to main head
|
|
368
|
+
head.appendChild(child);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Remove empty nested head container from the chunk
|
|
373
|
+
chunkHead.remove();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function createLanguageLinkTags(file, organization, urlObject) {
|
|
377
|
+
let generatedLinksString = `<link rel="alternate" hreflang="x-default" href="https://${urlObject.hostname}${file.pathname}">\n`;
|
|
378
|
+
for (const language of organization.languages) {
|
|
379
|
+
const hrefUrl = `https://${urlObject.hostname}/${language}${file.pathname}`;
|
|
380
|
+
generatedLinksString += `<link rel="alternate" hreflang="${language}" href="${hrefUrl}">\n`;
|
|
381
|
+
}
|
|
382
|
+
return generatedLinksString;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function translate(dom, file, langRegion, lang) {
|
|
386
|
+
if (!file?.translations || (!langRegion && !lang)) return dom;
|
|
387
|
+
|
|
388
|
+
for (let translation of file.translations) {
|
|
389
|
+
let elements = dom.querySelectorAll(translation.selector) || [];
|
|
390
|
+
for (let el of elements) {
|
|
391
|
+
if (!el) continue;
|
|
392
|
+
|
|
393
|
+
if (translation.innerHTML) {
|
|
394
|
+
let content = translation.innerHTML[langRegion] || translation.innerHTML[lang];
|
|
395
|
+
if (content) el.innerHTML = content;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (translation.attributes) {
|
|
399
|
+
for (let [key, languageObj] of Object.entries(translation.attributes)) {
|
|
400
|
+
let value = languageObj[langRegion] || languageObj[lang];
|
|
401
|
+
if (value) el.setAttribute(key, value);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return dom;
|
|
407
|
+
}
|