stack-service-base 0.0.99 → 0.0.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stack-service-base/command_init.rb +5 -3
  3. data/lib/stack-service-base/frontend_template/github/.github/workflows/main.yml +19 -0
  4. data/lib/stack-service-base/frontend_template/github/docker/docker-compose.yml +15 -0
  5. data/lib/stack-service-base/frontend_template/gitlab/.gitlab-ci.yml +48 -0
  6. data/lib/stack-service-base/frontend_template/gitlab/docker/Dockerfile.build +36 -0
  7. data/lib/stack-service-base/frontend_template/gitlab/docker/docker-compose.yml +15 -0
  8. data/lib/stack-service-base/frontend_template/gitlab/docker/local_build.sh +10 -0
  9. data/lib/stack-service-base/frontend_template/gitlab-c/.gitlab-ci.yml +24 -0
  10. data/lib/stack-service-base/frontend_template/gitlab-c/docker/Dockerfile.build +17 -0
  11. data/lib/stack-service-base/frontend_template/gitlab-c/docker/docker-compose.yml +15 -0
  12. data/lib/stack-service-base/frontend_template/gitlab-c/docker/local_build.sh +11 -0
  13. data/lib/stack-service-base/frontend_template/home/.editorconfig +12 -0
  14. data/lib/stack-service-base/frontend_template/home/.gitattributes +4 -0
  15. data/lib/stack-service-base/frontend_template/home/.gitignore +17 -0
  16. data/lib/stack-service-base/frontend_template/home/AGENTS.md +3 -0
  17. data/lib/stack-service-base/frontend_template/home/README.md +68 -0
  18. data/lib/stack-service-base/frontend_template/home/docker/.env +3 -0
  19. data/lib/stack-service-base/frontend_template/home/docker/frontend/15-runtime-settings.envsh +90 -0
  20. data/lib/stack-service-base/frontend_template/home/docker/frontend/40-path-prefix-settings.sh +30 -0
  21. data/lib/stack-service-base/frontend_template/home/docker/frontend/45-runtime-config-settings.sh +24 -0
  22. data/lib/stack-service-base/frontend_template/home/docker/frontend/Dockerfile +46 -0
  23. data/lib/stack-service-base/frontend_template/home/docker/frontend/nginx.conf.template +75 -0
  24. data/lib/stack-service-base/frontend_template/home/src/.dockerignore +9 -0
  25. data/lib/stack-service-base/frontend_template/home/src/.env.example +7 -0
  26. data/lib/stack-service-base/frontend_template/home/src/.version +1 -0
  27. data/lib/stack-service-base/frontend_template/home/src/app/main.ts +4 -0
  28. data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.ts +22 -0
  29. data/lib/stack-service-base/frontend_template/home/src/app/runtime-config.types.ts +5 -0
  30. data/lib/stack-service-base/frontend_template/home/src/app/style.css +82 -0
  31. data/lib/stack-service-base/frontend_template/home/src/app/vite-env.d.ts +1 -0
  32. data/lib/stack-service-base/frontend_template/home/src/eslint.config.js +34 -0
  33. data/lib/stack-service-base/frontend_template/home/src/index.html +24 -0
  34. data/lib/stack-service-base/frontend_template/home/src/package-lock.json +2812 -0
  35. data/lib/stack-service-base/frontend_template/home/src/package.json +30 -0
  36. data/lib/stack-service-base/frontend_template/home/src/proxy.config.json +8 -0
  37. data/lib/stack-service-base/frontend_template/home/src/public/runtime-config.js +1 -0
  38. data/lib/stack-service-base/frontend_template/home/src/tests/build-proxy.test.ts +28 -0
  39. data/lib/stack-service-base/frontend_template/home/src/tests/javascript-support.test.js +23 -0
  40. data/lib/stack-service-base/frontend_template/home/src/tests/path-prefix.test.ts +17 -0
  41. data/lib/stack-service-base/frontend_template/home/src/tests/runtime-config.test.ts +22 -0
  42. data/lib/stack-service-base/frontend_template/home/src/tests/vite-config.test.ts +16 -0
  43. data/lib/stack-service-base/frontend_template/home/src/tools/vite/build-proxy.ts +62 -0
  44. data/lib/stack-service-base/frontend_template/home/src/tools/vite/path-prefix.ts +27 -0
  45. data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config-dev-plugin.ts +27 -0
  46. data/lib/stack-service-base/frontend_template/home/src/tools/vite/runtime-config.ts +10 -0
  47. data/lib/stack-service-base/frontend_template/home/src/tsconfig.json +27 -0
  48. data/lib/stack-service-base/frontend_template/home/src/vite.config.ts +40 -0
  49. data/lib/stack-service-base/version.rb +1 -1
  50. metadata +47 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3db078c7ea6acaca31ec7b32b7991fd213dcaacd03fcab42733f8f3d299b6456
4
- data.tar.gz: 621b4b6c3b116f0f72555740ca83f9f62029e0a58b3d19655fdf77db45f27a5b
3
+ metadata.gz: 6d908ed870ee59a5ea085accfe3bc86f2276baeca5780407917af9a59e5eebb7
4
+ data.tar.gz: b7d31c74bbdcd98f55d827ed9630c17f42aaf5f503a9585ade7288b8c6754def
5
5
  SHA512:
6
- metadata.gz: 0ecbb4d62eb789d83cbbc72d422ed7316a5ac486d1cf3dbcf538cd253aefb156d3dcb71b5937e5bd9f99e9360c10310fea242d42abda559532d2ba73594b9213
7
- data.tar.gz: 4d3d7862b719bcb04d26881dada64385c2a61f3bcad4e1ce13d080b26249fb6accaa31bf650b820d5988de99c132570e09c03b19bbd74121f7f076af8f7b2f28
6
+ metadata.gz: e8e3b4ccb5793e85757223306b50afde56a28078ce28468baebd68e0a0dd302b5413afef0415d0679a5792c374521e5cf0a571f0399449399739dc312f6fd1a9
7
+ data.tar.gz: d68eadab213ce6003e64ad54495758a0cc5da87e0ae5ecf83c0587d097205488a386edc106e2f8da233870d79e5b1bd9f79c5c0555b18d289ac59f105dc2a47d
@@ -6,12 +6,13 @@ SSBase::CommandLine::COMMANDS[:init] = Class.new do
6
6
  parser.on('', '--gitlab-c', 'Gitlab C CI/CD')
7
7
  parser.on('', '--gitlab', 'Gitlab CI/CD')
8
8
  parser.on('', '--github', 'GitHub CI/CD')
9
+ parser.on('', '--frontend', 'Vite frontend service')
9
10
  end
10
11
 
11
12
  def run(obj, params, args, _extra)
12
13
  if params.empty? || args.empty?
13
14
  puts 'Usage:'
14
- puts "\tssbase init [option] <service name>"
15
+ puts "\tssbase init [--frontend] [option] <service name>"
15
16
  puts "\tssbase init stack [option]"
16
17
  puts 'option: --gitlab-c, --gitlab, --github'
17
18
  return
@@ -23,13 +24,14 @@ SSBase::CommandLine::COMMANDS[:init] = Class.new do
23
24
  type = :stack
24
25
  service_name = 'fake-service-name'
25
26
  else
26
- type = :project
27
+ type = params[:frontend] ? :frontend : :project
27
28
  service_name = first_arg
28
29
  end
29
30
 
30
31
  copy_folder type, :home
31
32
 
32
- copy_folder type, 'gitlab-c' if params[:gitlab_c]
33
+ default_platform = params.values_at(:gitlab_c, :gitlab, :github).none?
34
+ copy_folder type, 'gitlab-c' if params[:gitlab_c] || default_platform
33
35
  copy_folder type, 'gitlab' if params[:gitlab]
34
36
  copy_folder type, 'github' if params[:github]
35
37
 
@@ -0,0 +1,19 @@
1
+ on: { push: { branches: [main,release] } }
2
+
3
+ jobs:
4
+ build_service_images:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v3
8
+ - uses: docker/login-action@v2
9
+ with: { username: '${{ secrets.DOCKER_HUB_LOGIN }}', password: '${{ secrets.DOCKER_HUB_PASSWORD }}' }
10
+ - uses: docker/setup-buildx-action@v3
11
+ - uses: ruby/setup-ruby@v1
12
+ with: { ruby-version: 3.4.4 }
13
+ - run: gem install build-labels
14
+ - run: env && cd docker && build-labels -n -c docker-compose.yml $(if [ "${GITHUB_REF##*/}" = "release" ]; then echo "--full-version"; fi) github set_version to_dockerfiles to_compose | tee bake.yml
15
+ env:
16
+ GITHUB_RUN_NUMBER: ${{ github.run_number }}
17
+ - uses: docker/bake-action@v5
18
+ env: { REGISTRY_HOST: '${{ secrets.DOCKER_HUB_LOGIN }}' }
19
+ with: { workdir: ./docker, files: bake.yml, push: true }
@@ -0,0 +1,15 @@
1
+ services:
2
+ ${service_name}:
3
+ image: ${REGISTRY_HOST}/${service_name}
4
+ build:
5
+ context: ../src
6
+ additional_contexts:
7
+ docker: ../docker
8
+ dockerfile: ../docker/frontend/Dockerfile
9
+ ports:
10
+ - "7000:8080"
11
+ environment:
12
+ API_BASE_URL: ${API_BASE_URL:-}
13
+ BACKEND_URL: ${BACKEND_URL:-http://127.0.0.1:65535}
14
+ LOG_LEVEL: ${LOG_LEVEL:-info}
15
+ PATH_PREFIX: ${PATH_PREFIX:-}
@@ -0,0 +1,48 @@
1
+ stages:
2
+ - test
3
+ - build
4
+
5
+ .build_block: &build_block
6
+ # tags: [ build ]
7
+ image: docker:27.5.1-cli
8
+ services:
9
+ - name: docker:27.5.1-dind
10
+ alias: docker
11
+ variables:
12
+ DOCKER_HOST: tcp://docker:2375
13
+ DOCKER_TLS_CERTDIR: ""
14
+ script:
15
+ - export CI_COMMIT_TAG="${CI_COMMIT_TAG:-0.0.0}"
16
+ - |
17
+ if [ -n "${CI_REGISTRY:-}" ] && [ -n "${CI_REGISTRY_USER:-}" ] && [ -n "${CI_REGISTRY_PASSWORD:-}" ]; then
18
+ echo "${CI_REGISTRY_PASSWORD}" | docker login "${CI_REGISTRY}" -u "${CI_REGISTRY_USER}" --password-stdin
19
+ fi
20
+ - docker buildx create --name "${CI_PROJECT_NAME}-wrapper-builder" --driver docker-container --use || docker buildx use "${CI_PROJECT_NAME}-wrapper-builder"
21
+ - docker buildx inspect --bootstrap
22
+ - |
23
+ docker buildx build --load \
24
+ -t build/${CI_PROJECT_NAME} \
25
+ -f docker/Dockerfile.build \
26
+ --cache-from type=registry,ref=${CI_REGISTRY_IMAGE}/ci-wrapper:${CI_COMMIT_REF_SLUG}-cache \
27
+ --cache-to type=registry,ref=${CI_REGISTRY_IMAGE}/ci-wrapper:${CI_COMMIT_REF_SLUG}-cache,mode=max \
28
+ .
29
+ - docker run --rm `env | grep -o '^CI_[^=]*' | sed 's/^/-e /'`
30
+ -e REGISTRY_HOST=$CI_REGISTRY/$CI_PROJECT_NAMESPACE
31
+ -v /var/run/docker.sock:/var/run/docker.sock
32
+ -v /root/.docker:/root/.docker
33
+ build/${CI_PROJECT_NAME} 2>&1
34
+
35
+ build push:
36
+ <<: *build_block
37
+ stage: build
38
+ needs: [ tests ]
39
+
40
+ tests:
41
+ <<: *build_block
42
+ stage: test
43
+ variables:
44
+ DOCKER_HOST: tcp://docker:2375
45
+ DOCKER_TLS_CERTDIR: ""
46
+ CI_SKIP_PUSH: true
47
+ # build-labels sets target for each service in docker-compose.yml
48
+ CI_BUILD_TARGET: tests
@@ -0,0 +1,36 @@
1
+ FROM ruby:3.4.4-slim-bookworm AS base
2
+ RUN apt-get update && apt-get install -y --no-install-recommends bash ca-certificates curl git tar \
3
+ && rm -rf /var/lib/apt/lists/*
4
+ RUN gem install build-labels:0.0.79
5
+ RUN BUILDX_VERSION=v0.20.1 COMPOSE_VERSION=v2.33.0 install-docker-static 27.5.1
6
+
7
+ WORKDIR /build
8
+
9
+ COPY .. .
10
+
11
+ CMD ["bash", "-euo", "pipefail", "-c", "\
12
+ cd /build/docker; \
13
+ if [ -n \"${CI_REGISTRY:-}\" ] && [ -n \"${CI_REGISTRY_USER:-}\" ] && [ -n \"${CI_REGISTRY_PASSWORD:-}\" ]; then \
14
+ echo \"$CI_REGISTRY_PASSWORD\" | docker login \"$CI_REGISTRY\" -u \"$CI_REGISTRY_USER\" --password-stdin; \
15
+ fi; \
16
+ build-labels -n -c docker-compose.yml changed gitlab set_version cache to_dockerfiles to_compose | tee bake.yml; \
17
+ export OTEL_RESOURCE_ATTRIBUTES=\"service.name=docker-builder,pipeline.id=${CI_PIPELINE_ID:-local},project.name=${service_name}\"; \
18
+ export REGISTRY_HOST=\"${REGISTRY_HOST:-${CI_REGISTRY_HOST:-${CI_REGISTRY_IMAGE:-}}}\"; \
19
+ : \"${REGISTRY_HOST:?REGISTRY_HOST, CI_REGISTRY_IMAGE, or CI_REGISTRY_HOST is required}\"; \
20
+ export BUILDX_BAKE_ENTITLEMENTS_FS=0; \
21
+ if grep -q \"services: {}\" bake.yml; then \
22
+ echo \"No changed services to build.\"; \
23
+ exit 0; \
24
+ fi; \
25
+ docker buildx create --name \"${service_name}-builder\" --driver docker-container --use || docker buildx use \"${service_name}-builder\"; \
26
+ docker buildx inspect --bootstrap; \
27
+ if [ -n \"${CI_SKIP_PUSH:-}\" ]; then \
28
+ docker buildx bake --load --allow=fs.read=../src -f bake.yml; \
29
+ else \
30
+ docker buildx bake --push --allow=fs.read=../src -f bake.yml; \
31
+ fi; \
32
+ if [ \"${CI_BUILD_TARGET:-}\" = \"tests\" ]; then \
33
+ docker compose -f bake.yml down --remove-orphans; \
34
+ docker compose -f bake.yml up --no-build --force-recreate --abort-on-container-failure; \
35
+ fi \
36
+ "]
@@ -0,0 +1,15 @@
1
+ services:
2
+ ${service_name}:
3
+ image: ${REGISTRY_HOST}/${service_name}/${CI_COMMIT_REF_SLUG:-local}
4
+ build:
5
+ context: ../src
6
+ additional_contexts:
7
+ docker: ../docker
8
+ dockerfile: ../docker/frontend/Dockerfile
9
+ ports:
10
+ - "7000:8080"
11
+ environment:
12
+ API_BASE_URL: ${API_BASE_URL:-}
13
+ BACKEND_URL: ${BACKEND_URL:-http://127.0.0.1:65535}
14
+ LOG_LEVEL: ${LOG_LEVEL:-info}
15
+ PATH_PREFIX: ${PATH_PREFIX:-}
@@ -0,0 +1,10 @@
1
+ export CI_PROJECT_NAME=${service_name}
2
+ export CI_PIPELINE_ID=local
3
+ export CI_PIPELINE_IID=0
4
+ export CI_REGISTRY_HOST=localhost
5
+ export CI_SKIP_PUSH=true
6
+
7
+ # -v /root/.docker:/root/.docker \
8
+ docker run --rm --env-file <(env | grep ^CI_) \
9
+ -v /var/run/docker.sock:/var/run/docker.sock \
10
+ $(docker build -q -t build/${CI_PROJECT_NAME} -f Dockerfile.build ..) 2>&1
@@ -0,0 +1,24 @@
1
+ stages:
2
+ - first
3
+
4
+ .build_block: &build_block
5
+ stage: first
6
+ tags: [ build ]
7
+ needs: [ ]
8
+ image: docker:27.5.1
9
+ script:
10
+ - export CI_COMMIT_TAG="${CI_COMMIT_TAG:-0.0.0}"
11
+ - docker run --rm `env | grep -o '^CI_[^=]*' | sed 's/^/-e /'`
12
+ -v /var/run/docker.sock:/var/run/docker.sock
13
+ -v /root/.docker:/root/.docker
14
+ $(docker build -q -t build/${CI_PROJECT_NAME} -f docker/Dockerfile.build .) 2>&1
15
+
16
+ build push:
17
+ <<: *build_block
18
+
19
+ tests:
20
+ <<: *build_block
21
+ variables:
22
+ CI_SKIP_PUSH: true
23
+ # build-labels sets target for each service in docker-compose.yml
24
+ CI_BUILD_TARGET: tests
@@ -0,0 +1,17 @@
1
+ FROM ruby:3.4.4-slim-bookworm AS base
2
+ RUN apt update && apt install -y --no-install-recommends bash git
3
+ RUN gem install build-labels:0.0.79
4
+ RUN BUILDX_VERSION=v0.20.1 COMPOSE_VERSION=v2.33.0 install-docker-static 27.5.1
5
+
6
+ WORKDIR /build
7
+
8
+ COPY . .
9
+
10
+ CMD ["bash", "-c", "-x", "\
11
+ env && cd /build/docker && \
12
+ build-labels -n -c docker-compose.yml changed gitlab set_version to_dockerfiles to_compose | tee bake.yml && \
13
+ export OTEL_RESOURCE_ATTRIBUTES=service.name=docker-builder,pipeline.id=${CI_PIPELINE_ID},project.name=${CI_PROJECT_NAME} && \
14
+ export REGISTRY_HOST=$CI_REGISTRY_HOST && export BUILDX_BAKE_ENTITLEMENTS_FS=0 && \
15
+ grep \"services: {}\" bake.yml || docker buildx bake -f bake.yml $([ -z \"$CI_SKIP_PUSH\" ] && echo \"--push\") && \
16
+ ([ \"$CI_BUILD_TARGET\" = \"tests\" ] && (docker compose -f bake.yml down && docker compose -f bake.yml up --force-recreate ) || true) \
17
+ "]
@@ -0,0 +1,15 @@
1
+ services:
2
+ ${service_name}:
3
+ image: ${REGISTRY_HOST}/${service_name}/${CI_COMMIT_BRANCH}
4
+ build:
5
+ context: ../src
6
+ additional_contexts:
7
+ docker: ../docker
8
+ dockerfile: ../docker/frontend/Dockerfile
9
+ ports:
10
+ - "7000:8080"
11
+ environment:
12
+ API_BASE_URL: ${API_BASE_URL:-}
13
+ BACKEND_URL: ${BACKEND_URL:-http://127.0.0.1:65535}
14
+ LOG_LEVEL: ${LOG_LEVEL:-info}
15
+ PATH_PREFIX: ${PATH_PREFIX:-}
@@ -0,0 +1,11 @@
1
+ CI_PROJECT_NAME=${service_name}
2
+ export CI_PIPELINE_ID=local
3
+ export CI_PIPELINE_IID=0
4
+ export CI_REGISTRY_HOST=localhost
5
+ export CI_SKIP_PUSH=true
6
+
7
+ # -v /root/.docker:/root/.docker \
8
+ docker run --rm --env-file <(env | grep ^CI_) \
9
+ -v /var/run/docker.sock:/var/run/docker.sock \
10
+ $(docker build -q -t build/${CI_PROJECT_NAME} -f Dockerfile.build ..) 2>&1
11
+
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
@@ -0,0 +1,4 @@
1
+ * text=auto
2
+ *.sh text eol=lf
3
+ *.envsh text eol=lf
4
+ *.template text eol=lf
@@ -0,0 +1,17 @@
1
+ .idea/
2
+ .vscode/
3
+ .qodo
4
+ .idea
5
+ .codex
6
+ .agents
7
+ */spec/results
8
+ .env.local
9
+ */.env.*
10
+ *.iml
11
+ *.log
12
+ src/.env.local
13
+ src/node_modules/
14
+ src/dist/
15
+ src/coverage/
16
+ src/test-results/
17
+ src/playwright-report/
@@ -0,0 +1,3 @@
1
+
2
+ Refer to ~/.codex/shared/AGENTS.md for the actual instructions.
3
+ Refer to ~/.codex/shared/skills for the list actual skills.
@@ -0,0 +1,68 @@
1
+ # ${service_name}
2
+
3
+ Framework-neutral frontend service starter for the corporate stack. It includes
4
+ local development, checks, production build, nginx runtime, containers, and CI.
5
+ Choose the UI framework, router, and domain structure, and adapt the supplied
6
+ infrastructure to the service requirements.
7
+
8
+ ## Development
9
+
10
+ Node.js 24 LTS is required for local development.
11
+
12
+ cd src
13
+ cp .env.example .env.local
14
+ npm ci
15
+ npm start
16
+
17
+ The development server listens on http://localhost:3000. Set BACKEND_URL in
18
+ .env.local to enable the same-origin /api proxy.
19
+
20
+ Application code and tests may use JavaScript, TypeScript, or both. TypeScript
21
+ remains strict; JavaScript requires no type annotations and is checked by ESLint.
22
+
23
+ ## Checks
24
+
25
+ cd src
26
+ npm test
27
+ npm run check
28
+
29
+ `npm test` runs the tests. `npm run check` additionally runs TypeScript
30
+ validation, ESLint, and a production build.
31
+
32
+ Run a local production preview with:
33
+
34
+ npm run preview
35
+
36
+ The preview command builds before serving. For a prefixed deployment, run
37
+ `PATH_PREFIX=/example npm run preview` and open
38
+ http://localhost:3000/example/.
39
+
40
+ ## Container
41
+
42
+ docker-compose -f docker/docker-compose.yml up --build
43
+
44
+ The service is available at http://localhost:7000. nginx runs unprivileged on
45
+ container port 8080, exposes /healthcheck, and supports PATH_PREFIX without
46
+ rebuilding the image.
47
+
48
+ PATH_PREFIX may be empty or contain letters, digits, `_`, `-`, and `/`.
49
+ The complete prefixes `/api`, `/assets`, and `/healthcheck` are reserved.
50
+
51
+ Browser-visible runtime configuration:
52
+
53
+ - PATH_PREFIX
54
+ - API_BASE_URL
55
+ - LOG_LEVEL
56
+
57
+ runtime-config.js is public; never put credentials or secrets into these values.
58
+ BACKEND_URL is server-only and configures the nginx /api proxy.
59
+
60
+ nginx uses a same-origin Content Security Policy. Extend the corresponding
61
+ directive in `docker/frontend/nginx.conf.template` when external resources are
62
+ required.
63
+
64
+ ## CI
65
+
66
+ GitLab follows the same corporate `build-labels` orchestration as Ruby services.
67
+ Ruby only runs that tool; the frontend is tested and built with Node and served
68
+ by nginx at runtime.
@@ -0,0 +1,3 @@
1
+ REGISTRY_HOST=127.0.0.1
2
+ CI_COMMIT_BRANCH=local
3
+ COMPOSE_PROJECT_NAME=${service_name}
@@ -0,0 +1,90 @@
1
+ #!/bin/sh
2
+
3
+ raw_path_prefix=$(printf '%s' "${PATH_PREFIX:-}" |
4
+ sed -E 's#^[[:space:]]+|[[:space:]]+$##g; s#^/+##; s#/+$##')
5
+
6
+ case "$raw_path_prefix" in
7
+ '')
8
+ PATH_PREFIX=''
9
+ PATH_PREFIX_RULE=''
10
+ ;;
11
+ *[!A-Za-z0-9_/-]* | *'//'*)
12
+ echo "Invalid PATH_PREFIX: ${PATH_PREFIX:-}" >&2
13
+ exit 1
14
+ ;;
15
+ api | assets | healthcheck)
16
+ echo "PATH_PREFIX is reserved by the service runtime: /$raw_path_prefix" >&2
17
+ exit 1
18
+ ;;
19
+ *)
20
+ PATH_PREFIX="/$raw_path_prefix"
21
+ PATH_PREFIX_RULE=$(printf \
22
+ 'location = %s { absolute_redirect off; return 308 %s/; }\n location ^~ %s/ { rewrite ^%s/(.*)$ /$1 last; }' \
23
+ "$PATH_PREFIX" "$PATH_PREFIX" "$PATH_PREFIX" "$PATH_PREFIX")
24
+ ;;
25
+ esac
26
+
27
+ dns_resolver=${DNS_RESOLVER:-$(awk '$1 == "nameserver" { print $2; exit }' /etc/resolv.conf)}
28
+
29
+ case "$dns_resolver" in
30
+ '' | *[!A-Fa-f0-9:.]*)
31
+ echo "Unable to determine a valid DNS resolver" >&2
32
+ exit 1
33
+ ;;
34
+ esac
35
+
36
+ DNS_RESOLVER=$dns_resolver
37
+
38
+ backend_url=${BACKEND_URL:-http://127.0.0.1:65535}
39
+ backend_url=${backend_url%/}
40
+
41
+ case "$backend_url" in
42
+ http://* | https://*) ;;
43
+ *)
44
+ echo "BACKEND_URL must be an http or https origin" >&2
45
+ exit 1
46
+ ;;
47
+ esac
48
+
49
+ BACKEND_PROTO=${backend_url%%://*}
50
+ backend_authority=${backend_url#*://}
51
+
52
+ case "$backend_authority" in
53
+ '' | */* | *'?'* | *'#'* | *'@'*)
54
+ echo "BACKEND_URL must not contain credentials, query, fragment, or path" >&2
55
+ exit 1
56
+ ;;
57
+ esac
58
+
59
+ case "$backend_authority" in
60
+ *:*)
61
+ BACKEND_HOST=${backend_authority%%:*}
62
+ BACKEND_PORT=${backend_authority##*:}
63
+ ;;
64
+ *)
65
+ BACKEND_HOST=$backend_authority
66
+ if [ "$BACKEND_PROTO" = 'https' ]; then
67
+ BACKEND_PORT=443
68
+ else
69
+ BACKEND_PORT=80
70
+ fi
71
+ ;;
72
+ esac
73
+
74
+ case "$BACKEND_HOST" in
75
+ '' | *[!A-Za-z0-9_.-]*)
76
+ echo "BACKEND_URL contains an invalid host" >&2
77
+ exit 1
78
+ ;;
79
+ esac
80
+
81
+ case "$BACKEND_PORT" in
82
+ '' | *[!0-9]*)
83
+ echo "BACKEND_URL contains an invalid port" >&2
84
+ exit 1
85
+ ;;
86
+ esac
87
+
88
+ export PATH_PREFIX PATH_PREFIX_RULE DNS_RESOLVER BACKEND_PROTO BACKEND_HOST BACKEND_PORT
89
+
90
+ unset raw_path_prefix dns_resolver backend_url backend_authority
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ compiled_app_dir=${COMPILED_APP_DIR:-/usr/share/nginx/html}
5
+ placeholder='/__PATH_PREFIX__/'
6
+ path_prefix=$(printf '%s' "${PATH_PREFIX:-}" |
7
+ sed -E 's#^[[:space:]]+|[[:space:]]+$##g; s#^/+##; s#/+$##')
8
+
9
+ if [ -n "$path_prefix" ]; then
10
+ replacement="/$path_prefix/"
11
+ else
12
+ replacement='/'
13
+ fi
14
+
15
+ escaped_replacement=$(printf '%s' "$replacement" | sed -e 's/[\\&|]/\\&/g')
16
+
17
+ find "$compiled_app_dir" -type f \
18
+ \( -name '*.html' -o -name '*.js' -o -name '*.css' -o -name '*.map' \) -print |
19
+ while IFS= read -r file; do
20
+ if grep -q "$placeholder" "$file"; then
21
+ sed -i "s|$placeholder|$escaped_replacement|g" "$file"
22
+ fi
23
+ done
24
+
25
+ if find "$compiled_app_dir" -type f \
26
+ \( -name '*.html' -o -name '*.js' -o -name '*.css' -o -name '*.map' \) \
27
+ -exec grep -q "$placeholder" {} \; -print | grep -q .; then
28
+ echo "PATH_PREFIX placeholder remains in compiled assets" >&2
29
+ exit 1
30
+ fi
@@ -0,0 +1,24 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ compiled_app_dir=${COMPILED_APP_DIR:-/usr/share/nginx/html}
5
+ output="$compiled_app_dir/runtime-config.js"
6
+ temporary_output="$output.tmp"
7
+ path_prefix=$(printf '%s' "${PATH_PREFIX:-}" |
8
+ sed -E 's#^[[:space:]]+|[[:space:]]+$##g; s#^/+##; s#/+$##')
9
+
10
+ if [ -n "$path_prefix" ]; then
11
+ path_prefix="/$path_prefix"
12
+ fi
13
+
14
+ {
15
+ printf 'window.__APP_CONFIG__ = '
16
+ jq -cn \
17
+ --arg pathPrefix "$path_prefix" \
18
+ --arg apiBaseUrl "${API_BASE_URL:-}" \
19
+ --arg logLevel "${LOG_LEVEL:-info}" \
20
+ '{PATH_PREFIX: $pathPrefix, API_BASE_URL: $apiBaseUrl, LOG_LEVEL: $logLevel}'
21
+ printf ';\n'
22
+ } > "$temporary_output"
23
+
24
+ mv "$temporary_output" "$output"
@@ -0,0 +1,46 @@
1
+ FROM node:24.18-alpine AS base
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json package-lock.json ./
6
+ RUN npm ci
7
+
8
+ COPY . .
9
+
10
+ FROM base AS tests
11
+ CMD ["npm", "run", "check"]
12
+
13
+ FROM base AS build
14
+
15
+ RUN npm run build
16
+
17
+ FROM nginxinc/nginx-unprivileged:1.28.1-alpine AS deploy
18
+
19
+ USER root
20
+ RUN apk add --no-cache ca-certificates curl jq \
21
+ && rm -rf /usr/share/nginx/html/*
22
+
23
+ ENV PORT=8080 \
24
+ NODE_ENV=production \
25
+ PATH_PREFIX= \
26
+ API_BASE_URL= \
27
+ LOG_LEVEL=info \
28
+ BACKEND_URL=http://127.0.0.1:65535
29
+
30
+ COPY --from=docker /frontend/nginx.conf.template /etc/nginx/templates/default.conf.template
31
+ COPY --chmod=755 --from=docker /frontend/15-runtime-settings.envsh /docker-entrypoint.d/
32
+ COPY --chmod=755 --from=docker /frontend/40-path-prefix-settings.sh /docker-entrypoint.d/
33
+ COPY --chmod=755 --from=docker /frontend/45-runtime-config-settings.sh /docker-entrypoint.d/
34
+ RUN rm -f /etc/nginx/conf.d/default.conf \
35
+ && chown -R 101:101 /etc/nginx/conf.d /usr/share/nginx/html
36
+
37
+ COPY --chown=101:101 --from=build /app/dist /usr/share/nginx/html
38
+
39
+ USER 101:101
40
+
41
+ HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
42
+ CMD curl -fsS "http://127.0.0.1:${PORT}/healthcheck" || exit 1
43
+
44
+ EXPOSE 8080
45
+
46
+ CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,75 @@
1
+ server {
2
+ listen ${PORT};
3
+ server_name _;
4
+
5
+ resolver ${DNS_RESOLVER} valid=30s ipv6=off;
6
+ resolver_timeout 10s;
7
+
8
+ root /usr/share/nginx/html;
9
+ index index.html;
10
+
11
+ gzip on;
12
+ gzip_min_length 1024;
13
+ gzip_types text/plain text/css application/javascript application/json image/svg+xml;
14
+
15
+ add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data: blob:; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self' blob:" always;
16
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
17
+ add_header X-Content-Type-Options "nosniff" always;
18
+ add_header X-Frame-Options "SAMEORIGIN" always;
19
+
20
+ location = /healthcheck {
21
+ access_log off;
22
+ default_type application/json;
23
+ return 200 '{"Status":"Healthy"}';
24
+ }
25
+
26
+ location ^~ /api {
27
+ set $backend_origin "${BACKEND_PROTO}://${BACKEND_HOST}:${BACKEND_PORT}";
28
+
29
+ proxy_pass $backend_origin;
30
+ proxy_http_version 1.1;
31
+ proxy_set_header Connection "upgrade";
32
+ proxy_set_header Host ${BACKEND_HOST};
33
+ proxy_set_header Upgrade $http_upgrade;
34
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
35
+ proxy_set_header X-Forwarded-Host $host;
36
+ proxy_set_header X-Forwarded-Proto $scheme;
37
+ proxy_ssl_name ${BACKEND_HOST};
38
+ proxy_ssl_server_name on;
39
+ proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
40
+ proxy_ssl_verify on;
41
+ proxy_cookie_domain ~.* $host;
42
+ proxy_cookie_path / /;
43
+ }
44
+
45
+ location = /runtime-config.js {
46
+ expires -1;
47
+ add_header Cache-Control "no-store" always;
48
+ add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data: blob:; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self' blob:" always;
49
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
50
+ add_header X-Content-Type-Options "nosniff" always;
51
+ add_header X-Frame-Options "SAMEORIGIN" always;
52
+ try_files $uri =404;
53
+ }
54
+
55
+ location = /index.html {
56
+ expires -1;
57
+ add_header Cache-Control "no-store" always;
58
+ add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data: blob:; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self' blob:" always;
59
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
60
+ add_header X-Content-Type-Options "nosniff" always;
61
+ add_header X-Frame-Options "SAMEORIGIN" always;
62
+ try_files $uri =404;
63
+ }
64
+
65
+ location /assets/ {
66
+ expires 1y;
67
+ try_files $uri =404;
68
+ }
69
+
70
+ location / {
71
+ try_files $uri $uri/ /index.html;
72
+ }
73
+
74
+ ${PATH_PREFIX_RULE}
75
+ }
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ *.log
7
+ coverage
8
+ test-results
9
+ playwright-report