-
Notifications
You must be signed in to change notification settings - Fork 144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate static urls #11571
base: dev/8.0.x
Are you sure you want to change the base?
Generate static urls #11571
Changes from all commits
e6c7828
4535a9b
f4fd1fe
e8a1171
23a65c4
dae3bff
5d3ddf0
3b72327
ba72150
fbed86e
29516bd
d1eed5c
8e9e7cc
0452c6f
5639958
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { describe, it, expect } from "vitest"; | ||
import generateArchesURL from "@/arches/utils/generate-arches-url.ts"; | ||
|
||
// @ts-expect-error ARCHES_URLS is defined globally | ||
global.ARCHES_URLS = { | ||
example_url: "/{language_code}/admin/example/{id}", | ||
another_url: "/admin/another/{id}", | ||
multi_interpolation_url: | ||
"/{language_code}/resource/{resource_id}/edit/{field_id}/version/{version_id}", | ||
}; | ||
|
||
describe("generateArchesURL", () => { | ||
it("should return a valid URL with specified language code and parameters", () => { | ||
const result = generateArchesURL("example_url", { id: "123" }, "fr"); | ||
expect(result).toBe("/fr/admin/example/123"); | ||
}); | ||
|
||
it("should use the <html> lang attribute when no language code is provided", () => { | ||
Object.defineProperty(document.documentElement, "lang", { | ||
value: "de", | ||
configurable: true, | ||
}); | ||
|
||
const result = generateArchesURL("example_url", { id: "123" }); | ||
expect(result).toBe("/de/admin/example/123"); | ||
}); | ||
|
||
it("should throw an error if the URL name is not found", () => { | ||
expect(() => | ||
generateArchesURL("invalid_url", { id: "123" }, "fr"), | ||
).toThrowError("Key 'invalid_url' not found in JSON object"); | ||
}); | ||
|
||
it("should replace URL parameters correctly", () => { | ||
const result = generateArchesURL("another_url", { id: "456" }); | ||
expect(result).toBe("/admin/another/456"); | ||
}); | ||
|
||
it("should handle URLs without language code placeholder", () => { | ||
const result = generateArchesURL("another_url", { id: "789" }); | ||
expect(result).toBe("/admin/another/789"); | ||
}); | ||
|
||
it("should handle multiple interpolations in the URL", () => { | ||
const result = generateArchesURL( | ||
"multi_interpolation_url", | ||
{ | ||
resource_id: "42", | ||
field_id: "name", | ||
version_id: "7", | ||
}, | ||
"es", | ||
); | ||
expect(result).toBe("/es/resource/42/edit/name/version/7"); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export default function ( | ||
urlName: string, | ||
urlParams = {}, | ||
languageCode?: string, | ||
) { | ||
// @ts-expect-error ARCHES_URLS is defined globally | ||
let url = ARCHES_URLS[urlName]; | ||
|
||
if (!url) { | ||
throw new Error(`Key '${urlName}' not found in JSON object`); | ||
} | ||
|
||
if (url.includes("{language_code}")) { | ||
if (!languageCode) { | ||
const htmlLang = document.documentElement.lang; | ||
languageCode = htmlLang.split("-")[0]; | ||
} | ||
|
||
url = url.replace("{language_code}", languageCode); | ||
} | ||
|
||
Object.entries(urlParams).forEach(([key, value]) => { | ||
url = url.replace(new RegExp(`{${key}}`, "g"), value); | ||
}); | ||
|
||
return url; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import json | ||
import os | ||
import re | ||
import site | ||
import sys | ||
|
||
from django.conf import settings | ||
from django.urls import get_resolver, URLPattern, URLResolver | ||
from django.urls.resolvers import RegexPattern, RoutePattern, LocalePrefixPattern | ||
|
||
from arches.settings_utils import list_arches_app_names, list_arches_app_paths | ||
|
||
|
||
def generate_frontend_configuration(): | ||
try: | ||
_generate_frontend_configuration_directory() | ||
_generate_urls_json() | ||
_generate_webpack_configuration() | ||
_generate_tsconfig_paths() | ||
except Exception as e: | ||
# Ensures error message is shown if error encountered | ||
sys.stderr.write(str(e)) | ||
raise e | ||
|
||
|
||
def _generate_frontend_configuration_directory(): | ||
destination_dir = os.path.realpath( | ||
os.path.join(_get_base_path(), "..", "frontend_configuration") | ||
) | ||
|
||
os.makedirs(destination_dir, exist_ok=True) | ||
|
||
|
||
def _generate_urls_json(): | ||
def generate_human_readable_urls(patterns, prefix="", namespace="", result={}): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I try to avoid mutable default arguments like |
||
def join_paths(*args): | ||
return "/".join(filter(None, (arg.strip("/") for arg in args))) | ||
Comment on lines
+36
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We seem to be losing the trailing slash on some routes. Leaving aside whether it's a good idea to have trailing slashes required in your routes, if projects do have that, I think we should leave it to them (and whatever third-party packages even outside of arches they depend on). For instance, with status quo, we have on javascript.htm:
but now in urls.json:
giving: > _arches_utils_generate_arches_url_ts__WEBPACK_IMPORTED_MODULE_11__["default"]('reorder_cards')
'/reorder_cards' Which will cause a redirect (or not) depending on the value of Can we avoid a behavior change here? |
||
|
||
def interpolate_route(pattern): | ||
if isinstance(pattern, RoutePattern): | ||
return re.sub(r"<(?:[^:]+:)?([^>]+)>", r"{\1}", pattern._route) | ||
elif isinstance(pattern, RegexPattern): | ||
regex = pattern._regex.lstrip("^").rstrip("$") | ||
|
||
# Replace named capture groups (e.g., (?P<param>)) with {param} | ||
regex = re.sub(r"\(\?P<(\w+)>[^)]+\)", r"{\1}", regex) | ||
|
||
# Remove non-capturing groups (e.g., (?:...)) | ||
regex = re.sub(r"\(\?:[^\)]+\)", "", regex) | ||
|
||
# Remove character sets (e.g., [0-9]) | ||
regex = re.sub(r"\[[^\]]+\]", "", regex) | ||
|
||
# Remove backslashes (used to escape special characters in regex) | ||
regex = regex.replace("\\", "") | ||
|
||
# Remove regex-specific special characters (^, $, +, *, ?, (), etc.) | ||
regex = re.sub(r"[\^\$\+\*\?\(\)]", "", regex) | ||
|
||
return regex.strip("/") | ||
|
||
for pattern in patterns: | ||
if isinstance(pattern, URLPattern): | ||
if pattern.name: | ||
result[f"{namespace}{pattern.name}"] = "/" + join_paths( | ||
prefix, interpolate_route(pattern.pattern) | ||
) | ||
elif isinstance(pattern, URLResolver): | ||
current_namespace = namespace + ( | ||
f":{pattern.namespace}:" if pattern.namespace else "" | ||
) | ||
|
||
if isinstance( | ||
pattern.pattern, LocalePrefixPattern | ||
): # handles i18n_patterns | ||
new_prefix = join_paths(prefix, "{language_code}") | ||
else: | ||
new_prefix = join_paths(prefix, interpolate_route(pattern.pattern)) | ||
|
||
generate_human_readable_urls( | ||
pattern.url_patterns, new_prefix, current_namespace, result | ||
) | ||
return result | ||
|
||
resolver = get_resolver() | ||
human_readable_urls = generate_human_readable_urls(resolver.url_patterns) | ||
|
||
# manual additions | ||
human_readable_urls["static_url"] = settings.STATIC_URL | ||
human_readable_urls["media_url"] = settings.MEDIA_URL | ||
|
||
destination_path = os.path.realpath( | ||
os.path.join(_get_base_path(), "..", "frontend_configuration", "urls.json") | ||
) | ||
|
||
with open(destination_path, "w") as file: | ||
json.dump( | ||
{ | ||
"_comment": "This is a generated file. Do not edit directly.", | ||
**{ | ||
url_name: human_readable_urls[url_name] | ||
for url_name in sorted(human_readable_urls) | ||
}, | ||
}, | ||
file, | ||
indent=4, | ||
) | ||
|
||
|
||
def _generate_webpack_configuration(): | ||
app_root_path = os.path.realpath(settings.APP_ROOT) | ||
root_dir_path = os.path.realpath(settings.ROOT_DIR) | ||
|
||
arches_app_names = list_arches_app_names() | ||
arches_app_paths = list_arches_app_paths() | ||
|
||
destination_path = os.path.realpath( | ||
os.path.join( | ||
_get_base_path(), "..", "frontend_configuration", "webpack-metadata.json" | ||
) | ||
) | ||
|
||
with open(destination_path, "w") as file: | ||
json.dump( | ||
{ | ||
"_comment": "This is a generated file. Do not edit directly.", | ||
"APP_ROOT": app_root_path, | ||
"ARCHES_APPLICATIONS": arches_app_names, | ||
"ARCHES_APPLICATIONS_PATHS": dict( | ||
zip(arches_app_names, arches_app_paths, strict=True) | ||
), | ||
"SITE_PACKAGES_DIRECTORY": site.getsitepackages()[0], | ||
"PUBLIC_SERVER_ADDRESS": settings.PUBLIC_SERVER_ADDRESS, | ||
"ROOT_DIR": root_dir_path, | ||
"STATIC_URL": settings.STATIC_URL, | ||
"WEBPACK_DEVELOPMENT_SERVER_PORT": settings.WEBPACK_DEVELOPMENT_SERVER_PORT, | ||
}, | ||
file, | ||
indent=4, | ||
) | ||
|
||
|
||
def _generate_tsconfig_paths(): | ||
base_path = _get_base_path() | ||
root_dir_path = os.path.realpath(settings.ROOT_DIR) | ||
|
||
path_lookup = dict( | ||
zip(list_arches_app_names(), list_arches_app_paths(), strict=True) | ||
) | ||
|
||
tsconfig_paths_data = { | ||
"_comment": "This is a generated file. Do not edit directly.", | ||
"compilerOptions": { | ||
"paths": { | ||
"@/arches/*": [ | ||
os.path.join( | ||
".", | ||
os.path.relpath( | ||
root_dir_path, | ||
os.path.join(base_path, ".."), | ||
), | ||
"app", | ||
"src", | ||
"arches", | ||
"*", | ||
) | ||
], | ||
**{ | ||
os.path.join("@", path_name, "*"): [ | ||
os.path.join( | ||
".", | ||
os.path.relpath(path, os.path.join(base_path, "..")), | ||
"src", | ||
path_name, | ||
"*", | ||
) | ||
] | ||
for path_name, path in path_lookup.items() | ||
}, | ||
"*": ["./node_modules/*"], | ||
} | ||
}, | ||
} | ||
|
||
destination_path = os.path.realpath( | ||
os.path.join(base_path, "..", "frontend_configuration", "tsconfig-paths.json") | ||
) | ||
|
||
with open(destination_path, "w") as file: | ||
json.dump(tsconfig_paths_data, file, indent=4) | ||
|
||
|
||
def _get_base_path(): | ||
return ( | ||
os.path.realpath(settings.ROOT_DIR) | ||
if settings.APP_NAME == "Arches" | ||
else os.path.realpath(settings.APP_ROOT) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this more since yesterday, this might be alright after all, given the current pattern of using the i18n pattern routes from Django. On language switch, reload the page, see the updated /lang/... route. Hash out a different pattern if/when we need it.