evolit 0.1.0-alpha.2 → 0.1.0-alpha.21

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.
@@ -1,4 +1,5 @@
1
1
  import { renderBootstrap, renderDocument } from "@litsx/ssr";
2
+ import { createRouteSegmentPayload, renderRouteSegmentPayload } from "./route-segments.js";
2
3
 
3
4
  function escapeHtmlAttribute(value) {
4
5
  return String(value)
@@ -16,13 +17,52 @@ function normalizeHeadMarkup(value) {
16
17
  return typeof value === "string" ? value : "";
17
18
  }
18
19
 
20
+ function renderDescriptionMarkup(description) {
21
+ return description
22
+ ? `<meta name="description" content="${escapeHtmlAttribute(description)}" />`
23
+ : "";
24
+ }
25
+
26
+ function renderRouteHeadMarkup(markup) {
27
+ return `<!--evolit:route-head:start-->${markup}<!--evolit:route-head:end-->`;
28
+ }
29
+
30
+ function normalizeDocumentAttributes(attributes) {
31
+ if (!attributes || typeof attributes !== "object" || Array.isArray(attributes)) return {};
32
+ return Object.fromEntries(
33
+ Object.entries(attributes)
34
+ .filter(([name, value]) => name !== "lang" && /^[^\s"'>/=]+$/.test(name) && value !== false && value != null)
35
+ .map(([name, value]) => [name, value === true ? true : String(value)]),
36
+ );
37
+ }
38
+
39
+ function renderDocumentAttributes(attributes) {
40
+ return Object.entries(normalizeDocumentAttributes(attributes))
41
+ .map(([name, value]) => value === true ? name : `${name}="${escapeHtmlAttribute(value)}"`)
42
+ .join(" ");
43
+ }
44
+
45
+ function createDocumentState(metadata) {
46
+ return {
47
+ lang: String(metadata.lang ?? "en"),
48
+ htmlAttributes: normalizeDocumentAttributes(metadata.htmlAttributes),
49
+ bodyAttributes: normalizeDocumentAttributes(metadata.bodyAttributes),
50
+ };
51
+ }
52
+
53
+ function renderDocumentStateScript(metadata) {
54
+ const serialized = JSON.stringify(createDocumentState(metadata))
55
+ .replaceAll("<", "\\u003C")
56
+ .replaceAll(">", "\\u003E")
57
+ .replaceAll("&", "\\u0026");
58
+ return `<script type="application/json" id="__EVOLIT_DOCUMENT__">${serialized}</script>`;
59
+ }
60
+
19
61
  function resolveDocumentMetadata(routeMetadata = {}, adapterOptions = {}) {
20
62
  const description =
21
63
  routeMetadata.description ?? adapterOptions.description ?? "A LitSX application";
22
64
  const extraHead = [
23
- description
24
- ? `<meta name="description" content="${escapeHtmlAttribute(description)}" />`
25
- : "",
65
+ renderDescriptionMarkup(description),
26
66
  normalizeHeadMarkup(adapterOptions.head),
27
67
  normalizeHeadMarkup(routeMetadata.head),
28
68
  ]
@@ -32,6 +72,7 @@ function resolveDocumentMetadata(routeMetadata = {}, adapterOptions = {}) {
32
72
  return {
33
73
  lang: String(routeMetadata.lang ?? adapterOptions.lang ?? "en"),
34
74
  title: String(routeMetadata.title ?? adapterOptions.title ?? "evolit"),
75
+ description: String(description),
35
76
  head: extraHead,
36
77
  htmlAttributes: {
37
78
  ...(adapterOptions.htmlAttributes ?? {}),
@@ -46,19 +87,18 @@ function resolveDocumentMetadata(routeMetadata = {}, adapterOptions = {}) {
46
87
 
47
88
  function createHtmlDocument({ body, metadata = {} }) {
48
89
  const title = metadata.title ? String(metadata.title) : "evolit";
49
- const description =
50
- metadata.description ? String(metadata.description) : "A LitSX application";
90
+ const htmlAttributes = renderDocumentAttributes(metadata.htmlAttributes);
91
+ const bodyAttributes = renderDocumentAttributes(metadata.bodyAttributes);
51
92
 
52
93
  return `<!DOCTYPE html>
53
- <html lang="${escapeHtmlAttribute(metadata.lang ?? "en")}">
94
+ <html lang="${escapeHtmlAttribute(metadata.lang ?? "en")}"${htmlAttributes ? ` ${htmlAttributes}` : ""}>
54
95
  <head>
55
96
  <meta charset="utf-8" />
56
97
  <meta name="viewport" content="width=device-width, initial-scale=1" />
57
- <title>${title}</title>${description ? `
58
- <meta name="description" content="${escapeHtmlAttribute(description)}" />` : ""}
98
+ <title>${title}</title>
59
99
  ${metadata.head ? ` ${String(metadata.head).split("\n").join("\n ")}` : ""}
60
100
  </head>
61
- <body>${body}</body>
101
+ <body${bodyAttributes ? ` ${bodyAttributes}` : ""}>${body}</body>
62
102
  </html>`;
63
103
  }
64
104
 
@@ -69,6 +109,15 @@ function createRouteHeaders(routeResult, headers = {}) {
69
109
  };
70
110
  }
71
111
 
112
+ function renderHydrationDataScript(hydrationData) {
113
+ if (!hydrationData) return "";
114
+ const serialized = JSON.stringify(hydrationData)
115
+ .replaceAll("<", "\\u003C")
116
+ .replaceAll(">", "\\u003E")
117
+ .replaceAll("&", "\\u0026");
118
+ return `<script type="application/json" id="__LITSX_HYDRATION__">${serialized}</script>`;
119
+ }
120
+
72
121
  function responseHeadersToObject(headers) {
73
122
  const values = Object.fromEntries(headers.entries());
74
123
  if (typeof headers.getSetCookie === "function") {
@@ -111,6 +160,66 @@ export function createSsrAdapter(options = {}) {
111
160
  };
112
161
  }
113
162
 
163
+ if (typeof routeResult.tree === "string" && routeResult.ssrArtifacts) {
164
+ const result = routeResult.ssrArtifacts;
165
+ const metadata = resolveDocumentMetadata(routeResult.metadata, options);
166
+ const generatedBootstrap =
167
+ !options.bootstrap && !options.clientEntry && typeof options.resolveBootstrap === "function"
168
+ ? await options.resolveBootstrap({ routeResult, result })
169
+ : "";
170
+ const additionalHead =
171
+ typeof options.resolveAdditionalHead === "function"
172
+ ? await options.resolveAdditionalHead({ routeResult, result })
173
+ : "";
174
+ // Adapter head is global. Route metadata and renderer-provided head
175
+ // tags are delimited so browser navigation can replace only that part.
176
+ const routeHead = [
177
+ renderDescriptionMarkup(metadata.description),
178
+ normalizeHeadMarkup(routeResult.metadata?.head),
179
+ ...(result.headTags ?? []),
180
+ ]
181
+ .filter(Boolean)
182
+ .join("\n");
183
+ const composedHead = [
184
+ normalizeHeadMarkup(options.head),
185
+ renderRouteHeadMarkup(routeHead),
186
+ additionalHead,
187
+ ]
188
+ .filter(Boolean)
189
+ .join("\n");
190
+ const document = createHtmlDocument({
191
+ body: `${routeResult.tree}${renderHydrationDataScript(result.hydrationData)}`,
192
+ metadata: {
193
+ ...routeResult.metadata,
194
+ ...metadata,
195
+ head: composedHead,
196
+ },
197
+ });
198
+ const transformedDocument =
199
+ typeof options.transformDocument === "function"
200
+ ? await options.transformDocument({ routeResult, result, document })
201
+ : document;
202
+ const routeRuntimeScripts = [
203
+ renderRouteSegmentPayload(createRouteSegmentPayload(routeResult)),
204
+ renderDocumentStateScript(metadata),
205
+ ].filter(Boolean).join("\n");
206
+ const documentWithRouteSegments = routeRuntimeScripts
207
+ ? transformedDocument.replace("</body>", `${routeRuntimeScripts}\n</body>`)
208
+ : transformedDocument;
209
+ const body = generatedBootstrap
210
+ ? documentWithRouteSegments.replace(
211
+ "</body>",
212
+ `${renderBootstrap({ bootstrap: { content: generatedBootstrap } })}\n</body>`,
213
+ )
214
+ : documentWithRouteSegments;
215
+
216
+ return {
217
+ status: routeResult.status ?? 200,
218
+ headers: createRouteHeaders(routeResult, { "content-type": "text/html; charset=utf-8" }),
219
+ body,
220
+ };
221
+ }
222
+
114
223
  if (typeof routeResult.tree === "string") {
115
224
  const metadata = resolveDocumentMetadata(routeResult.metadata, options);
116
225
  return {
@@ -121,7 +230,6 @@ export function createSsrAdapter(options = {}) {
121
230
  metadata: {
122
231
  ...routeResult.metadata,
123
232
  ...metadata,
124
- description: routeResult.metadata?.description ?? options.description,
125
233
  },
126
234
  }),
127
235
  };
@@ -156,6 +264,13 @@ export function createSsrAdapter(options = {}) {
156
264
  document: documentWithAdditionalHead,
157
265
  })
158
266
  : documentWithAdditionalHead;
267
+ const routeRuntimeScripts = [
268
+ renderRouteSegmentPayload(createRouteSegmentPayload(routeResult)),
269
+ renderDocumentStateScript(documentOptions),
270
+ ].filter(Boolean).join("\n");
271
+ const documentWithRouteSegments = routeRuntimeScripts
272
+ ? transformedDocument.replace("</body>", `${routeRuntimeScripts}\n</body>`)
273
+ : transformedDocument;
159
274
 
160
275
  if (generatedBootstrap) {
161
276
  const bootstrapMarkup = renderBootstrap({
@@ -163,7 +278,7 @@ export function createSsrAdapter(options = {}) {
163
278
  content: generatedBootstrap,
164
279
  },
165
280
  });
166
- const documentWithBootstrap = transformedDocument.replace(
281
+ const documentWithBootstrap = documentWithRouteSegments.replace(
167
282
  "</body>",
168
283
  `${bootstrapMarkup}\n</body>`,
169
284
  );
@@ -178,7 +293,7 @@ export function createSsrAdapter(options = {}) {
178
293
  return {
179
294
  status: routeResult.status ?? 200,
180
295
  headers: createRouteHeaders(routeResult, { "content-type": "text/html; charset=utf-8" }),
181
- body: transformedDocument,
296
+ body: documentWithRouteSegments,
182
297
  };
183
298
  },
184
299
  };
@@ -0,0 +1,32 @@
1
+ const enabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
2
+
3
+ function style(open, value) {
4
+ return enabled ? `\u001B[${open}m${value}\u001B[0m` : String(value);
5
+ }
6
+
7
+ export const terminal = {
8
+ bold(value) {
9
+ return style("1", value);
10
+ },
11
+ dim(value) {
12
+ return style("2", value);
13
+ },
14
+ red(value) {
15
+ return style("31", value);
16
+ },
17
+ green(value) {
18
+ return style("32", value);
19
+ },
20
+ yellow(value) {
21
+ return style("33", value);
22
+ },
23
+ blue(value) {
24
+ return style("34", value);
25
+ },
26
+ magenta(value) {
27
+ return style("35", value);
28
+ },
29
+ cyan(value) {
30
+ return style("36", value);
31
+ },
32
+ };
@@ -0,0 +1 @@
1
+ nodeLinker: node-modules