diff --git a/.github/workflows/aws-proxy.yml b/.github/workflows/aws-proxy.yml
index 86e2bd4..cba3f17 100644
--- a/.github/workflows/aws-proxy.yml
+++ b/.github/workflows/aws-proxy.yml
@@ -25,6 +25,17 @@ jobs:
with:
python-version: '3.13'
+ - name: Use Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20
+
+ - name: Enable Corepack and set Yarn version
+ run: |
+ corepack enable
+ corepack prepare yarn@3.2.3 --activate
+ shell: bash
+
- name: Set up Terraform CLI
uses: hashicorp/setup-terraform@v2
@@ -37,11 +48,8 @@ jobs:
docker pull localstack/localstack-pro &
docker pull public.ecr.aws/lambda/python:3.8 &
- # install latest CLI packages (dev releases)
- pip install --upgrade --pre localstack localstack-ext
-
- # TODO remove
- mkdir ~/.localstack; echo '{"token":"test"}' > ~/.localstack/auth.json
+ # install latest CLI packages
+ pip install --upgrade localstack localstack-ext
# install dependencies
sudo apt-get update
@@ -52,7 +60,6 @@ jobs:
(
make install
. .venv/bin/activate
- pip install --upgrade --pre localstack localstack-ext
make build
make enable
)
diff --git a/aws-proxy/.gitignore b/aws-proxy/.gitignore
index 89fba19..00d16af 100644
--- a/aws-proxy/.gitignore
+++ b/aws-proxy/.gitignore
@@ -1,2 +1,5 @@
*.tfstate*
.terraform*
+node_modules
+static
+.yarn/
\ No newline at end of file
diff --git a/aws-proxy/MANIFEST.in b/aws-proxy/MANIFEST.in
index 8eb6be3..dfedfcc 100644
--- a/aws-proxy/MANIFEST.in
+++ b/aws-proxy/MANIFEST.in
@@ -1,3 +1,2 @@
-recursive-include aws_proxy *.html
-recursive-include aws_proxy *.js
-recursive-include aws_proxy *.png
+include pyproject.toml
+recursive-exclude aws_proxy/frontend *
diff --git a/aws-proxy/Makefile b/aws-proxy/Makefile
index 86e6235..c62ffa1 100644
--- a/aws-proxy/Makefile
+++ b/aws-proxy/Makefile
@@ -4,16 +4,13 @@ VENV_ACTIVATE = $(VENV_DIR)/bin/activate
VENV_RUN = . $(VENV_ACTIVATE)
TEST_PATH ?= tests
PIP_CMD ?= pip
+FRONTEND_FOLDER = aws_proxy/frontend
+COREPACK_EXISTS := $(shell command -v corepack)
+YARN_EXISTS := $(shell command -v yarn)
usage: ## Show this help
@grep -Fh "##" $(MAKEFILE_LIST) | grep -Fv fgrep | sed -e 's/:.*##\s*/##/g' | awk -F'##' '{ printf "%-25s %s\n", $$1, $$2 }'
-install: ## Install dependencies
- test -d .venv || $(VENV_BIN) .venv
- $(VENV_RUN); pip install -e .
- $(VENV_RUN); pip install -e .[test]
- touch $(VENV_DIR)/bin/activate
-
clean: ## Clean up
rm -rf .venv/
rm -rf build/
@@ -29,10 +26,10 @@ lint: ## Run code linter to check code style
test: ## Run tests
$(VENV_RUN); python -m pytest $(PYTEST_ARGS) $(TEST_PATH)
-entrypoints: ## Generate plugin entrypoints for Python package
+entrypoints: build-frontend ## Generate plugin entrypoints for Python package
$(VENV_RUN); python -m plux entrypoints
-build: entrypoints ## Build the extension
+build: build-frontend entrypoints ## Build the extension
$(VENV_RUN); python -m build --no-isolation . --outdir build
@# make sure that the entrypoints are contained in the dist folder and are non-empty
@test -s localstack_extension_aws_proxy.egg-info/entry_points.txt || (echo "Entrypoints were not correctly created! Aborting!" && exit 1)
@@ -45,6 +42,32 @@ enable: $(wildcard ./build/localstack_extension_aws_proxy-*.tar.gz) ## Enable t
publish: clean-dist venv dist
$(VENV_RUN); pip install --upgrade twine; twine upload dist/*
+check-frontend-deps:
+ @if [ -z "$(YARN_EXISTS)" ]; then \
+ npm install --global yarn; \
+ fi
+ @if [ -z "$(COREPACK_EXISTS)" ]; then \
+ npm install -g corepack; \
+ fi
+
+install-frontend: check-frontend-deps ## Install dependencies of the frontend
+ cd $(FRONTEND_FOLDER) && yarn install
+
+build-frontend: # Build the React app
+ @if [ ! -d "$(FRONTEND_FOLDER)/node_modules" ]; then \
+ $(MAKE) install-frontend; \
+ fi
+ cd $(FRONTEND_FOLDER); rm -rf build && NODE_ENV=prod npm run build
+
+start-frontend: ## Start the frontend in dev mode (hot reload)
+ cd $(FRONTEND_FOLDER); yarn start
+
+install: install-frontend ## Install dependencies
+ test -d .venv || $(VENV_BIN) .venv
+ $(VENV_RUN); pip install -e .
+ $(VENV_RUN); pip install -e .[test]
+ touch $(VENV_DIR)/bin/activate
+
clean-dist: clean
rm -rf dist/
diff --git a/aws-proxy/aws_proxy/frontend/.esbuild/esbuild.config.js b/aws-proxy/aws_proxy/frontend/.esbuild/esbuild.config.js
new file mode 100644
index 0000000..2ed5464
--- /dev/null
+++ b/aws-proxy/aws_proxy/frontend/.esbuild/esbuild.config.js
@@ -0,0 +1,99 @@
+/* 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 { writeFileSync } = require('fs');
+
+const CURRENT_ENV = process.env.NODE_ENV || 'development.local';
+const BUILD_PATH = path.join(__dirname, '..', '..', 'server', 'static');
+
+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: 'es2020',
+ 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,
+ options: {
+ force: true
+ }
+ }),
+ 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({
+ 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',
+ global: 'window',
+ },
+ external: [
+ ...Object.keys(packageJson.devDependencies || {}),
+ ],
+ loader: {
+ '.md': 'text',
+ '.gif': 'dataurl',
+ }
+};
+
+const build = async (overrides = {}) => {
+ try {
+ await esbuild.build({ ...BUILD_CONFIG, ...overrides });
+ writeFileSync(path.join(BUILD_PATH, '__init__.py'),'')
+ console.log('done building');
+ } catch (e) {
+ console.error(e);
+ process.exit(1);
+ }
+};
+
+module.exports = { build };
diff --git a/aws-proxy/aws_proxy/frontend/.esbuild/esbuild.shims.js b/aws-proxy/aws_proxy/frontend/.esbuild/esbuild.shims.js
new file mode 100644
index 0000000..c44743a
--- /dev/null
+++ b/aws-proxy/aws_proxy/frontend/.esbuild/esbuild.shims.js
@@ -0,0 +1,7 @@
+import * as React from 'react';
+
+export { React };
+
+export function dummyProcessCwd() {
+ return '';
+};
diff --git a/aws-proxy/aws_proxy/frontend/.esbuild/index.js b/aws-proxy/aws_proxy/frontend/.esbuild/index.js
new file mode 100644
index 0000000..66a0005
--- /dev/null
+++ b/aws-proxy/aws_proxy/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/aws-proxy/aws_proxy/frontend/.esbuild/plugins/html/index.js b/aws-proxy/aws_proxy/frontend/.esbuild/plugins/html/index.js
new file mode 100644
index 0000000..2161d8e
--- /dev/null
+++ b/aws-proxy/aws_proxy/frontend/.esbuild/plugins/html/index.js
@@ -0,0 +1,84 @@
+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_`)
+ */
+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 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('', [...headerAppends, ''].join("\n"))
+ .replace('