From f189c37282faa5b0d4afa544e38b362da51a4150 Mon Sep 17 00:00:00 2001 From: Server <10.9.8.250@reg.snw.su> Date: Mon, 10 Aug 2026 23:03:13 +0000 Subject: [PATCH] Expose HTML compile version and update event --- README.md | 20 ++++++++++++++++++++ docslib/__init__.py | 2 +- docslib/_version.py | 4 ++++ docslib/compiler.py | 8 +++++++- docslib/templates/client.js | 9 +++++++++ examples/assets/inject.js | 4 ++++ pyproject.toml | 5 ++++- tests/test_server.py | 9 ++++++++- uv.lock | 1 - 9 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 docslib/_version.py diff --git a/README.md b/README.md index d5cb591..ee7f4c2 100644 --- a/README.md +++ b/README.md @@ -232,8 +232,28 @@ document.addEventListener("on_manifest_update", (event) => { }); ``` +Сразу после несовпадения хеша из `version`, но ещё до запроса большого `manifest.json`, отправляется `on_update_available`: + +```javascript +document.addEventListener("on_update_available", (event) => { + const { revision, currentRevision, versionUrl, manifestUrl } = event.detail; + console.log(`Доступно обновление ${currentRevision} → ${revision}`, { + versionUrl, + manifestUrl, + }); +}); +``` + Код из `custom_js` считается доверенным и выполняется в контексте страницы. Изменение системного или пользовательского CSS/JS меняет `revision`, поэтому обновлённый runtime сохраняется в IndexedDB и применяется после перезагрузки клиента. +Версия библиотеки, которой был скомпилирован физический `index.html`, доступна пользовательскому JavaScript в глобальной строковой переменной: + +```javascript +console.log(globalThis.COMPILED_FROM_VERSION); // например, "0.3.7" +``` + +Присваивание добавляется в bootstrap-скрипт самой HTML-ки. Автообновление manifest не меняет `COMPILED_FROM_VERSION`: значение обновится только после новой компиляции и замены `index.html`. + ## Live-preview В live-режиме браузер опрашивает только `/live`. Сервер следит за файлами, в которых объявлены страницы, а также за системными и пользовательскими CSS/JS. При изменении он пересобирает manifest; клиент сохраняет его и перезагружается с новым runtime. В запускаемом файле вызов `run()` обязательно помещать под `if __name__ == "__main__"`, чтобы watcher не запускал второй сервер. diff --git a/docslib/__init__.py b/docslib/__init__.py index 30eb553..5a90b99 100644 --- a/docslib/__init__.py +++ b/docslib/__init__.py @@ -1,8 +1,8 @@ """Public API for SharedDocsLib.""" +from ._version import __version__ from .hooks import Component, Page from .server import build, create_app, run from .ui import HeaderButton __all__ = ["Component", "HeaderButton", "Page", "build", "create_app", "run"] -__version__ = "0.3.6" diff --git a/docslib/_version.py b/docslib/_version.py new file mode 100644 index 0000000..ae36319 --- /dev/null +++ b/docslib/_version.py @@ -0,0 +1,4 @@ +"""Single source of truth for the SharedDocsLib package version.""" + +__version__ = "0.3.7" + diff --git a/docslib/compiler.py b/docslib/compiler.py index d4bef68..168f4bb 100644 --- a/docslib/compiler.py +++ b/docslib/compiler.py @@ -12,6 +12,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Iterable, Mapping, Sequence +from ._version import __version__ from .registry import PageDefinition, registry from .ui import HeaderButton @@ -229,6 +230,8 @@ class Compiler: if not path.is_file(): raise FileNotFoundError(f"Template file does not exist: {path}") template = base_path.read_text(encoding="utf-8") + loader = loader_path.read_text(encoding="utf-8") + compiled_version = _safe_json_for_script(__version__) config = { "manifestUrl": self.manifest_url, "versionUrl": _sibling_resource_url(self.manifest_url, "version"), @@ -240,7 +243,10 @@ class Compiler: "{{DOCSLIB_TITLE}}": self.title.replace("&", "&").replace("<", "<").replace(">", ">"), "{{DOCSLIB_MANIFEST}}": _safe_json_for_script(manifest), "{{DOCSLIB_CONFIG}}": _safe_json_for_script(config), - "{{DOCSLIB_LOADER}}": loader_path.read_text(encoding="utf-8"), + "{{DOCSLIB_LOADER}}": ( + f"globalThis.COMPILED_FROM_VERSION = {compiled_version};\n" + f"{loader}" + ), } for marker, value in replacements.items(): if marker not in template: diff --git a/docslib/templates/client.js b/docslib/templates/client.js index 8654865..eb3541e 100644 --- a/docslib/templates/client.js +++ b/docslib/templates/client.js @@ -509,6 +509,15 @@ updateIdentity(); return changed; } + document.dispatchEvent(new CustomEvent("on_update_available", { + bubbles: true, + detail: { + revision: remoteRevision, + currentRevision: lastSyncedRevision, + versionUrl, + manifestUrl: config.manifestUrl, + }, + })); const response = await fetchNoCache(config.manifestUrl); if (!response.ok) throw new Error(String(response.status)); const remote = await response.json(); diff --git a/examples/assets/inject.js b/examples/assets/inject.js index 13bfb96..1316b34 100644 --- a/examples/assets/inject.js +++ b/examples/assets/inject.js @@ -5,3 +5,7 @@ document.addEventListener("docslib:rendered", (event) => { document.addEventListener("on_manifest_update", (event) => { document.documentElement.dataset.pendingRevision = event.detail.revision; }); + +document.addEventListener("on_update_available", (event) => { + document.documentElement.dataset.availableRevision = event.detail.revision; +}); diff --git a/pyproject.toml b/pyproject.toml index 92f5cfc..7451341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "shared-docs-lib" -version = "0.3.6" +dynamic = ["version"] description = "Build self-contained, Obsidian-like documentation sites from Python functions." readme = "README.md" requires-python = ">=3.10" @@ -24,6 +24,9 @@ dev = [ [tool.hatch.build.targets.wheel] packages = ["docslib"] +[tool.hatch.version] +path = "docslib/_version.py" + [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-q" diff --git a/tests/test_server.py b/tests/test_server.py index 308706e..5662c22 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -70,6 +70,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert "html, body { width: 100%; height: 100dvh" in manifest["client"]["css"]["system"] assert manifest["client"]["css"]["custom"] == ".content { --custom-test: yes; }" assert "placeholderTick" in manifest["client"]["js"]["system"] + assert "COMPILED_FROM_VERSION" not in manifest["client"]["js"]["system"] assert manifest["client"]["js"]["custom"] == 'document.body.dataset.customTest = "yes";' assert len(manifest["revision"]) == 20 assert manifest["ui"]["header_buttons"] == [ @@ -96,6 +97,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert html_response.status_code == 200 assert "http://127.0.0.1:9000/manifest.json" in html assert "http://127.0.0.1:9000/version" in html + assert 'globalThis.COMPILED_FROM_VERSION = "0.3.7";' in html assert ".content { --custom-test: yes; }" in html assert r'document.body.dataset.customTest = \"yes\";' in html assert 'id="docslib-loading"' in html @@ -113,6 +115,8 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert "previousRevision" in system_js assert "cached," in system_js assert "applied: false" in system_js + assert 'new CustomEvent("on_update_available"' in system_js + assert "currentRevision: lastSyncedRevision" in system_js assert 'config.versionUrl || siblingResourceUrl(config.manifestUrl, "version")' in system_js assert 'target.searchParams.set(' in system_js assert '"no_cache"' in system_js @@ -120,6 +124,9 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert system_js.index("await fetchNoCache(versionUrl)") < system_js.index( "await fetchNoCache(config.manifestUrl)" ) + assert system_js.index('new CustomEvent("on_update_available"') < system_js.index( + "await fetchNoCache(config.manifestUrl)" + ) assert "docslib-runtime-style" in html assert "indexedDB.open" in html assert "link.dataset.page = button.page" in html @@ -193,7 +200,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): def test_build_writes_static_bundle(tmp_path): - assert __version__ == "0.3.6" + assert __version__ == "0.3.7" @Page("1", "Static page") def static_page(): diff --git a/uv.lock b/uv.lock index bd48562..fe2ca34 100644 --- a/uv.lock +++ b/uv.lock @@ -452,7 +452,6 @@ wheels = [ [[package]] name = "shared-docs-lib" -version = "0.3.6" source = { editable = "." } dependencies = [ { name = "fastapi" },