@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.
@@ -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 escapeJsString(str) {
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 = escapeJsString(widget.id);
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 = escapeJsString(hmrUrl);
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
- const htmlClass = options?.defaultTheme === "dark" || options?.theme === "dark" ? ' class="dark"' : "";
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?.themeStorageKey) {
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-AZWQ5YMZ.js";
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 writeFile(outputPath, html, "utf-8");
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 writeFile(parentOutputPath, html, "utf-8");
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 writeFile(outputPath, html, "utf-8");
3841
+ await writeFile2(outputPath, html, "utf-8");
3762
3842
  generatedFiles.push(outputPath);
3763
3843
  }
3764
3844
  }
package/dist/cli/index.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  hyperlink,
5
5
  loadConfig,
6
6
  resolveConfig
7
- } from "../chunk-ZXGA4Z6W.js";
8
- import "../chunk-AZWQ5YMZ.js";
7
+ } from "../chunk-VL6BMHBL.js";
8
+ import "../chunk-C7LIB2RS.js";
9
9
 
10
10
  // src/cli/index.ts
11
11
  import { Command } from "commander";
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-ZXGA4Z6W.js";
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-AZWQ5YMZ.js";
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
- const currentTheme = appInstance.getState?.("theme");
48
- if (currentTheme) {
49
- updateThemeClass(currentTheme);
109
+ if (!hasThemeProvider) {
110
+ const currentTheme = appInstance.getState?.("theme");
111
+ if (currentTheme) {
112
+ updateThemeClass(currentTheme);
113
+ }
50
114
  }
51
- const unsubscribeTheme = appInstance.subscribe("theme", updateThemeClass);
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 */
@@ -4,7 +4,7 @@ import {
4
4
  generateMetaTags,
5
5
  renderPage,
6
6
  wrapHtml
7
- } from "../chunk-AZWQ5YMZ.js";
7
+ } from "../chunk-C7LIB2RS.js";
8
8
  export {
9
9
  evaluateMetaExpression,
10
10
  generateHydrationScript,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/start",
3
- "version": "1.8.26",
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": "1.0.2",
48
- "@constela/compiler": "0.14.7",
49
- "@constela/core": "0.16.2",
50
- "@constela/runtime": "0.19.6",
51
- "@constela/server": "12.0.2",
52
- "@constela/router": "18.0.1"
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",