9
0
mirror of https://github.com/donlon/cloudflare-error-page.git synced 2025-12-22 16:29:29 +00:00

26 Commits

Author SHA1 Message Date
Anthony Donlon
5a633b5958 readme: update description for JavaScript/NodeJS 2025-12-23 00:11:19 +08:00
Anthony Donlon
b2164729b4 js: bump version to 0.1.0 2025-12-22 23:47:00 +08:00
Anthony Donlon
dd29cf0904 move nodejs folder to javascript 2025-12-22 23:46:21 +08:00
Anthony Donlon
d9af59f14f nodejs: load template from bundled string 2025-12-22 23:43:07 +08:00
Anthony Donlon
5a5311212e python: bump package version to 0.2.0 2025-12-22 22:48:41 +08:00
Anthony Donlon
9fbcba1a34 python: separate css from html template 2025-12-22 22:28:46 +08:00
Anthony Donlon
c0e576478a editor/web: fix generating Python code 2025-12-22 22:27:36 +08:00
Anthony Donlon
6233cec91f editor/web: support exporting generated webpage 2025-12-22 22:01:58 +08:00
Anthony Donlon
728ce52529 editor: support ok/error icon in shared page 2025-12-22 21:52:50 +08:00
Anthony Donlon
d0a05329d5 editor/web: open preview in about:blank tab 2025-12-22 21:44:14 +08:00
Anthony Donlon
51b61a8a2d Merge pull request #8 from donlon/save-as-dialog
Add Save As dialog to provide language examples
2025-12-22 21:34:48 +08:00
Anthony Donlon
71f4e9507c editor/web: support responsive save as dialog 2025-12-22 21:27:47 +08:00
Anthony Donlon
38eb8d8c11 editor/web: add popovers for "Copy" buttons 2025-12-22 02:13:29 +08:00
Anthony Donlon
76a4696a40 editor/web: add Save As dialog to provide language examples 2025-12-22 01:52:12 +08:00
Anthony Donlon
21d14994de python: rename head block in base template to 'html_head' 2025-12-21 20:42:58 +08:00
Anthony Donlon
36a1119908 Merge pull request #7 from syrf109475/main
Add versioning configuration to pyproject.toml
2025-12-21 20:10:24 +08:00
syrf109475
e66b5357a4 Add versioning configuration to pyproject.toml
Fix "ValueError: Missing `tool.hatch.version` configuration" when using pip install
2025-12-21 14:54:52 +08:00
Anthony Donlon
5bc662dd06 editor/web: import bootstrap css from script 2025-12-19 01:17:13 +08:00
Anthony Donlon
7229ad7281 editor/server: set meta description from page 2025-12-19 01:10:13 +08:00
Anthony Donlon
a50b0289a0 editor: add social card images 2025-12-19 01:08:24 +08:00
Anthony Donlon
a85624031c editor/web: misc fixes 2025-12-19 00:40:56 +08:00
Anthony Donlon
df5daebe34 python: rename default_template -> base_template 2025-12-17 01:23:41 +08:00
Anthony Donlon
eb0d5a7d55 editor: support page icon 2025-12-17 01:21:43 +08:00
Anthony Donlon
245f5b1f6d python: use dynamic package version 2025-12-16 22:49:06 +08:00
Anthony Donlon
58bf0d6b79 python: support type hinting for input params 2025-12-16 22:48:32 +08:00
Anthony Donlon
ed711a2521 editor: support shortened share URLs 2025-12-16 21:39:48 +08:00
47 changed files with 1654 additions and 706 deletions

View File

@@ -19,6 +19,10 @@ Here's an online editor to create customized error pages. Try it out [here](http
Install `cloudflare-error-page` with pip. Install `cloudflare-error-page` with pip.
``` Bash ``` Bash
# Install from PyPI
pip install cloudflare-error-page
# Or, install the latest version from this repo
pip install git+https://github.com/donlon/cloudflare-error-page.git pip install git+https://github.com/donlon/cloudflare-error-page.git
``` ```
@@ -62,12 +66,52 @@ You can also see live demo [here](https://virt.moe/cferr/examples/default).
A demo server using Flask is also available in [flask_demo.py](examples/flask_demo.py). A demo server using Flask is also available in [flask_demo.py](examples/flask_demo.py).
### Node.js/NPM ### JavaScript/NodeJS
A Node.js package is available in [nodejs](nodejs) folder. However currently it supports only Node.js but not web browsers, Install the `cloudflare-error-page` package using npm:
and we plan to refactor it into a shared package, so it can work in both environments.
(Thanks [@junduck](https://github.com/junduck) for creating this.) ``` Bash
npm install cloudflare-error-page
```
The following example demonstrates how to create a simple Express server and return the error page to visitors.
``` JavaScript
import express from 'express';
import { render as render_cf_error_page } from 'cloudflare-error-page';
const app = express();
const port = 3000;
// Define a route for GET requests to the root URL
app.get('/', (req, res) => {
res.status(500).send(render_cf_error_page({
title: "Internal server error",
// Browser status is ok
browser_status: {
status: 'ok',
},
// Cloudflare status is error
cloudflare_status: {
status: 'error',
status_text: 'Error',
},
// Host status is also ok
host_status: {
status: 'ok',
location: 'example.com',
},
error_source: "cloudflare",
}));
});
// Start the server and listen on the specified port
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
```
(Thanks [@junduck](https://github.com/junduck) for creating the original NodeJS version.)
### PHP ### PHP

View File

@@ -1,21 +1,75 @@
import html import html
import secrets import secrets
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any, TypedDict, Literal
if sys.version_info >= (3, 11):
from typing import NotRequired
else:
from typing import _SpecialForm
NotRequired: _SpecialForm
from jinja2 import Environment, PackageLoader, Template, select_autoescape from jinja2 import Environment, PackageLoader, Template, select_autoescape
env = Environment( jinja_env = Environment(
loader=PackageLoader(__name__), loader=PackageLoader(__name__),
autoescape=select_autoescape(), autoescape=select_autoescape(),
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True, lstrip_blocks=True,
) )
default_template: Template = env.get_template("error.html") base_template: Template = jinja_env.get_template('template.html')
def render(params: dict, class ErrorPageParams(TypedDict):
class MoreInformation(TypedDict):
hidden: NotRequired[bool]
text: NotRequired[str]
link: NotRequired[str]
for_text: NotRequired[str] # renamed to avoid Python keyword conflict
class StatusItem(TypedDict):
status: NotRequired[Literal['ok', 'error']]
location: NotRequired[str]
name: NotRequired[str]
status_text: NotRequired[str]
status_text_color: NotRequired[str]
class PerfSecBy(TypedDict):
text: NotRequired[str]
link: NotRequired[str]
class CreatorInfo(TypedDict):
hidden: NotRequired[bool]
link: NotRequired[str]
text: NotRequired[str]
html_title: NotRequired[str]
title: NotRequired[str]
error_code: NotRequired[str]
time: NotRequired[str]
more_information: NotRequired[MoreInformation]
browser_status: NotRequired[StatusItem]
cloudflare_status: NotRequired[StatusItem]
host_status: NotRequired[StatusItem]
error_source: NotRequired[Literal['browser', 'cloudflare', 'host']]
what_happened: NotRequired[str]
what_can_i_do: NotRequired[str]
ray_id: NotRequired[str]
client_ip: NotRequired[str]
perf_sec_by: NotRequired[PerfSecBy]
creator_info: NotRequired[CreatorInfo]
def render(params: ErrorPageParams,
allow_html: bool = True, allow_html: bool = True,
template: Template | None = None, template: Template | None = None,
*args: Any, *args: Any,
@@ -31,13 +85,19 @@ def render(params: dict,
:return: The rendered error page as a string. :return: The rendered error page as a string.
''' '''
if not template: if not template:
template = default_template template = base_template
params = {**params} params = {**params}
more_information = params.get('more_information')
if more_information:
for_text = more_information.get('for_text')
if for_text is not None:
more_information['for'] = for_text
if not params.get('time'): if not params.get('time'):
utc_now = datetime.now(timezone.utc) utc_now = datetime.now(timezone.utc)
params['time'] = utc_now.strftime("%Y-%m-%d %H:%M:%S UTC") params['time'] = utc_now.strftime('%Y-%m-%d %H:%M:%S UTC')
if not params.get('ray_id'): if not params.get('ray_id'):
params['ray_id'] = secrets.token_hex(8) params['ray_id'] = secrets.token_hex(8)
if not allow_html: if not allow_html:
@@ -46,5 +106,5 @@ def render(params: dict,
return template.render(params=params, *args, **kwargs) return template.render(params=params, *args, **kwargs)
__version__ = '0.2.0'
__all__ = ['default_template', 'render'] __all__ = ['jinja_env', 'base_template', 'render']

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,16 @@
<meta http-equiv="X-UA-Compatible" content="IE=Edge" /> <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
{% block header %}{% endblock %} {% block html_head %}{% endblock %}
<!-- @INLINE_CSS_HERE@ --> <style>
{% if html_style %}
{# Support custom stylesheet #}
{{ html_style | safe }}
{% else %}
{# Default stylesheet #}
{% include 'main.css' %}
{% endif %}
</style>
</head> </head>
<body> <body>
<div id="cf-wrapper"> <div id="cf-wrapper">

View File

@@ -92,9 +92,11 @@ def create_app(test_config=None) -> Flask:
return '', 204 return '', 204
url_prefix = app.config.get('URL_PREFIX', '') url_prefix = app.config.get('URL_PREFIX', '')
short_share_url = app.config.get('SHORT_SHARE_URL', False)
app.register_blueprint(editor.bp, url_prefix=f'{url_prefix}/editor') app.register_blueprint(editor.bp, url_prefix=f'{url_prefix}/editor')
app.register_blueprint(examples.bp, url_prefix=f'{url_prefix}/examples') app.register_blueprint(examples.bp, url_prefix=f'{url_prefix}/examples')
app.register_blueprint(share.bp, url_prefix=f'{url_prefix}/s') app.register_blueprint(share.bp, url_prefix=f'{url_prefix}/s')
app.register_blueprint(share.bp_short, url_prefix=f'{url_prefix}' + ('' if short_share_url else '/s'))
return app return app

View File

@@ -1,6 +1,21 @@
# Url prefix for app urls # Url prefix for app urls
URL_PREFIX = '' URL_PREFIX = ''
# Digits of item name in shared links
SHARE_LINK_DIGITS = 7
# Use short share url (without '/s' path)
SHORT_SHARE_URL = false
# Icon URL for rendered pages
PAGE_ICON_URL = 'https://virt.moe/cferr/editor/assets/icon-{status}-32x32.png'
# MIME type of page icon
PAGE_ICON_TYPE = 'image/png'
# Image URL for rendered pages
PAGE_IMAGE_URL = 'https://virt.moe/cferr/editor/assets/icon-{status}-large-white.png'
# Set to true if trust X-Forwarded-For/X-Forwarded-Proto header # Set to true if trust X-Forwarded-For/X-Forwarded-Proto header
BEHIND_PROXY = true BEHIND_PROXY = true

View File

@@ -11,8 +11,10 @@ from flask import (
redirect, redirect,
) )
from cloudflare_error_page import render as render_cf_error_page from cloudflare_error_page import ErrorPageParams
from .utils import fill_cf_template_params from .utils import (
render_extended_template,
)
root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../') root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
examples_dir = os.path.join(root_dir, 'examples') examples_dir = os.path.join(root_dir, 'examples')
@@ -22,7 +24,7 @@ bp = Blueprint('examples', __name__, url_prefix='/')
param_cache: dict[str, dict] = {} param_cache: dict[str, dict] = {}
def get_page_params(name: str) -> dict: def get_page_params(name: str) -> ErrorPageParams:
name = re.sub(r'[^\w]', '', name) name = re.sub(r'[^\w]', '', name)
params = param_cache.get(name) params = param_cache.get(name)
if params is not None: if params is not None:
@@ -49,7 +51,5 @@ def index(name: str):
if params is None: if params is None:
abort(404) abort(404)
fill_cf_template_params(params)
# Render the error page # Render the error page
return render_cf_error_page(params) return render_extended_template(params=params)

View File

@@ -1,57 +1,33 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import html
import random import random
import string import string
from typing import cast
from cloudflare_error_page import ErrorPageParams
from flask import ( from flask import (
Blueprint, Blueprint,
current_app,
request, request,
abort, abort,
jsonify, jsonify,
redirect,
url_for, url_for,
) )
from jinja2 import Environment, select_autoescape
from cloudflare_error_page import (
default_template as cf_template,
render as render_cf_error_page,
)
from . import ( from . import (
db, db,
limiter, limiter,
models models,
) )
from .utils import (
from .utils import fill_cf_template_params, sanitize_page_param_links render_extended_template,
sanitize_page_param_links,
# root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../')
# examples_dir = os.path.join(root_dir, 'examples')
env = Environment(
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
) )
template = env.from_string('''
{% extends base %}
{% block header %}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="{{ html_title }}" />
<meta property="og:url" content="{{ url }}" />
<meta property="og:description" content="{{ description }}" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="{{ html_title }}" />
<meta property="twitter:description" content="{{ description }}" />
{% endblock %}
''')
bp = Blueprint('share', __name__, url_prefix='/') bp = Blueprint('share', __name__, url_prefix='/')
bp_short = Blueprint('share_short', __name__, url_prefix='/')
rand_charset = string.ascii_lowercase + string.digits rand_charset = string.ascii_lowercase + string.digits
@@ -81,7 +57,8 @@ def create():
# TODO: strip unused params # TODO: strip unused params
try: try:
item = models.Item() item = models.Item()
item.name = get_rand_name() digits = current_app.config.get('SHARE_LINK_DIGITS', 7)
item.name = get_rand_name(digits)
item.params = params item.params = params
db.session.add(item) db.session.add(item)
db.session.commit() db.session.commit()
@@ -93,12 +70,12 @@ def create():
return jsonify({ return jsonify({
'status': 'ok', 'status': 'ok',
'name': item.name, 'name': item.name,
'url': request.host_url[:-1] + url_for('share.get', name=item.name), 'url': request.host_url[:-1] + url_for('share_short.get', name=item.name),
# TODO: better way to handle this # TODO: better way to handle this
}) })
@bp.get('/<name>') @bp_short.get('/<name>')
def get(name: str): def get(name: str):
accept = request.headers.get('Accept', '') accept = request.headers.get('Accept', '')
is_json = 'application/json' in accept is_json = 'application/json' in accept
@@ -111,7 +88,7 @@ def get(name: str):
}) })
else: else:
return abort(404) return abort(404)
params: dict = item.params params = cast(ErrorPageParams, item.params)
params.pop('time', None) params.pop('time', None)
params.pop('ray_id', None) params.pop('ray_id', None)
params.pop('client_ip', None) params.pop('client_ip', None)
@@ -127,12 +104,15 @@ def get(name: str):
'text': 'CF Error Page Editor', 'text': 'CF Error Page Editor',
'link': request.host_url[:-1] + url_for('editor.index') + f'#from={name}', 'link': request.host_url[:-1] + url_for('editor.index') + f'#from={name}',
} }
fill_cf_template_params(params)
sanitize_page_param_links(params) sanitize_page_param_links(params)
return render_extended_template(params=params,
allow_html=False)
return render_cf_error_page(params=params,
allow_html=False, @bp.get('/<name>')
template=template, def get_redir(name: str):
base=cf_template, short_share_url = current_app.config.get('SHORT_SHARE_URL', False)
url=request.url, if short_share_url:
description='Cloudflare error page') return redirect(f'../{name}', code=308)
else:
return get(name=name)

View File

@@ -1,9 +1,53 @@
import json import json
import os import os
import re
from typing import Any
from cloudflare_error_page import (
ErrorPageParams,
base_template as base_template,
render as render_cf_error_page,
)
from flask import current_app, request
from jinja2 import Environment, select_autoescape
from flask import request
from . import root_dir from . import root_dir
env = Environment(
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
)
template = env.from_string('''{% extends base %}
{% block html_head %}
{% if page_icon_url %}
{% if page_icon_type %}
<link rel="icon" href="{{ page_icon_url }}" type="{{ page_icon_type }}">
{% else %}
<link rel="icon" href="{{ page_icon_url }}">
{% endif %}
{% endif %}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="{{ html_title }}" />
<meta property="og:url" content="{{ page_url }}" />
<meta property="og:description" content="{{ page_description }}" />
{% if page_image_url %}
<meta property="og:image" content="{{ page_image_url }}" />
{% endif %}
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="{{ html_title }}" />
<meta property="twitter:description" content="{{ page_description }}" />
{% if page_image_url %}
<meta property="twitter:image" content="{{ page_image_url }}" />
{% endif %}
{% endblock %}
''')
loc_data: dict = None loc_data: dict = None
@@ -31,13 +75,13 @@ def get_cf_location(loc: str):
return data.get('city') return data.get('city')
def fill_cf_template_params(params: dict): def fill_cf_template_params(params: ErrorPageParams):
# Get the real Ray ID / data center location from Cloudflare header # Get the real Ray ID / data center location from Cloudflare header
ray_id_loc = request.headers.get('Cf-Ray') ray_id_loc = request.headers.get('Cf-Ray')
if ray_id_loc: if ray_id_loc:
params['ray_id'] = ray_id_loc[:16] params['ray_id'] = ray_id_loc[:16]
cf_status: dict = params.get('cloudflare_status') cf_status = params.get('cloudflare_status')
if cf_status is None: if cf_status is None:
cf_status = params['cloudflare_status'] = {} cf_status = params['cloudflare_status'] = {}
if not cf_status.get('location'): if not cf_status.get('location'):
@@ -61,7 +105,7 @@ def sanitize_user_link(link: str):
return '#' + link return '#' + link
def sanitize_page_param_links(param: dict): def sanitize_page_param_links(param: ErrorPageParams):
more_info = param.get('more_information') more_info = param.get('more_information')
if more_info: if more_info:
link = more_info.get('link') link = more_info.get('link')
@@ -72,3 +116,31 @@ def sanitize_page_param_links(param: dict):
link = perf_sec_by.get('link') link = perf_sec_by.get('link')
if link: if link:
perf_sec_by['link'] = sanitize_user_link(link) perf_sec_by['link'] = sanitize_user_link(link)
def render_extended_template(params: ErrorPageParams,
*args: Any,
**kwargs: Any) -> str:
fill_cf_template_params(params)
description = params.get('what_happened') or 'There is an internal server error on Cloudflare\'s network.'
description = re.sub(r'<\/?.*?>', '', description).strip()
status = 'ok'
cf_status_obj = params.get('cloudflare_status')
if cf_status_obj:
cf_status = cf_status_obj.get('status')
if cf_status == 'error':
status = 'error'
page_icon_url = current_app.config.get('PAGE_ICON_URL', '').replace('{status}', status)
page_icon_type = current_app.config.get('PAGE_ICON_TYPE')
page_image_url = current_app.config.get('PAGE_IMAGE_URL', '').replace('{status}', status)
return render_cf_error_page(params=params,
template=template,
base=base_template,
page_icon_url=page_icon_url,
page_icon_type=page_icon_type,
page_url=request.url,
page_description=description,
page_image_url=page_image_url,
*args,
**kwargs)

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -14,22 +14,23 @@
<meta name="description" content="Online editor to create customized Cloudflare-style error pages."> <meta name="description" content="Online editor to create customized Cloudflare-style error pages.">
<meta name="keywords" content="cloudflare,error,page,editor"> <meta name="keywords" content="cloudflare,error,page,editor">
<link rel="canonical" href="https://virt.moe/cferr/editor/" /> <link rel="canonical" href="https://virt.moe/cferr/editor/" />
<link rel="icon" type="image/png" href="assets/icon-ok-32x32.png">
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="moe::virt" /> <meta property="og:site_name" content="moe::virt" />
<meta property="og:title" content="Cloudflare error page editor" /> <meta property="og:title" content="Cloudflare error page editor" />
<meta property="og:url" content="https://virt.moe/cferr/editor/" /> <meta property="og:url" content="https://virt.moe/cferr/editor/" />
<meta property="og:description" content="Online editor to create customized Cloudflare-style error pages" /> <meta property="og:description" content="Online editor to create customized Cloudflare-style error pages" />
<meta property="og:image" content="https://virt.moe/cferr/editor/assets/icon-ok-large.png" />
<meta property="twitter:card" content="summary" /> <meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="moe::virt" /> <meta property="twitter:site" content="moe::virt" />
<meta property="twitter:title" content="Cloudflare error page editor" /> <meta property="twitter:title" content="Cloudflare error page editor" />
<meta property="twitter:description" content="Online editor to create customized Cloudflare-style error pages" /> <meta property="twitter:description" content="Online editor to create customized Cloudflare-style error pages" />
<meta property="twitter:image" content="https://virt.moe/cferr/editor/assets/icon-ok-large.png" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<link href="https://virt.moe/assets/cloudflare-error-page/bootstrap.min.css" rel="stylesheet">
<script type="module" src="src/index.js"></script> <script type="module" src="src/index.js"></script>
<style> <style>
/* Layout: editor + preview */ /* Layout: editor + preview */
@@ -60,6 +61,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
iframe.preview-frame { iframe.preview-frame {
height: 100% !important; height: 100% !important;
} }
@@ -83,7 +85,6 @@
/* Compact form: label and control same row */ /* Compact form: label and control same row */
.form-row { .form-row {
display: flex; display: flex;
gap: .3rem;
align-items: center; align-items: center;
margin-bottom: .6rem; margin-bottom: .6rem;
} }
@@ -124,7 +125,6 @@
height: var(--expanded-height); height: var(--expanded-height);
border: 1px solid #ddd; border: 1px solid #ddd;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 360px;
border-radius: .375rem; border-radius: .375rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, .06); box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
} }
@@ -143,6 +143,80 @@
min-height: 80px; min-height: 80px;
resize: vertical; resize: vertical;
} }
.popover {
--bs-popover-body-padding-x: 0.7rem !important;
--bs-popover-body-padding-y: 0.5rem !important;
}
.save-as-dialog__container {
display: grid;
grid-template-areas:
"selector"
"code"
"buttons";
height: fit-content;
grid-template-columns: auto;
gap: 1em;
}
.save-as-dialog__selector {
grid-area: selector;
min-width: 10em;
}
.save-as-dialog__code {
grid-area: code;
width: 100%;
height: 100%;
min-height: 700px !important;
font-family: monospace;
font-size: 0.8em !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: row;
}
.save-as-dialog__buttons>* {
flex: 1 1 auto;
}
@media (min-width: 576px) {
.save-as-dialog__container {
grid-template-areas:
"selector code"
"buttons code";
grid-template-columns: auto 2fr;
height: 100%;
}
.save-as-dialog__selector {
text-align: right;
}
.save-as-dialog__code {
min-height: 0 !important;
}
.save-as-dialog__buttons {
grid-area: buttons;
flex-direction: column;
justify-content: flex-end !important;
}
.save-as-dialog__buttons>* {
flex: none;
}
}
@media (min-width: 992px) {
.save-as-dialog__selector {
grid-area: selector;
min-width: 13em;
}
}
</style> </style>
</head> </head>
@@ -199,8 +273,10 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Browser</strong> <strong>Browser</strong>
<div> <div>
<label for="err_browser" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" /> <input class="form-check-input" type="radio" name="error_source" id="err_browser" value="browser" />
<label for="err_browser" class="ms-1 small">Error here</label> Error here
</label>
</div> </div>
</div> </div>
@@ -241,9 +317,11 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Cloudflare</strong> <strong>Cloudflare</strong>
<div> <div>
<label for="err_cloudflare" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_cloudflare" <input class="form-check-input" type="radio" name="error_source" id="err_cloudflare"
value="cloudflare" /> value="cloudflare" />
<label for="err_cloudflare" class="ms-1 small">Error here</label> Error here
</label>
</div> </div>
</div> </div>
@@ -284,8 +362,10 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Host</strong> <strong>Host</strong>
<div> <div>
<label for="err_host" class="ms-1 small">
<input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" /> <input class="form-check-input" type="radio" name="error_source" id="err_host" value="host" />
<label for="err_host" class="ms-1 small">Error here</label> Error here
</label>
</div> </div>
</div> </div>
@@ -326,8 +406,10 @@
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<strong>Visit ...</strong> <strong>Visit ...</strong>
<div> <div>
<label for="more_hidden" class="ms-1 small">
<input id="more_hidden" class="form-check-input" type="checkbox" /> <input id="more_hidden" class="form-check-input" type="checkbox" />
<label for="more_hidden" class="ms-1 small">Hidden</label> Hidden
</label>
</div> </div>
</div> </div>
@@ -388,15 +470,19 @@
<div class="d-flex gap-2 mt-2 mb-2"> <div class="d-flex gap-2 mt-2 mb-2">
<!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> --> <!-- <button id="btnRender" class="btn btn-sm btn-primary">Render</button> -->
<button id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button> <button type="button" id="btnOpen" class="btn btn-sm btn-primary">Preview in new tab</button>
<button id="btnExport" class="btn btn-sm btn-primary">Export JSON</button> <!-- Button trigger modal -->
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saveAsDialog">
Save as...
</button>
</div> </div>
<button id="btnShare" class="btn btn-sm btn-primary">Create shareable link</button> <button type="button" id="btnShare" class="btn btn-sm btn-secondary">Create shareable link</button>
<div class="mt-2"> <div class="mt-2">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input id="shareLink" class="form-control" readonly /> <input id="shareLink" class="form-control" readonly />
<button id="btnCopyLink" class="btn btn-outline-secondary" type="button">Copy</button> <button type="button" id="btnCopyLink" class="btn btn-outline-secondary" data-bs-toggle="popover"
data-bs-placement="top" data-bs-trigger="manual" data-bs-content="Copied">Copy</button>
</div> </div>
</div> </div>
@@ -422,8 +508,51 @@
<!-- TODO: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. --> <!-- TODO: An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. -->
<iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe> <iframe id="previewFrame" class="preview-frame" sandbox="allow-scripts allow-same-origin"></iframe>
</div> </div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="saveAsDialog" tabindex="-1" aria-labelledby="saveAsDialogLabel">
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable">
<div class="modal-content h-100">
<div class="modal-header">
<h5 class="modal-title" id="saveAsDialogLabel">Save As ...</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mx-2 save-as-dialog__container">
<div id="saveAsDialogTypes" class="list-group save-as-dialog__selector">
<button type="button" data-type="json" class="list-group-item list-group-item-action active">
JSON
</button>
<button type="button" data-type="python" class="list-group-item list-group-item-action">
Python Example
</button>
<button type="button" data-type="js" class="list-group-item list-group-item-action">
NodeJS Example
</button>
<button type="button" data-type="static" class="list-group-item list-group-item-action">
Static Page
</button>
</div>
<div class="d-flex gap-1 save-as-dialog__buttons">
<button type="button" class="btn btn-success" id="saveAsDialogCopyBtn" data-bs-toggle="popover"
data-bs-placement="right" data-bs-trigger="manual" data-bs-content="Copied">
Copy
</button>
<button type="button" class="btn btn-primary" id="saveAsDialogSaveBtn">
Save
</button>
</div>
<textarea id="saveAsDialogCode" class="form-control save-as-dialog__code" spellcheck="false"
readonly></textarea>
</div> </div>
</div> </div>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -10,11 +10,15 @@
"preview": "npm run build && vite preview" "preview": "npm run build && vite preview"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/ejs": "^3.1.5",
"@types/html-minifier-terser": "^7.0.2",
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"prettier": "3.7.4", "prettier": "3.7.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.2.6" "vite": "^7.2.6",
"vite-plugin-static-copy": "^3.1.4"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",

4
editor/web/src/assets.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*?raw' {
const content: string;
export default content;
}

View File

@@ -0,0 +1,26 @@
import ejs from 'ejs';
import jsTemplate from './js.ejs?raw';
import jsonTemplate from './json.ejs?raw';
import pythonTemplate from './python.ejs?raw';
interface CodeGen {
name: string;
generate(params: any): string;
}
class EjsCodeGen implements CodeGen {
name: string;
private template: ejs.TemplateFunction;
constructor(name: string, templateContent: any) {
this.name = name;
this.template = ejs.compile(templateContent);
}
generate(params: any): string {
return this.template({ params });
}
}
export const jsCodeGen = new EjsCodeGen('NodeJS Example', jsTemplate);
export const jsonCodeGen = new EjsCodeGen('JSON', jsonTemplate);
export const pythonCodeGen = new EjsCodeGen('Python Example', pythonTemplate);

View File

@@ -0,0 +1,16 @@
import express from 'express';
import { render as render_cf_error_page } from 'cloudflare-error-page';
const app = express();
const port = 3000;
// Define a route for GET requests to the root URL
<%# TODO: format to JS-style object (key w/o parens) _%>
app.get('/', (req, res) => {
res.status(500).send(render_cf_error_page(<%-JSON.stringify(params, null, 2).replaceAll('\n', '\n ')%>));
});
// Start the server and listen on the specified port
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

View File

@@ -0,0 +1 @@
<%-JSON.stringify(params, null, 4)%>

View File

@@ -0,0 +1,29 @@
<%
// Covert the parameters to Python format object
const randomKey = Math.random() + ''
const paramsArg = JSON.stringify(params, (key, value) => {
if (typeof value === 'boolean') {
return randomKey + value.toString()
} else if (value === null) {
return randomKey + 'null'
} else {
return value
}
}, 4)
.replace(`"${randomKey}true"`, 'True')
.replace(`"${randomKey}false"`, 'False')
.replace(`"${randomKey}null"`, 'None')
_%>
from flask import Flask
from cloudflare_error_page import render as render_cf_error_page
app = Flask(__name__)
# Define a route for GET requests to the root URL
@app.route('/')
def index():
# Render the error page
return render_cf_error_page(<%- paramsArg.replaceAll('\n', '\n ') %>), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)

View File

@@ -5,9 +5,14 @@
- inputs call render() on change - inputs call render() on change
- "Open in new tab" opens the rendered HTML in a new window using a blob URL - "Open in new tab" opens the rendered HTML in a new window using a blob URL
*/ */
import ejs from 'ejs' import ejs from 'ejs';
import templateContent from './template.ejs?raw' import templateContent from './template.ejs?raw';
import 'bootstrap/js/src/modal.js';
import Popover from 'bootstrap/js/src/popover.js';
import 'bootstrap/dist/css/bootstrap.min.css';
import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen';
let template = ejs.compile(templateContent); let template = ejs.compile(templateContent);
@@ -139,7 +144,7 @@ function getDefaultPresetName() {
name = extractUrlParam(window.location.hash.substring(1), key); name = extractUrlParam(window.location.hash.substring(1), key);
} }
if (name) { if (name) {
name = name.replace(/[^\w\d]/g, '') name = name.replace(/[^\w\d]/g, '');
} }
return name; return name;
} }
@@ -300,10 +305,8 @@ function render() {
let lastRenderedHtml = ''; let lastRenderedHtml = '';
function openInNewTab() { function openInNewTab() {
if (!lastRenderedHtml) render(); if (!lastRenderedHtml) render();
const blob = new Blob([lastRenderedHtml], { type: 'text/html' }); const wnd = window.open()
const url = URL.createObjectURL(blob); wnd.document.documentElement.innerHTML = lastRenderedHtml
window.open(url, '_blank', 'noopener');
// note that this url won't be revoked
} }
function createShareableLink() { function createShareableLink() {
@@ -331,22 +334,6 @@ function createShareableLink() {
$('shareLink').value = result.url; $('shareLink').value = result.url;
}); });
} }
function exportJSON() {
let content = JSON.stringify(lastCfg, null, 4);
const file = new File([content], 'cloudflare-error-page-params.json', {
type: 'text/plain',
});
const url = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
function resizePreviewFrame() { function resizePreviewFrame() {
const iframe = $('previewFrame'); const iframe = $('previewFrame');
const height = iframe.contentWindow.document.body.scrollHeight + 2; const height = iframe.contentWindow.document.body.scrollHeight + 2;
@@ -371,15 +358,6 @@ function setBlockClass(id, cls) {
el.classList.add(cls); el.classList.add(cls);
} }
/* Simple debounce */
function debounce(fn, wait) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
/* Wire up events */ /* Wire up events */
// initialize form values from initialConfig // initialize form values from initialConfig
loadConfig(initialConfig); loadConfig(initialConfig);
@@ -395,42 +373,29 @@ $('presetSelect').addEventListener('change', (e) => {
// Render / Open button handlers // Render / Open button handlers
// $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); }); // $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); });
$('btnOpen').addEventListener('click', (e) => { $('btnOpen').addEventListener('click', (e) => {
e.preventDefault();
openInNewTab(); openInNewTab();
}); });
$('btnShare').addEventListener('click', (e) => { $('btnShare').addEventListener('click', (e) => {
e.preventDefault();
createShareableLink(); createShareableLink();
}); });
$('btnExport').addEventListener('click', (e) => { const shareLinkPopover = new Popover($('btnCopyLink'));
e.preventDefault();
exportJSON();
});
$('btnCopyLink').addEventListener('click', () => { $('btnCopyLink').addEventListener('click', () => {
const field = $('shareLink'); const field = $('shareLink');
field.select(); field.select();
field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => { navigator.clipboard.writeText(field.value).then(() => {
// No notification required unless you want one shareLinkPopover.show();
setTimeout(() => {
shareLinkPopover.hide();
}, 2000);
}); });
}); });
// Input change -> render // Input change -> render
const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select'); const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select');
inputs.forEach((inp) => { inputs.forEach((inp) => {
inp.addEventListener( inp.addEventListener('input', () => render());
'input',
debounce(() => {
// Update status block color classes for quick visual feedback in the editor
render();
}, 200)
);
// for radio change events (error_source) // for radio change events (error_source)
if (inp.type === 'radio') if (inp.type === 'radio') inp.addEventListener('change', () => render());
inp.addEventListener('change', () => {
render();
});
}); });
// Automatically update frame height // Automatically update frame height
@@ -439,3 +404,103 @@ const iframe = $('previewFrame');
observer.observe(iframe.contentWindow.document.body); observer.observe(iframe.contentWindow.document.body);
// resizePreviewFrame() // resizePreviewFrame()
setInterval(resizePreviewFrame, 1000); // TODO... setInterval(resizePreviewFrame, 1000); // TODO...
function saveFile(content, saveName) {
const file = new File([content], saveName, {
type: 'text/plain',
});
const url = URL.createObjectURL(file);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
let saveAsType;
let saveAsContent;
function updateSaveAsDialog(e) {
if (e) {
const target = e.target;
saveAsType = target.dataset.type;
} else {
saveAsType = 'json';
}
let codegen;
switch (saveAsType) {
case 'js':
codegen = jsCodeGen;
break;
case 'json':
codegen = jsonCodeGen;
break;
case 'python':
codegen = pythonCodeGen;
break;
}
const params = { ...lastCfg };
delete params.time;
if (codegen) {
saveAsContent = codegen.generate(params);
} else if (saveAsType == 'static') {
render() // rerender the page
saveAsContent = lastRenderedHtml;
} else {
throw new Error('unexpected saveAsType=' + saveAsType)
}
$('saveAsDialogCode').value = saveAsContent;
$('saveAsDialogCode').scrollTop = 0;
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
const isCurrent = element.dataset.type == saveAsType;
if (isCurrent) {
element.classList.add('active');
} else {
element.classList.remove('active');
}
element.ariaCurrent = isCurrent;
});
}
$('saveAsDialog').addEventListener('show.bs.modal', (e) => {
updateSaveAsDialog();
});
document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => {
element.addEventListener('click', updateSaveAsDialog);
});
const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn'));
$('saveAsDialogCopyBtn').addEventListener('click', (e) => {
const field = $('saveAsDialogCode');
field.select();
// field.setSelectionRange(0, field.value.length);
navigator.clipboard.writeText(field.value).then(() => {
saveAsDialogCopyPopover.show();
setTimeout(() => {
saveAsDialogCopyPopover.hide();
}, 2000);
});
});
$('saveAsDialogSaveBtn').addEventListener('click', (e) => {
let saveName = '';
switch (saveAsType) {
case 'js':
saveName = 'cf-error-page-example.js';
break;
case 'json':
saveName = 'cf-error-page-params.json';
break;
case 'python':
saveName = 'cf_error_page_example.py';
break;
case 'static':
saveName = 'cf_error_page.html';
break;
// TODO: name output files using page title
}
saveFile(saveAsContent, saveName);
});

View File

@@ -1,9 +1,8 @@
/// <reference types="vite/types/importMeta.d.ts" /> /// <reference types="vite/types/importMeta.d.ts" />
import { defineConfig, loadEnv } from 'vite'; import { defineConfig } from 'vite';
import { minify as htmlMinify } from 'html-minifier-terser'; import { minify as htmlMinify } from 'html-minifier-terser';
import process from 'node:process'; import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const baseUrl = mode === 'production' ? '' : '/editor/'; const baseUrl = mode === 'production' ? '' : '/editor/';
@@ -17,7 +16,7 @@ export default defineConfig(({ mode }) => {
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {
'/s': { '/s/': {
target: 'http://localhost:5000', target: 'http://localhost:5000',
}, },
}, },
@@ -38,6 +37,14 @@ export default defineConfig(({ mode }) => {
}, },
}, },
}, },
viteStaticCopy({
targets: [
{
src: 'assets/',
dest: '',
},
],
}),
], ],
}; };
}); });

View File

@@ -166,6 +166,11 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@popperjs/core@^2.9.2":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@rollup/rollup-android-arm-eabi@4.53.3": "@rollup/rollup-android-arm-eabi@4.53.3":
version "4.53.3" version "4.53.3"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb"
@@ -276,11 +281,28 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe"
integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==
"@types/bootstrap@^5.2.10":
version "5.2.10"
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.10.tgz#58506463bccc6602bc051487ad8d3a6458f94c6c"
integrity sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==
dependencies:
"@popperjs/core" "^2.9.2"
"@types/ejs@^3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117"
integrity sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==
"@types/estree@1.0.8": "@types/estree@1.0.8":
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/html-minifier-terser@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-7.0.2.tgz#2290fa13e6e49b6cc0ab0afa2d6cf6a66feedb48"
integrity sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==
"@types/node@^24.10.2": "@types/node@^24.10.2":
version "24.10.2" version "24.10.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26"
@@ -293,6 +315,14 @@ acorn@^8.15.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
async@^3.2.6: async@^3.2.6:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
@@ -303,6 +333,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
bootstrap@^5.3.8: bootstrap@^5.3.8:
version "5.3.8" version "5.3.8"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed"
@@ -315,6 +350,13 @@ brace-expansion@^2.0.1:
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^1.0.0"
braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -328,6 +370,21 @@ camel-case@^4.1.2:
pascal-case "^3.1.2" pascal-case "^3.1.2"
tslib "^2.0.3" tslib "^2.0.3"
chokidar@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
clean-css@~5.3.2: clean-css@~5.3.2:
version "5.3.3" version "5.3.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd"
@@ -409,11 +466,25 @@ filelist@^1.0.4:
dependencies: dependencies:
minimatch "^5.0.1" minimatch "^5.0.1"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
fsevents@~2.3.2, fsevents@~2.3.3: fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
html-minifier-terser@^7.2.0: html-minifier-terser@^7.2.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942"
@@ -427,6 +498,30 @@ html-minifier-terser@^7.2.0:
relateurl "^0.2.7" relateurl "^0.2.7"
terser "^5.15.1" terser "^5.15.1"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
jake@^10.8.5: jake@^10.8.5:
version "10.9.4" version "10.9.4"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6"
@@ -463,6 +558,16 @@ no-case@^3.0.4:
lower-case "^2.0.2" lower-case "^2.0.2"
tslib "^2.0.3" tslib "^2.0.3"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
p-map@^7.0.3:
version "7.0.4"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.4.tgz#b81814255f542e252d5729dca4d66e5ec14935b8"
integrity sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==
param-case@^3.0.4: param-case@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
@@ -484,6 +589,11 @@ picocolors@^1.1.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.3: picomatch@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
@@ -503,6 +613,13 @@ prettier@3.7.4:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
relateurl@^0.2.7: relateurl@^0.2.7:
version "0.2.7" version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
@@ -575,6 +692,13 @@ tinyglobby@^0.2.15:
fdir "^6.5.0" fdir "^6.5.0"
picomatch "^4.0.3" picomatch "^4.0.3"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tslib@^2.0.3: tslib@^2.0.3:
version "2.8.1" version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
@@ -590,6 +714,16 @@ undici-types@~7.16.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
vite-plugin-static-copy@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz#d8365b717c2506885ca9a51457a1bcfe6f3a2bef"
integrity sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==
dependencies:
chokidar "^3.6.0"
p-map "^7.0.3"
picocolors "^1.1.1"
tinyglobby "^0.2.15"
vite@^7.2.6: vite@^7.2.6:
version "7.2.7" version "7.2.7"
resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.7.tgz#0789a4c3206081699f34a9ecca2dda594a07478e" resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.7.tgz#0789a4c3206081699f34a9ecca2dda594a07478e"

View File

@@ -1,19 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import re
import sys import sys
from flask import ( from flask import (
Flask, Flask,
request, request,
send_from_directory
) )
# Append this directory to sys.path is not required if the package is already installed # Append this directory to sys.path is not required if the package is already installed
examples_dir = os.path.dirname(os.path.abspath(__file__)) examples_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(examples_dir)) sys.path.append(os.path.dirname(examples_dir))
from cloudflare_error_page import ErrorPageParams
from cloudflare_error_page import render as render_cf_error_page from cloudflare_error_page import render as render_cf_error_page
app = Flask(__name__) app = Flask(__name__)
@@ -21,7 +20,7 @@ app = Flask(__name__)
@app.route('/') @app.route('/')
def index(): def index():
params = { params: ErrorPageParams = {
"title": "Internal server error", "title": "Internal server error",
"error_code": 500, "error_code": 500,
"browser_status": { "browser_status": {

View File

@@ -1,5 +1,7 @@
node_modules/ node_modules/
dist/ dist/
*.tgz
*.log *.log
*.html *.html
.DS_Store .DS_Store

View File

@@ -11,7 +11,7 @@ npm install cloudflare-error-page
Or install from GitHub: Or install from GitHub:
```bash ```bash
npm install git+https://github.com/donlon/cloudflare-error-page.git#main:nodejs npm install git+https://github.com/donlon/cloudflare-error-page.git#javascriptnodejs
``` ```
## Quick Start ## Quick Start
@@ -34,7 +34,7 @@ fs.writeFileSync('error.html', errorPage);
## API Reference ## API Reference
### `render(params: ErrorPageParams, allowHtml?: boolean): string` ### `render(params: ErrorPageParams, allowHtml?: boolean, moreArgs?: { [name: string]: any; }): string`
Generates an HTML error page based on the provided parameters. Generates an HTML error page based on the provided parameters.
@@ -42,6 +42,7 @@ Generates an HTML error page based on the provided parameters.
- `params`: An object containing error page configuration - `params`: An object containing error page configuration
- `allowHtml` (optional): Whether to allow HTML in `what_happened` and `what_can_i_do` fields. Default: `true` - `allowHtml` (optional): Whether to allow HTML in `what_happened` and `what_can_i_do` fields. Default: `true`
- `moreArgs` (optional): More arguments passed to the ejs template
#### ErrorPageParams Interface #### ErrorPageParams Interface

677
javascript/package-lock.json generated Normal file
View File

@@ -0,0 +1,677 @@
{
"name": "cloudflare-error-page",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cloudflare-error-page",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"ejs": "^3.1.10"
},
"devDependencies": {
"@rollup/plugin-typescript": "^12.3.0",
"@rollup/pluginutils": "^5.3.0",
"@types/ejs": "^3.1.5",
"@types/node": "^20.0.0",
"rollup": "^4.54.0",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@rollup/plugin-typescript": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz",
"integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.14.0||^3.0.0||^4.0.0",
"tslib": "*",
"typescript": ">=3.7.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"tslib": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/ejs": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz",
"integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
"integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jake": {
"version": "10.9.4",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.6",
"filelist": "^1.0.4",
"picocolors": "^1.1.1"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.54.0",
"@rollup/rollup-android-arm64": "4.54.0",
"@rollup/rollup-darwin-arm64": "4.54.0",
"@rollup/rollup-darwin-x64": "4.54.0",
"@rollup/rollup-freebsd-arm64": "4.54.0",
"@rollup/rollup-freebsd-x64": "4.54.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
"@rollup/rollup-linux-arm64-musl": "4.54.0",
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
"@rollup/rollup-linux-x64-gnu": "4.54.0",
"@rollup/rollup-linux-x64-musl": "4.54.0",
"@rollup/rollup-openharmony-arm64": "4.54.0",
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
"@rollup/rollup-win32-x64-gnu": "4.54.0",
"@rollup/rollup-win32-x64-msvc": "4.54.0",
"fsevents": "~2.3.2"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"peer": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@@ -1,12 +1,13 @@
{ {
"name": "cloudflare-error-page", "name": "cloudflare-error-page",
"version": "0.0.1", "version": "0.1.0",
"description": "Cloudflare Error Page Generator", "description": "Cloudflare Error Page Generator",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc", "build": "node scripts/copy-files.js && rollup -c",
"dev": "rollup -c -w",
"prepublishOnly": "pnpm run build", "prepublishOnly": "pnpm run build",
"example": "node examples/example.js" "example": "node examples/example.js"
}, },
@@ -32,9 +33,12 @@
"ejs": "^3.1.10" "ejs": "^3.1.10"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-typescript": "^12.3.0",
"@rollup/pluginutils": "^5.3.0",
"@types/ejs": "^3.1.5", "@types/ejs": "^3.1.5",
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"typescript": "^5.3.0" "rollup": "^4.54.0",
"tslib": "^2.8.1"
}, },
"files": [ "files": [
"dist/**/*", "dist/**/*",

View File

@@ -0,0 +1,35 @@
import typescript from "@rollup/plugin-typescript";
import { createFilter } from "@rollup/pluginutils";
// import pkg from './package.json' with { type: 'json' };
function createRawImportPlugin(include: string) {
const rawFilter = createFilter(include);
return {
name: "raw-import",
transform(code: string, id: string): any {
if (rawFilter(id)) {
return {
code: `export default ${JSON.stringify(code)};`,
map: { mappings: "" },
};
}
},
};
}
export default {
input: "src/index.ts",
output: {
// file: pkg.module,
file: "dist/index.js",
format: "esm",
sourcemap: true,
},
watch: {
include: "src/**",
},
external: ["ejs"],
plugins: [typescript(), createRawImportPlugin("**/templates/**")],
};

View File

@@ -0,0 +1,14 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// Resolve the directory of this script
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Define paths relative to scripts/copy.js
const src = path.resolve(__dirname, "../../resources/styles/main.css");
const dest = path.resolve(__dirname, "../src/templates/main.css");
// Copy file
fs.copyFileSync(src, dest);

4
javascript/src/assets.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "./templates/*" {
const content: string;
export default content;
}

101
javascript/src/index.ts Normal file
View File

@@ -0,0 +1,101 @@
import * as ejs from "ejs";
import templateString from "./templates/template.ejs";
import cssString from "./templates/main.css";
export interface StatusItem {
status?: "ok" | "error";
status_text?: string;
status_text_color?: string;
location?: string;
name?: string;
}
export interface MoreInformation {
hidden?: boolean;
link?: string;
text?: string;
for?: string;
}
export interface PerfSecBy {
link?: string;
text?: string;
}
export interface CreatorInfo {
hidden?: boolean;
link?: string;
text?: string;
}
export interface ErrorPageParams {
error_code?: number;
title?: string;
html_title?: string;
time?: string;
ray_id?: string;
client_ip?: string;
browser_status?: StatusItem;
cloudflare_status?: StatusItem;
host_status?: StatusItem;
error_source?: "browser" | "cloudflare" | "host";
what_happened?: string;
what_can_i_do?: string;
more_information?: MoreInformation;
perf_sec_by?: PerfSecBy;
creator_info?: CreatorInfo;
}
// Load EJS template
export const baseTemplate: ejs.TemplateFunction = ejs.compile(templateString);
/**
* Generate random hex string for ray-id
*/
function genHexString(digits: number): string {
const hex = "0123456789ABCDEF";
let output = "";
for (let i = 0; i < digits; i++) {
output += hex.charAt(Math.floor(Math.random() * hex.length));
}
return output;
}
/**
* Render a customized Cloudflare error page
* @param params - The parameters for the error page
* @param allowHtml - Whether to allow HTML in what_happened and what_can_i_do fields (default: true)
* @param moreArgs - More arguments passed to the ejs template
* @returns The rendered HTML string
*/
export function render(
params: ErrorPageParams,
allowHtml: boolean = true,
moreArgs: {
[name: string]: any;
} = {}
): string {
params = { ...params };
if (!params.time) {
const now = new Date();
params.time = now.toISOString().replace("T", " ").substring(0, 19) + " UTC";
}
if (!params.ray_id) {
params.ray_id = genHexString(16);
}
if (!allowHtml) {
params.what_happened = ejs.escapeXML(params.what_happened ?? "");
params.what_can_i_do = ejs.escapeXML(params.what_can_i_do ?? "");
}
return baseTemplate({ params, html_style: cssString, ...moreArgs });
}
export default render;

1
javascript/src/templates/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
main.css

View File

@@ -4,18 +4,18 @@
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]--> <!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]--> <!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
<head> <head>
<% <% const error_code = params.error_code || 500; %>
let error_code = params.error_code || 500; <% const title = params.title || 'Internal server error'; %>
let title = params.title || 'Internal server error'; <% const html_title = params.html_title || (error_code + ': ' + title); %>
let html_title_output = params.html_title || (error_code + ': ' + title); <title><%= html_title %></title>
%>
<title><%= html_title_output %></title>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" /> <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- @INLINE_CSS_HERE@ --> <style>
<%- html_style %>
</style>
</head> </head>
<body> <body>
<div id="cf-wrapper"> <div id="cf-wrapper">
@@ -25,10 +25,10 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<span class="inline-block"><%= title %></span> <span class="inline-block"><%= title %></span>
<span class="code-label">Error code <%= error_code %></span> <span class="code-label">Error code <%= error_code %></span>
</h1> </h1>
<% let more_info = params.more_information || {}; %> <% const more_info = params.more_information || {}; %>
<% if (!more_info.hidden) { %> <% if (!more_info.hidden) { %>
<div> <div>
Visit <a href="<%= more_info.link || 'https://www.cloudflare.com/' %>" target="_blank" rel="noopener noreferrer"><%= more_info.text || 'cloudflare.com' %></a> for <%= more_info.for || "more information" %>. Visit <a href="<%= more_info.link || 'https://www.cloudflare.com/' %>" target="_blank" rel="noopener noreferrer"><%= more_info.text || 'cloudflare.com' %></a> for <%= more_info.for || 'more information' %>.
</div> </div>
<% } %> <% } %>
<div class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div> <div class="<%= more_info.hidden ? '' : 'mt-3' %>"><%= params.time %></div>
@@ -36,27 +36,16 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<div class="my-8 bg-gradient-gray"> <div class="my-8 bg-gradient-gray">
<div class="w-240 lg:w-full mx-auto"> <div class="w-240 lg:w-full mx-auto">
<div class="clearfix md:px-8"> <div class="clearfix md:px-8">
<% for (let item_id of ['browser', 'cloudflare', 'host']) { %>
<% <%
let icon, default_location, default_name, text_color, status_text; const items = [
{ id: 'browser', icon: 'browser', default_location: 'You', default_name: 'Browser' },
if (item_id === 'browser') { { id: 'cloudflare', icon: 'cloud', default_location: 'San Francisco', default_name: 'Cloudflare' },
icon = 'browser'; { id: 'host', icon: 'server', default_location: 'Website', default_name: 'Host' }
default_location = 'You'; ];
default_name = 'Browser'; items.forEach(({ id, icon, default_location, default_name }) => {
} else if (item_id === 'cloudflare') { const item = params[id + '_status'] || {};
icon = 'cloud'; const status = item.status || 'ok';
default_location = 'San Francisco'; let text_color;
default_name = 'Cloudflare';
} else {
icon = 'server';
default_location = 'Website';
default_name = 'Host';
}
let item = params[item_id + '_status'] || {};
let status = item.status || 'ok';
if (item.status_text_color) { if (item.status_text_color) {
text_color = item.status_text_color; text_color = item.status_text_color;
} else if (status === 'ok') { } else if (status === 'ok') {
@@ -64,27 +53,19 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
} else if (status === 'error') { } else if (status === 'error') {
text_color = '#bd2426'; // text-red-error text_color = '#bd2426'; // text-red-error
} }
const status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error');
status_text = item.status_text || (status === 'ok' ? 'Working' : 'Error'); const is_error_source = params.error_source === id;
%> %>
<div id="cf-<%= item_id %>-status" class="<% if (params.error_source === item_id) { %>cf-error-source<% } %> relative w-1/3 md:w-full py-15 md:p-0 md:py-8 md:text-left md:border-solid md:border-0 md:border-b md:border-gray-400 overflow-hidden float-left md:float-none text-center"> <div id="cf-<%= id %>-status" class="<%= is_error_source ? 'cf-error-source' : '' %> relative w-1/3 md:w-full py-15 md:p-0 md:py-8 md:text-left md:border-solid md:border-0 md:border-b md:border-gray-400 overflow-hidden float-left md:float-none text-center">
<div class="relative mb-10 md:m-0"> <div class="relative mb-10 md:m-0">
<span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span> <span class="cf-icon-<%= icon %> block md:hidden h-20 bg-center bg-no-repeat"></span>
<span class="cf-icon-<%= status %> w-12 h-12 absolute left-1/2 md:left-auto md:right-0 md:top-0 -ml-6 -bottom-4"></span> <span class="cf-icon-<%= status %> w-12 h-12 absolute left-1/2 md:left-auto md:right-0 md:top-0 -ml-6 -bottom-4"></span>
</div> </div>
<span class="md:block w-full truncate"><%= item.location || default_location %></span> <span class="md:block w-full truncate"><%= item.location || default_location %></span>
<% <h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3" <%- (item.name || default_name) === 'Cloudflare' ? 'style="color: #2f7bbf;"' : '' %>><%= item.name || default_name %></h3>
let _name_style;
if ((item.name || default_name) === 'Cloudflare') {
_name_style = 'style="color: #2f7bbf;"'
} else{
_name_style = ''
}
%>
<h3 class="md:inline-block mt-3 md:mt-0 text-2xl text-gray-600 font-light leading-1.3" <%-_name_style %>><%= item.name || default_name %></h3>
<span class="leading-1.3 text-2xl" style="color: <%= text_color %>"><%= status_text %></span> <span class="leading-1.3 text-2xl" style="color: <%= text_color %>"><%= status_text %></span>
</div> </div>
<% } %> <% }); %>
</div> </div>
</div> </div>
</div> </div>
@@ -93,11 +74,11 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<div class="clearfix"> <div class="clearfix">
<div class="w-1/2 md:w-full float-left pr-6 md:pb-10 md:pr-0 leading-relaxed"> <div class="w-1/2 md:w-full float-left pr-6 md:pb-10 md:pr-0 leading-relaxed">
<h2 class="text-3xl font-normal leading-1.3 mb-4">What happened?</h2> <h2 class="text-3xl font-normal leading-1.3 mb-4">What happened?</h2>
<%= (params.what_happened || 'There is an internal server error on Cloudflare\'s network.') %> <%- params.what_happened || '<p>There is an internal server error on Cloudflare\'s network.</p>' %>
</div> </div>
<div class="w-1/2 md:w-full float-left leading-relaxed"> <div class="w-1/2 md:w-full float-left leading-relaxed">
<h2 class="text-3xl font-normal leading-1.3 mb-4">What can I do?</h2> <h2 class="text-3xl font-normal leading-1.3 mb-4">What can I do?</h2>
<%= (params.what_can_i_do || 'Please try again in a few minutes.') %> <%- params.what_can_i_do || '<p>Please try again in a few minutes.</p>' %>
</div> </div>
</div> </div>
</div> </div>
@@ -112,8 +93,14 @@ let html_title_output = params.html_title || (error_code + ': ' + title);
<span class="hidden" id="cf-footer-ip"><%= params.client_ip || '1.1.1.1' %></span> <span class="hidden" id="cf-footer-ip"><%= params.client_ip || '1.1.1.1' %></span>
<span class="cf-footer-separator sm:hidden">&bull;</span> <span class="cf-footer-separator sm:hidden">&bull;</span>
</span> </span>
<% let perf_sec_by = params.perf_sec_by || {}; %> <% const perf_sec_by = params.perf_sec_by || {}; %>
<span class="cf-footer-item sm:block sm:mb-1"><span>Performance &amp; security by</span> <a rel="noopener noreferrer" href="<%= perf_sec_by.link || 'https://www.cloudflare.com/' %>" id="brand_link" target="_blank"><%= perf_sec_by.text || 'Cloudflare' %></a></span> <span class="cf-footer-item sm:block sm:mb-1"><span>Performance &amp; security by</span> <a rel="noopener noreferrer" href="<%= perf_sec_by.link || 'https://www.cloudflare.com/' %>" id="brand_link" target="_blank"><%= perf_sec_by.text || 'Cloudflare' %></a></span>
<% const creator_info = params.creator_info || {}; %>
<% if (!(creator_info.hidden ?? true)) { %>
<span class="cf-footer-separator sm:hidden">&bull;</span>
<span class="cf-footer-item sm:block sm:mb-1">Created with <a href="<%= creator_info.link %>" target="_blank"><%= creator_info.text %></a></span>
<% } %>
</p> </p>
</div><!-- /.error-footer --> </div><!-- /.error-footer -->
</div> </div>

142
nodejs/package-lock.json generated
View File

@@ -1,142 +0,0 @@
{
"name": "cloudflare-error-page",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cloudflare-error-page",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"ejs": "^3.1.10"
},
"devDependencies": {
"@types/ejs": "^3.1.5",
"@types/node": "^20.0.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@types/ejs": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz",
"integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz",
"integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/jake": {
"version": "10.9.4",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
"integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.6",
"filelist": "^1.0.4",
"picocolors": "^1.1.1"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@@ -1,123 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import * as ejs from "ejs";
import * as crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export interface StatusItem {
status?: "ok" | "error";
status_text?: string;
status_text_color?: string;
location?: string;
name?: string;
}
export interface MoreInformation {
hidden?: boolean;
link?: string;
text?: string;
for?: string;
}
export interface PerfSecBy {
link?: string;
text?: string;
}
export interface CreatorInfo {
hidden?: boolean;
link?: string;
text?: string;
}
export interface ErrorPageParams {
error_code?: number;
title?: string;
html_title?: string;
time?: string;
ray_id?: string;
client_ip?: string;
browser_status?: StatusItem;
cloudflare_status?: StatusItem;
host_status?: StatusItem;
error_source?: "browser" | "cloudflare" | "host";
what_happened?: string;
what_can_i_do?: string;
more_information?: MoreInformation;
perf_sec_by?: PerfSecBy;
creator_info?: CreatorInfo;
}
/**
* Fill default parameters if not provided
*/
function fillParams(params: ErrorPageParams): ErrorPageParams {
const filledParams = { ...params };
if (!filledParams.time) {
const now = new Date();
filledParams.time =
now.toISOString().replace("T", " ").substring(0, 19) + " UTC";
}
if (!filledParams.ray_id) {
filledParams.ray_id = crypto.randomBytes(8).toString("hex");
}
return filledParams;
}
/**
* Escape HTML special characters
*/
function escapeHtml(text: string): string {
const htmlEscapeMap: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
return text.replace(/[&<>"']/g, (char) => htmlEscapeMap[char] || char);
}
/**
* Render a customized Cloudflare error page
* @param params - The parameters for the error page
* @param allowHtml - Whether to allow HTML in what_happened and what_can_i_do fields (default: true)
* @returns The rendered HTML string
*/
export function render(
params: ErrorPageParams,
allowHtml: boolean = true
): string {
let processedParams = fillParams(params);
if (!allowHtml) {
processedParams = { ...processedParams };
if (processedParams.what_happened) {
processedParams.what_happened = escapeHtml(processedParams.what_happened);
}
if (processedParams.what_can_i_do) {
processedParams.what_can_i_do = escapeHtml(processedParams.what_can_i_do);
}
}
// Load EJS template
const templatePath = path.join(__dirname, "..", "templates", "error.ejs");
const template = fs.readFileSync(templatePath, "utf-8");
const rendered = ejs.render(template, { params: processedParams });
return rendered;
}
export default render;

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,6 @@ build-backend = "hatchling.build"
[project] [project]
name = "cloudflare-error-page" name = "cloudflare-error-page"
version = "0.1.0"
description = "A customizable Cloudflare error page generator" description = "A customizable Cloudflare error page generator"
authors = [{ name = "Anthony Donlon" }] authors = [{ name = "Anthony Donlon" }]
license = { text = "MIT" } license = { text = "MIT" }
@@ -13,6 +12,7 @@ requires-python = ">=3.10"
dependencies = [ dependencies = [
"jinja2>=3.0" "jinja2>=3.0"
] ]
dynamic = [ "version" ]
[project.urls] [project.urls]
Homepage = "https://github.com/donlon/cloudflare-error-page" Homepage = "https://github.com/donlon/cloudflare-error-page"
@@ -22,3 +22,9 @@ include = [
"cloudflare_error_page/*.py", "cloudflare_error_page/*.py",
"cloudflare_error_page/templates/*", "cloudflare_error_page/templates/*",
] ]
[tool.hatch.build.targets.wheel.hooks.custom]
path = "scripts/hatch_build.py"
[tool.hatch.version]
path = "cloudflare_error_page/__init__.py"

File diff suppressed because one or more lines are too long

17
scripts/hatch_build.py Normal file
View File

@@ -0,0 +1,17 @@
import os
import sys
import shutil
from pathlib import Path
from typing import Any
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
sys.path.append(os.path.dirname(__file__))
from inline_resources import generate_inlined_css
class CustomBuildHook(BuildHookInterface):
def initialize(self, version: str, build_data: dict[str, Any]):
generate_inlined_css()
src = Path(self.root) / 'resources' / 'styles' / 'main.css'
dst = Path(self.root) / 'cloudflare_error_page' / 'templates'
shutil.copy(src, dst)

View File

@@ -49,7 +49,7 @@ def inline_css_resource(original_file: str, css_file: str, output_file: str):
write_file(output_file, original_data) write_file(output_file, original_data)
if __name__ == '__main__': def generate_inlined_css():
inline_svg_resources( inline_svg_resources(
os.path.join(resources_folder, 'styles/main-original.css'), os.path.join(resources_folder, 'styles/main-original.css'),
[ [
@@ -61,13 +61,7 @@ if __name__ == '__main__':
], ],
os.path.join(resources_folder, 'styles/main.css'), os.path.join(resources_folder, 'styles/main.css'),
) )
inline_css_resource(
os.path.join(resources_folder, 'templates/error.html'),
os.path.join(resources_folder, 'styles/main.css'), if __name__ == '__main__':
os.path.join(root, 'cloudflare_error_page/templates/error.html'), generate_inlined_css()
)
inline_css_resource(
os.path.join(resources_folder, 'templates/error.ejs'),
os.path.join(resources_folder, 'styles/main.css'),
os.path.join(root, 'editor/web/src/template.ejs'),
)