-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Sven Groot
committed
Dec 14, 2024
0 parents
commit 52c1518
Showing
15 changed files
with
775 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Python-generated files | ||
__pycache__/ | ||
*.py[oc] | ||
build/ | ||
dist/ | ||
wheels/ | ||
*.egg-info | ||
|
||
# Virtual environments | ||
.venv | ||
|
||
.coverage | ||
.pytest_cache | ||
.ruff_cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
3.13 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
MIT License | ||
|
||
Copyright 2024 Sven Groot | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# django-ts-routes | ||
|
||
**django-ts-routes** is a Django application allowing to expose and perform reverse lookups of Django named URL patterns in a TypeScript code base. This codebase is based on [django-js-routes](https://github.com/ellmetha/django-js-routes). Big thanks to the original author of this package. | ||
|
||
## Table of Contents | ||
|
||
- [Installation](#installation) | ||
- [Usage](#usage) | ||
- [Settings](#settings) | ||
- [Advanced features](#advanced-features) | ||
- [License](#license) | ||
|
||
## Installation | ||
|
||
To install django-ts-routes, please use the pip command as follows: | ||
|
||
```shell | ||
$ pip install django-ts-routes | ||
``` | ||
|
||
Once the package is installed, you'll have to add the application to `INSTALLED_APPS` in your project's settings module: | ||
|
||
```python | ||
INSTALLED_APPS = ( | ||
# all other apps... | ||
'ts_routes', | ||
) | ||
``` | ||
|
||
You can then define which URL patterns or URL namespaces you want to expose by setting the `TS_ROUTES_INCLUSION_LIST` setting (for compatibility with [django-js-routes](https://github.com/ellmetha/django-js-routes) you can also use `JS_ROUTES_INCLUSION_LIST`). This setting allows to define which URLs should be serialized and made available to the client side through the generated and / or exported TypeScript helper. This list should contain only URL pattern names or namespaces. Here is an example: | ||
|
||
```python | ||
TS_ROUTES_INCLUSION_LIST = [ | ||
'home', | ||
'catalog:product_list', | ||
'catalog:product_detail', | ||
] | ||
``` | ||
|
||
Note that if a namespace is included in this list, all the underlying URLs will be made available to the client side through the generated TypeScript helper. Django-ts-routes is safe by design in the sense that _only_ the URLs that you configure in this inclusion list will be publicly exposed on the client side. | ||
|
||
Once the list of URLs to expose is configured, you can dump the routes with the management command `dump_routes_resolver`: | ||
|
||
```shell | ||
$ python manage.py dump_routes_resolver --output-dir=static/src/routes | ||
``` | ||
|
||
## Usage | ||
|
||
The URL patterns you configured through the `TS_ROUTES_INCLUSION_LIST` setting will be exported to TypeScript files in the `output-dir` directory. This directory will contain a `index.ts` with routes of the default language, other supported languages will be in separate files. The files export the `reverseUrl` function that can be used to generate URLs in your TypeScript code. | ||
|
||
```typescript | ||
import reverseUrl from "@/routes"; | ||
|
||
reverseUrl("home"); | ||
reverseUrl("catalog:product_list"); | ||
reverseUrl("catalog:product_detail", { pk: productId }); | ||
``` | ||
|
||
## Settings | ||
|
||
### TS_ROUTES_INCLUSION_LIST | ||
|
||
Default: `[]` | ||
|
||
The `TS_ROUTES_INCLUSION_LIST` setting allows to define the URL patterns and URL namespaces that should be exposed. | ||
|
||
## License | ||
|
||
MIT. See `LICENSE` for more details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
[project] | ||
name = "django-ts-routes" | ||
version = "0.1.0.dev0" | ||
description = "Expose and perform reverse lookups of Django URLs in the TypeScript world." | ||
readme = "README.md" | ||
license = {text = "MIT"} | ||
authors = [ | ||
{ name = "Sven Groot" } | ||
] | ||
requires-python = ">=3.8" | ||
keywords = ["django", "urls", "reverse", "typescript", "export"] | ||
classifiers = [ | ||
"Development Status :: 4 - Beta", | ||
"Environment :: Web Environment", | ||
"Framework :: Django", | ||
"Intended Audience :: Developers", | ||
"License :: OSI Approved :: MIT License", | ||
"Natural Language :: English", | ||
"Programming Language :: Python", | ||
"Programming Language :: Python :: 3", | ||
"Programming Language :: Python :: 3.8", | ||
"Programming Language :: Python :: 3.9", | ||
"Programming Language :: Python :: 3.10", | ||
"Programming Language :: Python :: 3.11", | ||
"Programming Language :: Python :: 3.12", | ||
"Programming Language :: Python :: 3.13", | ||
] | ||
dependencies = [ | ||
"django >= 4.2", | ||
] | ||
|
||
[dependency-groups] | ||
dev = [ | ||
"pytest-cov==5.0.0", | ||
"pytest==8.3.4", | ||
"ruff==0.8.3", | ||
"pytest-django==4.9.0", | ||
] | ||
|
||
[project.urls] | ||
homepage = "https://github.com/svengt/django-ts-routes" | ||
repository = "https://github.com/svengt/django-ts-routes" | ||
|
||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" | ||
|
||
[tool.hatch.build.targets.wheel] | ||
packages = ["src/ts_routes"] | ||
|
||
[[tool.uv.index]] | ||
name = "pypi" | ||
url = "https://pypi.org/simple/" | ||
publish-url = "https://upload.pypi.org/legacy/" | ||
|
||
[[tool.uv.index]] | ||
name = "testpypi" | ||
url = "https://test.pypi.org/simple/" | ||
publish-url = "https://test.pypi.org/legacy/" | ||
|
||
[tool.ruff.lint] | ||
select = [ | ||
"E", | ||
"F", | ||
"B", | ||
"SIM", | ||
"I", | ||
] | ||
ignore = ["E501"] | ||
unfixable = ["B", "SIM"] | ||
|
||
[tool.pytest.ini_options] | ||
django_find_project = false | ||
addopts = "--ds=tests.pytest.settings --reuse-db" | ||
pythonpath = "./" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
def hello() -> str: | ||
return "Hello from django-ts-routes!" |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from os import path | ||
|
||
from django.conf import settings | ||
from django.core.management.base import BaseCommand | ||
from django.template.loader import render_to_string | ||
from django.utils import translation | ||
|
||
from ...serializers import url_patterns_serializer | ||
|
||
|
||
class Command(BaseCommand): | ||
"""Dump Django URLs and the resolver helper to a TS file per language.""" | ||
|
||
help = "Dump Django URLs and the resolver helper to a TS file per language." | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument( | ||
"-o", | ||
"--output-dir", | ||
required=True, | ||
help="Specifies a directory to which the output is written.", | ||
) | ||
|
||
def _write_resolver_file(self, filename): | ||
result = render_to_string( | ||
"ts_routes/_dump/resolver.ts", | ||
{"routes": url_patterns_serializer.to_json()}, | ||
) | ||
with open(filename, "w") as output: | ||
output.write(result) | ||
|
||
def handle(self, *args, **options): | ||
filename = path.join(options["output_dir"], "index.ts") | ||
self._write_resolver_file(filename) | ||
|
||
for lang, _ in settings.LANGUAGES: | ||
with translation.override(lang): | ||
filename = path.join(options["output_dir"], f"{lang}.ts") | ||
self._write_resolver_file(filename) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
""" | ||
Copied and modified from django-js-routes https://github.com/ellmetha/django-js-routes | ||
JS Routes serializers | ||
===================== | ||
This modules defines serializer classes responsible for extracting Django URLs that should be | ||
exposed to the client side to a specific format (eg. JSON). The resoluting exports are likely to | ||
be used in a Javascript module to provide reverse lookup functionality on the client side. | ||
""" | ||
|
||
import json | ||
import re | ||
from urllib.parse import urljoin | ||
|
||
from django.conf import settings | ||
from django.urls import get_resolver | ||
from django.urls.resolvers import ( | ||
LocalePrefixPattern, | ||
RegexPattern, | ||
RoutePattern, | ||
URLPattern, | ||
URLResolver, | ||
) | ||
|
||
from .utils.text import replace | ||
|
||
|
||
class URLPatternsSerializer: | ||
"""The main class responsible for the serialization of the URLs exposed on the client side. | ||
This class implements a mechanism allowing to traverse the tree of URLs of a specific URL | ||
resolver in order to return the URLs that should be exposed on the client side in a specific | ||
format such as JSON. The exported object provides a mapping between fully qualified (namespaced) | ||
URL names and the corresponding paths. | ||
This class uses the default URL resolver if no one is set at initialization time. | ||
""" | ||
|
||
_url_arg_re = re.compile(r"(\(.*?\))") | ||
_url_kwarg_re = re.compile(r"(\(\?P\<(.*?)\>.*?\))") | ||
_url_optional_char_re = re.compile(r"(?:\w|/)(?:\?|\*)") | ||
_url_optional_group_re = re.compile(r"\(\?\:.*\)(?:\?|\*)") | ||
_url_path_re = re.compile(r"<(.*?)>") | ||
|
||
def __init__(self, resolver=None): | ||
self.resolver = resolver or get_resolver() | ||
|
||
def to_json(self): | ||
"""Serializes the URLs to be exported in a JSON array.""" | ||
return json.dumps(dict(self._parse())) | ||
|
||
def _get_routes_inclustion_list(self): | ||
return getattr(settings, "TS_ROUTES_INCLUSION_LIST", []) or getattr( | ||
settings, "JS_ROUTES_INCLUSION_LIST", [] | ||
) | ||
|
||
def _parse(self): | ||
return self._parse_resolver(self.resolver) | ||
|
||
def _parse_resolver( | ||
self, resolver, parent_namespace=None, parent_url_prefix=None, include_all=False | ||
): | ||
namespace = ":".join( | ||
filter(lambda n: n, [parent_namespace, resolver.namespace]) | ||
) | ||
include_all = include_all or (namespace in self._get_routes_inclustion_list()) | ||
|
||
urls = [] | ||
for url_pattern in resolver.url_patterns: | ||
if isinstance(url_pattern, URLResolver): | ||
url_prefix = urljoin( | ||
parent_url_prefix or "/", self._prepare_url_part(url_pattern) | ||
) | ||
urls = urls + self._parse_resolver( | ||
url_pattern, | ||
parent_namespace=namespace, | ||
parent_url_prefix=url_prefix, | ||
include_all=include_all, | ||
) | ||
elif isinstance(url_pattern, URLPattern) and url_pattern.name: | ||
url_name = ":".join(filter(lambda n: n, [namespace, url_pattern.name])) | ||
if url_name in self._get_routes_inclustion_list() or include_all: | ||
full_url = self._prepare_url_part(url_pattern) | ||
urls.append((url_name, urljoin(parent_url_prefix or "/", full_url))) | ||
|
||
return urls | ||
|
||
def _prepare_url_part(self, url_pattern): | ||
url = "" | ||
|
||
if isinstance(url_pattern.pattern, RegexPattern): | ||
url = url_pattern.pattern._regex | ||
elif isinstance(url_pattern.pattern, RoutePattern): | ||
url = url_pattern.pattern._route | ||
elif isinstance(url_pattern.pattern, LocalePrefixPattern): | ||
url = url_pattern.pattern.language_prefix | ||
else: # pragma: no cover | ||
raise ValueError( | ||
'url_pattern must be a valid URL pattern ; "{}" is not'.format( | ||
url_pattern | ||
) | ||
) | ||
|
||
final_url = replace(url, [("^", ""), ("$", "")]) | ||
final_url = self._remove_optional_groups_from_url(final_url) | ||
final_url = self._remove_optional_characters_from_url(final_url) | ||
final_url = self._replace_arguments_in_url(final_url) | ||
|
||
return final_url | ||
|
||
def _remove_optional_groups_from_url(self, url): | ||
matches = self._url_optional_group_re.findall(url) | ||
return replace(url, [(el, "") for el in matches]) if matches else url | ||
|
||
def _remove_optional_characters_from_url(self, url): | ||
matches = self._url_optional_char_re.findall(url) | ||
return replace(url, [(el, "") for el in matches]) if matches else url | ||
|
||
def _replace_arguments_in_url(self, url): | ||
# Identifies and replaces named URL arguments inside the URL. | ||
kwarg_matches = self._url_kwarg_re.findall(url) | ||
url = ( | ||
replace(url, [(el[0], "<{}>".format(el[1])) for el in kwarg_matches]) | ||
if kwarg_matches | ||
else url | ||
) | ||
|
||
# Identifies and replaces unnamed URL arguments inside the URL. | ||
args_matches = self._url_arg_re.findall(url) | ||
url = replace(url, [(el, "<>") for el in args_matches]) if args_matches else url | ||
|
||
return url | ||
|
||
|
||
url_patterns_serializer = URLPatternsSerializer() |
Oops, something went wrong.