Skip to content

Commit 430cfe3

Browse files
committed
Rewrite RAGOT imports in copied JS assets
Rewrite local JS/MJS asset imports that point at ragot.esm.min.js so user-authored vanilla JS can import RAGOT from /vendor/ragot.esm.min.js while builds resolve the emitted runtime location correctly. Document vanilla JavaScript RAGOT interop, add static asset README files to the default and labs templates, and clarify when to use app/static/ versus project-root public/. Extend asset build coverage for rewritten static and surface script imports, including dynamic imports and external CDN imports.
1 parent 92f37e7 commit 430cfe3

5 files changed

Lines changed: 122 additions & 2 deletions

File tree

docs/app/content/docs/ragot/dom.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,45 @@ class ChartModule(Module):
9999
## Lower-level Ragot helpers
100100

101101
If you need to drop below the `dom.*` helper surface, the shipped Ragot runtime also includes lower-level primitives such as `createLazyLoader(...)` and `createInfiniteScroll(...)`. SPRAG's normal authoring path usually reaches those through `ui.LazyImage`, `@infinite_scroll`, and virtual-scroll integration, but they are part of the underlying runtime.
102+
103+
## Vanilla JavaScript with Ragot
104+
105+
For complex browser-only behavior, put a normal JavaScript module in
106+
`app/static/` and import Ragot from the vendored runtime:
107+
108+
```javascript
109+
// app/static/js/gallery.mjs
110+
import { Component, createElement, VirtualScroller } from "/vendor/ragot.esm.min.js";
111+
112+
class GalleryRail extends Component {
113+
render() {
114+
return createElement("div", { className: "gallery-rail" });
115+
}
116+
}
117+
118+
export function mountGallery(root) {
119+
const rail = new GalleryRail();
120+
rail.mount(root);
121+
return rail;
122+
}
123+
```
124+
125+
Attach it to a page or mount as a module script:
126+
127+
```python
128+
from sprag import page, script
129+
130+
gallery = page(
131+
path="/gallery",
132+
controller=GalleryController,
133+
screen=GalleryScreen,
134+
mode="hybrid",
135+
js=[script("app/static/js/gallery.mjs", module=True)],
136+
)
137+
```
138+
139+
During builds, SPRAG copies `app/static/js/gallery.mjs` to
140+
`/static/js/gallery.mjs` and rewrites imports that point at
141+
`ragot.esm.min.js` to the emitted runtime location. That means the source can
142+
use `/vendor/ragot.esm.min.js`, while static and packaged builds still get the
143+
correct relative import path.

sprag/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Public SPRAG framework surface."""
22

3-
__version__ = "0.1.14"
3+
__version__ = "0.1.15"
44

55
from .runtime import dom
66
from .runtime.app import App
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Static Assets
2+
3+
Files in `app/static/` are served at `/static/...` and copied into builds.
4+
Use this folder for images, fonts, vendored JavaScript, CSS, web workers,
5+
WASM, and small browser-only bridge modules.
6+
7+
Examples:
8+
9+
- `app/static/images/logo.png` -> `/static/images/logo.png`
10+
- `app/static/js/gallery.mjs` -> `/static/js/gallery.mjs`
11+
- `app/static/vendor/widget.js` -> `/static/vendor/widget.js`
12+
13+
Use `page(js=[...])`, `mount(js=[...])`, or `modules={...}` to attach scripts
14+
from here to a surface. For vanilla JavaScript that needs Ragot directly,
15+
import from `/vendor/ragot.esm.min.js`; SPRAG rewrites that import to the
16+
correct emitted runtime path during builds.
17+
18+
The project-root `public/` folder is different: it is copied to the build
19+
output root for files like `favicon.ico`, `robots.txt`, and `sitemap.xml`.

sprag/runtime/assets.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import hashlib
66
import html
77
import importlib
8+
import posixpath
9+
import re
810
import shutil
911
from dataclasses import dataclass, field
1012
from pathlib import Path
@@ -241,6 +243,46 @@ def emit_assets(output_dir: str | Path, assets: Iterable[Asset]):
241243
target_path = target_root / asset.web_path.lstrip("/")
242244
target_path.parent.mkdir(parents=True, exist_ok=True)
243245
shutil.copy2(asset.source_path, target_path)
246+
_rewrite_copied_js_ragot_imports(target_path, asset.web_path)
247+
248+
249+
_JS_IMPORT_SPEC_RE = re.compile(
250+
r"""(?P<head>\bimport\s*(?:\(\s*)?(?:[^'";]*?\s+from\s*)?)(?P<quote>['"])(?P<spec>[^'"]+)(?P=quote)""",
251+
re.M,
252+
)
253+
254+
255+
def _rewrite_copied_js_ragot_imports(target_path: Path, web_path: str) -> None:
256+
if target_path.suffix.lower() not in {".js", ".mjs"}:
257+
return
258+
try:
259+
content = target_path.read_text(encoding="utf-8")
260+
except UnicodeDecodeError:
261+
return
262+
rewritten = _rewrite_ragot_import_specs(content, web_path)
263+
if rewritten != content:
264+
target_path.write_text(rewritten, encoding="utf-8")
265+
266+
267+
def _rewrite_ragot_import_specs(content: str, web_path: str) -> str:
268+
current_dir = posixpath.dirname(web_path.lstrip("/"))
269+
if current_dir:
270+
runtime_spec = posixpath.relpath("vendor/ragot.esm.min.js", current_dir)
271+
else:
272+
runtime_spec = "vendor/ragot.esm.min.js"
273+
if not runtime_spec.startswith("."):
274+
runtime_spec = "./" + runtime_spec
275+
276+
def replace(match: re.Match) -> str:
277+
spec = match.group("spec")
278+
if _is_external_path(spec):
279+
return match.group(0)
280+
spec_path = spec.split("#", 1)[0].split("?", 1)[0]
281+
if posixpath.basename(spec_path) != "ragot.esm.min.js":
282+
return match.group(0)
283+
return f"{match.group('head')}{match.group('quote')}{runtime_spec}{match.group('quote')}"
284+
285+
return _JS_IMPORT_SPEC_RE.sub(replace, content)
244286

245287

246288
def render_css_links(assets: Iterable[Asset], *, document_path: str | None = None) -> str:

tests/test_assets.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,21 @@ def test_build_web_preview_emits_surface_assets_and_static_tree(self):
117117
)
118118
(root / "app" / "shell.css").write_text("body { color: red; }\n", encoding="utf-8")
119119
(root / "app" / "routes" / "docs" / "module-helper.mjs").write_text(
120-
"window.__SPRAG_TEST_MODULE__ = true;\n",
120+
"import { bus } from '/vendor/ragot.esm.min.js';\n"
121+
"window.__SPRAG_TEST_MODULE__ = bus;\n",
121122
encoding="utf-8",
122123
)
123124
(root / "app" / "static" / "vendor" / "dayjs.mjs").write_text(
124125
"export default function dayjs() { return { format() { return '2026-04-12'; } }; }\n",
125126
encoding="utf-8",
126127
)
128+
(root / "app" / "static" / "vendor" / "ragot-widget.mjs").write_text(
129+
"import { Component } from '/vendor/ragot.esm.min.js';\n"
130+
"import('./ragot.esm.min.js');\n"
131+
"import('https://cdn.example.test/ragot.esm.min.js');\n"
132+
"export { Component };\n",
133+
encoding="utf-8",
134+
)
127135
(root / "app" / "static" / "vendor" / "widget.js").write_text(
128136
"window.__SPRAG_WIDGET__ = true;\n",
129137
encoding="utf-8",
@@ -173,13 +181,22 @@ def test_build_web_preview_emits_surface_assets_and_static_tree(self):
173181
self.assertTrue((root / "dist" / "assets" / "app" / "shell.css").exists())
174182
self.assertTrue((root / "dist" / "assets" / "app" / "routes" / "docs" / "module-helper.mjs").exists())
175183
self.assertTrue((root / "dist" / "static" / "vendor" / "dayjs.mjs").exists())
184+
self.assertTrue((root / "dist" / "static" / "vendor" / "ragot-widget.mjs").exists())
176185
self.assertTrue((root / "dist" / "static" / "vendor" / "widget.js").exists())
177186
self.assertTrue((root / "dist" / "static" / "images" / "logo.svg").exists())
178187

188+
module_helper = (root / "dist" / "assets" / "app" / "routes" / "docs" / "module-helper.mjs").read_text(encoding="utf-8")
189+
ragot_widget = (root / "dist" / "static" / "vendor" / "ragot-widget.mjs").read_text(encoding="utf-8")
190+
self.assertIn("from '../../../../vendor/ragot.esm.min.js'", module_helper)
191+
self.assertIn("from '../../vendor/ragot.esm.min.js'", ragot_widget)
192+
self.assertIn("import('../../vendor/ragot.esm.min.js')", ragot_widget)
193+
self.assertIn("import('https://cdn.example.test/ragot.esm.min.js')", ragot_widget)
194+
179195
asset_paths = {asset["web_path"] for asset in manifest["assets"]}
180196
self.assertIn("/assets/app/shell.css", asset_paths)
181197
self.assertIn("/assets/app/routes/docs/module-helper.mjs", asset_paths)
182198
self.assertIn("/static/vendor/dayjs.mjs", asset_paths)
199+
self.assertIn("/static/vendor/ragot-widget.mjs", asset_paths)
183200
self.assertIn("/static/vendor/widget.js", asset_paths)
184201
self.assertIn("/static/images/logo.svg", asset_paths)
185202

0 commit comments

Comments
 (0)