[feat] Update extension builder for 1.4.1 (#3725)
Co-authored-by: dni ⚡ <office@dnilabs.com>
This commit is contained in:
@@ -269,10 +269,30 @@ class ExtensionRelease(BaseModel):
|
||||
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
|
||||
try:
|
||||
github_releases = await cls.fetch_github_releases(org, repo)
|
||||
return [
|
||||
extension_releases = [
|
||||
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
|
||||
for r in github_releases
|
||||
]
|
||||
for release in extension_releases:
|
||||
if not release.details_link:
|
||||
continue
|
||||
try:
|
||||
config = await ExtensionConfig.fetch_github_release_config(
|
||||
org, repo, release.version
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
config = None
|
||||
if not config:
|
||||
continue
|
||||
|
||||
release.min_lnbits_version = config.min_lnbits_version
|
||||
release.max_lnbits_version = config.max_lnbits_version
|
||||
release.is_version_compatible = config.is_version_compatible()
|
||||
|
||||
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
||||
|
||||
return extension_releases
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return []
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -55,6 +57,29 @@ class DataField(BaseModel):
|
||||
field_type += ' = "sat"'
|
||||
return f"{field_name}: {field_type}"
|
||||
|
||||
def field_to_random_value(self) -> str:
|
||||
field_name = camel_to_snake(self.name)
|
||||
field_value: Any = f'"{self.type}"'
|
||||
if self.type == "json":
|
||||
field_value = '"{}"'
|
||||
elif self.type == "wallet":
|
||||
field_value = f'"{uuid.uuid4()}"'
|
||||
elif self.type == "currency":
|
||||
field_value = '"sat"'
|
||||
elif self.type in ["str", "text"]:
|
||||
field_value = f'"{field_name}_{urlsafe_short_hash()}"'
|
||||
elif self.type == "int":
|
||||
field_value = random.randint(1, 100) # noqa: S311
|
||||
elif self.type == "float":
|
||||
field_value = random.uniform(1.0, 100.0) # noqa: S311
|
||||
elif self.type == "bool":
|
||||
field_value = random.choice([True, False]) # noqa: S311
|
||||
elif self.type == "datetime":
|
||||
random_days = random.randint(-30, 30) # noqa: S311
|
||||
random_date = datetime.now(timezone.utc) - timedelta(days=random_days)
|
||||
field_value = f"""datetime.fromisoformat("{random_date.isoformat()}")"""
|
||||
return f"{field_name} = {field_value},"
|
||||
|
||||
def field_to_js(self) -> str:
|
||||
field_name = camel_to_snake(self.name)
|
||||
default_value = "null"
|
||||
|
||||
@@ -20,6 +20,7 @@ from lnbits.helpers import (
|
||||
camel_to_words,
|
||||
download_url,
|
||||
lowercase_first_letter,
|
||||
snake_to_camel,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -195,13 +196,29 @@ def _copy_ext_stub_to_build_dir(
|
||||
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
|
||||
shutil.rmtree(ext_build_dir, True)
|
||||
|
||||
shutil.copytree(ext_stub_cache_dir, ext_build_dir)
|
||||
shutil.copytree(
|
||||
ext_stub_cache_dir,
|
||||
ext_build_dir,
|
||||
ignore=shutil.ignore_patterns(
|
||||
"__pycache__",
|
||||
".git",
|
||||
".env",
|
||||
".venv",
|
||||
"*.env",
|
||||
"*.log",
|
||||
"node_modules",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
".ruff_cache",
|
||||
),
|
||||
)
|
||||
return ext_build_dir
|
||||
|
||||
|
||||
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
|
||||
parsed_data = _parse_extension_data(data)
|
||||
for py_file in py_files:
|
||||
test_files = [f"tests/{p.name}" for p in Path(ext_stub_dir, "tests").glob("*.py")]
|
||||
for py_file in py_files + test_files:
|
||||
template_path = Path(ext_stub_dir, py_file).as_posix()
|
||||
rederer = _render_file(template_path, parsed_data)
|
||||
with open(template_path, "w", encoding="utf-8") as f:
|
||||
@@ -209,7 +226,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
||||
|
||||
_remove_lines_with_string(template_path, remove_line_marker)
|
||||
|
||||
template_path = Path(ext_stub_dir, "static", "js", "index.js").as_posix()
|
||||
template_path = Path(ext_stub_dir, "static", "index.js").as_posix()
|
||||
rederer = _render_file(
|
||||
template_path, {"preview": data.preview_action, **parsed_data}
|
||||
)
|
||||
@@ -234,9 +251,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
||||
"settingsFormDialog.data",
|
||||
ext_stub_dir,
|
||||
)
|
||||
template_path = Path(
|
||||
ext_stub_dir, "templates", "extension_builder_stub", "index.html"
|
||||
).as_posix()
|
||||
template_path = Path(ext_stub_dir, "static", "index.vue").as_posix()
|
||||
rederer = _render_file(
|
||||
template_path,
|
||||
{
|
||||
@@ -263,12 +278,12 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
||||
"publicClientData",
|
||||
ext_stub_dir,
|
||||
)
|
||||
public_template_path = Path(
|
||||
ext_stub_dir, "templates", "extension_builder_stub", "public_page.html"
|
||||
)
|
||||
public_template_path = Path(ext_stub_dir, "static", "public_page.vue")
|
||||
public_component_path = Path(ext_stub_dir, "static", "public_page.js")
|
||||
template_path = public_template_path.as_posix()
|
||||
if not data.public_page.has_public_page:
|
||||
public_template_path.unlink(missing_ok=True)
|
||||
public_component_path.unlink(missing_ok=True)
|
||||
else:
|
||||
rederer = _render_file(
|
||||
template_path,
|
||||
@@ -308,8 +323,10 @@ def zip_directory(source_dir, zip_path):
|
||||
|
||||
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
|
||||
extension_dir_path = extension_dir.as_posix()
|
||||
# the order of fields is important, do not chage
|
||||
rename_values = {
|
||||
"extension_builder_stub_name": data.name,
|
||||
"extension_builder_stub_camel_name": snake_to_camel(data.id, True),
|
||||
"extension_builder_stub_short_description": data.short_description or "",
|
||||
"extension_builder_stub": data.id,
|
||||
"OwnerData": data.owner_data.name,
|
||||
@@ -328,7 +345,7 @@ def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) ->
|
||||
directory=extension_dir_path,
|
||||
old_text=old_text,
|
||||
new_text=new_text,
|
||||
file_extensions=[".py", ".js", ".html", ".md", ".json", ".toml"],
|
||||
file_extensions=[".py", ".js", ".vue", ".html", ".md", ".json", ".toml"],
|
||||
)
|
||||
|
||||
_rename_files_and_dirs_in_directory(
|
||||
@@ -406,6 +423,7 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
||||
"owner_data": {
|
||||
"name": data.owner_data.name,
|
||||
"editable": data.owner_data.editable,
|
||||
"fields": [field.name for field in data.owner_data.fields],
|
||||
"js_fields": [
|
||||
field.field_to_js()
|
||||
for field in data.owner_data.fields
|
||||
@@ -432,6 +450,18 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
||||
],
|
||||
"db_fields": [field.field_to_db() for field in data.owner_data.fields],
|
||||
"all_fields": [field.field_to_py() for field in data.owner_data.fields],
|
||||
"random_fields_values": [
|
||||
field.field_to_random_value() for field in data.owner_data.fields
|
||||
],
|
||||
"public_fields": [
|
||||
field.field_to_py()
|
||||
for field in data.owner_data.fields
|
||||
if field.name
|
||||
in [
|
||||
data.public_page.owner_data_fields.name,
|
||||
data.public_page.owner_data_fields.description,
|
||||
]
|
||||
],
|
||||
},
|
||||
"client_data": {
|
||||
"name": data.client_data.name,
|
||||
@@ -457,6 +487,9 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
||||
],
|
||||
"db_fields": [field.field_to_db() for field in data.client_data.fields],
|
||||
"all_fields": [field.field_to_py() for field in data.client_data.fields],
|
||||
"random_fields_values": [
|
||||
field.field_to_random_value() for field in data.client_data.fields
|
||||
],
|
||||
},
|
||||
"settings_data": {
|
||||
"enabled": data.settings_data.enabled,
|
||||
@@ -536,7 +569,7 @@ def _rename_files_and_dirs_in_directory(directory, old_text, new_text):
|
||||
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
|
||||
|
||||
|
||||
def _is_excluded_dir(path):
|
||||
def _is_excluded_dir(path: str) -> bool:
|
||||
for excluded_dir in excluded_dirs:
|
||||
if path.startswith(excluded_dir):
|
||||
return True
|
||||
|
||||
@@ -42,54 +42,38 @@ async def robots():
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/extensions/builder/preview/{ext_id}",
|
||||
"/extensions/builder/preview/{ext_id}/{resource}",
|
||||
name="extensions builder",
|
||||
dependencies=[Depends(check_extension_builder)],
|
||||
)
|
||||
async def extensions_builder_preview(
|
||||
request: Request,
|
||||
ext_id: str,
|
||||
resource: str | None = None,
|
||||
page_name: str | None = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> HTMLResponse:
|
||||
) -> FileResponse:
|
||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||
html_file_name = "index.html"
|
||||
if page_name == "public_page":
|
||||
html_file_name = "public_page.html"
|
||||
|
||||
html_file_path = Path(
|
||||
file_name = "index"
|
||||
if page_name == "public_page":
|
||||
file_name = "public_page"
|
||||
|
||||
resource_path = Path(
|
||||
settings.extension_builder_working_dir_path,
|
||||
"extension_builder_stub",
|
||||
ext_id,
|
||||
working_dir_name,
|
||||
ext_id,
|
||||
"templates",
|
||||
ext_id,
|
||||
html_file_name,
|
||||
"static",
|
||||
)
|
||||
|
||||
html_file_full_path = Path(
|
||||
settings.extension_builder_working_dir_path, html_file_path
|
||||
)
|
||||
file_ext = ".vue" if resource == "template" else ".js"
|
||||
file_full_path = Path(resource_path, file_name + file_ext)
|
||||
|
||||
if not html_file_full_path.is_file():
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"error.html",
|
||||
{
|
||||
"status_code": 404,
|
||||
"message": f"Extension {ext_id} not found, refresh Preview.",
|
||||
},
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
|
||||
response = template_renderer().TemplateResponse(
|
||||
request,
|
||||
html_file_path.as_posix(),
|
||||
{
|
||||
"user": user.json(),
|
||||
},
|
||||
)
|
||||
if not file_full_path.is_file():
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||
|
||||
response = FileResponse(file_full_path.absolute().as_posix())
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
"default-src 'self'; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
@@ -208,6 +192,9 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
||||
@generic_router.get(
|
||||
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
||||
)
|
||||
@generic_router.get(
|
||||
"/extensions/builder/preview", dependencies=[Depends(check_extension_builder)]
|
||||
)
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists)
|
||||
) -> HTMLResponse:
|
||||
|
||||
@@ -391,6 +391,14 @@ def camel_to_snake(name: str) -> str:
|
||||
return name.lower()
|
||||
|
||||
|
||||
def snake_to_camel(name: str, capitalize_first: bool | None = False) -> str:
|
||||
components = name.split("_")
|
||||
camel = components[0] + "".join(x.capitalize() for x in components[1:])
|
||||
if capitalize_first:
|
||||
camel = camel[0].upper() + camel[1:]
|
||||
return camel
|
||||
|
||||
|
||||
def is_camel_case(v: str) -> bool:
|
||||
return re.match(r"^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$", v) is not None
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -106,6 +106,11 @@ const routes = [
|
||||
name: 'ExtensionsBuilder',
|
||||
component: PageExtensionBuilder
|
||||
},
|
||||
{
|
||||
path: '/extensions/builder/preview',
|
||||
name: 'ExtensionsBuilderPreview',
|
||||
component: PageExtensionBuilderPreview
|
||||
},
|
||||
{
|
||||
path: '/extensions',
|
||||
name: 'Extensions',
|
||||
|
||||
@@ -288,11 +288,10 @@ window.PageExtensionBuilder = {
|
||||
`/api/v1/extension/${stub_ext_id}/releases`
|
||||
)
|
||||
|
||||
this.extensionStubVersions = data.sort((a, b) =>
|
||||
a.version < b.version ? 1 : -1
|
||||
)
|
||||
this.extensionData.stub_version = this.extensionStubVersions[0]
|
||||
? this.extensionStubVersions[0].version
|
||||
this.extensionStubVersions = data
|
||||
const validVersions = data.filter(v => v.is_version_compatible)
|
||||
this.extensionData.stub_version = validVersions[0]
|
||||
? validVersions[0].version
|
||||
: ''
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
@@ -311,7 +310,20 @@ window.PageExtensionBuilder = {
|
||||
iframeDoc.body.style.transform = 'scale(0.8)'
|
||||
iframeDoc.body.style.transformOrigin = 'center top'
|
||||
}
|
||||
iframe.src = `/extensions/builder/preview/${this.extensionData.id}?page_name=${previewPageName}`
|
||||
|
||||
let componentName =
|
||||
'Page' +
|
||||
this.extensionData.id
|
||||
.toLowerCase()
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('')
|
||||
if (previewPageName === 'public_page') componentName += 'Public'
|
||||
iframe.src =
|
||||
`/extensions/builder/preview?` +
|
||||
`ext_id=${this.extensionData.id}` +
|
||||
`&page=${previewPageName}` +
|
||||
`&component=${componentName}`
|
||||
},
|
||||
initBasicData() {
|
||||
this.extensionData.owner_data.fields = [
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
window.PageExtensionBuilderPreview = {
|
||||
template: '#page-extension-builder-preview',
|
||||
mixins: [windowMixin],
|
||||
watch: {
|
||||
name: 'reload'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
extId: '',
|
||||
pageName: '',
|
||||
componentName: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async reload() {
|
||||
await LNbits.utils.loadTemplate(
|
||||
`/extensions/builder/preview/${this.extId}/template?page_name=${this.pageName}`
|
||||
)
|
||||
await LNbits.utils.loadScript(
|
||||
`/extensions/builder/preview/${this.extId}/component?page_name=${this.pageName}`
|
||||
)
|
||||
|
||||
this._component = window[this.componentName]
|
||||
console.log(
|
||||
'LNbits preview reloaded componentName:',
|
||||
this.componentName,
|
||||
!!this._component
|
||||
)
|
||||
this.$forceUpdate()
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
this.extId = urlParams.get('ext_id') || ''
|
||||
this.pageName = urlParams.get('page') || ''
|
||||
this.componentName = urlParams.get('component') || ''
|
||||
|
||||
await this.reload()
|
||||
},
|
||||
render() {
|
||||
if (this._component) {
|
||||
return Vue.h(this._component)
|
||||
}
|
||||
return Vue.h('div', 'Loading...')
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@
|
||||
"js/pages/error.js",
|
||||
"js/pages/home.js",
|
||||
"js/pages/extensions_builder.js",
|
||||
"js/pages/extensions_builder.js",
|
||||
"js/pages/extensions_builder_preview.js",
|
||||
"js/pages/extensions.js",
|
||||
"js/pages/first-install.js",
|
||||
"js/pages/payments.js",
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
"js/pages/error.js",
|
||||
"js/pages/home.js",
|
||||
"js/pages/extensions_builder.js",
|
||||
"js/pages/extensions_builder.js",
|
||||
"js/pages/extensions_builder_preview.js",
|
||||
"js/pages/extensions.js",
|
||||
"js/pages/first-install.js",
|
||||
"js/pages/payments.js",
|
||||
|
||||
Reference in New Issue
Block a user