From 03207ea7b684ec1a590e73e251b5ce1f3bb8c58a Mon Sep 17 00:00:00 2001
From: Pive01
Date: Tue, 25 Jun 2024 14:51:27 +0200
Subject: [PATCH 01/35] Created first raw version of react extension
---
template/{ => react}/README.md | 2 +-
template/{ => react}/cookiecutter.json | 0
.../{{cookiecutter.project_slug}}/Makefile | 13 ++-
.../{{cookiecutter.project_slug}}/README.md | 0
.../{{cookiecutter.project_slug}}/setup.cfg | 19 +++-
.../{{cookiecutter.project_slug}}/setup.py | 0
.../{{cookiecutter.project_slug}}/utf8_fail | 28 +++++
.../{{cookiecutter.module_name}}/__init__.py | 0
.../backend/web.py | 0
.../{{cookiecutter.module_name}}/extension.py | 44 ++++++++
.../frontend/.esbuild/esbuild.config.js | 102 ++++++++++++++++++
.../frontend/.esbuild/esbuild.shims.js | 7 ++
.../frontend/.esbuild/index.js | 11 ++
.../frontend/.esbuild/plugins/html/index.js | 93 ++++++++++++++++
.../frontend/__init__.py | 0
.../frontend/package.json | 37 +++++++
.../frontend/public/index.html | 43 ++++++++
.../frontend/public/manifest.json | 25 +++++
.../frontend/public/robots.txt | 3 +
.../frontend/src/CustomRoutes.tsx | 12 +++
.../frontend/src/Dashboard.tsx | 17 +++
.../frontend/src/PageOne.tsx | 16 +++
.../frontend/src/index.css | 13 +++
.../frontend/src/index.html | 21 ++++
.../frontend/src/index.tsx | 19 ++++
.../frontend/src/react-app-env.d.ts | 1 +
.../{{cookiecutter.module_name}}/util.py | 51 +++++++++
.../{{cookiecutter.module_name}}/extension.py | 22 ----
28 files changed, 572 insertions(+), 27 deletions(-)
rename template/{ => react}/README.md (87%)
rename template/{ => react}/cookiecutter.json (100%)
rename template/{ => react}/{{cookiecutter.project_slug}}/Makefile (63%)
rename template/{ => react}/{{cookiecutter.project_slug}}/README.md (100%)
rename template/{ => react}/{{cookiecutter.project_slug}}/setup.cfg (55%)
rename template/{ => react}/{{cookiecutter.project_slug}}/setup.py (100%)
create mode 100644 template/react/{{cookiecutter.project_slug}}/utf8_fail
rename template/{ => react}/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/__init__.py (100%)
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/backend/web.py
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/extension.py
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.config.js
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.shims.js
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/index.js
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/plugins/html/index.js
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/__init__.py
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/package.json
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/public/index.html
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/public/manifest.json
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/public/robots.txt
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/src/CustomRoutes.tsx
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/src/Dashboard.tsx
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/src/PageOne.tsx
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/src/index.css
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/src/index.html
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/src/index.tsx
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/src/react-app-env.d.ts
create mode 100644 template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/util.py
delete mode 100644 template/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/extension.py
diff --git a/template/README.md b/template/react/README.md
similarity index 87%
rename from template/README.md
rename to template/react/README.md
index 0213a57..ae0ce10 100644
--- a/template/README.md
+++ b/template/react/README.md
@@ -4,7 +4,7 @@ Extension Template
This is a [cookiecutter](https://github.com/cookiecutter/cookiecutter) template that is used when you invoke.
```console
-localstack extensions dev new
+localstack extensions dev new --ui
```
It contains a simple python distribution config, and some boilerplate extension code.
diff --git a/template/cookiecutter.json b/template/react/cookiecutter.json
similarity index 100%
rename from template/cookiecutter.json
rename to template/react/cookiecutter.json
diff --git a/template/{{cookiecutter.project_slug}}/Makefile b/template/react/{{cookiecutter.project_slug}}/Makefile
similarity index 63%
rename from template/{{cookiecutter.project_slug}}/Makefile
rename to template/react/{{cookiecutter.project_slug}}/Makefile
index 0f12f48..ad16da1 100644
--- a/template/{{cookiecutter.project_slug}}/Makefile
+++ b/template/react/{{cookiecutter.project_slug}}/Makefile
@@ -2,6 +2,7 @@ VENV_BIN = python3 -m venv
VENV_DIR ?= .venv
VENV_ACTIVATE = $(VENV_DIR)/bin/activate
VENV_RUN = . $(VENV_ACTIVATE)
+WEB_LOCATION_PREFIX = /_localstack/{{cookiecutter.module_name}}/
venv: $(VENV_ACTIVATE)
@@ -17,10 +18,18 @@ clean:
rm -rf .eggs/
rm -rf *.egg-info/
-install: venv
+install-backend: venv
$(VENV_RUN); python setup.py develop
-dist: venv
+install-frontend: venv
+ cd {{cookiecutter.module_name}}/frontend && yarn install
+
+build-frontend:
+ cd {{cookiecutter.module_name}}/frontend; rm -rf build && WEB_LOCATION_PREFIX=$(WEB_LOCATION_PREFIX) npm run build
+
+install: venv install-backend install-frontend
+
+dist: venv build-frontend
$(VENV_RUN); python setup.py sdist bdist_wheel
publish: clean-dist venv dist
diff --git a/template/{{cookiecutter.project_slug}}/README.md b/template/react/{{cookiecutter.project_slug}}/README.md
similarity index 100%
rename from template/{{cookiecutter.project_slug}}/README.md
rename to template/react/{{cookiecutter.project_slug}}/README.md
diff --git a/template/{{cookiecutter.project_slug}}/setup.cfg b/template/react/{{cookiecutter.project_slug}}/setup.cfg
similarity index 55%
rename from template/{{cookiecutter.project_slug}}/setup.cfg
rename to template/react/{{cookiecutter.project_slug}}/setup.cfg
index 2bb3f02..0477963 100644
--- a/template/{{cookiecutter.project_slug}}/setup.cfg
+++ b/template/react/{{cookiecutter.project_slug}}/setup.cfg
@@ -13,8 +13,23 @@ long_description_content_type = text/markdown; charset=UTF-8
zip_safe = False
packages = find:
install_requires =
- localstack>=1.0
-
+ localstack>=2.2
+ localstack-core
+ localstack-ext
+ werkzeug
+ flask
+ rolo
+
[options.entry_points]
localstack.extensions =
{{ cookiecutter.project_slug }} = {{ cookiecutter.module_name }}.extension:MyExtension
+
+[options.package_data]
+{{ cookiecutter.project_slug }} =
+ {{ cookiecutter.module_name }}/frontend/build/*.html
+ {{ cookiecutter.module_name }}/frontend/build/*.js
+ {{ cookiecutter.module_name }}/frontend/build/*.js.map
+ {{ cookiecutter.module_name }}/frontend/build/*.png
+ {{ cookiecutter.module_name }}/frontend/build/*.css
+ {{ cookiecutter.module_name }}/frontend/build/*.css.map
+ {{ cookiecutter.module_name }}/frontend/build/*.json
diff --git a/template/{{cookiecutter.project_slug}}/setup.py b/template/react/{{cookiecutter.project_slug}}/setup.py
similarity index 100%
rename from template/{{cookiecutter.project_slug}}/setup.py
rename to template/react/{{cookiecutter.project_slug}}/setup.py
diff --git a/template/react/{{cookiecutter.project_slug}}/utf8_fail b/template/react/{{cookiecutter.project_slug}}/utf8_fail
new file mode 100644
index 0000000..3930e7f
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/utf8_fail
@@ -0,0 +1,28 @@
+./{{cookiecutter.module_name}}/frontend/.git/index
+./{{cookiecutter.module_name}}/frontend/.git/objects/74/b5e053450a48a6bdb4d71aad648e7af821975c
+./{{cookiecutter.module_name}}/frontend/.git/objects/9d/fc1c058cebbef8b891c5062be6f31033d7d186
+./{{cookiecutter.module_name}}/frontend/.git/objects/8f/2609b7b3e0e3897ab3bcaad13caf6876e48699
+./{{cookiecutter.module_name}}/frontend/.git/objects/a5/3698aab3c66049c61980112dd0109dd2cd0845
+./{{cookiecutter.module_name}}/frontend/.git/objects/49/a2a16e0fbc7636ee16bf907257a5282b856493
+./{{cookiecutter.module_name}}/frontend/.git/objects/e9/e57dc4d41b9b46e05112e9f45b7ea6ac0ba15e
+./{{cookiecutter.module_name}}/frontend/.git/objects/2a/68616d9846ed7d3bfb9f28ca1eb4d51b2c2f84
+./{{cookiecutter.module_name}}/frontend/.git/objects/ec/2585e8c0bb8188184ed1e0703c4c8f2a8419b0
+./{{cookiecutter.module_name}}/frontend/.git/objects/4d/29575de80483b005c29bfcac5061cd2f45313e
+./{{cookiecutter.module_name}}/frontend/.git/objects/08/0d6c77ac21bb2ef88a6992b2b73ad93daaca92
+./{{cookiecutter.module_name}}/frontend/.git/objects/a1/1777cc471a4344702741ab1c8a588998b1311a
+./{{cookiecutter.module_name}}/frontend/.git/objects/03/2464fb6ec40a523899b8c8a593242f3108a420
+./{{cookiecutter.module_name}}/frontend/.git/objects/64/31bc5fc6b2c932dfe5d0418fc667b86c18b9fc
+./{{cookiecutter.module_name}}/frontend/.git/objects/8e/29b36dea7f04ae8729d8b33ecc05c3c9b0fe46
+./{{cookiecutter.module_name}}/frontend/.git/objects/13/b31acae787d809ccb930cb109be523fd3d5d2b
+./{{cookiecutter.module_name}}/frontend/.git/objects/19/50edfd6c40f283a255c45d6a9737cdbf7a4ac9
+./{{cookiecutter.module_name}}/frontend/.git/objects/b8/7cb00449efa5b6131f56b7e45cc63eddf37373
+./{{cookiecutter.module_name}}/frontend/.git/objects/7f/f18e5bf7ad6a79c120ab2c3759ca4f5826e7a5
+./{{cookiecutter.module_name}}/frontend/.git/objects/7f/3d46d66f77670c0bd362bd37f4e593a67fe1e6
+./{{cookiecutter.module_name}}/frontend/.git/objects/a4/e47a6545bc15971f8f63fba70e4013df88a664
+./{{cookiecutter.module_name}}/frontend/.git/objects/fc/44b0a3796c0e0a64c3d858ca038bd4570465d9
+./{{cookiecutter.module_name}}/frontend/.git/objects/a2/73b0cfc0e965c35524e3cd0d3574cbe1ad2d0d
+./{{cookiecutter.module_name}}/frontend/.git/objects/39/af63c4854e72b9d665ea4053f818a15abf01aa
+./{{cookiecutter.module_name}}/frontend/.git/objects/aa/069f27cbd9d53394428171c3989fd03db73c76
+./{{cookiecutter.module_name}}/frontend/public/logo192.png
+./{{cookiecutter.module_name}}/frontend/public/favicon.ico
+./{{cookiecutter.module_name}}/frontend/public/logo512.png
diff --git a/template/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/__init__.py b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/__init__.py
similarity index 100%
rename from template/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/__init__.py
rename to template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/__init__.py
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/backend/web.py b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/backend/web.py
new file mode 100644
index 0000000..e69de29
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/extension.py b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/extension.py
new file mode 100644
index 0000000..1672c21
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/extension.py
@@ -0,0 +1,44 @@
+import logging
+
+from localstack.extensions.api import Extension, http, aws
+
+from localstack.services.internal import get_internal_apis
+from localstack import config
+
+from react_test.backend.web import WebApp
+
+from .util import Routes, Subdomain, Submount
+
+LOG = logging.getLogger(__name__)
+
+class MyExtension(Extension):
+ name = "{{ cookiecutter.project_slug }}"
+
+ def on_extension_load(self):
+ print("MyExtension: extension is loaded")
+
+ def on_platform_start(self):
+ print("MyExtension: localstack is starting")
+
+ def on_platform_ready(self):
+ print("MyExtension: localstack is running")
+
+ def update_gateway_routes(self, router: http.Router[http.RouteHandler]):
+ LOG.info("Adding route for %s", self.name)
+
+ get_internal_apis().add(WebApp())
+ from localstack.aws.handlers.cors import ALLOWED_CORS_ORIGINS
+
+ webapp = Routes(WebApp())
+
+ ALLOWED_CORS_ORIGINS.append(f"http://{self.name}.{config.LOCALSTACK_HOST}")
+ ALLOWED_CORS_ORIGINS.append(f"https://{self.name}.{config.LOCALSTACK_HOST}")
+
+ router.add(Submount(f"/{self.name}", webapp))
+ router.add(Subdomain(f"{self.name}", webapp))
+
+ def update_request_handlers(self, handlers: aws.CompositeHandler):
+ pass
+
+ def update_response_handlers(self, handlers: aws.CompositeResponseHandler):
+ pass
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.config.js b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.config.js
new file mode 100644
index 0000000..3c42058
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.config.js
@@ -0,0 +1,102 @@
+/* eslint-disable global-require */
+
+const esbuild = require('esbuild');
+const path = require('path');
+
+const SvgrPlugin = require('esbuild-plugin-svgr');
+const CopyPlugin = require('esbuild-plugin-copy').default;
+const CleanPlugin = require('esbuild-plugin-clean').default;
+const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill');
+
+const packageJson = require('../package.json');
+const HtmlPlugin = require('./plugins/html');
+
+const CURRENT_ENV = process.env.NODE_ENV || 'development.local';
+const BUILD_PATH = path.join(__dirname, '..', 'build');
+const WEB_LOCATION_PREFIX = process.env.WEB_LOCATION_PREFIX;
+
+// Load .env.* file depending on the active environment
+require('dotenv').config({ path: `.env.${CURRENT_ENV}` });
+
+const BUILD_CONFIG = {
+ entryPoints: [
+ path.join(__dirname, '..', 'src', 'index.tsx'),
+ path.join(__dirname, '..', 'src', 'index.html'),
+ ],
+ assetNames: '[name]-[hash]',
+ entryNames: '[name]-[hash]',
+ outdir: BUILD_PATH,
+ bundle: true,
+ minify: !CURRENT_ENV.includes('development.local'),
+ sourcemap: true,
+ target: 'es2015',
+ metafile: true,
+ // splitting: true,
+ // set in case file loader is added below
+ plugins: [
+ CleanPlugin({
+ patterns: [`${BUILD_PATH}/*`, `!${BUILD_PATH}/index.html`],
+ sync: true,
+ verbose: false
+ }),
+ SvgrPlugin({
+ prettier: false,
+ svgo: false,
+ svgoConfig: {
+ plugins: [{ removeViewBox: false }],
+ },
+ titleProp: true,
+ ref: true,
+ }),
+ CopyPlugin({
+ copyOnStart: true,
+ // https://github.com/LinbuduLab/nx-plugins/issues/57
+ assets: [
+ {
+ from: ['./public/*'],
+ to: ['./'],
+ },
+ ],
+ }),
+ NodeModulesPolyfillPlugin(),
+ HtmlPlugin({
+ prefix: WEB_LOCATION_PREFIX,
+ filename: path.join(BUILD_PATH, 'index.html'),
+ env: true,
+ }),
+ ],
+ inject: [path.join(__dirname, 'esbuild.shims.js')],
+ define: {
+ // Define replacements for env vars starting with `REACT_APP_`
+ ...Object.entries(process.env).reduce(
+ (memo, [name, value]) => name.startsWith('REACT_APP_') ?
+ { ...memo, [`process.env.${name}`]: JSON.stringify(value) } :
+ memo,
+ {},
+ ),
+ 'process.cwd': 'dummyProcessCwd',
+ 'process.env.PUBLIC_URL': '""', // empty for now to point to the root
+ 'process.env.BASE_PATH': '"/"',
+ 'process.env.NODE_ENV': JSON.stringify(CURRENT_ENV),
+ global: 'window',
+ },
+ external: [
+ ...Object.keys(packageJson.devDependencies || {}),
+ ],
+ loader: {
+ '.md': 'text',
+ '.gif': 'dataurl',
+ }
+};
+
+const build = async (overrides = {}) => {
+ try {
+ await esbuild.build({ ...BUILD_CONFIG, ...overrides });
+ console.log('done building');
+ } catch (e) {
+ console.error(e);
+ process.exit(1);
+ }
+};
+
+module.exports = { build };
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.shims.js b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.shims.js
new file mode 100644
index 0000000..c44743a
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/esbuild.shims.js
@@ -0,0 +1,7 @@
+import * as React from 'react';
+
+export { React };
+
+export function dummyProcessCwd() {
+ return '';
+};
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/index.js b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/index.js
new file mode 100644
index 0000000..66a0005
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/index.js
@@ -0,0 +1,11 @@
+const { build, serve } = require('./esbuild.config');
+
+(async () => {
+ if (process.argv.includes('--serve')) {
+ await serve();
+ } else if (process.argv.includes('--watch')) {
+ await build({ watch: true });
+ } else {
+ await build();
+ }
+})();
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/plugins/html/index.js b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/plugins/html/index.js
new file mode 100644
index 0000000..ec65ab6
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/.esbuild/plugins/html/index.js
@@ -0,0 +1,93 @@
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+/**
+ * @param {object} config
+ * @param {string} config.filename - HTML file to process and override
+ * @param {boolean} config.env - Whether to replace env vars or not (default - `false`)
+ * @param {string} config.envPrefix - Limit env vars to pick (default - `REACT_APP_`)
+ * @param {string} config.prefix - prefix to add to the links
+ */
+const HtmlPlugin = (config) => ({
+ name: 'html',
+ setup(build) {
+ build.onResolve({ filter: /\.html$/ }, args => ({
+ path: path.resolve(args.resolveDir, args.path),
+ namespace: 'html',
+ }));
+ build.onLoad({ filter: /.html/, namespace: 'html' }, (args) => {
+ let htmlContent = fs.readFileSync(args.path).toString('utf-8');
+
+ // replace env vars
+ if (config.env) {
+ const envPrefix = config.envPrefix || 'REACT_APP_';
+ const envVars = Object.entries(process.env || {}).filter(([name]) => name.startsWith(envPrefix));
+ htmlContent = envVars.reduce(
+ (memo, [name, value]) => memo.replace(new RegExp(`%${name}%`, 'igm'), value),
+ htmlContent,
+ );
+ }
+
+ return {
+ contents: htmlContent,
+ loader: 'file'
+ };
+ });
+
+ build.onEnd((result) => {
+ const outFiles = Object.keys((result.metafile || {}).outputs);
+ const jsFiles = outFiles.filter((p) => p.endsWith('.js'));
+ const cssFiles = outFiles.filter((p) => p.endsWith('.css'));
+ const htmlFiles = outFiles.filter((p) => p.endsWith('.html'));
+
+ const headerAppends = cssFiles.reduce(
+ (memo, p) => {
+ const filename = p.split(path.sep).slice(-1)[0];
+ return [...memo, ``];
+ },
+ [],
+ );
+
+ const preHeadersAppend = []
+ if (config.prefix) {
+ preHeadersAppend.push(
+ ``
+ )
+ }
+
+ const bodyAppends = jsFiles.reduce(
+ (memo, p) => {
+ const filename = p.split(path.sep).slice(-1)[0];
+ return [...memo, ``];
+ },
+ [],
+ );
+
+ for (const htmlFile of htmlFiles) {
+ let htmlContent = fs.readFileSync(htmlFile).toString('utf-8');
+
+ // replace env vars
+ if (config.env) {
+ const envPrefix = config.envPrefix || 'REACT_APP_';
+ const envVars = Object.entries(process.env).filter(([name]) => name.startsWith(envPrefix));
+
+ htmlContent = envVars.reduce(
+ (memo, [name, value]) => memo.replace(new RegExp(`%${name}%`, 'igm'), value),
+ htmlContent,
+ );
+ }
+
+ // inject references to js and css files
+ htmlContent = htmlContent
+ .replace('', ['', ...preHeadersAppend].join("\n"))
+ .replace('', [...headerAppends, ''].join("\n"))
+ .replace('
+
+
+
+ ', [...bodyAppends, ''].join("\n"));
+
+ fs.writeFileSync(config.filename.replace('-[^.]+', ''), htmlContent);
+ }
+ });
+ },
+});
+
+module.exports = HtmlPlugin;
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/__init__.py b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/package.json b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/package.json
new file mode 100644
index 0000000..d409dfb
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "frontend",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "@mui/material": "^5.15.20",
+ "@testing-library/react": "^13.4.0",
+ "@types/node": "^16.18.99",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.24.0",
+ "react-scripts": "5.0.1",
+ "typescript": "^4.9.5"
+ },
+ "devDependencies": {
+ "@esbuild-plugins/node-modules-polyfill": "^0.1.4",
+ "esbuild": "^0.16.6",
+ "esbuild-envfile-plugin": "^1.0.2",
+ "esbuild-plugin-clean": "^1.0.1",
+ "esbuild-plugin-copy": "^0.3.0",
+ "esbuild-plugin-svgr": "^1.0.0"
+ },
+ "scripts": {
+ "start": "concurrently --restart-tries -1 --raw \"yarn serve\" \"yarn watch\"",
+ "watch": "node .esbuild --watch",
+ "build": "node .esbuild"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app"
+ ]
+ }
+}
diff --git a/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/public/index.html b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/public/index.html
new file mode 100644
index 0000000..aa069f2
--- /dev/null
+++ b/template/react/{{cookiecutter.project_slug}}/{{cookiecutter.module_name}}/frontend/public/index.html
@@ -0,0 +1,43 @@
+
+
+