Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom favicons #1062

Merged
merged 4 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions docs/guides/static-assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
Mesop allows you to specify a folder for storing static assets that will be served by
the Mesop server.

This feature provides a simple way to serving images, CSS stylesheets, and other files
without having to rely on CDNs, external servers, or mounting Mesop onto FastAPI/Flask.
This feature provides a simple way to serving images, favicons, CSS stylesheets, and
other files without having to rely on CDNs, external servers, or mounting Mesop onto
FastAPI/Flask.

## Enable a static folder

Expand Down Expand Up @@ -86,6 +87,20 @@ def foo():
me.image(src="/static/logo.png")
```

### Use a custom favicon

This example shows you how to use a custom favicon in your Mesop app.

Let's assume you have a directory like this:

- static/favicon.ico
- main.py
- requirements.txt

If you have a static folder enabled, Mesop will look for a `favicon.ico` file in your
static folder. If the file exists, Mesop will use your custom favicon instead of the
default Mesop favicon.

### Load a Tailwind stylesheet

This example shows you how to use [Tailwind CSS](https://tailwindcss.com/) with Mesop.
Expand Down
53 changes: 10 additions & 43 deletions mesop/server/server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import base64
import os
import secrets
import threading
from typing import Generator, Sequence
Expand All @@ -12,7 +11,6 @@
request,
stream_with_context,
)
from werkzeug.security import safe_join

import mesop.protos.ui_pb2 as pb
from mesop.component_helpers import diff_component
Expand All @@ -23,14 +21,15 @@
MESOP_WEBSOCKETS_ENABLED,
)
from mesop.events import LoadEvent
from mesop.exceptions import MesopDeveloperException, format_traceback
from mesop.exceptions import format_traceback
from mesop.runtime import runtime
from mesop.server.config import app_config
from mesop.server.constants import WEB_COMPONENTS_PATH_SEGMENT
from mesop.server.server_debug_routes import configure_debug_routes
from mesop.server.server_utils import (
STREAM_END,
create_update_state_event,
get_static_folder,
get_static_url_path,
is_same_site,
make_sse_response,
serialize,
Expand All @@ -44,10 +43,15 @@
def configure_flask_app(
*, prod_mode: bool = True, exceptions_to_propagate: Sequence[type] = ()
) -> Flask:
static_folder = get_static_folder()
static_url_path = get_static_url_path()
if static_folder and static_url_path:
print(f"Static folder enabled: {static_folder}")

flask_app = Flask(
__name__,
static_folder=get_static_folder(),
static_url_path=get_static_url_path(),
static_folder=static_folder,
static_url_path=static_url_path,
)

def render_loop(
Expand Down Expand Up @@ -322,40 +326,3 @@ def ws_generate_data(ws, ui_request):
runtime().delete_context(websocket_session_id)

return flask_app


def get_static_folder() -> str | None:
static_folder_name = app_config.static_folder.strip()
if not static_folder_name:
print("Static folder disabled.")
return None

if static_folder_name in {
".",
"..",
"." + os.path.sep,
".." + os.path.sep,
}:
raise MesopDeveloperException(
"Static folder cannot be . or ..: {static_folder_name}"
)
if os.path.isabs(static_folder_name):
raise MesopDeveloperException(
"Static folder cannot be an absolute path: static_folder_name}"
)

static_folder_path = safe_join(os.getcwd(), static_folder_name)

if not static_folder_path:
raise MesopDeveloperException(
"Invalid static folder specified: {static_folder_name}"
)

print(f"Static folder enabled: {static_folder_path}")
return static_folder_path


def get_static_url_path() -> str | None:
if not app_config.static_folder:
return None
return app_config.static_url_path
63 changes: 63 additions & 0 deletions mesop/server/server_utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import base64
import json
import os
import secrets
import urllib.parse as urlparse
from typing import Any, Generator, Iterable
from urllib import request as urllib_request

from flask import Response, abort, request
from werkzeug.security import safe_join

import mesop.protos.ui_pb2 as pb
from mesop.env.env import EXPERIMENTAL_EDITOR_TOOLBAR_ENABLED
from mesop.exceptions import MesopDeveloperException
from mesop.runtime import runtime
from mesop.server.config import app_config

Expand Down Expand Up @@ -124,3 +127,63 @@ def make_sse_response(
# See https://nginx.org/en/docs/http/ngx_http_proxy_module.html
headers={"X-Accel-Buffering": "no"},
)


def get_static_folder() -> str | None:
static_folder_name = app_config.static_folder.strip()
if not static_folder_name:
return None

if static_folder_name in {
".",
"..",
"." + os.path.sep,
".." + os.path.sep,
}:
raise MesopDeveloperException(
"Static folder cannot be . or ..: {static_folder_name}"
)
if os.path.isabs(static_folder_name):
raise MesopDeveloperException(
"Static folder cannot be an absolute path: static_folder_name}"
)

static_folder_path = safe_join(os.getcwd(), static_folder_name)

if not static_folder_path:
raise MesopDeveloperException(
"Invalid static folder specified: {static_folder_name}"
)

return static_folder_path


def get_static_url_path() -> str | None:
if not app_config.static_folder:
return None

static_url_path = app_config.static_url_path.strip()
if not static_url_path.startswith("/"):
raise MesopDeveloperException(
"Invalid static url path. It must start with a slash: {static_folder_name}"
)

if not static_url_path.endswith("/"):
static_url_path += "/"

return static_url_path


def get_favicon() -> str | None:
default_favicon_path = "./favicon.ico"

static_folder = get_static_folder()
static_url_path = get_static_url_path()
if not static_folder or not static_url_path:
return default_favicon_path

favicon_path = safe_join(static_folder, "favicon.ico")
if not favicon_path or not os.path.isfile(favicon_path):
return default_favicon_path

return static_url_path + "favicon.ico"
6 changes: 5 additions & 1 deletion mesop/server/static_file_serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from mesop.exceptions import MesopException
from mesop.runtime import runtime
from mesop.server.constants import WEB_COMPONENTS_PATH_SEGMENT
from mesop.server.server_utils import get_favicon
from mesop.utils import terminal_colors as tc
from mesop.utils.runfiles import get_runfile_location, has_runfiles
from mesop.utils.url_utils import sanitize_url_for_csp
Expand Down Expand Up @@ -58,7 +59,10 @@ def retrieve_index_html() -> io.BytesIO | str:
lines[i] = (
f'<script src="{livereload_script_url}" nonce={g.csp_nonce}></script>\n'
)

if line.strip() == "<!-- Inject favicon -->":
lines[i] = (
f'<link rel="shortcut icon" href="{get_favicon()}" type="image/x-icon" />\n'
)
if (
page_config
and page_config.stylesheets
Expand Down
Binary file added mesop/static/favicon.ico
Binary file not shown.
20 changes: 17 additions & 3 deletions mesop/tests/e2e/static_folder_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import {test, expect} from '@playwright/test';

test.describe('Static Folders', () => {
test.describe('MESOP_STATIC_FOLDER disabled', () => {
if (process.env['MESOP_STATIC_FOLDER'] !== undefined) {
test.skip('Test skipped because MESOP_STATIC_FOLDER is set.');
}
test('static folder not viewable if MESOP_STATIC_FOLDER not enabled', async ({
page,
}) => {
if (process.env['MESOP_STATIC_FOLDER'] !== undefined) {
test.skip('Test skipped because MESOP_STATIC_FOLDER is set.');
}
const response = await page.goto('/static/tailwind.css');
expect(response!.status()).toBe(500);
});
test('default favicon is used', async ({page}) => {
await page.goto('/tailwind');
const faviconLink = await page
.locator('link[rel="shortcut icon"]')
.first();
await expect(faviconLink).toHaveAttribute('href', './favicon.ico');
});
});

test.describe('MESOP_STATIC_FOLDER enabled', () => {
Expand All @@ -30,5 +37,12 @@ test.describe('Static Folders', () => {
const response = await page.goto('/static/../config.py');
expect(response!.status()).toBe(500);
});
test('custom favicon is used', async ({page}) => {
await page.goto('/tailwind');
const faviconLink = await page
.locator('link[rel="shortcut icon"]')
.first();
await expect(faviconLink).toHaveAttribute('href', '/static/favicon.ico');
});
});
});
2 changes: 1 addition & 1 deletion mesop/web/src/app/editor/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/>
<link rel="stylesheet" href="./styles.css" />
<!-- Inject stylesheet (if needed) -->
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />
<!-- Inject favicon -->
</head>
<body>
<mesop-editor-app ngCspNonce="$$INSERT_CSP_NONCE$$"></mesop-editor-app>
Expand Down
2 changes: 1 addition & 1 deletion mesop/web/src/app/prod/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/>
<link rel="stylesheet" href="./styles.css" />
<!-- Inject stylesheet (if needed) -->
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />
<!-- Inject favicon -->
</head>
<body>
<mesop-app ngCspNonce="$$INSERT_CSP_NONCE$$"></mesop-app>
Expand Down
Loading