diff --git a/.env.development b/.env.development new file mode 100644 index 000000000000..3461637c76c0 --- /dev/null +++ b/.env.development @@ -0,0 +1,4 @@ +FRONTEND_HMR=true +FRONTEND_HOSTNAME=http://localhost:8010 +DJANGO_HOSTNAME=http://localhost:8080 + diff --git a/Makefile b/Makefile index b279153005eb..487ea2f520e9 100644 --- a/Makefile +++ b/Makefile @@ -14,21 +14,37 @@ makemigrations-dev: shell-dev: DJANGO_DB=sqlite LOG_DIR=tmp DEBUG=true LOG_LEVEL=DEBUG DJANGO_SETTINGS_MODULE=core.settings.label_studio python label_studio/manage.py shell_plus +env-dev-setup: + if [ ! -f .env ]; then \ + cp .env.development .env; \ + fi + +docker-dev-override: + if [ ! -f docker-compose.override.yml ]; then \ + cp docker-compose.override.example.yml docker-compose.override.yml; \ + fi + +# Configure Django dev server with Hot Module Replacement in docker +docker-dev-setup: env-dev-setup docker-dev-override + docker-run-dev: docker-compose up --build docker-migrate-dev: docker-compose run app python3 /label-studio/label_studio/manage.py migrate - # Install modules -frontend-setup: +frontend-install: cd web && yarn install --frozen-lockfile; -# Keep it here for potential rollback -## Fetch DM and LSF -#frontend-fetch: -# cd label_studio/frontend && yarn run download:all; +# Alias for backward compatibility +frontend-setup: frontend-install + +# Run frontend dev server in Hot Module Replacement mode +# For more information on HMR, see the "Environment Configuration" section in: +# web/README.md +frontend-dev: + cd web && yarn run dev # Build frontend continuously on files changes frontend-watch: diff --git a/README.md b/README.md index 0a7b89078009..ad92406abdfe 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ You can deploy Label Studio with one click in Heroku, Microsoft Azure, or Google #### Apply frontend changes -For information about updating the frontend, see [label-studio/web/README.md](https://github.com/HumanSignal/label-studio/blob/develop/web/README.md#usage-instructions). +For information about updating the frontend, see [label-studio/web/README.md](https://github.com/HumanSignal/label-studio/blob/develop/web/README.md#installation-instructions). #### Install dependencies on Windows diff --git a/docker-compose.override.example.yml b/docker-compose.override.example.yml new file mode 100644 index 000000000000..ac7e1d427881 --- /dev/null +++ b/docker-compose.override.example.yml @@ -0,0 +1,5 @@ +version: "3.9" +services: + app: + env_file: + - .env diff --git a/label_studio/core/settings/base.py b/label_studio/core/settings/base.py index b59b46d2ffe2..98e23e9787c2 100644 --- a/label_studio/core/settings/base.py +++ b/label_studio/core/settings/base.py @@ -1,5 +1,5 @@ -"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license. -""" +"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.""" + """ Django Base settings for Label Studio. @@ -104,6 +104,9 @@ if FORCE_SCRIPT_NAME: logger.info('=> Django URL prefix is set to: %s', FORCE_SCRIPT_NAME) +FRONTEND_HMR = get_bool_env('FRONTEND_HMR', False) +FRONTEND_HOSTNAME = get_env('FRONTEND_HOSTNAME', 'http://localhost:8010' if FRONTEND_HMR else HOSTNAME) + DOMAIN_FROM_REQUEST = get_bool_env('DOMAIN_FROM_REQUEST', False) if DOMAIN_FROM_REQUEST: diff --git a/label_studio/templates/base.html b/label_studio/templates/base.html index b8b581c28948..1101cef6615b 100644 --- a/label_studio/templates/base.html +++ b/label_studio/templates/base.html @@ -24,7 +24,7 @@ - + {% block app-scripts %} {% endblock %} @@ -112,13 +112,13 @@ {% block app-js %} - + {% comment %} NOTE: purposely setting this to not cache using backend commit as we do not intend this to change frequently. If for any reason we need to invalidate the cache, we can do so by changing the version number. {% endcomment %} - - + + {% endblock %} {% block bottomjs %} diff --git a/label_studio/templates/simple.html b/label_studio/templates/simple.html index 266a1764aaff..3b050914bdc0 100644 --- a/label_studio/templates/simple.html +++ b/label_studio/templates/simple.html @@ -16,7 +16,7 @@ - + {% block head %} {% endblock %} diff --git a/web/README.md b/web/README.md index ec452a12f495..2dd5eeb98284 100644 --- a/web/README.md +++ b/web/README.md @@ -18,18 +18,35 @@ Datamanager is an advanced tool specifically for data exploration within Label S 1 - **Dependencies Installation:** - Execute `yarn install --frozen-lockfile` to install all necessary dependencies. -2 - **Environment Configuration:** +2 - **Environment Configuration (Optional for HMR):** +- If you want to enable Hot Module Replacement (HMR), create an `.env` file in the root Label Studio directory. +- Add the following configuration: + - `FRONTEND_HMR=true`: Enables Hot Module Replacement in Django. + +Optional configurations (defaults should work for most setups): + - `FRONTEND_HOSTNAME`: HMR server address (default: http://localhost:8010). + - `DJANGO_HOSTNAME`: Django server address (default: http://localhost:8080). + +If using Docker Compose with HMR: +- Update the `env_file: .env` directive in `docker-compose.override.yml` under the app service. +- Rerun the app or docker compose service from the project root for changes to take effect. + +To start the development server with HMR: +- From the `web` directory: Run `yarn dev` +- Or from the project root: Run `make frontend-dev` + #### Custom Configuration for DataManager: - If you need to customize the configuration specifically for DataManager, follow these steps: - Duplicate the `.env.example` file located in the DataManager directory and rename the copy to `.env`. - Make your desired changes in this new `.env` file. The key configurations to consider are: - - `NX_API_GATEWAY`: Set this to your API root. For example, `https://localhost:8080/api/dm`. + - `NX_API_GATEWAY`: Set this to your API root. For example, `http://localhost:8080/api/dm`. - `LS_ACCESS_TOKEN`: This is the access token for Label Studio, which can be obtained from your Label Studio account page. - This process allows you to have a customized configuration for DataManager, separate from the default settings in the .env.local files. ## Usage Instructions ### Key Development and Build Commands - **Label Studio App:** + - `yarn ls:dev`: Build the main Label Studio app with Hot Module Reload for development. - `yarn ls:watch`: Build the main Label Studio app continuously for development. - `yarn ls:e2e`: Run end-to-end tests for the Label Studio app. - `yarn ls:unit`: Run unit tests for the Label Studio app. diff --git a/web/apps/labelstudio/src/app/App.jsx b/web/apps/labelstudio/src/app/App.jsx index 2bb32853ce7e..f51170e33279 100644 --- a/web/apps/labelstudio/src/app/App.jsx +++ b/web/apps/labelstudio/src/app/App.jsx @@ -79,3 +79,7 @@ const root = document.querySelector(".app-wrapper"); const content = document.querySelector("#main-content"); render(, root); + +if (module?.hot) { + module.hot.accept(); // Enable HMR for React components +} diff --git a/web/package.json b/web/package.json index 4cca687c2c69..6972b6558cdb 100644 --- a/web/package.json +++ b/web/package.json @@ -3,16 +3,17 @@ "version": "0.0.0", "license": "MIT", "scripts": { - "ui:serve": "nx storybook storybook", + "ui:serve": "FRONTEND_HOSTNAME=http://localhost:3000 nx storybook storybook", "ui:test:component": "nx run storybook:test-component", "lint": "biome check --apply .", "lint-scss": "yarn stylelint '**/*.scss' --fix", + "ls:dev": "nx run labelstudio:serve:development", "ls:watch": "nx run labelstudio:build:development --watch", "ls:build": "nx run labelstudio:build:production", "ls:unit": "nx run labelstudio:unit", "ls:e2e": "nx run labelstudio-e2e:e2e", "lsf:watch": "nx run editor:build:development --watch", - "lsf:serve": "MODE=standalone nx run editor:serve:development", + "lsf:serve": "FRONTEND_HOSTNAME=http://localhost:3000 MODE=standalone nx run editor:serve:development", "lsf:e2e": "cd libs/editor/tests/e2e && yarn test", "lsf:integration": "nx run editor:integration", "lsf:integration:watch": "nx run editor:integration --watch", @@ -23,6 +24,7 @@ "version:libs": "nx run-many --target=version", "docs": "nx run-many --target=docs", "watch": "NODE_ENV=development BUILD_NO_SERVER=true yarn ls:watch", + "dev": "NODE_ENV=development BUILD_NO_SERVER=true yarn ls:dev", "test:e2e": "yarn ls:e2e && yarn lsf:e2e", "test:integration": "yarn lsf:integration", "test:unit": "nx run-many --target=unit", diff --git a/web/webpack.config.js b/web/webpack.config.js index de120dad24eb..9a812e460277 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -4,7 +4,10 @@ const { composePlugins, withNx } = require("@nx/webpack"); const { withReact } = require("@nx/react"); const { merge } = require("webpack-merge"); -require("dotenv").config(); +require("dotenv").config({ + // resolve the .env file in the root of the project ../ + path: path.resolve(__dirname, "../.env"), +}); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const { EnvironmentPlugin, DefinePlugin, ProgressPlugin, optimize } = require("webpack"); @@ -14,8 +17,12 @@ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const RELEASE = require("./release").getReleaseName(); const css_prefix = "lsf-"; - const mode = process.env.BUILD_MODULE ? "production" : process.env.NODE_ENV || "development"; +const isDevelopment = mode !== "production"; +const devtool = process.env.NODE_ENV === "production" ? "source-map" : "cheap-module-source-map"; +const FRONTEND_HOSTNAME = process.env.FRONTEND_HOSTNAME || "http://localhost:8010"; +const DJANGO_HOSTNAME = process.env.DJANGO_HOSTNAME || "http://localhost:8080"; +const HMR_PORT = +new URL(FRONTEND_HOSTNAME).port; const LOCAL_ENV = { NODE_ENV: mode, @@ -23,20 +30,10 @@ const LOCAL_ENV = { RELEASE_NAME: RELEASE, }; -const devtool = process.env.NODE_ENV === "production" ? "source-map" : "cheap-module-source-map"; - -const isDevelopment = mode !== "production"; -const customDistDir = !!process.env.WORK_DIR; - const BUILD = { NO_MINIMIZE: isDevelopment || !!process.env.BUILD_NO_MINIMIZATION, }; -const dirPrefix = { - js: customDistDir ? "js/" : isDevelopment ? "" : "static/js/", - css: customDistDir ? "css/" : isDevelopment ? "" : "static/css/", -}; - const plugins = [ new MiniCssExtractPlugin(), new DefinePlugin({ @@ -92,10 +89,11 @@ module.exports = composePlugins( import: path.resolve(__dirname, "apps/labelstudio/src/main.tsx"), }, }; + config.output = { ...config.output, uniqueName: "labelstudio", - publicPath: "auto", + publicPath: isDevelopment && FRONTEND_HOSTNAME ? `${FRONTEND_HOSTNAME}/react-app/` : "auto", scriptType: "text/javascript", }; @@ -222,16 +220,48 @@ module.exports = composePlugins( loader: "file-loader", options: { name: "[name].[ext]", - outputPath: dirPrefix.js, // colocate wasm with js }, }, ); + if (isDevelopment) { + config.optimization = { + ...config.optimization, + moduleIds: "named", + }; + } + return merge(config, { devtool, mode, plugins, optimization: optimizer(), + devServer: + process.env.MODE === "standalone" + ? {} + : { + // Port for the Webpack dev server + port: HMR_PORT, + // Enable HMR + hot: true, + // Allow cross-origin requests from Django + headers: { "Access-Control-Allow-Origin": "*" }, + static: { + directory: path.resolve(__dirname, "../label_studio/core/static/"), + publicPath: "/static/", + }, + devMiddleware: { + publicPath: `${FRONTEND_HOSTNAME}/react-app/`, + }, + allowedHosts: "all", // Allow access from Django's server + proxy: [ + { + router: { + "/api": `${DJANGO_HOSTNAME}/api`, // Proxy api requests to Django's server + }, + }, + ], + }, }); }, );