mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-03 06:03:25 +00:00
Unify vendor assets configuration in single JSON file (#141)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""Hatch build hook to vendor CDN assets for offline use.
|
"""Hatch build hook to vendor CDN assets for offline use.
|
||||||
|
|
||||||
During wheel builds, this hook:
|
During wheel builds, this hook:
|
||||||
1. Parses base.html to find elements with data-vendor attributes
|
1. Reads vendor-assets.json to find assets marked for vendoring
|
||||||
2. Downloads each CDN asset to a temporary vendor directory
|
2. Downloads each CDN asset to a temporary vendor directory
|
||||||
3. Rewrites base.html to use local /static/vendor/ paths
|
3. Rewrites base.html to use local /static/vendor/ paths
|
||||||
4. Fetches and bundles license information
|
4. Fetches and bundles license information
|
||||||
@@ -13,6 +13,7 @@ distributed wheel has vendored assets.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -23,22 +24,6 @@ from urllib.request import Request, urlopen
|
|||||||
|
|
||||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||||
|
|
||||||
# Matches elements with data-vendor attribute: extracts URL and target filename
|
|
||||||
# Example: <script src="https://..." data-vendor="htmx.js">
|
|
||||||
# Captures: (1) src/href, (2) URL, (3) attributes between, (4) vendor filename
|
|
||||||
VENDOR_PATTERN = re.compile(r'(src|href)="(https://[^"]+)"([^>]*?)data-vendor="([^"]+)"')
|
|
||||||
|
|
||||||
# License URLs for each package (GitHub raw URLs)
|
|
||||||
LICENSE_URLS: dict[str, tuple[str, str]] = {
|
|
||||||
"htmx": ("MIT", "https://raw.githubusercontent.com/bigskysoftware/htmx/master/LICENSE"),
|
|
||||||
"xterm": ("MIT", "https://raw.githubusercontent.com/xtermjs/xterm.js/master/LICENSE"),
|
|
||||||
"daisyui": ("MIT", "https://raw.githubusercontent.com/saadeghi/daisyui/master/LICENSE"),
|
|
||||||
"tailwindcss": (
|
|
||||||
"MIT",
|
|
||||||
"https://raw.githubusercontent.com/tailwindlabs/tailwindcss/master/LICENSE",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _download(url: str) -> bytes:
|
def _download(url: str) -> bytes:
|
||||||
"""Download a URL, trying urllib first then curl as fallback."""
|
"""Download a URL, trying urllib first then curl as fallback."""
|
||||||
@@ -61,7 +46,14 @@ def _download(url: str) -> bytes:
|
|||||||
return bytes(result.stdout)
|
return bytes(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
def _generate_licenses_file(temp_dir: Path) -> None:
|
def _load_vendor_assets(root: Path) -> dict[str, Any]:
|
||||||
|
"""Load vendor-assets.json from the web module."""
|
||||||
|
json_path = root / "src" / "compose_farm" / "web" / "vendor-assets.json"
|
||||||
|
with json_path.open() as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_licenses_file(temp_dir: Path, licenses: dict[str, dict[str, str]]) -> None:
|
||||||
"""Download and combine license files into LICENSES.txt."""
|
"""Download and combine license files into LICENSES.txt."""
|
||||||
lines = [
|
lines = [
|
||||||
"# Vendored Dependencies - License Information",
|
"# Vendored Dependencies - License Information",
|
||||||
@@ -73,7 +65,9 @@ def _generate_licenses_file(temp_dir: Path) -> None:
|
|||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
for pkg_name, (license_type, license_url) in LICENSE_URLS.items():
|
for pkg_name, license_info in licenses.items():
|
||||||
|
license_type = license_info["type"]
|
||||||
|
license_url = license_info["url"]
|
||||||
lines.append(f"## {pkg_name} ({license_type})")
|
lines.append(f"## {pkg_name} ({license_type})")
|
||||||
lines.append(f"Source: {license_url}")
|
lines.append(f"Source: {license_url}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -107,44 +101,57 @@ class VendorAssetsHook(BuildHookInterface): # type: ignore[misc]
|
|||||||
if not base_html_path.exists():
|
if not base_html_path.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Load vendor assets configuration
|
||||||
|
vendor_config = _load_vendor_assets(Path(self.root))
|
||||||
|
assets_to_vendor = vendor_config["assets"]
|
||||||
|
|
||||||
|
if not assets_to_vendor:
|
||||||
|
return
|
||||||
|
|
||||||
# Create temp directory for vendored assets
|
# Create temp directory for vendored assets
|
||||||
temp_dir = Path(tempfile.mkdtemp(prefix="compose_farm_vendor_"))
|
temp_dir = Path(tempfile.mkdtemp(prefix="compose_farm_vendor_"))
|
||||||
vendor_dir = temp_dir / "vendor"
|
vendor_dir = temp_dir / "vendor"
|
||||||
vendor_dir.mkdir()
|
vendor_dir.mkdir()
|
||||||
|
|
||||||
# Read and parse base.html
|
# Read base.html
|
||||||
html_content = base_html_path.read_text()
|
html_content = base_html_path.read_text()
|
||||||
|
|
||||||
|
# Build URL to filename mapping and download assets
|
||||||
url_to_filename: dict[str, str] = {}
|
url_to_filename: dict[str, str] = {}
|
||||||
|
for asset in assets_to_vendor:
|
||||||
# Find all elements with data-vendor attribute and download them
|
url = asset["url"]
|
||||||
for match in VENDOR_PATTERN.finditer(html_content):
|
filename = asset["filename"]
|
||||||
url = match.group(2)
|
|
||||||
filename = match.group(4)
|
|
||||||
|
|
||||||
if url in url_to_filename:
|
|
||||||
continue
|
|
||||||
|
|
||||||
url_to_filename[url] = filename
|
url_to_filename[url] = filename
|
||||||
|
filepath = vendor_dir / filename
|
||||||
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||||
content = _download(url)
|
content = _download(url)
|
||||||
(vendor_dir / filename).write_bytes(content)
|
filepath.write_bytes(content)
|
||||||
|
|
||||||
if not url_to_filename:
|
# Generate LICENSES.txt from the JSON config
|
||||||
return
|
_generate_licenses_file(vendor_dir, vendor_config["licenses"])
|
||||||
|
|
||||||
# Generate LICENSES.txt
|
# Rewrite HTML: replace CDN URLs with local paths and remove data-vendor attributes
|
||||||
_generate_licenses_file(vendor_dir)
|
# Pattern matches: src="URL" ... data-vendor="filename" or href="URL" ... data-vendor="filename"
|
||||||
|
vendor_pattern = re.compile(r'(src|href)="(https://[^"]+)"([^>]*?)data-vendor="([^"]+)"')
|
||||||
|
|
||||||
# Rewrite HTML to use local paths (remove data-vendor, update URL)
|
|
||||||
def replace_vendor_tag(match: re.Match[str]) -> str:
|
def replace_vendor_tag(match: re.Match[str]) -> str:
|
||||||
attr = match.group(1) # src or href
|
attr = match.group(1) # src or href
|
||||||
url = match.group(2)
|
url = match.group(2)
|
||||||
between = match.group(3) # attributes between URL and data-vendor
|
between = match.group(3) # attributes between URL and data-vendor
|
||||||
filename = match.group(4)
|
|
||||||
if url in url_to_filename:
|
if url in url_to_filename:
|
||||||
|
filename = url_to_filename[url]
|
||||||
return f'{attr}="/static/vendor/{filename}"{between}'
|
return f'{attr}="/static/vendor/{filename}"{between}'
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
|
|
||||||
modified_html = VENDOR_PATTERN.sub(replace_vendor_tag, html_content)
|
modified_html = vendor_pattern.sub(replace_vendor_tag, html_content)
|
||||||
|
|
||||||
|
# Inject vendored mode flag for JavaScript to detect
|
||||||
|
# Insert right after <head> tag so it's available early
|
||||||
|
modified_html = modified_html.replace(
|
||||||
|
"<head>",
|
||||||
|
"<head>\n <script>window.CF_VENDORED=true;</script>",
|
||||||
|
1, # Only replace first occurrence
|
||||||
|
)
|
||||||
|
|
||||||
# Write modified base.html to temp
|
# Write modified base.html to temp
|
||||||
templates_dir = temp_dir / "templates"
|
templates_dir = temp_dir / "templates"
|
||||||
|
|||||||
@@ -1,78 +1,39 @@
|
|||||||
"""CDN asset definitions and caching for tests and demo recordings.
|
"""CDN asset definitions and caching for tests and demo recordings.
|
||||||
|
|
||||||
This module provides a single source of truth for CDN asset URLs used in
|
This module provides CDN asset URLs used in browser tests and demo recordings.
|
||||||
browser tests and demo recordings. Assets are intercepted and served from
|
Assets are intercepted and served from a local cache to eliminate network
|
||||||
a local cache to eliminate network variability.
|
variability.
|
||||||
|
|
||||||
Note: The canonical list of CDN assets for production is in base.html
|
The canonical list of CDN assets is in vendor-assets.json. This module loads
|
||||||
(with data-vendor attributes). This module includes those plus dynamically
|
that file and provides the CDN_ASSETS dict for test caching.
|
||||||
loaded assets (like Monaco editor modules loaded by app.js).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import TYPE_CHECKING
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _load_cdn_assets() -> dict[str, tuple[str, str]]:
|
||||||
|
"""Load CDN assets from vendor-assets.json.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping URL to (filename, content_type) tuple.
|
||||||
|
|
||||||
|
"""
|
||||||
|
json_path = Path(__file__).parent / "vendor-assets.json"
|
||||||
|
with json_path.open() as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
return {asset["url"]: (asset["filename"], asset["content_type"]) for asset in config["assets"]}
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# CDN assets to cache locally for tests/demos
|
# CDN assets to cache locally for tests/demos
|
||||||
# Format: URL -> (local_filename, content_type)
|
# Format: URL -> (local_filename, content_type)
|
||||||
#
|
#
|
||||||
# If tests fail with "Uncached CDN request", add the URL here.
|
# If tests fail with "Uncached CDN request", add the URL to vendor-assets.json.
|
||||||
CDN_ASSETS: dict[str, tuple[str, str]] = {
|
CDN_ASSETS: dict[str, tuple[str, str]] = _load_cdn_assets()
|
||||||
# From base.html (data-vendor attributes)
|
|
||||||
"https://cdn.jsdelivr.net/npm/daisyui@5/themes.css": ("daisyui-themes.css", "text/css"),
|
|
||||||
"https://cdn.jsdelivr.net/npm/daisyui@5": ("daisyui.css", "text/css"),
|
|
||||||
"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4": (
|
|
||||||
"tailwind.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css": ("xterm.css", "text/css"),
|
|
||||||
"https://unpkg.com/htmx.org@2.0.4": ("htmx.js", "application/javascript"),
|
|
||||||
"https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js": (
|
|
||||||
"xterm.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js": (
|
|
||||||
"xterm-fit.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://unpkg.com/idiomorph/dist/idiomorph.min.js": (
|
|
||||||
"idiomorph.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://unpkg.com/idiomorph/dist/idiomorph-ext.min.js": (
|
|
||||||
"idiomorph-ext.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
# Monaco editor - dynamically loaded by app.js
|
|
||||||
"https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js": (
|
|
||||||
"monaco-loader.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.js": (
|
|
||||||
"monaco-editor-main.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.css": (
|
|
||||||
"monaco-editor-main.css",
|
|
||||||
"text/css",
|
|
||||||
),
|
|
||||||
"https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/base/worker/workerMain.js": (
|
|
||||||
"monaco-workerMain.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/basic-languages/yaml/yaml.js": (
|
|
||||||
"monaco-yaml.js",
|
|
||||||
"application/javascript",
|
|
||||||
),
|
|
||||||
"https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/base/browser/ui/codicons/codicon/codicon.ttf": (
|
|
||||||
"monaco-codicon.ttf",
|
|
||||||
"font/ttf",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def download_url(url: str) -> bytes | None:
|
def download_url(url: str) -> bytes | None:
|
||||||
@@ -107,6 +68,7 @@ def ensure_vendor_cache(cache_dir: Path) -> Path:
|
|||||||
filepath = cache_dir / filename
|
filepath = cache_dir / filename
|
||||||
if filepath.exists():
|
if filepath.exists():
|
||||||
continue
|
continue
|
||||||
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||||
content = download_url(url)
|
content = download_url(url)
|
||||||
if not content:
|
if not content:
|
||||||
msg = f"Failed to download {url} - check network/curl"
|
msg = f"Failed to download {url} - check network/curl"
|
||||||
|
|||||||
@@ -332,10 +332,14 @@ function loadMonaco(callback) {
|
|||||||
monacoLoading = true;
|
monacoLoading = true;
|
||||||
|
|
||||||
// Load the Monaco loader script
|
// Load the Monaco loader script
|
||||||
|
// Use local paths when running from vendored wheel, CDN otherwise
|
||||||
|
const monacoBase = window.CF_VENDORED
|
||||||
|
? '/static/vendor/monaco'
|
||||||
|
: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs';
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js';
|
script.src = monacoBase + '/loader.js';
|
||||||
script.onload = function() {
|
script.onload = function() {
|
||||||
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' }});
|
require.config({ paths: { vs: monacoBase }});
|
||||||
require(['vs/editor/editor.main'], function() {
|
require(['vs/editor/editor.main'], function() {
|
||||||
monacoLoaded = true;
|
monacoLoaded = true;
|
||||||
monacoLoading = false;
|
monacoLoading = false;
|
||||||
|
|||||||
@@ -97,8 +97,8 @@
|
|||||||
|
|
||||||
<!-- Scripts - HTMX first -->
|
<!-- Scripts - HTMX first -->
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4" data-vendor="htmx.js"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4" data-vendor="htmx.js"></script>
|
||||||
<script src="https://unpkg.com/idiomorph/dist/idiomorph.min.js"></script>
|
<script src="https://unpkg.com/idiomorph/dist/idiomorph.min.js" data-vendor="idiomorph.js"></script>
|
||||||
<script src="https://unpkg.com/idiomorph/dist/idiomorph-ext.min.js"></script>
|
<script src="https://unpkg.com/idiomorph/dist/idiomorph-ext.min.js" data-vendor="idiomorph-ext.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js" data-vendor="xterm.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js" data-vendor="xterm.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js" data-vendor="xterm-fit.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js" data-vendor="xterm-fit.js"></script>
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
|
|||||||
122
src/compose_farm/web/vendor-assets.json
Normal file
122
src/compose_farm/web/vendor-assets.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$comment": "CDN assets vendored into production builds and cached for tests",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/daisyui@5",
|
||||||
|
"filename": "daisyui.css",
|
||||||
|
"content_type": "text/css",
|
||||||
|
"package": "daisyui"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/daisyui@5/themes.css",
|
||||||
|
"filename": "daisyui-themes.css",
|
||||||
|
"content_type": "text/css",
|
||||||
|
"package": "daisyui"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4",
|
||||||
|
"filename": "tailwind.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "tailwindcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css",
|
||||||
|
"filename": "xterm.css",
|
||||||
|
"content_type": "text/css",
|
||||||
|
"package": "xterm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://unpkg.com/htmx.org@2.0.4",
|
||||||
|
"filename": "htmx.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "htmx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js",
|
||||||
|
"filename": "xterm.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "xterm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js",
|
||||||
|
"filename": "xterm-fit.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "xterm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://unpkg.com/idiomorph/dist/idiomorph.min.js",
|
||||||
|
"filename": "idiomorph.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "idiomorph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://unpkg.com/idiomorph/dist/idiomorph-ext.min.js",
|
||||||
|
"filename": "idiomorph-ext.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "idiomorph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js",
|
||||||
|
"filename": "monaco/loader.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "monaco-editor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.js",
|
||||||
|
"filename": "monaco/editor/editor.main.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "monaco-editor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/editor/editor.main.css",
|
||||||
|
"filename": "monaco/editor/editor.main.css",
|
||||||
|
"content_type": "text/css",
|
||||||
|
"package": "monaco-editor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/base/worker/workerMain.js",
|
||||||
|
"filename": "monaco/base/worker/workerMain.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "monaco-editor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/basic-languages/yaml/yaml.js",
|
||||||
|
"filename": "monaco/basic-languages/yaml/yaml.js",
|
||||||
|
"content_type": "application/javascript",
|
||||||
|
"package": "monaco-editor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/base/browser/ui/codicons/codicon/codicon.ttf",
|
||||||
|
"filename": "monaco/base/browser/ui/codicons/codicon/codicon.ttf",
|
||||||
|
"content_type": "font/ttf",
|
||||||
|
"package": "monaco-editor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"licenses": {
|
||||||
|
"htmx": {
|
||||||
|
"type": "MIT",
|
||||||
|
"url": "https://raw.githubusercontent.com/bigskysoftware/htmx/master/LICENSE"
|
||||||
|
},
|
||||||
|
"idiomorph": {
|
||||||
|
"type": "BSD-2-Clause",
|
||||||
|
"url": "https://raw.githubusercontent.com/bigskysoftware/idiomorph/main/LICENSE"
|
||||||
|
},
|
||||||
|
"xterm": {
|
||||||
|
"type": "MIT",
|
||||||
|
"url": "https://raw.githubusercontent.com/xtermjs/xterm.js/master/LICENSE"
|
||||||
|
},
|
||||||
|
"daisyui": {
|
||||||
|
"type": "MIT",
|
||||||
|
"url": "https://raw.githubusercontent.com/saadeghi/daisyui/master/LICENSE"
|
||||||
|
},
|
||||||
|
"tailwindcss": {
|
||||||
|
"type": "MIT",
|
||||||
|
"url": "https://raw.githubusercontent.com/tailwindlabs/tailwindcss/master/LICENSE"
|
||||||
|
},
|
||||||
|
"monaco-editor": {
|
||||||
|
"type": "MIT",
|
||||||
|
"url": "https://raw.githubusercontent.com/microsoft/monaco-editor/main/LICENSE.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ Run with: uv run pytest tests/web/test_htmx_browser.py -v --no-cov
|
|||||||
|
|
||||||
CDN assets are cached locally (in .pytest_cache/vendor/) to eliminate network
|
CDN assets are cached locally (in .pytest_cache/vendor/) to eliminate network
|
||||||
variability. If a test fails with "Uncached CDN request", add the URL to
|
variability. If a test fails with "Uncached CDN request", add the URL to
|
||||||
compose_farm.web.cdn.CDN_ASSETS.
|
src/compose_farm/web/vendor-assets.json.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -90,7 +90,7 @@ def page(page: Page, vendor_cache: Path) -> Page:
|
|||||||
return
|
return
|
||||||
# Uncached CDN request - abort with helpful error
|
# Uncached CDN request - abort with helpful error
|
||||||
route.abort("failed")
|
route.abort("failed")
|
||||||
msg = f"Uncached CDN request: {url}\n\nAdd this URL to CDN_ASSETS in tests/web/test_htmx_browser.py"
|
msg = f"Uncached CDN request: {url}\n\nAdd this URL to src/compose_farm/web/vendor-assets.json"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
page.route(re.compile(r"https://(cdn\.jsdelivr\.net|unpkg\.com)/.*"), handle_cdn)
|
page.route(re.compile(r"https://(cdn\.jsdelivr\.net|unpkg\.com)/.*"), handle_cdn)
|
||||||
|
|||||||
Reference in New Issue
Block a user