From c48d093657e1d7712a93d9f5012340ef90c3ba01 Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:12:33 +0200 Subject: [PATCH 1/3] chore: update github action versions --- .github/workflows/pr.yml | 12 ++++++------ .github/workflows/prod.yml | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 1724578..310ab67 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,11 +16,11 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '22' cache: 'npm' @@ -37,11 +37,11 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '22' cache: 'npm' @@ -58,11 +58,11 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: '22' cache: 'npm' diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 2d80b6c..1c56ce9 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -19,9 +19,9 @@ jobs: node-version: [24.x] steps: - name: Checkout ๐Ÿ›Ž๏ธ - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Install and Build components ๐Ÿ”ง @@ -37,7 +37,7 @@ jobs: npm run docs:json npx nx run demo:build-storybook - name: Upload Pages artifact ๐Ÿ“ฆ - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: dist/storybook/demo @@ -50,7 +50,7 @@ jobs: steps: - name: Deploy ๐Ÿš€ id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 # - name: Build Portfolio Micro App # run: npx nx run portfolio:build # - name: Deploy Portfolio Micro App ๐Ÿš€ From 79f5b071b52a3210002e5ad9919a85b3a42df25d Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:13:46 +0200 Subject: [PATCH 2/3] chore: display version on Storybook --- apps/demo/src/app/intro.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/demo/src/app/intro.mdx b/apps/demo/src/app/intro.mdx index c1f3f40..46c6cd9 100644 --- a/apps/demo/src/app/intro.mdx +++ b/apps/demo/src/app/intro.mdx @@ -1,6 +1,7 @@ import { Meta } from '@storybook/addon-docs/blocks'; +import { version } from 'root-package-json'; - + # Nx Reference From 17cccf84ba311cc1d145f1d1b6999efe72e29b8d Mon Sep 17 00:00:00 2001 From: mdvanes <4253562+mdvanes@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:55:38 +0200 Subject: [PATCH 3/3] feat: deploy multiple apps on one github pages instance --- .github/actions/deploy-subdir/action.yml | 117 ++++++++++++++++++ .github/workflows/deploy-portfolio.yml | 63 ++++++++++ .github/workflows/deploy-shell.yml | 71 +++++++++++ .github/workflows/deploy-storybook.yml | 65 ++++++++++ .github/workflows/prod.yml | 73 ----------- README.md | 54 +++++++- apps/demo/.storybook/main.ts | 12 ++ apps/demo/federation.manifest.prod.json | 3 + apps/demo/project.json | 3 +- apps/demo/src/app/intro.mdx | 3 + apps/portfolio/project.json | 3 +- .../0007-github-pages-subdirectory-hosting.md | 75 +++++++++++ docs/architecture/overview.md | 12 +- hosting.md | 78 ++++++++++++ package.json | 2 +- 15 files changed, 553 insertions(+), 81 deletions(-) create mode 100644 .github/actions/deploy-subdir/action.yml create mode 100644 .github/workflows/deploy-portfolio.yml create mode 100644 .github/workflows/deploy-shell.yml create mode 100644 .github/workflows/deploy-storybook.yml delete mode 100644 .github/workflows/prod.yml create mode 100644 apps/demo/federation.manifest.prod.json create mode 100644 docs/architecture/adr/0007-github-pages-subdirectory-hosting.md create mode 100644 hosting.md diff --git a/.github/actions/deploy-subdir/action.yml b/.github/actions/deploy-subdir/action.yml new file mode 100644 index 0000000..aa10ba3 --- /dev/null +++ b/.github/actions/deploy-subdir/action.yml @@ -0,0 +1,117 @@ +name: Assemble Pages subdirectory +description: >- + Overlays a freshly built app onto the assembled-site storage branch, adds the + SPA fallback (.nojekyll + 404.html), uploads the merged site as a Pages + artifact, and persists the merged tree back to the storage branch. This lets + each micro app deploy independently while GitHub Pages still serves a single + site (see README "Hosting"). + +inputs: + source-dir: + description: Freshly built directory to publish (e.g. dist/apps/portfolio/browser). + required: true + target-subdir: + description: Subdirectory within the site. Empty for the site root (the shell). + required: false + default: '' + storage-branch: + description: Branch that stores the full assembled site between deploys. + required: false + default: pages-content + preserve: + description: >- + Space-separated top-level entries to keep when publishing the site root, + so independent micro-app subdirectories survive a shell redeploy. + required: false + default: 'portfolio storybook' + github-token: + description: Token used to read and update the storage branch. + required: true + +runs: + using: composite + steps: + - name: Assemble merged site ๐Ÿงฉ + shell: bash + env: + SOURCE_DIR: ${{ inputs.source-dir }} + TARGET_SUBDIR: ${{ inputs.target-subdir }} + STORAGE_BRANCH: ${{ inputs.storage-branch }} + PRESERVE: ${{ inputs.preserve }} + GH_TOKEN: ${{ inputs.github-token }} + run: | + set -euo pipefail + + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::source-dir '$SOURCE_DIR' does not exist" >&2 + exit 1 + fi + + REPO_URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + + rm -rf _site _storage + mkdir -p _site + + # Pull the current full site from the storage branch (if it exists). + if git ls-remote --exit-code --heads "$REPO_URL" "$STORAGE_BRANCH" >/dev/null 2>&1; then + git clone --quiet --depth 1 --branch "$STORAGE_BRANCH" "$REPO_URL" _storage + ( cd _storage && tar -cf - --exclude=.git . ) | ( cd _site && tar -xf - ) + rm -rf _storage + fi + + # Overlay the freshly built app into its slot. + if [ -z "$TARGET_SUBDIR" ] || [ "$TARGET_SUBDIR" = "." ]; then + # Site root (shell): replace root entries but keep sibling micro apps. + keep=".git .nojekyll $PRESERVE" + shopt -s dotglob nullglob + for entry in _site/*; do + name="$(basename "$entry")" + case " $keep " in + *" $name "*) continue ;; + esac + rm -rf "$entry" + done + shopt -u dotglob nullglob + cp -R "$SOURCE_DIR"/. _site/ + else + rm -rf "_site/${TARGET_SUBDIR}" + mkdir -p "_site/${TARGET_SUBDIR}" + cp -R "$SOURCE_DIR"/. "_site/${TARGET_SUBDIR}/" + fi + + # SPA fallback + disable Jekyll processing. + touch _site/.nojekyll + if [ -f _site/index.html ]; then + cp _site/index.html _site/404.html + fi + + echo "Assembled site tree:" + find _site -maxdepth 2 -mindepth 1 | sort + + - name: Upload Pages artifact ๐Ÿ“ฆ + uses: actions/upload-pages-artifact@v5 + with: + path: _site + + - name: Persist assembled site to storage branch ๐Ÿ’พ + shell: bash + env: + TARGET_SUBDIR: ${{ inputs.target-subdir }} + STORAGE_BRANCH: ${{ inputs.storage-branch }} + GH_TOKEN: ${{ inputs.github-token }} + run: | + set -euo pipefail + REPO_URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + SLOT="${TARGET_SUBDIR:-/ (shell)}" + + cd _site + rm -rf .git + git init -q + git add -A + git -c user.name="github-actions[bot]" \ + -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ + commit -q -m "deploy: update ${SLOT} (${GITHUB_SHA})" || { + echo "Nothing to persist."; exit 0; + } + git branch -M "$STORAGE_BRANCH" + git push -f "$REPO_URL" "$STORAGE_BRANCH" diff --git a/.github/workflows/deploy-portfolio.yml b/.github/workflows/deploy-portfolio.yml new file mode 100644 index 0000000..2f40785 --- /dev/null +++ b/.github/workflows/deploy-portfolio.yml @@ -0,0 +1,63 @@ +name: Deploy portfolio + +# The portfolio Module Federation remote is served (and exposes its +# remoteEntry.json) at: +# http://codestar.nl/nx-reference/portfolio/ +# Only apps/portfolio (and shared libs) changes trigger this workflow, so the +# remote deploys independently of the shell. +on: + push: + branches: [main] + paths: + - 'apps/portfolio/**' + - 'libs/**' + - 'package.json' + - 'package-lock.json' + - 'nx.json' + - 'tsconfig.base.json' + - '.github/workflows/deploy-portfolio.yml' + - '.github/actions/deploy-subdir/**' + workflow_dispatch: + +permissions: + contents: write + pages: write + id-token: write + +# Serialize with the other Pages deploys so storage-branch writes never race. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout ๐Ÿ›Ž๏ธ + uses: actions/checkout@v7 + + - name: Use Node.js 24 ๐ŸŸข + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies ๐Ÿ”ง + run: npm ci + + - name: Build portfolio remote ๐Ÿ—๏ธ + run: npx nx build portfolio --configuration=production + + - name: Assemble /portfolio ๐Ÿงฉ + uses: ./.github/actions/deploy-subdir + with: + source-dir: dist/apps/portfolio/browser + target-subdir: portfolio + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to GitHub Pages ๐Ÿš€ + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/deploy-shell.yml b/.github/workflows/deploy-shell.yml new file mode 100644 index 0000000..177dea3 --- /dev/null +++ b/.github/workflows/deploy-shell.yml @@ -0,0 +1,71 @@ +name: Deploy shell + +# The shell (Module Federation host) is served at the site root: +# http://codestar.nl/nx-reference/ +# Only apps/demo (and shared libs) changes trigger this workflow โ€” a portfolio +# remote change never rebuilds the shell, demonstrating module-federation +# loose coupling. +on: + push: + branches: [main] + paths: + - 'apps/demo/src/**' + - 'apps/demo/public/**' + - 'apps/demo/federation.config.js' + - 'apps/demo/federation.manifest.prod.json' + - 'apps/demo/project.json' + - 'apps/demo/tsconfig*.json' + - 'libs/**' + - 'package.json' + - 'package-lock.json' + - 'nx.json' + - 'tsconfig.base.json' + - '.github/workflows/deploy-shell.yml' + - '.github/actions/deploy-subdir/**' + workflow_dispatch: + +permissions: + contents: write + pages: write + id-token: write + +# Serialize with the other Pages deploys so storage-branch writes never race. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout ๐Ÿ›Ž๏ธ + uses: actions/checkout@v7 + + - name: Use Node.js 24 ๐ŸŸข + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies ๐Ÿ”ง + run: npm ci + + - name: Build shell ๐Ÿ—๏ธ + run: npx nx build demo --configuration=production + + - name: Apply production federation manifest ๐Ÿ”— + run: cp apps/demo/federation.manifest.prod.json dist/apps/demo/browser/federation.manifest.json + + - name: Assemble site root ๐Ÿงฉ + uses: ./.github/actions/deploy-subdir + with: + source-dir: dist/apps/demo/browser + target-subdir: '' + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to GitHub Pages ๐Ÿš€ + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml new file mode 100644 index 0000000..ec3c1c0 --- /dev/null +++ b/.github/workflows/deploy-storybook.yml @@ -0,0 +1,65 @@ +name: Deploy storybook + +# The aggregated Storybook catalog is served at: +# http://codestar.nl/nx-reference/storybook/ +# Triggered by Storybook config, story files, or shared library changes. +on: + push: + branches: [main] + paths: + - 'apps/demo/.storybook/**' + - 'libs/**' + - '**/*.stories.ts' + - '**/*.stories.tsx' + - '**/*.mdx' + - 'tsconfig.doc.json' + - 'package.json' + - 'package-lock.json' + - '.github/workflows/deploy-storybook.yml' + - '.github/actions/deploy-subdir/**' + workflow_dispatch: + +permissions: + contents: write + pages: write + id-token: write + +# Serialize with the other Pages deploys so storage-branch writes never race. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout ๐Ÿ›Ž๏ธ + uses: actions/checkout@v7 + + - name: Use Node.js 24 ๐ŸŸข + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies ๐Ÿ”ง + run: npm ci + + - name: Build Storybook ๐Ÿ“š + run: | + npm run docs:json + npx nx run demo:build-storybook + + - name: Assemble /storybook ๐Ÿงฉ + uses: ./.github/actions/deploy-subdir + with: + source-dir: dist/storybook/demo + target-subdir: storybook + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to GitHub Pages ๐Ÿš€ + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml deleted file mode 100644 index 1c56ce9..0000000 --- a/.github/workflows/prod.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Build Storybook and deploy to Github Pages - -on: - push: - branches: - - main - -permissions: - contents: read - pages: write - id-token: write - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [24.x] - steps: - - name: Checkout ๐Ÿ›Ž๏ธ - uses: actions/checkout@v7 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - - name: Install and Build components ๐Ÿ”ง - run: | - npm ci - # TODO not needed to build after install? npm run build - # TODO - name: Validate โ˜‘๏ธ - # run: npm run validate - # env: - # CI: false # true -> fails on warning - - name: Build Storybook ๐Ÿ“š - run: | - npm run docs:json - npx nx run demo:build-storybook - - name: Upload Pages artifact ๐Ÿ“ฆ - uses: actions/upload-pages-artifact@v5 - with: - path: dist/storybook/demo - - deploy: - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy ๐Ÿš€ - id: deployment - uses: actions/deploy-pages@v5 - # - name: Build Portfolio Micro App - # run: npx nx run portfolio:build - # - name: Deploy Portfolio Micro App ๐Ÿš€ - # uses: JamesIves/github-pages-deploy-action@4.1.5 - # with: - # token: ${{ secrets.ACCESS_TOKEN }} # Access token from the Prod repo, set in Settings > Secrets - # repository-name: code-star/nx-reference-portfolio - # branch: gh-pages # The branch the action should deploy to. - # folder: dist/apps/portfolio # The folder the action should deploy. - # clean: true # Automatically remove deleted files from the deploy branch - # - name: Build Shell Micro App - # run: npx nx run demo:build - # - name: Deploy Shell Micro App ๐Ÿš€ - # uses: JamesIves/github-pages-deploy-action@4.1.5 - # with: - # token: ${{ secrets.ACCESS_TOKEN }} # Access token from the Prod repo, set in Settings > Secrets - # repository-name: code-star/nx-reference-shell - # branch: gh-pages # The branch the action should deploy to. - # folder: dist/apps/demo # The folder the action should deploy. - # clean: true # Automatically remove deleted files from the deploy branch diff --git a/README.md b/README.md index 4e794fb..d4e64a8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A reference monorepo demonstrating **Module Federation in Nx with Angular**, plu Made by [Codestar](https://code-star.github.io). -For a component tour see the [Storybook Intro](https://code-star.github.io/nx-reference/?path=/docs/introduction--docs). +For a component tour see the [Storybook Intro](https://code-star.github.io/nx-reference/storybook/?path=/docs/introduction--docs). > **Rebuilt on the latest toolchain** โ€” Nx 23 ยท Angular 21 (standalone + signals + zoneless) ยท > Storybook 10 ยท Native Federation. The full rationale and audit trail live under [`docs/`](./docs). @@ -90,6 +90,58 @@ npx nx build-storybook demo # build the Storybook catalog The latest verification results are recorded in [`docs/reports/test-report.md`](./docs/reports/test-report.md). +## Hosting + +Everything is served from a **single GitHub Pages site** for this repo +(`code-star.github.io/nx-reference/`) using +subdirectory deployment: + +| URL | Serves | +| ------------------------------------------- | ----------------------------------------------------------------- | +| `http://codestar.nl/nx-reference/` | Shell โ€” Module Federation host (`apps/demo`) | +| `http://codestar.nl/nx-reference/portfolio` | Portfolio remote micro app + `remoteEntry.json` (`apps/portfolio`) | +| `http://codestar.nl/nx-reference/storybook` | Storybook component catalog | + +The `server` Express app is not deployed to Pages. + +### How it works + +- Each app builds with its own base href (`/nx-reference/` for the shell, + `/nx-reference/portfolio/` for the remote โ€” set on the production build + configuration in each `project.json`) and is published to its own subdirectory. +- At runtime the shell reads `federation.manifest.json`. The production manifest + ([`apps/demo/federation.manifest.prod.json`](./apps/demo/federation.manifest.prod.json)) + points the `portfolio` remote at `/nx-reference/portfolio/remoteEntry.json`; + the committed dev manifest keeps pointing at `http://localhost:4201` so local + development is unchanged. +- A root `404.html` (copy of the shell `index.html`) plus a `.nojekyll` marker + provide SPA deep-link fallback on GitHub Pages. + +### Independent deployments + +Three path-filtered workflows deploy each piece **independently**, all through the +`actions/deploy-pages` action: + +| Workflow | Triggered by | Deploys | +| ------------------------------------------ | ------------------------------------- | ------------- | +| `.github/workflows/deploy-shell.yml` | `apps/demo/**`, shared libs | site root `/` | +| `.github/workflows/deploy-portfolio.yml` | `apps/portfolio/**`, shared libs | `/portfolio` | +| `.github/workflows/deploy-storybook.yml` | `.storybook`, story files, shared libs | `/storybook` | + +A change confined to `apps/portfolio` rebuilds and redeploys **only** the portfolio +remote โ€” the shell is never rebuilt, and vice-versa. That is the module-federation +payoff: host and remotes are decoupled at deploy time. + +Because `deploy-pages` publishes one artifact that replaces the whole site, the +shared [`.github/actions/deploy-subdir`](./.github/actions/deploy-subdir) composite +action keeps the full assembled site on a `pages-content` storage branch and +overlays only the changed subdirectory before each deploy. All Pages deploys share a +`pages` concurrency group so they serialise safely. + +> **Setup:** set the repo's **Settings โ†’ Pages โ†’ Source** to **GitHub Actions**, then +> trigger each workflow once via **Run workflow** (`workflow_dispatch`) to seed all +> three subdirectories on the first deploy. + ## Documentation & audit trail - [Architecture overview](./docs/architecture/overview.md) and diff --git a/apps/demo/.storybook/main.ts b/apps/demo/.storybook/main.ts index 3c431c5..2ff3c24 100644 --- a/apps/demo/.storybook/main.ts +++ b/apps/demo/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from '@storybook/angular'; +import { join } from 'node:path'; /** * Single deployable catalog: aggregates the demo app, the @star/ui component @@ -15,6 +16,17 @@ const config: StorybookConfig = { name: '@storybook/angular', options: {}, }, + // Mirror the `root-package-json` tsconfig path alias for the webpack builder + // so MDX/story imports of the root package.json resolve during the catalog build. + webpackFinal: async (webpackConfig) => { + webpackConfig.resolve ??= {}; + webpackConfig.resolve.alias = { + ...(webpackConfig.resolve.alias ?? {}), + 'root-package-json': join(process.cwd(), 'package.json'), + }; + return webpackConfig; + }, }; export default config; + diff --git a/apps/demo/federation.manifest.prod.json b/apps/demo/federation.manifest.prod.json new file mode 100644 index 0000000..2c3de27 --- /dev/null +++ b/apps/demo/federation.manifest.prod.json @@ -0,0 +1,3 @@ +{ + "portfolio": "/nx-reference/portfolio/remoteEntry.json" +} diff --git a/apps/demo/project.json b/apps/demo/project.json index bc1f342..12851f3 100644 --- a/apps/demo/project.json +++ b/apps/demo/project.json @@ -11,7 +11,8 @@ "options": {}, "configurations": { "production": { - "target": "demo:esbuild:production" + "target": "demo:esbuild:production", + "baseHref": "/nx-reference/" }, "development": { "target": "demo:esbuild:development", diff --git a/apps/demo/src/app/intro.mdx b/apps/demo/src/app/intro.mdx index 46c6cd9..e00ef5b 100644 --- a/apps/demo/src/app/intro.mdx +++ b/apps/demo/src/app/intro.mdx @@ -5,6 +5,9 @@ import { version } from 'root-package-json'; # Nx Reference +Version {version} + + A reference example monorepo with [Nx](https://nx.dev) + [Storybook](https://storybook.js.org) + [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/) in [Angular](https://angular.dev). Made by [Codestar](https://code-star.github.io) powered by [Sopra Steria](https://www.soprasteria.nl). diff --git a/apps/portfolio/project.json b/apps/portfolio/project.json index 5298742..7e92510 100644 --- a/apps/portfolio/project.json +++ b/apps/portfolio/project.json @@ -12,7 +12,8 @@ "options": {}, "configurations": { "production": { - "target": "portfolio:esbuild:production" + "target": "portfolio:esbuild:production", + "baseHref": "/nx-reference/portfolio/" }, "development": { "target": "portfolio:esbuild:development", diff --git a/docs/architecture/adr/0007-github-pages-subdirectory-hosting.md b/docs/architecture/adr/0007-github-pages-subdirectory-hosting.md new file mode 100644 index 0000000..7994a80 --- /dev/null +++ b/docs/architecture/adr/0007-github-pages-subdirectory-hosting.md @@ -0,0 +1,75 @@ +# ADR-0007: Single GitHub Pages site with subdirectory hosting and independent per-app deploys + +> **date:** 2026-07-03\ +> **status:** accepted + +## context + +The Module Federation front-ends were previously hosted from **separate GitHub repositories**, one +per front-end, and the Storybook catalog was deployed at the **Pages root** of this repo (the legacy +`prod.yml` workflow). `hosting.md` frames the choice: GitHub Pages serves only **one** site per repo, +so multiple origins are not possible from a single repo (Option 1), but everything can live under one +Pages site via **subdirectory deployment** (Option 2). + +We want to consolidate the demo into this single monorepo and still host the shell, the remote, and +the Storybook catalog โ€” while keeping the module-federation selling point visible: the host and its +remotes are **decoupled at deploy time**. + +## decision + +Adopt **Option 2 โ€” one GitHub Pages site with subdirectory deployment** for `code-star/nx-reference` +(custom domain `codestar.nl`, project base path `/nx-reference/`): + +| URL | Serves | +| ------------------------------ | ---------------------------------------------------------- | +| `/nx-reference/` | Shell โ€” Native Federation host (`apps/demo`) | +| `/nx-reference/portfolio/` | Portfolio remote + `remoteEntry.json` (`apps/portfolio`) | +| `/nx-reference/storybook/` | Storybook component catalog | + +- **Per-app base href** is set on the **production** build configuration in each `project.json` + (`/nx-reference/` for the shell, `/nx-reference/portfolio/` for the remote). Development + configurations keep base href `/`, so local dev is unchanged. +- **Production federation manifest** (`apps/demo/federation.manifest.prod.json`) points the + `portfolio` remote at `/nx-reference/portfolio/remoteEntry.json`; the committed dev manifest keeps + pointing at `http://localhost:4201`. The deploy job swaps the prod manifest into the built shell. +- **`actions/deploy-pages` for every deployment.** Because it publishes one artifact that replaces + the whole site, the full assembled tree is kept on a `pages-content` **storage branch**; a shared + composite action (`.github/actions/deploy-subdir`) overlays only the changed subdirectory, adds the + SPA fallback (`404.html` + `.nojekyll`), uploads the merged site, and persists it back. +- **Independent, path-filtered workflows** โ€” `deploy-shell.yml`, `deploy-portfolio.yml`, + `deploy-storybook.yml` โ€” so a change confined to `apps/portfolio` rebuilds and deploys **only** the + remote, and a change to `apps/demo` deploys **only** the shell. Shared libraries trigger all three. + All deploys share a `pages` **concurrency group** so storage-branch writes serialise. + +## alternatives considered + +- **Option 1 โ€” multiple `gh-pages`-style branches (one per module):** rejected. A repo has exactly one + Pages source, so multiple branches cannot yield multiple origins (`hosting.md`). +- **Status quo โ€” one repo per front-end:** rejected. More repos, tokens, and CI to maintain; obscures + the monorepo/loose-coupling story the demo exists to tell. +- **Single "rebuild-and-deploy-everything" workflow:** rejected. Simpler, but every remote change would + rebuild and redeploy the shell, hiding the module-federation decoupling advantage. +- **Pages "Deploy from a branch" source instead of `deploy-pages`:** rejected. The Actions artifact + flow (`upload-pages-artifact` + `deploy-pages`) is the current first-class path and was the + requested mechanism. + +## rationale + +- Subdirectory hosting is the only way to serve shell + remote + catalog from one repo's Pages site. +- Native Federation resolves remote chunks relative to `remoteEntry.json`, so a correct base href and + a manifest pointing at `/nx-reference/portfolio/remoteEntry.json` are sufficient for the host to + lazy-load the remote in production. +- Path-filtered per-app workflows make the loose coupling **observable in CI**: independent build and + deploy per micro app. + +## consequences + +- **Easier:** one repo and CI surface; independent per-app deploys; standard `deploy-pages` flow; + local dev untouched (dev manifest + base href `/`). +- **Harder / to note:** + - `deploy-pages` replaces the whole site, so the `pages-content` storage branch is required to + preserve unchanged subdirectories between independent deploys. + - **First run** must seed all three subdirectories โ€” trigger each workflow once + (`workflow_dispatch`) and set **Pages โ†’ Source** to **GitHub Actions**. + - A shared-library change intentionally triggers all three deploys (real coupling through `@star/*`). + - The custom domain `codestar.nl` is managed at the org Pages level; no per-repo `CNAME` is added. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 22ab45e..6cc4230 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -46,8 +46,9 @@ flowchart LR DataAccess["@star/shared/data-access"] --> Types Services["@star/shared/services"] --> Types Server --> Btc["@star/btc"] - subgraph Deployed - SB[Storybook 10 catalog] --> UI + subgraph Deployed["Deployed โ€” one GitHub Pages site (/nx-reference/)"] + Shell["/ shell (demo)"] -. Native Federation .-> Remote["/portfolio remote"] + SB["/storybook catalog"] --> UI SB --> Stories[shared + app stories] end ``` @@ -108,8 +109,10 @@ The Phase-0 open questions below were resolved during implementation: - **Build/DX:** `nx serve demo` + `nx serve portfolio` run the MF demo; `nx run demo:build-storybook` produces the deployable catalog; `nx run-many -t lint,test,build` is green. - **Reliability:** rate fetch failures degrade gracefully (logged alert, no crash) โ€” preserved. -- **Deployability:** Storybook static output hostable on GitHub Pages with correct base-href; prod - remote URL configurable to the Pages URL. +- **Deployability:** one GitHub Pages site (`/nx-reference/`) hosts the shell (root), the + `portfolio` remote (`/portfolio`, also serving `remoteEntry.json`), and the Storybook catalog + (`/storybook`) via subdirectory deployment with per-app base-href. Each app builds and deploys + independently through `actions/deploy-pages` (see ADR-0007). - **Security posture:** demo-grade; server has no secrets; cors open (unchanged, demo scope). ## key decisions @@ -120,6 +123,7 @@ The Phase-0 open questions below were resolved during implementation: - [ADR-0004](./adr/0004-storybook-catalog.md) โ€” Storybook 10 as the deployed component catalog. - [ADR-0005](./adr/0005-testing-strategy.md) โ€” Testing strategy & coverage parity. - [ADR-0006](./adr/0006-standalone-signals.md) โ€” Standalone components + signal APIs. +- [ADR-0007](./adr/0007-github-pages-subdirectory-hosting.md) โ€” Single GitHub Pages site, subdirectory hosting, independent per-app deploys. ## open questions diff --git a/hosting.md b/hosting.md new file mode 100644 index 0000000..9ab880d --- /dev/null +++ b/hosting.md @@ -0,0 +1,78 @@ +I now use multiple github repos to host multiple front-ends for module federation, for a demo project to demonstrate module federation. I use github pages. Can I somehow integrate the repos into a single repo and still host multiple modules on github pages? + +--- + +Yes, you can consolidate into a single repo and still host multiple sites on GitHub Pages. There are two solid approaches: + +**Option 1: Multiple `gh-pages`-style branches (one per module)** + +GitHub Pages can serve from any branch. You can have: +- `gh-pages-shell` โ†’ serves the shell app +- `gh-pages-remote1` โ†’ serves remote1 +- `gh-pages-remote2` โ†’ serves remote2 + +Each gets its own GitHub Pages URL like `https://.github.io//` ... but actually this doesn't work cleanly โ€” a repo only has **one** Pages site, served from one source. So this approach alone won't give you multiple origins. + +**Option 2: Subdirectory deployment (recommended)** + +Host everything under one Pages site using subdirectories: + +``` +https://.github.io//shell/ +https://.github.io//remote1/ +https://.github.io//remote2/ +``` + +Your repo structure: + +``` +/ +โ”œโ”€โ”€ packages/ +โ”‚ โ”œโ”€โ”€ shell/ +โ”‚ โ”œโ”€โ”€ remote1/ +โ”‚ โ””โ”€โ”€ remote2/ +โ””โ”€โ”€ .github/workflows/deploy.yml +``` + +In each app's webpack/vite config, set `publicPath` / `base` to match: + +```js +// vite.config.js for remote1 +base: '/your-repo-name/remote1/' +``` + +And in the shell's module federation config, point remotes at the deployed paths: + +```js +remotes: { + remote1: 'remote1@https://.github.io//remote1/remoteEntry.js', + remote2: 'remote2@https://.github.io//remote2/remoteEntry.js', +} +``` + +Your deploy workflow builds each package and puts the output in the right subdirectory: + +```yaml +- name: Build all + run: | + npm run build --workspace=packages/shell + npm run build --workspace=packages/remote1 + npm run build --workspace=packages/remote2 + +- name: Arrange dist + run: | + mkdir -p dist/shell dist/remote1 dist/remote2 + cp -r packages/shell/dist/* dist/shell/ + cp -r packages/remote1/dist/* dist/remote1/ + cp -r packages/remote2/dist/* dist/remote2/ + +- name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist +``` + +**Bonus: you can still develop independently** using an npm/pnpm workspaces monorepo โ€” each package has its own `package.json`, and the root orchestrates builds. This is probably the cleanest fit for a demo project since it keeps module boundaries clear while unifying the repo and CI. + +The main thing to watch is that all `remoteEntry.js` URLs and `publicPath` values match the subdirectory structure โ€” module federation is sensitive to those base URLs being exactly right. diff --git a/package.json b/package.json index 45bb5dc..e646f57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nx-reference", - "version": "2.0.2", + "version": "2.2.0", "license": "MIT", "scripts": { "docs:json": "compodoc -p ./tsconfig.doc.json -e json -d ."