pulse-framework 0.1.62__py3-none-any.whl

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.
Files changed (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
pulse/code_analysis.py ADDED
@@ -0,0 +1,38 @@
1
+ """Code analysis utilities for inspecting Python source."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import inspect
7
+ import textwrap
8
+ from collections.abc import Callable
9
+ from typing import Any
10
+
11
+
12
+ def is_stub_function(fn: Callable[..., Any]) -> bool:
13
+ """Check if function body is just ... or pass (no real implementation)."""
14
+ try:
15
+ source = inspect.getsource(fn)
16
+ tree = ast.parse(textwrap.dedent(source))
17
+ func_def = tree.body[0]
18
+ if not isinstance(func_def, ast.FunctionDef):
19
+ return False
20
+ body = func_def.body
21
+ # Skip docstring
22
+ if body and isinstance(body[0], ast.Expr):
23
+ if isinstance(body[0].value, ast.Constant) and isinstance(
24
+ body[0].value.value, str
25
+ ):
26
+ body = body[1:]
27
+ if not body:
28
+ return True
29
+ if len(body) == 1:
30
+ stmt = body[0]
31
+ if isinstance(stmt, ast.Pass):
32
+ return True
33
+ if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant):
34
+ if stmt.value.value is ...:
35
+ return True
36
+ return False
37
+ except (OSError, TypeError, SyntaxError):
38
+ return False
File without changes
@@ -0,0 +1,359 @@
1
+ import logging
2
+ from collections.abc import Sequence
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pulse.cli.helpers import ensure_gitignore_has
8
+ from pulse.codegen.templates.layout import LAYOUT_TEMPLATE
9
+ from pulse.codegen.templates.route import generate_route
10
+ from pulse.codegen.templates.routes_ts import (
11
+ ROUTES_CONFIG_TEMPLATE,
12
+ ROUTES_RUNTIME_TEMPLATE,
13
+ )
14
+ from pulse.env import env
15
+ from pulse.routing import Layout, Route, RouteTree
16
+ from pulse.transpiler.assets import get_registered_assets
17
+
18
+ if TYPE_CHECKING:
19
+ from pulse.app import ConnectionStatusConfig
20
+
21
+ logger = logging.getLogger(__file__)
22
+
23
+
24
+ @dataclass
25
+ class CodegenConfig:
26
+ """Configuration for code generation output paths.
27
+
28
+ Controls where generated React Router files are written. All paths
29
+ can be relative (resolved against base_dir) or absolute.
30
+
31
+ Args:
32
+ web_dir: Root directory for web output. Defaults to "web".
33
+ pulse_dir: Subdirectory for generated Pulse files. Defaults to "pulse".
34
+ base_dir: Base directory for resolving relative paths. If not provided,
35
+ resolved from PULSE_APP_FILE, PULSE_APP_DIR, or cwd.
36
+
37
+ Attributes:
38
+ web_dir: Root directory for web output.
39
+ pulse_dir: Subdirectory name for generated files.
40
+ base_dir: Explicit base directory, if provided.
41
+
42
+ Example:
43
+ ```python
44
+ app = ps.App(
45
+ codegen=ps.CodegenConfig(
46
+ web_dir="frontend",
47
+ pulse_dir="generated",
48
+ ),
49
+ )
50
+ # Generated files will be at: frontend/app/generated/
51
+ ```
52
+ """
53
+
54
+ web_dir: Path | str = "web"
55
+ """Root directory for the web output."""
56
+
57
+ pulse_dir: Path | str = "pulse"
58
+ """Name of the Pulse app directory."""
59
+
60
+ base_dir: Path | None = None
61
+ """Directory containing the user's app file. If not provided, resolved from env."""
62
+
63
+ @property
64
+ def resolved_base_dir(self) -> Path:
65
+ """Resolve the base directory where relative paths should be anchored.
66
+
67
+ Returns:
68
+ Resolved base directory path.
69
+
70
+ Resolution precedence:
71
+ 1. Explicit `base_dir` if provided
72
+ 2. Directory of PULSE_APP_FILE env var
73
+ 3. PULSE_APP_DIR env var
74
+ 4. Current working directory
75
+ """
76
+ if isinstance(self.base_dir, Path):
77
+ return self.base_dir
78
+ app_file = env.pulse_app_file
79
+ if app_file:
80
+ return Path(app_file).parent
81
+ app_dir = env.pulse_app_dir
82
+ if app_dir:
83
+ return Path(app_dir)
84
+ return Path.cwd()
85
+
86
+ @property
87
+ def web_root(self) -> Path:
88
+ """Absolute path to the web root directory.
89
+
90
+ Returns:
91
+ Absolute path to web_dir (e.g., `<base_dir>/web`).
92
+ """
93
+ wd = Path(self.web_dir)
94
+ if wd.is_absolute():
95
+ return wd
96
+ return self.resolved_base_dir / wd
97
+
98
+ @property
99
+ def pulse_path(self) -> Path:
100
+ """Full path to the generated Pulse app directory.
101
+
102
+ Returns:
103
+ Absolute path where generated files are written
104
+ (e.g., `<web_root>/app/<pulse_dir>`).
105
+ """
106
+ return self.web_root / "app" / self.pulse_dir
107
+
108
+
109
+ def write_file_if_changed(path: Path, content: str | bytes) -> Path:
110
+ """Write content to file only if it has changed."""
111
+ if path.exists():
112
+ try:
113
+ if isinstance(content, bytes):
114
+ current_content = path.read_bytes()
115
+ else:
116
+ current_content = path.read_text()
117
+ if current_content == content:
118
+ return path # Skip writing, content is the same
119
+ except Exception as exc:
120
+ logging.warning("Can't read file %s: %s", path.absolute(), exc)
121
+ # If we can't read the file for any reason, just write it
122
+ pass
123
+
124
+ path.parent.mkdir(exist_ok=True, parents=True)
125
+ if isinstance(content, bytes):
126
+ path.write_bytes(content)
127
+ else:
128
+ path.write_text(content)
129
+ return path
130
+
131
+
132
+ class Codegen:
133
+ cfg: CodegenConfig
134
+ routes: RouteTree
135
+
136
+ def __init__(self, routes: RouteTree, config: CodegenConfig) -> None:
137
+ self.cfg = config
138
+ self.routes = routes
139
+ self._copied_files: set[Path] = set()
140
+
141
+ @property
142
+ def output_folder(self):
143
+ return self.cfg.pulse_path
144
+
145
+ @property
146
+ def assets_folder(self):
147
+ return self.output_folder / "assets"
148
+
149
+ def generate_all(
150
+ self,
151
+ server_address: str,
152
+ internal_server_address: str | None = None,
153
+ api_prefix: str = "",
154
+ connection_status: "ConnectionStatusConfig | None" = None,
155
+ ):
156
+ # Ensure generated files are gitignored
157
+ ensure_gitignore_has(self.cfg.web_root, f"app/{self.cfg.pulse_dir}/")
158
+
159
+ self._copied_files = set()
160
+
161
+ # Copy all registered local files to the assets directory
162
+ self._copy_local_files()
163
+
164
+ # Keep track of all generated files
165
+ generated_files = set(
166
+ [
167
+ self.generate_layout_tsx(
168
+ server_address,
169
+ internal_server_address,
170
+ api_prefix,
171
+ connection_status,
172
+ ),
173
+ self.generate_routes_ts(),
174
+ self.generate_routes_runtime_ts(),
175
+ *(
176
+ self.generate_route(route, server_address=server_address)
177
+ for route in self.routes.flat_tree.values()
178
+ ),
179
+ ]
180
+ )
181
+ generated_files.update(self._copied_files)
182
+
183
+ # Clean up any remaining files that are not part of the generated files
184
+ for path in self.output_folder.rglob("*"):
185
+ if path.is_file() and path not in generated_files:
186
+ try:
187
+ path.unlink()
188
+ logger.debug(f"Removed stale file: {path}")
189
+ except Exception as e:
190
+ logger.warning(f"Could not remove stale file {path}: {e}")
191
+
192
+ def _copy_local_files(self) -> None:
193
+ """Copy all registered local assets to the assets directory.
194
+
195
+ Uses the unified asset registry which tracks local files from both
196
+ Import objects and DynamicImport expressions.
197
+ """
198
+ assets = get_registered_assets()
199
+
200
+ if not assets:
201
+ return
202
+
203
+ self.assets_folder.mkdir(parents=True, exist_ok=True)
204
+
205
+ for asset in assets:
206
+ dest_path = self.assets_folder / asset.asset_filename
207
+
208
+ # Copy file if source exists
209
+ if asset.source_path.exists():
210
+ self._copied_files.add(dest_path)
211
+ try:
212
+ content = asset.source_path.read_bytes()
213
+ except OSError as exc:
214
+ logger.warning(
215
+ "Can't read asset %s: %s",
216
+ asset.source_path,
217
+ exc,
218
+ )
219
+ continue
220
+ write_file_if_changed(dest_path, content)
221
+
222
+ def generate_layout_tsx(
223
+ self,
224
+ server_address: str,
225
+ internal_server_address: str | None = None,
226
+ api_prefix: str = "",
227
+ connection_status: "ConnectionStatusConfig | None" = None,
228
+ ):
229
+ """Generates the content of _layout.tsx"""
230
+ from pulse.app import ConnectionStatusConfig
231
+
232
+ connection_status = connection_status or ConnectionStatusConfig()
233
+ content = str(
234
+ LAYOUT_TEMPLATE.render_unicode(
235
+ server_address=server_address,
236
+ internal_server_address=internal_server_address or server_address,
237
+ api_prefix=api_prefix,
238
+ connection_status=connection_status,
239
+ )
240
+ )
241
+ # The underscore avoids an eventual naming conflict with a generated
242
+ # /layout route.
243
+ return write_file_if_changed(self.output_folder / "_layout.tsx", content)
244
+
245
+ def generate_routes_ts(self):
246
+ """Generate TypeScript code for the routes configuration."""
247
+ routes_str = self._render_routes_ts(self.routes.tree, 2)
248
+ content = str(
249
+ ROUTES_CONFIG_TEMPLATE.render_unicode(
250
+ routes_str=routes_str,
251
+ pulse_dir=self.cfg.pulse_dir,
252
+ )
253
+ )
254
+ return write_file_if_changed(self.output_folder / "routes.ts", content)
255
+
256
+ def generate_routes_runtime_ts(self):
257
+ """Generate a runtime React Router object tree for server-side matching."""
258
+ routes_str = self._render_routes_runtime(self.routes.tree, indent_level=0)
259
+ content = str(
260
+ ROUTES_RUNTIME_TEMPLATE.render_unicode(
261
+ routes_str=routes_str,
262
+ )
263
+ )
264
+ return write_file_if_changed(self.output_folder / "routes.runtime.ts", content)
265
+
266
+ def _render_routes_ts(
267
+ self, routes: Sequence[Route | Layout], indent_level: int
268
+ ) -> str:
269
+ lines: list[str] = []
270
+ indent_str = " " * indent_level
271
+ for route in routes:
272
+ if isinstance(route, Layout):
273
+ children_str = ""
274
+ if route.children:
275
+ children_str = f"\n{self._render_routes_ts(route.children, indent_level + 1)}\n{indent_str}"
276
+ lines.append(
277
+ f'{indent_str}layout("{self.cfg.pulse_dir}/layouts/{route.file_path()}", [{children_str}]),'
278
+ )
279
+ else:
280
+ if route.children:
281
+ children_str = f"\n{self._render_routes_ts(route.children, indent_level + 1)}\n{indent_str}"
282
+ lines.append(
283
+ f'{indent_str}route("{route.path}", "{self.cfg.pulse_dir}/routes/{route.file_path()}", [{children_str}]),'
284
+ )
285
+ elif route.is_index:
286
+ lines.append(
287
+ f'{indent_str}index("{self.cfg.pulse_dir}/routes/{route.file_path()}"),'
288
+ )
289
+ else:
290
+ lines.append(
291
+ f'{indent_str}route("{route.path}", "{self.cfg.pulse_dir}/routes/{route.file_path()}"),'
292
+ )
293
+ return "\n".join(lines)
294
+
295
+ def generate_route(
296
+ self,
297
+ route: Route | Layout,
298
+ server_address: str,
299
+ ):
300
+ route_file_path = route.file_path()
301
+ if isinstance(route, Layout):
302
+ output_path = self.output_folder / "layouts" / route_file_path
303
+ full_route_path = f"layouts/{route_file_path}"
304
+ else:
305
+ output_path = self.output_folder / "routes" / route_file_path
306
+ full_route_path = f"routes/{route_file_path}"
307
+
308
+ content = generate_route(
309
+ path=route.unique_path(),
310
+ route_file_path=full_route_path,
311
+ )
312
+ return write_file_if_changed(output_path, content)
313
+
314
+ def _render_routes_runtime(
315
+ self, routes: list[Route | Layout], indent_level: int
316
+ ) -> str:
317
+ """
318
+ Render an array of RRRouteObject literals suitable for matchRoutes.
319
+ """
320
+
321
+ def render_node(node: Route | Layout, indent: int) -> str:
322
+ ind = " " * indent
323
+ lines: list[str] = [f"{ind}{{"]
324
+ # Common: id and uniquePath
325
+ lines.append(f'{ind} id: "{node.unique_path()}",')
326
+ lines.append(f'{ind} uniquePath: "{node.unique_path()}",')
327
+ if isinstance(node, Layout):
328
+ # Pathless layout
329
+ lines.append(
330
+ f'{ind} file: "{self.cfg.pulse_dir}/layouts/{node.file_path()}",'
331
+ )
332
+ else:
333
+ # Route: index vs path
334
+ if node.is_index:
335
+ lines.append(f"{ind} index: true,")
336
+ else:
337
+ lines.append(f'{ind} path: "{node.path}",')
338
+ lines.append(
339
+ f'{ind} file: "{self.cfg.pulse_dir}/routes/{node.file_path()}",'
340
+ )
341
+ if node.children:
342
+ lines.append(f"{ind} children: [")
343
+ for c in node.children:
344
+ lines.append(render_node(c, indent + 2))
345
+ lines.append(f"{ind} ,")
346
+ if lines[-1] == f"{ind} ,":
347
+ lines.pop()
348
+ lines.append(f"{ind} ],")
349
+ lines.append(f"{ind}}}")
350
+ return "\n".join(lines)
351
+
352
+ ind = " " * indent_level
353
+ out: list[str] = [f"{ind}["]
354
+ for index, r in enumerate(routes):
355
+ out.append(render_node(r, indent_level + 1))
356
+ if index != len(routes) - 1:
357
+ out.append(f"{ind} ,")
358
+ out.append(f"{ind}]")
359
+ return "\n".join(out)
File without changes
@@ -0,0 +1,106 @@
1
+ from mako.template import Template
2
+
3
+ LAYOUT_TEMPLATE = Template(
4
+ """import { deserialize, extractServerRouteInfo, PulseProvider, type PulseConfig, type PulsePrerender } from "pulse-ui-client";
5
+ import { Outlet, data, type LoaderFunctionArgs, type ClientLoaderFunctionArgs } from "react-router";
6
+ import { matchRoutes } from "react-router";
7
+ import { rrPulseRouteTree } from "./routes.runtime";
8
+ import { useLoaderData } from "react-router";
9
+
10
+ // This config is used to initialize the client
11
+ export const config: PulseConfig = {
12
+ serverAddress: "${server_address}",
13
+ apiPrefix: "${api_prefix}",
14
+ connectionStatus: {
15
+ initialConnectingDelay: ${int(connection_status.initial_connecting_delay * 1000)},
16
+ initialErrorDelay: ${int(connection_status.initial_error_delay * 1000)},
17
+ reconnectErrorDelay: ${int(connection_status.reconnect_error_delay * 1000)},
18
+ },
19
+ };
20
+
21
+
22
+ // Server loader: perform initial prerender, abort on first redirect/not-found
23
+ export async function loader(args: LoaderFunctionArgs) {
24
+ const url = new URL(args.request.url);
25
+ const matches = matchRoutes(rrPulseRouteTree, url.pathname) ?? [];
26
+ const paths = matches.map(m => m.route.uniquePath);
27
+ // Build minimal, safe headers for cross-origin API call
28
+ const incoming = args.request.headers;
29
+ const fwd = new Headers();
30
+ const cookie = incoming.get("cookie");
31
+ const authorization = incoming.get("authorization");
32
+ if (cookie) fwd.set("cookie", cookie);
33
+ if (authorization) fwd.set("authorization", authorization);
34
+ fwd.set("content-type", "application/json");
35
+ const res = await fetch(`${internal_server_address}$${"{"}config.apiPrefix}/prerender`, {
36
+ method: "POST",
37
+ headers: fwd,
38
+ body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args) }),
39
+ });
40
+ if (!res.ok) throw new Error("Failed to prerender batch:" + res.status);
41
+ const body = await res.json();
42
+ if (body.redirect) return new Response(null, { status: 302, headers: { Location: body.redirect } });
43
+ if (body.notFound) {
44
+ console.error("Not found:", url.pathname);
45
+ throw new Response("Not Found", { status: 404 });
46
+ }
47
+ const prerenderData = deserialize(body) as PulsePrerender;
48
+ const setCookies =
49
+ (res.headers.getSetCookie?.() as string[] | undefined) ??
50
+ (res.headers.get("set-cookie") ? [res.headers.get("set-cookie") as string] : []);
51
+ const headers = new Headers();
52
+ for (const c of setCookies) headers.append("Set-Cookie", c);
53
+ return data(prerenderData, { headers });
54
+ }
55
+
56
+ // Client loader: re-prerender on navigation while reusing renderId
57
+ export async function clientLoader(args: ClientLoaderFunctionArgs) {
58
+ const url = new URL(args.request.url);
59
+ const matches = matchRoutes(rrPulseRouteTree, url.pathname) ?? [];
60
+ const paths = matches.map(m => m.route.uniquePath);
61
+ const renderId =
62
+ typeof window !== "undefined" && typeof sessionStorage !== "undefined"
63
+ ? (sessionStorage.getItem("__PULSE_RENDER_ID") ?? undefined)
64
+ : undefined;
65
+ const directives =
66
+ typeof window !== "undefined" && typeof sessionStorage !== "undefined"
67
+ ? (JSON.parse(sessionStorage.getItem("__PULSE_DIRECTIVES") ?? "{}"))
68
+ : {};
69
+ const headers: HeadersInit = { "content-type": "application/json" };
70
+ if (directives?.headers) {
71
+ for (const [key, value] of Object.entries(directives.headers)) {
72
+ headers[key] = value as string;
73
+ }
74
+ }
75
+ const res = await fetch(`$${"{"}config.serverAddress}$${"{"}config.apiPrefix}/prerender`, {
76
+ method: "POST",
77
+ headers,
78
+ credentials: "include",
79
+ body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args), renderId }),
80
+ });
81
+ if (!res.ok) throw new Error("Failed to prerender batch:" + res.status);
82
+ const body = await res.json();
83
+ if (body.redirect) return new Response(null, { status: 302, headers: { Location: body.redirect } });
84
+ if (body.notFound) throw new Response("Not Found", { status: 404 });
85
+ const prerenderData = deserialize(body) as PulsePrerender;
86
+ if (typeof window !== "undefined" && typeof sessionStorage !== "undefined" && prerenderData.directives) {
87
+ sessionStorage.setItem("__PULSE_DIRECTIVES", JSON.stringify(prerenderData.directives));
88
+ }
89
+ return prerenderData as PulsePrerender;
90
+ }
91
+
92
+ export default function PulseLayout() {
93
+ const data = useLoaderData<typeof loader>();
94
+ if (typeof window !== "undefined" && typeof sessionStorage !== "undefined") {
95
+ sessionStorage.setItem("__PULSE_RENDER_ID", data.renderId);
96
+ sessionStorage.setItem("__PULSE_DIRECTIVES", JSON.stringify(data.directives));
97
+ }
98
+ return (
99
+ <PulseProvider config={config} prerender={data}>
100
+ <Outlet />
101
+ </PulseProvider>
102
+ );
103
+ }
104
+ // Persist renderId and directives in sessionStorage for reuse in clientLoader is handled within the component
105
+ """
106
+ )