Expose HTML compile version and update event
This commit is contained in:
parent
214f134843
commit
f189c37282
9 changed files with 57 additions and 5 deletions
20
README.md
20
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 и применяется после перезагрузки клиента.
|
Код из `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-preview
|
||||||
|
|
||||||
В live-режиме браузер опрашивает только `/live`. Сервер следит за файлами, в которых объявлены страницы, а также за системными и пользовательскими CSS/JS. При изменении он пересобирает manifest; клиент сохраняет его и перезагружается с новым runtime. В запускаемом файле вызов `run()` обязательно помещать под `if __name__ == "__main__"`, чтобы watcher не запускал второй сервер.
|
В live-режиме браузер опрашивает только `/live`. Сервер следит за файлами, в которых объявлены страницы, а также за системными и пользовательскими CSS/JS. При изменении он пересобирает manifest; клиент сохраняет его и перезагружается с новым runtime. В запускаемом файле вызов `run()` обязательно помещать под `if __name__ == "__main__"`, чтобы watcher не запускал второй сервер.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"""Public API for SharedDocsLib."""
|
"""Public API for SharedDocsLib."""
|
||||||
|
|
||||||
|
from ._version import __version__
|
||||||
from .hooks import Component, Page
|
from .hooks import Component, Page
|
||||||
from .server import build, create_app, run
|
from .server import build, create_app, run
|
||||||
from .ui import HeaderButton
|
from .ui import HeaderButton
|
||||||
|
|
||||||
__all__ = ["Component", "HeaderButton", "Page", "build", "create_app", "run"]
|
__all__ = ["Component", "HeaderButton", "Page", "build", "create_app", "run"]
|
||||||
__version__ = "0.3.6"
|
|
||||||
|
|
|
||||||
4
docslib/_version.py
Normal file
4
docslib/_version.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""Single source of truth for the SharedDocsLib package version."""
|
||||||
|
|
||||||
|
__version__ = "0.3.7"
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, Mapping, Sequence
|
from typing import Iterable, Mapping, Sequence
|
||||||
|
|
||||||
|
from ._version import __version__
|
||||||
from .registry import PageDefinition, registry
|
from .registry import PageDefinition, registry
|
||||||
from .ui import HeaderButton
|
from .ui import HeaderButton
|
||||||
|
|
||||||
|
|
@ -229,6 +230,8 @@ class Compiler:
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise FileNotFoundError(f"Template file does not exist: {path}")
|
raise FileNotFoundError(f"Template file does not exist: {path}")
|
||||||
template = base_path.read_text(encoding="utf-8")
|
template = base_path.read_text(encoding="utf-8")
|
||||||
|
loader = loader_path.read_text(encoding="utf-8")
|
||||||
|
compiled_version = _safe_json_for_script(__version__)
|
||||||
config = {
|
config = {
|
||||||
"manifestUrl": self.manifest_url,
|
"manifestUrl": self.manifest_url,
|
||||||
"versionUrl": _sibling_resource_url(self.manifest_url, "version"),
|
"versionUrl": _sibling_resource_url(self.manifest_url, "version"),
|
||||||
|
|
@ -240,7 +243,10 @@ class Compiler:
|
||||||
"{{DOCSLIB_TITLE}}": self.title.replace("&", "&").replace("<", "<").replace(">", ">"),
|
"{{DOCSLIB_TITLE}}": self.title.replace("&", "&").replace("<", "<").replace(">", ">"),
|
||||||
"{{DOCSLIB_MANIFEST}}": _safe_json_for_script(manifest),
|
"{{DOCSLIB_MANIFEST}}": _safe_json_for_script(manifest),
|
||||||
"{{DOCSLIB_CONFIG}}": _safe_json_for_script(config),
|
"{{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():
|
for marker, value in replacements.items():
|
||||||
if marker not in template:
|
if marker not in template:
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,15 @@
|
||||||
updateIdentity();
|
updateIdentity();
|
||||||
return changed;
|
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);
|
const response = await fetchNoCache(config.manifestUrl);
|
||||||
if (!response.ok) throw new Error(String(response.status));
|
if (!response.ok) throw new Error(String(response.status));
|
||||||
const remote = await response.json();
|
const remote = await response.json();
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,7 @@ document.addEventListener("docslib:rendered", (event) => {
|
||||||
document.addEventListener("on_manifest_update", (event) => {
|
document.addEventListener("on_manifest_update", (event) => {
|
||||||
document.documentElement.dataset.pendingRevision = event.detail.revision;
|
document.documentElement.dataset.pendingRevision = event.detail.revision;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("on_update_available", (event) => {
|
||||||
|
document.documentElement.dataset.availableRevision = event.detail.revision;
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "shared-docs-lib"
|
name = "shared-docs-lib"
|
||||||
version = "0.3.6"
|
dynamic = ["version"]
|
||||||
description = "Build self-contained, Obsidian-like documentation sites from Python functions."
|
description = "Build self-contained, Obsidian-like documentation sites from Python functions."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
@ -24,6 +24,9 @@ dev = [
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["docslib"]
|
packages = ["docslib"]
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "docslib/_version.py"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
addopts = "-q"
|
addopts = "-q"
|
||||||
|
|
|
||||||
|
|
@ -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 "html, body { width: 100%; height: 100dvh" in manifest["client"]["css"]["system"]
|
||||||
assert manifest["client"]["css"]["custom"] == ".content { --custom-test: yes; }"
|
assert manifest["client"]["css"]["custom"] == ".content { --custom-test: yes; }"
|
||||||
assert "placeholderTick" in manifest["client"]["js"]["system"]
|
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 manifest["client"]["js"]["custom"] == 'document.body.dataset.customTest = "yes";'
|
||||||
assert len(manifest["revision"]) == 20
|
assert len(manifest["revision"]) == 20
|
||||||
assert manifest["ui"]["header_buttons"] == [
|
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 html_response.status_code == 200
|
||||||
assert "http://127.0.0.1:9000/manifest.json" in html
|
assert "http://127.0.0.1:9000/manifest.json" in html
|
||||||
assert "http://127.0.0.1:9000/version" 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 ".content { --custom-test: yes; }" in html
|
||||||
assert r'document.body.dataset.customTest = \"yes\";' in html
|
assert r'document.body.dataset.customTest = \"yes\";' in html
|
||||||
assert 'id="docslib-loading"' 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 "previousRevision" in system_js
|
||||||
assert "cached," in system_js
|
assert "cached," in system_js
|
||||||
assert "applied: false" 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 'config.versionUrl || siblingResourceUrl(config.manifestUrl, "version")' in system_js
|
||||||
assert 'target.searchParams.set(' in system_js
|
assert 'target.searchParams.set(' in system_js
|
||||||
assert '"no_cache"' 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(
|
assert system_js.index("await fetchNoCache(versionUrl)") < system_js.index(
|
||||||
"await fetchNoCache(config.manifestUrl)"
|
"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 "docslib-runtime-style" in html
|
||||||
assert "indexedDB.open" in html
|
assert "indexedDB.open" in html
|
||||||
assert "link.dataset.page = button.page" 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):
|
def test_build_writes_static_bundle(tmp_path):
|
||||||
assert __version__ == "0.3.6"
|
assert __version__ == "0.3.7"
|
||||||
|
|
||||||
@Page("1", "Static page")
|
@Page("1", "Static page")
|
||||||
def static_page():
|
def static_page():
|
||||||
|
|
|
||||||
1
uv.lock
generated
1
uv.lock
generated
|
|
@ -452,7 +452,6 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shared-docs-lib"
|
name = "shared-docs-lib"
|
||||||
version = "0.3.6"
|
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue