diff --git a/.gitignore b/.gitignore index 56854b48cb4..227652838cc 100644 --- a/.gitignore +++ b/.gitignore @@ -43,5 +43,4 @@ pip-wheel-metadata webpack-stats.json .DS_STORE CACHE -.tsconfig-paths.json -.frontend-configuration-settings.json +frontend_configuration diff --git a/arches/app/media/js/utils/create-vue-application.js b/arches/app/media/js/utils/create-vue-application.js index 2f1620adc48..21bd78507ac 100644 --- a/arches/app/media/js/utils/create-vue-application.js +++ b/arches/app/media/js/utils/create-vue-application.js @@ -10,8 +10,9 @@ import Tooltip from 'primevue/tooltip'; import { createApp } from 'vue'; import { createGettext } from "vue3-gettext"; -import arches from 'arches'; import { DEFAULT_THEME } from "@/arches/themes/default.ts"; +import generateArchesURL from '@/arches/utils/generate-arches-url.ts'; + export default async function createVueApplication(vueComponent, themeConfiguration) { /** @@ -27,7 +28,8 @@ export default async function createVueApplication(vueComponent, themeConfigurat * TODO: cbyrd #10501 - we should add an event listener that will re-fetch i18n data * and rebuild the app when a specific event is fired from the LanguageSwitcher component. **/ - return fetch(arches.urls.api_get_frontend_i18n_data).then(function(resp) { + + return fetch(generateArchesURL("get_frontend_i18n_data")).then(function(resp) { if (!resp.ok) { throw new Error(resp.statusText); } diff --git a/arches/app/src/arches/utils/generate-arches-url.test.ts b/arches/app/src/arches/utils/generate-arches-url.test.ts new file mode 100644 index 00000000000..68fab4bf889 --- /dev/null +++ b/arches/app/src/arches/utils/generate-arches-url.test.ts @@ -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 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"); + }); +}); diff --git a/arches/app/src/arches/utils/generate-arches-url.ts b/arches/app/src/arches/utils/generate-arches-url.ts new file mode 100644 index 00000000000..148272868bc --- /dev/null +++ b/arches/app/src/arches/utils/generate-arches-url.ts @@ -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; +} diff --git a/arches/app/templates/base-root.htm b/arches/app/templates/base-root.htm index 0413379c6b1..d85e6a13f40 100644 --- a/arches/app/templates/base-root.htm +++ b/arches/app/templates/base-root.htm @@ -22,7 +22,7 @@ - + {% block head %} @@ -77,7 +77,6 @@ {% endblock pre_require_js %} {% block arches_modules %} - {% include "arches_urls.htm" %} {% endblock arches_modules %} {% if main_script %} diff --git a/arches/app/templates/base.htm b/arches/app/templates/base.htm index 990736c20e3..d6c2651623f 100644 --- a/arches/app/templates/base.htm +++ b/arches/app/templates/base.htm @@ -17,16 +17,9 @@ --> {% extends "base-root.htm" %} -{% load static %} {% load i18n %} -{% load webpack_static from webpack_loader %} {% load render_bundle from webpack_loader %} - - - - - {% if use_livereload %} {% endif %} @@ -61,5 +54,4 @@ {% block arches_modules %} {% include 'javascript.htm' %} -{% endblock arches_modules %} - +{% endblock arches_modules %} \ No newline at end of file diff --git a/arches/app/utils/frontend_configuration_utils.py b/arches/app/utils/frontend_configuration_utils.py new file mode 100644 index 00000000000..810dd90a8e1 --- /dev/null +++ b/arches/app/utils/frontend_configuration_utils.py @@ -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={}): + def join_paths(*args): + return "/".join(filter(None, (arg.strip("/") for arg in args))) + + 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)) 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) + ) diff --git a/arches/app/utils/index_database.py b/arches/app/utils/index_database.py index 81fdae83450..69a6616d1cc 100644 --- a/arches/app/utils/index_database.py +++ b/arches/app/utils/index_database.py @@ -5,9 +5,6 @@ import uuid import pyprind import sys -import django - -django.setup() from datetime import datetime from django.db import connection, connections diff --git a/arches/apps.py b/arches/apps.py index 7f7a7ce81e2..277ae579689 100644 --- a/arches/apps.py +++ b/arches/apps.py @@ -11,7 +11,9 @@ from semantic_version import SimpleSpec, Version from arches import __version__ -from arches.settings_utils import generate_frontend_configuration +from arches.app.utils.frontend_configuration_utils import ( + generate_frontend_configuration, +) class ArchesAppConfig(AppConfig): diff --git a/arches/install/arches-templates/.gitignore b/arches/install/arches-templates/.gitignore index c90cc0d864c..89a6e0398d5 100644 --- a/arches/install/arches-templates/.gitignore +++ b/arches/install/arches-templates/.gitignore @@ -15,5 +15,4 @@ webpack-stats.json *.egg-info .DS_STORE CACHE -.tsconfig-paths.json -.frontend-configuration-settings.json +frontend_configuration diff --git a/arches/install/arches-templates/project_name/apps.py-tpl b/arches/install/arches-templates/project_name/apps.py-tpl index 6d933396a45..6588c66c61d 100644 --- a/arches/install/arches-templates/project_name/apps.py-tpl +++ b/arches/install/arches-templates/project_name/apps.py-tpl @@ -1,7 +1,7 @@ from django.apps import AppConfig from django.conf import settings -from arches.settings_utils import generate_frontend_configuration +from arches.app.utils.frontend_configuration_utils import generate_frontend_configuration class {{ project_name_title_case }}Config(AppConfig): diff --git a/arches/install/arches-templates/tsconfig.json b/arches/install/arches-templates/tsconfig.json index c223f2c6ad0..67e112fd782 100644 --- a/arches/install/arches-templates/tsconfig.json +++ b/arches/install/arches-templates/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./.tsconfig-paths.json", + "extends": "./frontend_configuration/tsconfig-paths.json", "compilerOptions": { "target": "ESNext", "module": "ESNext", diff --git a/arches/install/arches-templates/vitest.config.mts b/arches/install/arches-templates/vitest.config.mts index 4dda8725d1a..dd171bcf69b 100644 --- a/arches/install/arches-templates/vitest.config.mts +++ b/arches/install/arches-templates/vitest.config.mts @@ -19,7 +19,7 @@ function generateConfig(): Promise { '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', ]; - const rawData = fs.readFileSync(path.join(__dirname, '.frontend-configuration-settings.json'), 'utf-8'); + const rawData = fs.readFileSync(path.join(__dirname, 'frontend_configuration', 'webpack-metadata.json'), 'utf-8'); const parsedData = JSON.parse(rawData); const alias: { [key: string]: string } = { diff --git a/arches/install/arches-templates/webpack/webpack.common.js b/arches/install/arches-templates/webpack/webpack.common.js index cb62dd40ed7..79886fcbc22 100644 --- a/arches/install/arches-templates/webpack/webpack.common.js +++ b/arches/install/arches-templates/webpack/webpack.common.js @@ -14,13 +14,13 @@ const { buildFilepathLookup } = require('./webpack-utils/build-filepath-lookup') module.exports = () => { return new Promise((resolve, _reject) => { - // BEGIN get data from `.frontend-configuration-settings.json` + // BEGIN get data from `webpack-metadata.json` - const rawData = fs.readFileSync(Path.join(__dirname, "..", '.frontend-configuration-settings.json'), 'utf-8'); + const rawData = fs.readFileSync(Path.join(__dirname, "..", "frontend_configuration", 'webpack-metadata.json'), 'utf-8'); const parsedData = JSON.parse(rawData); - console.log('Data imported from .frontend-configuration-settings.json:', parsedData); - + console.log('Data imported from webpack-metadata.json:', parsedData); + global.APP_ROOT = parsedData['APP_ROOT']; global.ARCHES_APPLICATIONS = parsedData['ARCHES_APPLICATIONS']; global.ARCHES_APPLICATIONS_PATHS = parsedData['ARCHES_APPLICATIONS_PATHS']; @@ -30,7 +30,7 @@ module.exports = () => { global.PUBLIC_SERVER_ADDRESS = parsedData['PUBLIC_SERVER_ADDRESS']; global.WEBPACK_DEVELOPMENT_SERVER_PORT = parsedData['WEBPACK_DEVELOPMENT_SERVER_PORT']; - // END get data from `.frontend-configuration-settings.json` + // END get data from `webpack-metadata.json` // BEGIN workaround for handling node_modules paths in arches-core vs projects let PROJECT_RELATIVE_NODE_MODULES_PATH; @@ -242,8 +242,8 @@ module.exports = () => { const universalConstants = { APP_ROOT_DIRECTORY: JSON.stringify(APP_ROOT).replace(/\\/g, '/'), - ARCHES_CORE_DIRECTORY: JSON.stringify(ROOT_DIR).replace(/\\/g, '/'), ARCHES_APPLICATIONS: JSON.stringify(ARCHES_APPLICATIONS), + ARCHES_CORE_DIRECTORY: JSON.stringify(ROOT_DIR).replace(/\\/g, '/'), SITE_PACKAGES_DIRECTORY: JSON.stringify(SITE_PACKAGES_DIRECTORY).replace(/\\/g, '/'), }; @@ -279,6 +279,15 @@ module.exports = () => { plugins: [ new CleanWebpackPlugin(), new webpack.DefinePlugin(universalConstants), + new webpack.DefinePlugin({ + ARCHES_URLS: webpack.DefinePlugin.runtimeValue( + () => fs.readFileSync( + Path.resolve(__dirname, PROJECT_RELATIVE_NODE_MODULES_PATH, '..', 'frontend_configuration', 'urls.json'), + 'utf-8' + ), + true // should be re-evaluated on rebuild + ), + }), new webpack.DefinePlugin({ __VUE_OPTIONS_API__: 'true', __VUE_PROD_DEVTOOLS__: 'false', @@ -290,9 +299,9 @@ module.exports = () => { jquery: Path.resolve(__dirname, PROJECT_RELATIVE_NODE_MODULES_PATH, 'jquery', 'dist', 'jquery.min') }), new MiniCssExtractPlugin(), - new BundleTracker({ + new BundleTracker({ path: Path.resolve(__dirname), - filename: 'webpack-stats.json' + filename: 'webpack-stats.json' }), new VueLoaderPlugin(), ], @@ -351,6 +360,7 @@ module.exports = () => { { test: /\.css$/, exclude: [ + /node_modules/, Path.resolve(__dirname, APP_ROOT, 'media', 'css'), Path.resolve(__dirname, ROOT_DIR, 'app', 'media', 'css'), ...archesApplicationsCSSFilepaths @@ -450,7 +460,7 @@ module.exports = () => { if (serverAddress.charAt(serverAddress.length - 1) === '/') { serverAddress = serverAddress.slice(0, -1) } - + resp = await fetch(serverAddress + templatePath); if (resp.status === 500) { diff --git a/arches/install/arches-templates/webpack/webpack.config.dev.js b/arches/install/arches-templates/webpack/webpack.config.dev.js index 2e3da3f6c00..50b98bc30df 100644 --- a/arches/install/arches-templates/webpack/webpack.config.dev.js +++ b/arches/install/arches-templates/webpack/webpack.config.dev.js @@ -43,7 +43,16 @@ module.exports = () => { }), new StylelintPlugin({ files: Path.join('src', '**/*.s?(a|c)ss'), - }) + }), + { + apply: (compiler) => { + compiler.hooks.afterCompile.tap('WatchArchesUrlsPlugin', (compilation) => { + compilation.fileDependencies.add( + Path.resolve(__dirname, APP_ROOT, '..', 'frontend_configuration', 'urls.json') + ); + }); + }, + }, ], })); }); diff --git a/arches/management/commands/updateproject.py b/arches/management/commands/updateproject.py index eadff5409d3..1ac119043f7 100644 --- a/arches/management/commands/updateproject.py +++ b/arches/management/commands/updateproject.py @@ -1,5 +1,10 @@ +import os +import shutil + from django.core.management.base import BaseCommand +from arches.app.models.system_settings import settings + class Command(BaseCommand): # pragma: no cover """ @@ -18,4 +23,45 @@ def handle(self, *args, **options): self.stdout.write("Operation aborted.") def update_to_v8(self): - pass + # Removes unnecessary files + self.stdout.write("Removing unnecessary files...") + + for file_to_delete in [ + ".frontend-configuration-settings.json", + ".tsconfig-paths.json", + ]: + if os.path.exists(os.path.join(settings.APP_ROOT, "..", file_to_delete)): + self.stdout.write("Deleting {}".format(file_to_delete)) + os.remove(os.path.join(settings.APP_ROOT, "..", file_to_delete)) + + self.stdout.write("Done!") + + # Updates webpack + self.stdout.write("Creating updated webpack directory...") + + if os.path.isdir(os.path.join(settings.APP_ROOT, "..", "webpack")): + shutil.rmtree( + os.path.join(settings.APP_ROOT, "..", "webpack"), ignore_errors=True + ) + + shutil.copytree( + os.path.join(settings.ROOT_DIR, "install", "arches-templates", "webpack"), + os.path.join(settings.APP_ROOT, "..", "webpack"), + ) + + self.stdout.write("Done!") + + # Replaces tsconfig.json + self.stdout.write("Updating tsconfig.json...") + + if os.path.exists(os.path.join(settings.APP_ROOT, "..", "tsconfig.json")): + os.remove(os.path.join(settings.APP_ROOT, "..", "tsconfig.json")) + + shutil.copy2( + os.path.join( + settings.ROOT_DIR, "install", "arches-templates", "tsconfig.json" + ), + os.path.join(settings.APP_ROOT, "..", "tsconfig.json"), + ) + + self.stdout.write("Done!") diff --git a/arches/settings_utils.py b/arches/settings_utils.py index d8ceb934996..9ad59295116 100644 --- a/arches/settings_utils.py +++ b/arches/settings_utils.py @@ -1,11 +1,7 @@ -import json import os -import site import sys -from contextlib import contextmanager from django.apps import apps -from django.conf import settings from django.contrib.staticfiles.finders import AppDirectoriesFinder @@ -134,90 +130,3 @@ def build_templates_config( # Ensures error message is shown if error encountered in webpack build sys.stdout.write(str(e)) raise e - - -def generate_frontend_configuration(): - try: - 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() - path_lookup = dict(zip(arches_app_names, arches_app_paths, strict=True)) - - frontend_configuration_settings_data = { - "_comment": "This is a generated file. Do not edit directly.", - "APP_ROOT": app_root_path, - "ARCHES_APPLICATIONS": arches_app_names, - "ARCHES_APPLICATIONS_PATHS": path_lookup, - "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, - } - - if settings.APP_NAME == "Arches": - base_path = root_dir_path - else: - base_path = app_root_path - - frontend_configuration_settings_path = os.path.realpath( - os.path.join(base_path, "..", ".frontend-configuration-settings.json") - ) - sys.stdout.write( - f"Writing frontend configuration to: {frontend_configuration_settings_path} \n" - ) - - with open( - frontend_configuration_settings_path, - "w", - ) as file: - json.dump(frontend_configuration_settings_data, file, indent=4) - - 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/*"], - } - }, - } - - tsconfig_path = os.path.realpath( - os.path.join(base_path, "..", ".tsconfig-paths.json") - ) - sys.stdout.write(f"Writing tsconfig path data to: {tsconfig_path} \n") - - with open(tsconfig_path, "w") as file: - json.dump(tsconfig_paths_data, file, indent=4) - - except Exception as e: - # Ensures error message is shown if error encountered - sys.stderr.write(str(e)) - raise e diff --git a/releases/8.0.0.md b/releases/8.0.0.md index d8e203ec284..46c52b5af18 100644 --- a/releases/8.0.0.md +++ b/releases/8.0.0.md @@ -21,6 +21,7 @@ Arches 8.0.0 Release Notes - Improve handling of longer model names [#11317](https://github.com/archesproject/arches/issues/11317) - Support more expressive plugin URLs [#11320](https://github.com/archesproject/arches/issues/11320) - Concepts API no longer responds with empty body for error conditions [#11519](https://github.com/archesproject/arches/issues/11519) +- Generates static `urls.json` file for frontend consumption [#11567](https://github.com/archesproject/arches/issues/11567) ### Dependency changes ``` @@ -57,6 +58,8 @@ JavaScript: - `ensure_userprofile_exists()` was removed from the `Tile` model. +- The `arches` object is no longer available in frontend components that exist outside of defined Arches patterns. If your project contains Vue components that are mounted on templates that inherit directly from `base-root.htm`, they will need to be updated to use `generateArchesURL` and the settings API instead. + ### Upgrading Arches 1. You must be upgraded to at least version before proceeding. If you are on an earlier version, please refer to the upgrade process in the []() @@ -69,9 +72,9 @@ JavaScript: 1. In settings.py, add the following key to `DATABASES` to [improve indexing performance](https://github.com/archesproject/arches/issues/11382): ``` - "OPTIONS": { - "options": "-c cursor_tuple_fraction=1", - }, + "OPTIONS": { + "options": "-c cursor_tuple_fraction=1", + }, ``` 1. Update your frontend dependencies: @@ -80,21 +83,36 @@ JavaScript: npm install ``` -2. Within your project, with your Python 3 virtual environment activated: +1. Update `.gitignore`: + 1. Remove `.tsconfig-paths.json` and `.frontend-configuration-settings.json` + 2. Add `frontend_configuration` + +1. Replace the `generate_frontend_configuration` import statement in `apps.py`: + ``` + from arches.settings_utils import generate_frontend_configuration + ``` + + Should be updated to: + + ``` + from arches.app.utils.frontend_configuration_utils import generate_frontend_configuration + ``` + +1. Within your project, with your Python 3 virtual environment activated: ``` python manage.py migrate ``` -3. Create editable_future_graphs for your Resource Models using the command `python manage.py graph create_editable_future_graphs`. This will publish new versions of each Graph. +1. Create editable_future_graphs for your Resource Models using the command `python manage.py graph create_editable_future_graphs`. This will publish new versions of each Graph. -4. Update your Graph publications and Resource instances to point to the newly published Graphs by running `python manage.py graph publish --update -ui` +1. Update your Graph publications and Resource instances to point to the newly published Graphs by running `python manage.py graph publish --update -ui` -5. Within your project with your Python 3 virtual environment activated: +1. Within your project with your Python 3 virtual environment activated: ``` python manage.py es reindex_database ``` -6. Run `npm start` or `npm run build_development` to rebuild your static asset bundle: +1. Run `npm start` or `npm run build_development` to rebuild your static asset bundle: - If running your project in development: - `npm start` will build the frontend of the application and then start a webpack development server - `npm run build_development` will build a development bundle for the frontend assests of the application -- this should complete in less than 2 minutes diff --git a/tsconfig.json b/tsconfig.json index c223f2c6ad0..67e112fd782 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./.tsconfig-paths.json", + "extends": "./frontend_configuration/tsconfig-paths.json", "compilerOptions": { "target": "ESNext", "module": "ESNext", diff --git a/vitest.config.mts b/vitest.config.mts index 4cf706fa8c1..c1e36b0fe1c 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -19,7 +19,7 @@ function generateConfig(): Promise { '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', ]; - const rawData = fs.readFileSync(path.join(__dirname, '.frontend-configuration-settings.json'), 'utf-8'); + const rawData = fs.readFileSync(path.join(__dirname, 'frontend_configuration', 'webpack-metadata.json'), 'utf-8'); const parsedData = JSON.parse(rawData); const alias: { [key: string]: string } = { diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index cb62dd40ed7..79886fcbc22 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -14,13 +14,13 @@ const { buildFilepathLookup } = require('./webpack-utils/build-filepath-lookup') module.exports = () => { return new Promise((resolve, _reject) => { - // BEGIN get data from `.frontend-configuration-settings.json` + // BEGIN get data from `webpack-metadata.json` - const rawData = fs.readFileSync(Path.join(__dirname, "..", '.frontend-configuration-settings.json'), 'utf-8'); + const rawData = fs.readFileSync(Path.join(__dirname, "..", "frontend_configuration", 'webpack-metadata.json'), 'utf-8'); const parsedData = JSON.parse(rawData); - console.log('Data imported from .frontend-configuration-settings.json:', parsedData); - + console.log('Data imported from webpack-metadata.json:', parsedData); + global.APP_ROOT = parsedData['APP_ROOT']; global.ARCHES_APPLICATIONS = parsedData['ARCHES_APPLICATIONS']; global.ARCHES_APPLICATIONS_PATHS = parsedData['ARCHES_APPLICATIONS_PATHS']; @@ -30,7 +30,7 @@ module.exports = () => { global.PUBLIC_SERVER_ADDRESS = parsedData['PUBLIC_SERVER_ADDRESS']; global.WEBPACK_DEVELOPMENT_SERVER_PORT = parsedData['WEBPACK_DEVELOPMENT_SERVER_PORT']; - // END get data from `.frontend-configuration-settings.json` + // END get data from `webpack-metadata.json` // BEGIN workaround for handling node_modules paths in arches-core vs projects let PROJECT_RELATIVE_NODE_MODULES_PATH; @@ -242,8 +242,8 @@ module.exports = () => { const universalConstants = { APP_ROOT_DIRECTORY: JSON.stringify(APP_ROOT).replace(/\\/g, '/'), - ARCHES_CORE_DIRECTORY: JSON.stringify(ROOT_DIR).replace(/\\/g, '/'), ARCHES_APPLICATIONS: JSON.stringify(ARCHES_APPLICATIONS), + ARCHES_CORE_DIRECTORY: JSON.stringify(ROOT_DIR).replace(/\\/g, '/'), SITE_PACKAGES_DIRECTORY: JSON.stringify(SITE_PACKAGES_DIRECTORY).replace(/\\/g, '/'), }; @@ -279,6 +279,15 @@ module.exports = () => { plugins: [ new CleanWebpackPlugin(), new webpack.DefinePlugin(universalConstants), + new webpack.DefinePlugin({ + ARCHES_URLS: webpack.DefinePlugin.runtimeValue( + () => fs.readFileSync( + Path.resolve(__dirname, PROJECT_RELATIVE_NODE_MODULES_PATH, '..', 'frontend_configuration', 'urls.json'), + 'utf-8' + ), + true // should be re-evaluated on rebuild + ), + }), new webpack.DefinePlugin({ __VUE_OPTIONS_API__: 'true', __VUE_PROD_DEVTOOLS__: 'false', @@ -290,9 +299,9 @@ module.exports = () => { jquery: Path.resolve(__dirname, PROJECT_RELATIVE_NODE_MODULES_PATH, 'jquery', 'dist', 'jquery.min') }), new MiniCssExtractPlugin(), - new BundleTracker({ + new BundleTracker({ path: Path.resolve(__dirname), - filename: 'webpack-stats.json' + filename: 'webpack-stats.json' }), new VueLoaderPlugin(), ], @@ -351,6 +360,7 @@ module.exports = () => { { test: /\.css$/, exclude: [ + /node_modules/, Path.resolve(__dirname, APP_ROOT, 'media', 'css'), Path.resolve(__dirname, ROOT_DIR, 'app', 'media', 'css'), ...archesApplicationsCSSFilepaths @@ -450,7 +460,7 @@ module.exports = () => { if (serverAddress.charAt(serverAddress.length - 1) === '/') { serverAddress = serverAddress.slice(0, -1) } - + resp = await fetch(serverAddress + templatePath); if (resp.status === 500) { diff --git a/webpack/webpack.config.dev.js b/webpack/webpack.config.dev.js index 2e3da3f6c00..50b98bc30df 100644 --- a/webpack/webpack.config.dev.js +++ b/webpack/webpack.config.dev.js @@ -43,7 +43,16 @@ module.exports = () => { }), new StylelintPlugin({ files: Path.join('src', '**/*.s?(a|c)ss'), - }) + }), + { + apply: (compiler) => { + compiler.hooks.afterCompile.tap('WatchArchesUrlsPlugin', (compilation) => { + compilation.fileDependencies.add( + Path.resolve(__dirname, APP_ROOT, '..', 'frontend_configuration', 'urls.json') + ); + }); + }, + }, ], })); });