@constela/start 1.8.26 → 1.9.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/dist/{chunk-AZWQ5YMZ.js → chunk-C7LIB2RS.js} +125 -6
- package/dist/{chunk-ZXGA4Z6W.js → chunk-VL6BMHBL.js} +87 -7
- package/dist/cli/index.js +2 -2
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2 -2
- package/dist/runtime/entry-client.js +76 -4
- package/dist/runtime/entry-server.d.ts +5 -0
- package/dist/runtime/entry-server.js +1 -1
- package/package.json +7 -7
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
// src/runtime/entry-server.ts
|
|
2
2
|
import { renderToString } from "@constela/server";
|
|
3
|
+
|
|
4
|
+
// src/runtime/theme.ts
|
|
5
|
+
function escapeCssValue(value) {
|
|
6
|
+
return value.replace(/[;<>{}]/g, "");
|
|
7
|
+
}
|
|
8
|
+
function escapeJsString(str) {
|
|
9
|
+
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/<\/script>/gi, "<\\/script>");
|
|
10
|
+
}
|
|
11
|
+
function generateThemeCss(options) {
|
|
12
|
+
const { config, cssPrefix: optionsPrefix } = options;
|
|
13
|
+
if (!config) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
const { colors, darkColors, fonts, cssPrefix: configPrefix } = config;
|
|
17
|
+
const prefix = optionsPrefix ?? configPrefix ?? "";
|
|
18
|
+
const hasColors = colors && Object.keys(colors).length > 0;
|
|
19
|
+
const hasDarkColors = darkColors && Object.keys(darkColors).length > 0;
|
|
20
|
+
const hasFonts = fonts && Object.keys(fonts).length > 0;
|
|
21
|
+
if (!hasColors && !hasDarkColors && !hasFonts) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
const lines = [];
|
|
25
|
+
const rootVars = [];
|
|
26
|
+
const separator = prefix && !prefix.endsWith("-") ? "-" : "";
|
|
27
|
+
if (colors) {
|
|
28
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
29
|
+
if (value !== void 0) {
|
|
30
|
+
rootVars.push(` --${prefix}${separator}${key}: ${escapeCssValue(value)};`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (fonts) {
|
|
35
|
+
for (const [key, value] of Object.entries(fonts)) {
|
|
36
|
+
if (value !== void 0) {
|
|
37
|
+
rootVars.push(` --${prefix}${separator}font-${key}: ${escapeCssValue(value)};`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (rootVars.length > 0) {
|
|
42
|
+
lines.push(":root {");
|
|
43
|
+
lines.push(...rootVars);
|
|
44
|
+
lines.push("}");
|
|
45
|
+
}
|
|
46
|
+
if (darkColors) {
|
|
47
|
+
const darkVars = [];
|
|
48
|
+
for (const [key, value] of Object.entries(darkColors)) {
|
|
49
|
+
if (value !== void 0) {
|
|
50
|
+
darkVars.push(` --${prefix}${separator}${key}: ${escapeCssValue(value)};`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (darkVars.length > 0) {
|
|
54
|
+
lines.push(".dark {");
|
|
55
|
+
lines.push(...darkVars);
|
|
56
|
+
lines.push("}");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
function generateThemeScript(storageKey = "theme") {
|
|
62
|
+
if (storageKey === "") {
|
|
63
|
+
storageKey = "theme";
|
|
64
|
+
}
|
|
65
|
+
const escapedKey = escapeJsString(storageKey);
|
|
66
|
+
return `(function() {
|
|
67
|
+
try {
|
|
68
|
+
var theme;
|
|
69
|
+
var cookies = document.cookie.split(';');
|
|
70
|
+
for (var i = 0; i < cookies.length; i++) {
|
|
71
|
+
var cookie = cookies[i].trim();
|
|
72
|
+
if (cookie.indexOf('${escapedKey}=') === 0) {
|
|
73
|
+
theme = decodeURIComponent(cookie.substring('${escapedKey}='.length));
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!theme) {
|
|
78
|
+
theme = localStorage.getItem('${escapedKey}');
|
|
79
|
+
}
|
|
80
|
+
if (theme === 'dark') {
|
|
81
|
+
document.documentElement.classList.add('dark');
|
|
82
|
+
} else if (theme === 'light') {
|
|
83
|
+
document.documentElement.classList.remove('dark');
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {}
|
|
86
|
+
})();`;
|
|
87
|
+
}
|
|
88
|
+
function getHtmlThemeClass(config, cookieMode) {
|
|
89
|
+
if (cookieMode === "dark") {
|
|
90
|
+
return "dark";
|
|
91
|
+
}
|
|
92
|
+
if (cookieMode === "light" || cookieMode === "system") {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
if (config?.mode === "dark") {
|
|
96
|
+
return "dark";
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/runtime/entry-server.ts
|
|
3
102
|
async function renderPage(program, ctx) {
|
|
4
103
|
const stateOverrides = {};
|
|
5
104
|
if (ctx.cookies?.["theme"] && program.state?.["theme"]) {
|
|
@@ -26,7 +125,7 @@ async function renderPage(program, ctx) {
|
|
|
26
125
|
}
|
|
27
126
|
return await renderToString(program, options);
|
|
28
127
|
}
|
|
29
|
-
function
|
|
128
|
+
function escapeJsString2(str) {
|
|
30
129
|
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
31
130
|
}
|
|
32
131
|
function escapeJsonForScript(json) {
|
|
@@ -70,7 +169,7 @@ function generateHydrationScript(program, widgets, route, hmrUrl) {
|
|
|
70
169
|
}).join("\n") : "";
|
|
71
170
|
const widgetMounting = hasWidgets ? widgets.map((widget) => {
|
|
72
171
|
const jsId = toJsIdentifier(widget.id);
|
|
73
|
-
const escapedId =
|
|
172
|
+
const escapedId = escapeJsString2(widget.id);
|
|
74
173
|
return `
|
|
75
174
|
const container_${jsId} = document.getElementById('${escapedId}');
|
|
76
175
|
if (container_${jsId}) {
|
|
@@ -93,7 +192,7 @@ if (container_${jsId}) {
|
|
|
93
192
|
}`;
|
|
94
193
|
let hmrSetup = "";
|
|
95
194
|
if (enableHmr) {
|
|
96
|
-
const escapedHmrUrl =
|
|
195
|
+
const escapedHmrUrl = escapeJsString2(hmrUrl);
|
|
97
196
|
const handlerOptions = route ? `{
|
|
98
197
|
container: document.getElementById('app'),
|
|
99
198
|
program,
|
|
@@ -130,7 +229,15 @@ function wrapHtml(content, hydrationScript, head, options) {
|
|
|
130
229
|
}
|
|
131
230
|
langAttr = ` lang="${options.lang}"`;
|
|
132
231
|
}
|
|
133
|
-
|
|
232
|
+
let htmlClass = "";
|
|
233
|
+
if (options?.themeConfig) {
|
|
234
|
+
const themeClass = getHtmlThemeClass(options.themeConfig, options.themeCookie);
|
|
235
|
+
if (themeClass) {
|
|
236
|
+
htmlClass = ` class="${themeClass}"`;
|
|
237
|
+
}
|
|
238
|
+
} else if (options?.defaultTheme === "dark" || options?.theme === "dark") {
|
|
239
|
+
htmlClass = ' class="dark"';
|
|
240
|
+
}
|
|
134
241
|
let processedScript = hydrationScript;
|
|
135
242
|
let importMapScript = "";
|
|
136
243
|
if (options?.runtimePath) {
|
|
@@ -155,8 +262,20 @@ ${importMapJson}
|
|
|
155
262
|
</script>
|
|
156
263
|
`;
|
|
157
264
|
}
|
|
265
|
+
let themeCssStyle = "";
|
|
266
|
+
if (options?.themeConfig) {
|
|
267
|
+
const themeCss = generateThemeCss({ config: options.themeConfig });
|
|
268
|
+
if (themeCss) {
|
|
269
|
+
themeCssStyle = `<style>${themeCss}</style>
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
158
273
|
let themeScript = "";
|
|
159
|
-
if (options?.
|
|
274
|
+
if (options?.themeConfig) {
|
|
275
|
+
const storageKey = options.themeStorageKey ?? "theme";
|
|
276
|
+
themeScript = `<script>${generateThemeScript(storageKey)}</script>
|
|
277
|
+
`;
|
|
278
|
+
} else if (options?.themeStorageKey) {
|
|
160
279
|
if (!/^[a-zA-Z0-9_-]+$/.test(options.themeStorageKey)) {
|
|
161
280
|
throw new Error(`Invalid themeStorageKey: ${options.themeStorageKey}. Only alphanumeric characters, underscores, and hyphens are allowed.`);
|
|
162
281
|
}
|
|
@@ -195,7 +314,7 @@ ${importMapJson}
|
|
|
195
314
|
<head>
|
|
196
315
|
<meta charset="utf-8">
|
|
197
316
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
198
|
-
${themeScript}${importMapScript}${head ?? ""}
|
|
317
|
+
${themeCssStyle}${themeScript}${importMapScript}${head ?? ""}
|
|
199
318
|
</head>
|
|
200
319
|
<body>
|
|
201
320
|
<div id="app">${content}</div>
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
generateMetaTags,
|
|
4
4
|
renderPage,
|
|
5
5
|
wrapHtml
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-C7LIB2RS.js";
|
|
7
7
|
|
|
8
8
|
// src/router/file-router.ts
|
|
9
9
|
import fg from "fast-glob";
|
|
@@ -1394,8 +1394,65 @@ function parseCookies(cookieHeader) {
|
|
|
1394
1394
|
}
|
|
1395
1395
|
return cookies;
|
|
1396
1396
|
}
|
|
1397
|
+
function wrapHtmlStream(contentStream, hydrationScript, options) {
|
|
1398
|
+
const encoder = new TextEncoder();
|
|
1399
|
+
const lang = options?.lang ?? "en";
|
|
1400
|
+
const themeClass = options?.theme === "dark" ? 'class="dark"' : "";
|
|
1401
|
+
const docStart = `<!DOCTYPE html>
|
|
1402
|
+
<html lang="${lang}" ${themeClass}>
|
|
1403
|
+
<head>
|
|
1404
|
+
<meta charset="UTF-8">
|
|
1405
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1406
|
+
${options?.head ?? ""}
|
|
1407
|
+
</head>
|
|
1408
|
+
<body>
|
|
1409
|
+
<div id="app">`;
|
|
1410
|
+
const docEnd = `</div>
|
|
1411
|
+
${options?.runtimePath ? `<script type="module" src="${options.runtimePath}"></script>` : ""}
|
|
1412
|
+
<script type="module">
|
|
1413
|
+
${hydrationScript}
|
|
1414
|
+
</script>
|
|
1415
|
+
</body>
|
|
1416
|
+
</html>`;
|
|
1417
|
+
let contentReader = null;
|
|
1418
|
+
let startSent = false;
|
|
1419
|
+
let contentDone = false;
|
|
1420
|
+
return new ReadableStream({
|
|
1421
|
+
async start() {
|
|
1422
|
+
contentReader = contentStream.getReader();
|
|
1423
|
+
},
|
|
1424
|
+
async pull(controller) {
|
|
1425
|
+
try {
|
|
1426
|
+
if (!startSent) {
|
|
1427
|
+
controller.enqueue(encoder.encode(docStart));
|
|
1428
|
+
startSent = true;
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
if (!contentDone && contentReader) {
|
|
1432
|
+
const { done, value } = await contentReader.read();
|
|
1433
|
+
if (done) {
|
|
1434
|
+
contentDone = true;
|
|
1435
|
+
controller.enqueue(encoder.encode(docEnd));
|
|
1436
|
+
controller.close();
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
controller.enqueue(value);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
controller.close();
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
controller.error(error);
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
cancel(reason) {
|
|
1448
|
+
if (contentReader) {
|
|
1449
|
+
contentReader.cancel(reason);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1397
1454
|
function createAdapter(options) {
|
|
1398
|
-
const { routes, loadModule = defaultLoadModule } = options;
|
|
1455
|
+
const { routes, loadModule = defaultLoadModule, streaming = false } = options;
|
|
1399
1456
|
async function fetch2(request) {
|
|
1400
1457
|
try {
|
|
1401
1458
|
const url = new URL(request.url);
|
|
@@ -1456,6 +1513,29 @@ function createAdapter(options) {
|
|
|
1456
1513
|
initialTheme = themeState.initial;
|
|
1457
1514
|
}
|
|
1458
1515
|
}
|
|
1516
|
+
if (streaming) {
|
|
1517
|
+
const encoder = new TextEncoder();
|
|
1518
|
+
const contentStream = new ReadableStream({
|
|
1519
|
+
start(controller) {
|
|
1520
|
+
controller.enqueue(encoder.encode(content));
|
|
1521
|
+
controller.close();
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
const streamOptions = {
|
|
1525
|
+
lang: "en"
|
|
1526
|
+
};
|
|
1527
|
+
if (initialTheme) {
|
|
1528
|
+
streamOptions.theme = initialTheme;
|
|
1529
|
+
}
|
|
1530
|
+
const htmlStream = wrapHtmlStream(contentStream, hydrationScript, streamOptions);
|
|
1531
|
+
return new Response(htmlStream, {
|
|
1532
|
+
status: 200,
|
|
1533
|
+
headers: {
|
|
1534
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1535
|
+
"Transfer-Encoding": "chunked"
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1459
1539
|
const html = wrapHtml(content, hydrationScript, void 0, initialTheme ? {
|
|
1460
1540
|
theme: initialTheme,
|
|
1461
1541
|
defaultTheme: initialTheme,
|
|
@@ -2969,14 +3049,14 @@ async function resolveConfig(fileConfig, cliOptions) {
|
|
|
2969
3049
|
|
|
2970
3050
|
// src/build/index.ts
|
|
2971
3051
|
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
2972
|
-
import { mkdir as mkdir2, writeFile, cp, readdir } from "fs/promises";
|
|
3052
|
+
import { mkdir as mkdir2, writeFile as writeFile2, cp, readdir } from "fs/promises";
|
|
2973
3053
|
import { join as join11, dirname as dirname6, relative as relative5, basename as basename4, isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
|
|
2974
3054
|
import { isCookieInitialExpr as isCookieInitialExpr3 } from "@constela/core";
|
|
2975
3055
|
|
|
2976
3056
|
// src/build/bundler.ts
|
|
2977
3057
|
import * as esbuild from "esbuild";
|
|
2978
3058
|
import { existsSync as existsSync8 } from "fs";
|
|
2979
|
-
import { mkdir, readFile } from "fs/promises";
|
|
3059
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2980
3060
|
import { createRequire } from "module";
|
|
2981
3061
|
import { join as join10, dirname as dirname5, isAbsolute as isAbsolute2, relative as relative4 } from "path";
|
|
2982
3062
|
import { fileURLToPath } from "url";
|
|
@@ -3724,14 +3804,14 @@ async function build2(options) {
|
|
|
3724
3804
|
const program = await convertToCompiledProgram(processedPageInfo);
|
|
3725
3805
|
const html = await renderPageToHtml(program, params, routePath, runtimePath, cssPath, processedPageInfo.page.externalImports, processedPageInfo.widgets, seoLang);
|
|
3726
3806
|
await mkdir2(dirname6(outputPath), { recursive: true });
|
|
3727
|
-
await
|
|
3807
|
+
await writeFile2(outputPath, html, "utf-8");
|
|
3728
3808
|
generatedFiles.push(outputPath);
|
|
3729
3809
|
const slugValue = params["slug"];
|
|
3730
3810
|
if (slugValue && (slugValue === "index" || slugValue.endsWith("/index"))) {
|
|
3731
3811
|
const parentOutputPath = join11(dirname6(dirname6(outputPath)), "index.html");
|
|
3732
3812
|
if (!generatedFiles.includes(parentOutputPath)) {
|
|
3733
3813
|
await mkdir2(dirname6(parentOutputPath), { recursive: true });
|
|
3734
|
-
await
|
|
3814
|
+
await writeFile2(parentOutputPath, html, "utf-8");
|
|
3735
3815
|
generatedFiles.push(parentOutputPath);
|
|
3736
3816
|
}
|
|
3737
3817
|
}
|
|
@@ -3758,7 +3838,7 @@ async function build2(options) {
|
|
|
3758
3838
|
const program = await convertToCompiledProgram(pageInfo);
|
|
3759
3839
|
const html = await renderPageToHtml(program, {}, routePath, runtimePath, cssPath, pageInfo.page.externalImports, pageInfo.widgets, seoLang);
|
|
3760
3840
|
await mkdir2(dirname6(outputPath), { recursive: true });
|
|
3761
|
-
await
|
|
3841
|
+
await writeFile2(outputPath, html, "utf-8");
|
|
3762
3842
|
generatedFiles.push(outputPath);
|
|
3763
3843
|
}
|
|
3764
3844
|
}
|
package/dist/cli/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,10 @@ interface ConstelaConfig {
|
|
|
80
80
|
edge?: {
|
|
81
81
|
adapter?: 'cloudflare' | 'vercel' | 'deno' | 'node';
|
|
82
82
|
};
|
|
83
|
+
/** Enable streaming SSR */
|
|
84
|
+
streaming?: boolean;
|
|
85
|
+
/** Flush strategy for streaming SSR */
|
|
86
|
+
streamingFlushStrategy?: 'immediate' | 'batched' | 'manual';
|
|
83
87
|
}
|
|
84
88
|
/**
|
|
85
89
|
* Development server options
|
|
@@ -97,6 +101,8 @@ interface DevServerOptions {
|
|
|
97
101
|
seo?: {
|
|
98
102
|
lang?: string;
|
|
99
103
|
};
|
|
104
|
+
/** Enable streaming SSR */
|
|
105
|
+
streaming?: boolean;
|
|
100
106
|
}
|
|
101
107
|
/**
|
|
102
108
|
* Build options
|
|
@@ -115,6 +121,8 @@ interface BuildOptions {
|
|
|
115
121
|
seo?: {
|
|
116
122
|
lang?: string;
|
|
117
123
|
};
|
|
124
|
+
/** Enable streaming SSR */
|
|
125
|
+
streaming?: boolean;
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
/**
|
|
@@ -279,6 +287,10 @@ interface AdapterOptions {
|
|
|
279
287
|
platform: PlatformAdapter;
|
|
280
288
|
routes: ScannedRoute[];
|
|
281
289
|
loadModule?: (file: string) => Promise<unknown>;
|
|
290
|
+
/** Enable streaming SSR */
|
|
291
|
+
streaming?: boolean;
|
|
292
|
+
/** Flush strategy for streaming SSR */
|
|
293
|
+
streamingFlushStrategy?: 'immediate' | 'batched' | 'manual';
|
|
282
294
|
}
|
|
283
295
|
interface EdgeAdapter {
|
|
284
296
|
fetch: (request: Request) => Promise<Response>;
|
package/dist/index.js
CHANGED
|
@@ -28,14 +28,14 @@ import {
|
|
|
28
28
|
transformCsv,
|
|
29
29
|
transformMdx,
|
|
30
30
|
transformYaml
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-VL6BMHBL.js";
|
|
32
32
|
import {
|
|
33
33
|
evaluateMetaExpression,
|
|
34
34
|
generateHydrationScript,
|
|
35
35
|
generateMetaTags,
|
|
36
36
|
renderPage,
|
|
37
37
|
wrapHtml
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-C7LIB2RS.js";
|
|
39
39
|
|
|
40
40
|
// src/build/ssg.ts
|
|
41
41
|
import { mkdir, writeFile } from "fs/promises";
|
|
@@ -5,6 +5,58 @@ import {
|
|
|
5
5
|
createHMRHandler,
|
|
6
6
|
createErrorOverlay
|
|
7
7
|
} from "@constela/runtime";
|
|
8
|
+
|
|
9
|
+
// src/runtime/theme-provider.ts
|
|
10
|
+
import { createThemeProvider } from "@constela/runtime";
|
|
11
|
+
var instance = null;
|
|
12
|
+
var ThemeProvider = {
|
|
13
|
+
/**
|
|
14
|
+
* Initializes the ThemeProvider with the given configuration.
|
|
15
|
+
* Creates a new instance if one doesn't exist.
|
|
16
|
+
*/
|
|
17
|
+
init(config) {
|
|
18
|
+
if (instance) {
|
|
19
|
+
instance.destroy();
|
|
20
|
+
}
|
|
21
|
+
instance = createThemeProvider({ config });
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* Sets the color scheme mode.
|
|
25
|
+
*/
|
|
26
|
+
setMode(mode) {
|
|
27
|
+
if (instance) {
|
|
28
|
+
instance.setMode(mode);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
/**
|
|
32
|
+
* Applies CSS variables to the document.
|
|
33
|
+
* Called internally by init, but can be called manually if needed.
|
|
34
|
+
*/
|
|
35
|
+
applyCssVariables() {
|
|
36
|
+
if (instance) {
|
|
37
|
+
const currentMode = instance.getMode();
|
|
38
|
+
instance.setMode(currentMode);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Destroys the ThemeProvider instance and cleans up resources.
|
|
43
|
+
*/
|
|
44
|
+
destroy() {
|
|
45
|
+
if (instance) {
|
|
46
|
+
instance.destroy();
|
|
47
|
+
instance = null;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Gets the underlying ThemeProvider instance.
|
|
52
|
+
* Returns null if not initialized.
|
|
53
|
+
*/
|
|
54
|
+
getInstance() {
|
|
55
|
+
return instance;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/runtime/entry-client.ts
|
|
8
60
|
function initClient(options) {
|
|
9
61
|
const { program, container, escapeHandlers = [], route } = options;
|
|
10
62
|
const appInstance = hydrateApp({ program, container, ...route && { route } });
|
|
@@ -36,6 +88,16 @@ function initClient(options) {
|
|
|
36
88
|
} catch {
|
|
37
89
|
}
|
|
38
90
|
}
|
|
91
|
+
const programTheme = program.theme;
|
|
92
|
+
const hasThemeProvider = !!programTheme;
|
|
93
|
+
if (hasThemeProvider) {
|
|
94
|
+
ThemeProvider.init(programTheme);
|
|
95
|
+
ThemeProvider.applyCssVariables();
|
|
96
|
+
const currentTheme = appInstance.getState?.("theme");
|
|
97
|
+
if (currentTheme === "dark") {
|
|
98
|
+
document.documentElement.classList.add("dark");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
39
101
|
if (program.state?.["theme"]) {
|
|
40
102
|
const updateThemeClass = (value) => {
|
|
41
103
|
if (value === "dark") {
|
|
@@ -44,11 +106,18 @@ function initClient(options) {
|
|
|
44
106
|
document.documentElement.classList.remove("dark");
|
|
45
107
|
}
|
|
46
108
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
109
|
+
if (!hasThemeProvider) {
|
|
110
|
+
const currentTheme = appInstance.getState?.("theme");
|
|
111
|
+
if (currentTheme) {
|
|
112
|
+
updateThemeClass(currentTheme);
|
|
113
|
+
}
|
|
50
114
|
}
|
|
51
|
-
const unsubscribeTheme = appInstance.subscribe("theme",
|
|
115
|
+
const unsubscribeTheme = appInstance.subscribe("theme", (value) => {
|
|
116
|
+
updateThemeClass(value);
|
|
117
|
+
if (hasThemeProvider && typeof value === "string") {
|
|
118
|
+
ThemeProvider.setMode(value);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
52
121
|
cleanupFns.push(unsubscribeTheme);
|
|
53
122
|
}
|
|
54
123
|
let destroyed = false;
|
|
@@ -59,6 +128,9 @@ function initClient(options) {
|
|
|
59
128
|
for (const cleanup of cleanupFns) {
|
|
60
129
|
cleanup();
|
|
61
130
|
}
|
|
131
|
+
if (hasThemeProvider) {
|
|
132
|
+
ThemeProvider.destroy();
|
|
133
|
+
}
|
|
62
134
|
appInstance.destroy();
|
|
63
135
|
},
|
|
64
136
|
setState(name, value) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CompiledRouteDefinition, CompiledExpression, CompiledProgram } from '@constela/compiler';
|
|
2
|
+
import { ThemeConfig, ColorScheme } from '@constela/core';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Server-side entry point for Constela applications
|
|
@@ -23,6 +24,10 @@ interface WrapHtmlOptions {
|
|
|
23
24
|
themeStorageKey?: string;
|
|
24
25
|
/** Default theme to use when no stored preference exists */
|
|
25
26
|
defaultTheme?: 'dark' | 'light';
|
|
27
|
+
/** Full theme configuration from program */
|
|
28
|
+
themeConfig?: ThemeConfig;
|
|
29
|
+
/** Theme from cookie (takes precedence over themeConfig.mode) */
|
|
30
|
+
themeCookie?: ColorScheme;
|
|
26
31
|
}
|
|
27
32
|
interface WidgetConfig {
|
|
28
33
|
/** The DOM element ID where the widget should be mounted */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/start",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Meta-framework for Constela applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
"@tailwindcss/postcss": "^4.0.0",
|
|
45
45
|
"tailwindcss": "^4.0.0",
|
|
46
46
|
"ws": "^8.18.0",
|
|
47
|
-
"@constela/ai": "
|
|
48
|
-
"@constela/
|
|
49
|
-
"@constela/
|
|
50
|
-
"@constela/
|
|
51
|
-
"@constela/server": "
|
|
52
|
-
"@constela/
|
|
47
|
+
"@constela/ai": "2.0.0",
|
|
48
|
+
"@constela/core": "0.17.0",
|
|
49
|
+
"@constela/router": "19.0.0",
|
|
50
|
+
"@constela/compiler": "0.15.0",
|
|
51
|
+
"@constela/server": "13.0.0",
|
|
52
|
+
"@constela/runtime": "1.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/ws": "^8.5.0",
|