Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions module/quest3Tier/ui/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license SPDX-FileCopyrightText: © 2026 Zenme Pty Ltd <info@zenme.com.au>
* @license SPDX-License-Identifier: MIT
*/
// @ts-nocheck

import { useRoutes } from "react-router-dom";
import { useEffect, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import * as AddQuestionModule from "./routes/AddQuestion";
import * as AnswerQuestionModule from "./routes/AnswerQuestion";
import * as EditQuestionModule from "./routes/EditQuestion";
import * as FollowUpQuestionModule from "./routes/FollowUpQuestion";
import * as QuestionCombinationListModule from "./routes/QuestionCombinationList";
import * as QuestionDetailModule from "./routes/QuestionDetail";
import * as QuestionDetailAddModule from "./routes/QuestionDetailAdd";
import * as ShareQuestionModule from "./routes/ShareQuestion";

type FrameworkRouteModule = {
default: (props: { loaderData?: unknown }) => JSX.Element;
clientLoader?: (args: { params: Record<string, string | undefined> }) => Promise<unknown> | unknown;
};

function RouteModuleElement({ routeModule }: { routeModule: FrameworkRouteModule }) {
const location = useLocation();
const params = useParams();
const [loaderData, setLoaderData] = useState<unknown>(undefined);
const [isLoading, setIsLoading] = useState(Boolean(routeModule.clientLoader));

useEffect(() => {
let isMounted = true;

const load = async () => {
if (!routeModule.clientLoader) {
setIsLoading(false);
return;
}

setIsLoading(true);
const nextData = await routeModule.clientLoader({ params });
if (isMounted) {
setLoaderData(nextData);
setIsLoading(false);
}
};

load();

return () => {
isMounted = false;
};
}, [routeModule, location.pathname, location.search, params]);
Comment on lines +30 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== AppRoutes.tsx ==\n'
sed -n '1,220p' module/quest3Tier/ui/AppRoutes.tsx

printf '\n== QuestionDetailAdd.tsx ==\n'
sed -n '1,220p' module/quest3Tier/ui/routes/QuestionDetailAdd.tsx

printf '\n== Search for clientLoader / redirect usage in quest3Tier UI ==\n'
rg -n "clientLoader|redirect\(" module/quest3Tier/ui

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 11384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== QuestionCombinationList.tsx ==\n'
sed -n '1,120p' module/quest3Tier/ui/routes/QuestionCombinationList.tsx

printf '\n== AnswerQuestion2.tsx ==\n'
sed -n '1,140p' module/quest3Tier/ui/routes/AnswerQuestion2.tsx

printf '\n== Any other redirect/error patterns in quest3Tier routes ==\n'
rg -n "throw new Error|return redirect\(" module/quest3Tier/ui/routes

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 10119


Handle loader errors and redirect responses in RouteModuleElement (module/quest3Tier/ui/AppRoutes.tsx:34-45) await routeModule.clientLoader(...) can reject and leave isLoading stuck on true, and a returned redirect() response is currently stored as loaderData instead of navigating. Catch failures, clear loading in a finally, and special-case Response redirects before rendering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@module/quest3Tier/ui/AppRoutes.tsx` around lines 30 - 52, In
RouteModuleElement’s load effect, clientLoader can reject or return a redirect
Response, so loading may stay true and redirects may be rendered as data. Update
the load flow around routeModule.clientLoader in AppRoutes.tsx to catch errors,
always clear isLoading in a finally block, and detect Response redirects before
calling setLoaderData so navigation happens instead of storing the response.


if (isLoading) {
return null;
}

const RouteComponent = routeModule.default;
return <RouteComponent loaderData={loaderData} />;
}

export default function Quest3TierAppRoutes() {
return useRoutes([
{ index: true, element: <RouteModuleElement routeModule={QuestionCombinationListModule} /> },
{ path: "add", element: <RouteModuleElement routeModule={AddQuestionModule} /> },
{ path: ":id", element: <RouteModuleElement routeModule={QuestionDetailModule} /> },
{ path: ":id/add", element: <RouteModuleElement routeModule={QuestionDetailAddModule} /> },
{ path: ":id/answer", element: <RouteModuleElement routeModule={AnswerQuestionModule} /> },
{ path: ":id/followUp", element: <RouteModuleElement routeModule={FollowUpQuestionModule} /> },
{ path: ":id/edit", element: <RouteModuleElement routeModule={EditQuestionModule} /> },
{ path: ":id/share", element: <RouteModuleElement routeModule={ShareQuestionModule} /> },
]);
}
11 changes: 11 additions & 0 deletions module/quest3Tier/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>quest3Tier remote</title>
</head>
<body>
<script type="module" src="./AppRoutes.tsx"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions module/quest3Tier/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"type": "module",
"scripts": {
"build": "cross-env NODE_ENV=production react-router build",
"build:federation": "vite build",
"dev": "react-router dev",
"dev:federation": "vite",
"preview:federation": "vite preview --port 5174 --strictPort",
"start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
Expand Down Expand Up @@ -36,6 +39,7 @@
},
"devDependencies": {
"@eslint/js": "^9.38.0",
"@originjs/vite-plugin-federation": "^1.4.1",
"@react-router/dev": "^7.6.2",
"@types/lodash": "^4.17.17",
"@types/node": "^20",
Expand Down
39 changes: 39 additions & 0 deletions module/quest3Tier/ui/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license SPDX-FileCopyrightText: © 2026 Zenme Pty Ltd <info@zenme.com.au>
* @license SPDX-License-Identifier: MIT
*/

import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
esbuild: {
jsx: "automatic", // this is just a workaround for not having tsconfig.json set up yet
},
plugins: [
federation({
name: "quest3TierRemote",
filename: "remoteEntry.js",
exposes: {
"./AppRoutes": "./AppRoutes.tsx",
},
shared: ["react", "react-dom", "react-router", "react-router-dom"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@originjs/vite-plugin-federation shared singleton react

💡 Result:

In @originjs/vite-plugin-federation, the singleton property for shared dependencies is a point of significant community discussion and inconsistent behavior due to changes in the plugin's internal implementation [1][2][3]. While many developers continue to include singleton: true in their configuration for dependencies like react and react-dom [1][4], its effectiveness is not guaranteed in recent versions of the plugin [2][3]. Users have reported that shared dependencies are sometimes loaded multiple times despite setting singleton: true [1][2]. Key considerations regarding shared singletons and React: 1. Implementation Status: The singleton property was removed from or commented out in the plugin's type definitions in certain versions [1][2]. Whether it remains functional depends on the specific version of the plugin being used; some users report it works in earlier versions (e.g., 1.1.8) but breaks in later releases (e.g., 1.2.x) [2][3]. 2. Alternative Configurations: When encountering multiple instances of React, some users have found success by adjusting their Vite configuration—specifically, setting the JSX runtime to classic instead of automatic [2]. vite.config.js plugins: [ react({ jsxRuntime: 'classic' }) ] 3. Versioning Issues: Ensure that host and remote applications are using compatible versions of shared dependencies [5][6]. Mismatched versions can lead the plugin to fallback to local copies, effectively ignoring the singleton requirement [5][6]. 4. Ecosystem Limitations: The official documentation does not explicitly detail the current status of the singleton flag, and the maintainers have acknowledged challenges with supporting shared dependencies—particularly when mixing Vite and Webpack, or when handling CommonJS modules [5][3][4]. If you are facing issues with multiple React instances, consider checking your plugin version and verifying that both host and remote are configured with identical shared dependencies and version requirements [1][5][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant Vite configs and the federation plugin version.
git ls-files 'module/quest3Tier/ui/vite.config.ts' 'ui/vite.config.ts' '**/package.json' '**/pnpm-lock.yaml' '**/package-lock.json' '**/yarn.lock' | sed -n '1,200p'

printf '\n--- vite.config.ts files ---\n'
for f in module/quest3Tier/ui/vite.config.ts ui/vite.config.ts; do
  if [ -f "$f" ]; then
    echo "### $f"
    cat -n "$f" | sed -n '1,220p'
    echo
  fi
done

printf '\n--- package.json references ---\n'
rg -n '"`@originjs/vite-plugin-federation`"|singleton|react-router|react-dom|react-router"' -g 'package.json' -g 'pnpm-lock.yaml' -g 'package-lock.json' -g 'yarn.lock' -g 'vite.config.ts' .

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 31200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the package metadata for the federation plugin version in the workspace that owns the file.
fd -a 'package.json' .

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 954


🌐 Web query:

@originjs/vite-plugin-federation 1.4.1 singleton shared react documentation

💡 Result:

In @originjs/vite-plugin-federation, support for the singleton property in the shared configuration has been inconsistent and subject to changes in internal type definitions [1][2]. While earlier versions supported it explicitly, subsequent updates led to reports where the property appeared ignored or missing from TypeScript definitions, even if functionality remained for some users [1][2]. If you are using @originjs/vite-plugin-federation and encountering issues with multiple React instances: 1. Configuration: You can attempt to define singleton in your shared object as follows [1][3]: shared: { react: { singleton: true, requiredVersion: "^18.3.1" }, 'react-dom': { singleton: true, requiredVersion: "^18.3.1" } } 2. Compatibility Notes: Users have reported that issues with multiple React instances may persist due to how Vite/Rollup handles shared modules versus Webpack [4][3]. Some developers have mitigated this by using the 'classic' JSX runtime in @vitejs/plugin-react instead of the automatic runtime to resolve hydration or instance-duplication errors [2]. 3. Alternative Plugin: If you require robust, well-documented support for Module Federation in Vite, consider the @module-federation/vite plugin (often used with Nx or standalone) [5][6]. This plugin explicitly documents and maintains first-class support for singleton: true in shared dependency configurations [5][7]. Note: Ensure you do not confuse @originjs/vite-plugin-federation with newer community plugins under the @module-federation scope, which offer more standardized API support for modern Module Federation features [8][5][7].

Citations:


🌐 Web query:

site:github.com/originjs/vite-plugin-federation singleton shared react 1.4.1

💡 Result:

In originjs/vite-plugin-federation, the latest release shown on GitHub is v1.4.1 (Apr 12, 2025). The README documents shared for dependencies like React, and warns that mixing Vite and Webpack in React projects can cause shared chunk issues. (github.com)

For React specifically, a GitHub issue reports that sharing react, react-dom, and react-router-dom can still create multiple instances, and that adding singleton: true was the workaround in both host and remote configs. (github.com)

If you want, I can turn that into the exact vite.config.js config snippet for host/remote.

Citations:


Use singleton shared deps for React and router. The array shorthand here can still let host and remote load separate React/router instances; switch these entries to object form with singleton: true and keep the versions aligned across all federation configs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@module/quest3Tier/ui/vite.config.ts` at line 21, The shared dependency setup
in the Vite federation config is using array shorthand, which can allow separate
React and router instances to be loaded. Update the shared config in the Vite
federation settings to use object entries for react, react-dom, react-router,
and react-router-dom with singleton enabled, and make sure their versions stay
aligned with the other federation configs so host and remote resolve the same
instances.

}),
],
build: {
target: "esnext",
minify: false,
modulePreload: false,
cssCodeSplit: false,
},
resolve: {
alias: {
"@zenmechat/shared-ui": fileURLToPath(new URL("../../../ui", import.meta.url)),
},
},
server: {
port: 5174,
strictPort: true,
},
});
73 changes: 73 additions & 0 deletions module/quest5Tier/ui/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license SPDX-FileCopyrightText: © 2026 Zenme Pty Ltd <info@zenme.com.au>
* @license SPDX-License-Identifier: MIT
*/
// @ts-nocheck

import { useRoutes } from "react-router-dom";
import { useEffect, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import * as AddQuestionModule from "./routes/AddQuestion";
import * as AnswerQuestionModule from "./routes/AnswerQuestion";
import * as EditQuestionModule from "./routes/EditQuestion";
import * as FollowUpQuestionModule from "./routes/FollowUpQuestion";
import * as QuestionCombinationListModule from "./routes/QuestionCombinationList";
import * as QuestionDetailModule from "./routes/QuestionDetail";
import * as QuestionDetailAddModule from "./routes/QuestionDetailAdd";
import * as ShareQuestionModule from "./routes/ShareQuestion";

type FrameworkRouteModule = {
default: (props: { loaderData?: unknown }) => JSX.Element;
clientLoader?: (args: { params: Record<string, string | undefined> }) => Promise<unknown> | unknown;
};

function RouteModuleElement({ routeModule }: { routeModule: FrameworkRouteModule }) {
const location = useLocation();
const params = useParams();
const [loaderData, setLoaderData] = useState<unknown>(undefined);
const [isLoading, setIsLoading] = useState(Boolean(routeModule.clientLoader));

useEffect(() => {
let isMounted = true;

const load = async () => {
if (!routeModule.clientLoader) {
setIsLoading(false);
return;
}

setIsLoading(true);
const nextData = await routeModule.clientLoader({ params });
if (isMounted) {
setLoaderData(nextData);
setIsLoading(false);
}
};
Comment on lines +33 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled clientLoader rejection leaves the route stuck loading forever.

Per the upstream AnswerQuestion.tsx clientLoader, loaders can throw new Response(...) on invalid/missing input. load() has no try/catch, so a thrown rejection never reaches setIsLoading(false), leaves isLoading true, and the component renders null indefinitely with no error surfaced to the user (also produces an unhandled promise rejection).

🐛 Proposed fix
     const load = async () => {
       if (!routeModule.clientLoader) {
         setIsLoading(false);
         return;
       }
 
       setIsLoading(true);
-      const nextData = await routeModule.clientLoader({ params });
-      if (isMounted) {
-        setLoaderData(nextData);
-        setIsLoading(false);
-      }
+      try {
+        const nextData = await routeModule.clientLoader({ params });
+        if (isMounted) {
+          setLoaderData(nextData);
+        }
+      } catch (error) {
+        console.error("Route loader failed:", error);
+        if (isMounted) {
+          setLoaderData(undefined);
+        }
+      } finally {
+        if (isMounted) {
+          setIsLoading(false);
+        }
+      }
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const load = async () => {
if (!routeModule.clientLoader) {
setIsLoading(false);
return;
}
setIsLoading(true);
const nextData = await routeModule.clientLoader({ params });
if (isMounted) {
setLoaderData(nextData);
setIsLoading(false);
}
};
const load = async () => {
if (!routeModule.clientLoader) {
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const nextData = await routeModule.clientLoader({ params });
if (isMounted) {
setLoaderData(nextData);
}
} catch (error) {
console.error("Route loader failed:", error);
if (isMounted) {
setLoaderData(undefined);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@module/quest5Tier/ui/AppRoutes.tsx` around lines 33 - 45, The load flow in
AppRoutes.tsx leaves the route stuck when routeModule.clientLoader rejects,
because setIsLoading(false) is only reached on success. Wrap the await in load()
with try/catch/finally so isLoading is always cleared, and surface the failure
through the route’s existing error handling instead of letting an unhandled
rejection escape. Keep the fix localized to load() and its use of
routeModule.clientLoader, setLoaderData, and setIsLoading.


load();

return () => {
isMounted = false;
};
}, [routeModule, location.pathname, location.search, params]);

if (isLoading) {
return null;
}

const RouteComponent = routeModule.default;
return <RouteComponent loaderData={loaderData} />;
}

export default function Quest5TierAppRoutes() {
return useRoutes([
{ index: true, element: <RouteModuleElement routeModule={QuestionCombinationListModule} /> },
{ path: "add", element: <RouteModuleElement routeModule={AddQuestionModule} /> },
{ path: ":id", element: <RouteModuleElement routeModule={QuestionDetailModule} /> },
{ path: ":id/add", element: <RouteModuleElement routeModule={QuestionDetailAddModule} /> },
{ path: ":id/answer", element: <RouteModuleElement routeModule={AnswerQuestionModule} /> },
{ path: ":id/followUp", element: <RouteModuleElement routeModule={FollowUpQuestionModule} /> },
{ path: ":id/edit", element: <RouteModuleElement routeModule={EditQuestionModule} /> },
{ path: ":id/share", element: <RouteModuleElement routeModule={ShareQuestionModule} /> },
]);
}
11 changes: 11 additions & 0 deletions module/quest5Tier/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>quest5Tier remote</title>
</head>
<body>
<script type="module" src="./AppRoutes.tsx"></script>
</body>
Comment on lines +8 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does @originjs/vite-plugin-federation remote index.html need its own ReactDOM render entry, or does the host always own mounting?

💡 Result:

In the Module Federation architecture utilized by @originjs/vite-plugin-federation, the host application (or shell) always owns the mounting process [1][2]. A remote application does not need its own ReactDOM render entry (i.e., it should not have its own self-contained root mount point) when running as a federated module [1]. Instead, the standard pattern is as follows: 1. Host Ownership: The host application acts as the shell and is responsible for defining the page structure, routing, and providing the mount point for remote components [1][2]. 2. Remote Role: Remote applications act as providers of components, routes, or modules [1][2]. They expose these assets via the plugin, which are then imported and rendered by the host [3][1]. 3. Rendering: The host imports the remote component (typically using dynamic imports like React.lazy) and renders it within its own component tree [4][1]. The remote component mounts into the DOM elements managed by the host [1]. If a remote app has its own index.html with a dedicated ReactDOM render entry, that entry is only intended for when the remote is run in standalone mode (e.g., for development or testing in isolation) [5]. When loaded as a federated remote, that standalone entry point is bypassed; attempting to mount a remote independently within a host's page—or having multiple competing render entries—can cause conflicts, blank screens, or shared dependency initialization issues [4][6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'ROOT FILES:\n'
git ls-files | sed -n '1,200p' | grep -E '^(module/quest5Tier/ui/|package.json|vite\.config|tsconfig|src/|module/quest5Tier/)'

printf '\nINDEX.HTML:\n'
cat -n module/quest5Tier/ui/index.html

printf '\nAPPROUTES SEARCH:\n'
rg -n "useRoutes|createRoot|ReactDOM|Router|BrowserRouter|HashRouter|Routes|Route|useParams|useLocation" module/quest5Tier/ui -S

printf '\nPACKAGE.JSON:\n'
cat -n package.json

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'FILE TREE (quest5Tier ui):\n'
find module/quest5Tier/ui -maxdepth 2 -type f | sort

printf '\nOUTLINE AppRoutes.tsx:\n'
ast-grep outline module/quest5Tier/ui/AppRoutes.tsx --view expanded || true

printf '\nREAD AppRoutes.tsx:\n'
cat -n module/quest5Tier/ui/AppRoutes.tsx

printf '\nREAD main-ish entry files:\n'
for f in module/quest5Tier/ui/main.tsx module/quest5Tier/ui/main.ts module/quest5Tier/ui/index.tsx module/quest5Tier/ui/index.ts; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f"
  fi
done

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 4496


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'SEARCH FOR VITE FEDERATION CONFIG / STANDALONE ENTRY:\n'
rg -n "federation|exposes|remotes|dev:federation|preview:federation|standalone|index.html|createRoot|hydrateRoot" -S . --glob '!**/node_modules/**'

printf '\nSEARCH index.html scripts in ui:\n'
rg -n "<script|type=\"module\"" module/quest5Tier/ui/index.html -n -S

printf '\nCHECK if AppRoutes.tsx imports ReactDOM or Router providers:\n'
python3 - <<'PY'
from pathlib import Path
p = Path("module/quest5Tier/ui/AppRoutes.tsx")
text = p.read_text()
for needle in ["ReactDOM", "createRoot", "BrowserRouter", "HashRouter", "RouterProvider", "useRoutes", "useParams", "useLocation"]:
    print(needle, needle in text)
PY

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 11796


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- module/quest5Tier/ui/package.json ---'
cat -n module/quest5Tier/ui/package.json

echo
echo '--- module/quest5Tier/ui/vite.config.ts ---'
cat -n module/quest5Tier/ui/vite.config.ts

echo
echo '--- module/quest5Tier/ui/README.md (if present) ---'
if [ -f module/quest5Tier/ui/README.md ]; then
  cat -n module/quest5Tier/ui/README.md
else
  echo '(absent)'
fi

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 3894


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- host route references to quest5Tier remote ---'
rg -n "quest5Tier|Quest5TierAppRoutes|AppRoutes" ui module/quest5Tier -S

echo
echo '--- module/quest5Tier/ui/vite.config.ts (imports and exposes) ---'
ast-grep outline module/quest5Tier/ui/vite.config.ts --view expanded || true
cat -n module/quest5Tier/ui/vite.config.ts

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 8115


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("module/quest5Tier/ui/vite.config.ts")
text = p.read_text()
for needle in ["exposes", "remotes", "build:federation", "preview:federation", "dev:federation", "base", "server", "preview"]:
    idx = text.find(needle)
    print(needle, idx)
PY

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- module/quest5Tier/ui/package.json ---'
cat -n module/quest5Tier/ui/package.json

echo
echo '--- module/quest5Tier/ui/vite.config.ts ---'
cat -n module/quest5Tier/ui/vite.config.ts

echo
echo '--- host / remote references ---'
rg -n "quest5Tier|Quest5TierAppRoutes|AppRoutes" ui module/quest5Tier -S

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 10422


Add a standalone mount for the federated UI. module/quest5Tier/ui/index.html only loads AppRoutes.tsx; it never calls createRoot(...).render(...) or provides a Router, so dev:federation/preview:federation will open to a blank page and router hooks can fail when run outside the host.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@module/quest5Tier/ui/index.html` around lines 8 - 10, The federated UI entry
currently only loads AppRoutes.tsx and does not mount the app itself, so add a
standalone bootstrap that calls createRoot(...).render(...) and wraps the routes
in an appropriate Router for the module/quest5Tier/ui entry. Update the UI entry
path referenced by index.html so dev:federation and preview:federation render
the app independently while keeping the existing AppRoutes component as the
route definition.

</html>
4 changes: 4 additions & 0 deletions module/quest5Tier/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"type": "module",
"scripts": {
"build": "cross-env NODE_ENV=production react-router build",
"build:federation": "vite build",
"dev": "react-router dev",
"dev:federation": "vite",
"preview:federation": "vite preview --port 5175 --strictPort",
"start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
Expand Down Expand Up @@ -36,6 +39,7 @@
},
"devDependencies": {
"@eslint/js": "^9.38.0",
"@originjs/vite-plugin-federation": "^1.4.1",
"@react-router/dev": "^7.6.2",
"@types/lodash": "^4.17.17",
"@types/node": "^20",
Expand Down
39 changes: 39 additions & 0 deletions module/quest5Tier/ui/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license SPDX-FileCopyrightText: © 2026 Zenme Pty Ltd <info@zenme.com.au>
* @license SPDX-License-Identifier: MIT
*/

import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";
import federation from "@originjs/vite-plugin-federation";

export default defineConfig({
esbuild: {
jsx: "automatic", // this is just a workaround for not having tsconfig.json set up yet
},
plugins: [
federation({
name: "quest5TierRemote",
filename: "remoteEntry.js",
exposes: {
"./AppRoutes": "./AppRoutes.tsx",
},
shared: ["react", "react-dom", "react-router", "react-router-dom"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files matching vite.config.ts ==\n'
git ls-files '**/vite.config.ts'

printf '\n== Relevant configs in module/quest5Tier/ui ==\n'
ast-grep outline module/quest5Tier/ui/vite.config.ts --view expanded || true
cat -n module/quest5Tier/ui/vite.config.ts | sed -n '1,120p'

printf '\n== Search for shared federation configs ==\n'
rg -n --glob 'vite.config.ts' 'shared\s*:' module ui . || true
rg -n --glob 'vite.config.ts' '`@originjs/vite-plugin-federation`|singleton|requiredVersion|shared\s*:\s*\[' module ui . || true

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 4272


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Package manifests ==\n'
git ls-files 'package.json' '**/package.json'

printf '\n== React/router dependency declarations ==\n'
for f in package.json ui/package.json module/quest3Tier/ui/package.json module/quest5Tier/ui/package.json module/quest5TierEg/ui/package.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat "$f" | python3 - <<'PY'
import json,sys
data=json.load(sys.stdin)
for section in ("dependencies","devDependencies","peerDependencies"):
    deps=data.get(section,{})
    hits={k:v for k,v in deps.items() if k in {"react","react-dom","react-router","react-router-dom","`@originjs/vite-plugin-federation`"}}
    if hits:
        print(section, hits)
PY
  fi
done

printf '\n== Any federation singleton examples ==\n'
rg -n 'singleton\s*:\s*true|requiredVersion\s*:\s*' . || true

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 1495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Package manifests ==\n'
git ls-files 'package.json' '**/package.json'

printf '\n== React/router dependency declarations ==\n'
for f in package.json ui/package.json module/quest3Tier/ui/package.json module/quest5Tier/ui/package.json module/quest5TierEg/ui/package.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    python3 - <<'PY' "$f"
import json,sys
path=sys.argv[1]
with open(path) as fh:
    data=json.load(fh)
for section in ("dependencies","devDependencies","peerDependencies"):
    deps=data.get(section,{})
    hits={k:v for k,v in deps.items() if k in {"react","react-dom","react-router","react-router-dom","`@originjs/vite-plugin-federation`"}}
    if hits:
        print(section, hits)
PY
  fi
done

printf '\n== Any federation singleton examples ==\n'
rg -n 'singleton\s*:\s*true|requiredVersion\s*:\s*' . || true

Repository: ZenMe-AU/ZBReactArchitecture

Length of output: 1660


🌐 Web query:

@originjs/vite-plugin-federation shared array singleton requiredVersion docs

💡 Result:

In vite-plugin-federation, the shared configuration allows you to define dependencies shared between host and remote modules using either an array or an object [1][2]. When using an object configuration, each shared package can be defined with specific settings, including singleton and requiredVersion [1][2]. singleton: boolean When set to true, this option ensures that only one instance of the shared module is used across the host and all remotes [1][2]. This is commonly required for libraries that maintain internal state, such as React, to prevent issues like multiple instances of hooks (e.g., TypeError: Cannot read properties of null (reading 'useState')) [3][4]. requiredVersion: string This setting specifies the version range required from the host [5][6]. It is primarily used by the remote side [5][6]. If the version of the module provided by the host does not meet the specified requiredVersion, the remote will fall back to using its own bundled version, provided that import is not set to false [5][6]. Example configuration: shared: { react: { singleton: true, requiredVersion: '^18.3.1', }, 'react-dom': { singleton: true, requiredVersion: '^18.3.1', }, } Note: While older versions or specific forks (such as the jskits/vite-plugin-federation) explicitly document these fields in their API, users of the original @originjs/vite-plugin-federation have also reported using the singleton option to resolve dependency conflicts [3][4]. Always ensure your implementation matches the specific version of the plugin you are using [7].

Citations:


Use singleton shared deps for React/router
Switch shared to object form and mark react, react-dom, react-router, and react-router-dom as singleton: true in this remote and the host. The current array form can load separate router copies, and this repo already mixes router versions between ui and the quest-tier remotes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@module/quest5Tier/ui/vite.config.ts` at line 21, The shared dependency setup
currently uses array form, which can allow separate React/router instances to be
loaded; update the shared config in the Vite setup to object form and mark
react, react-dom, react-router, and react-router-dom as singleton: true. Apply
the same singleton sharing in both this remote and the host so the quest-tier UI
and existing remotes all resolve to one React/router instance.

}),
],
build: {
target: "esnext",
minify: false,
modulePreload: false,
cssCodeSplit: false,
},
resolve: {
alias: {
"@zenmechat/shared-ui": fileURLToPath(new URL("../../../ui", import.meta.url)),
},
},
server: {
port: 5175,
strictPort: true,
},
});
73 changes: 73 additions & 0 deletions module/quest5TierEg/ui/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license SPDX-FileCopyrightText: © 2026 Zenme Pty Ltd <info@zenme.com.au>
* @license SPDX-License-Identifier: MIT
*/
// @ts-nocheck

import { useRoutes } from "react-router-dom";
import { useEffect, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import * as AddQuestionModule from "./routes/AddQuestion";
import * as AnswerQuestionModule from "./routes/AnswerQuestion";
import * as EditQuestionModule from "./routes/EditQuestion";
import * as FollowUpQuestionModule from "./routes/FollowUpQuestion";
import * as QuestionCombinationListModule from "./routes/QuestionCombinationList";
import * as QuestionDetailModule from "./routes/QuestionDetail";
import * as QuestionDetailAddModule from "./routes/QuestionDetailAdd";
import * as ShareQuestionModule from "./routes/ShareQuestion";

type FrameworkRouteModule = {
default: (props: { loaderData?: unknown }) => JSX.Element;
clientLoader?: (args: { params: Record<string, string | undefined> }) => Promise<unknown> | unknown;
};

function RouteModuleElement({ routeModule }: { routeModule: FrameworkRouteModule }) {
const location = useLocation();
const params = useParams();
const [loaderData, setLoaderData] = useState<unknown>(undefined);
const [isLoading, setIsLoading] = useState(Boolean(routeModule.clientLoader));

useEffect(() => {
let isMounted = true;

const load = async () => {
if (!routeModule.clientLoader) {
setIsLoading(false);
return;
}

setIsLoading(true);
const nextData = await routeModule.clientLoader({ params });
if (isMounted) {
setLoaderData(nextData);
setIsLoading(false);
}
};

load();

return () => {
isMounted = false;
};
}, [routeModule, location.pathname, location.search, params]);

if (isLoading) {
return null;
}

const RouteComponent = routeModule.default;
return <RouteComponent loaderData={loaderData} />;
}

export default function Quest5TierEgAppRoutes() {
return useRoutes([
{ index: true, element: <RouteModuleElement routeModule={QuestionCombinationListModule} /> },
{ path: "add", element: <RouteModuleElement routeModule={AddQuestionModule} /> },
{ path: ":id", element: <RouteModuleElement routeModule={QuestionDetailModule} /> },
{ path: ":id/add", element: <RouteModuleElement routeModule={QuestionDetailAddModule} /> },
{ path: ":id/answer", element: <RouteModuleElement routeModule={AnswerQuestionModule} /> },
{ path: ":id/followUp", element: <RouteModuleElement routeModule={FollowUpQuestionModule} /> },
{ path: ":id/edit", element: <RouteModuleElement routeModule={EditQuestionModule} /> },
{ path: ":id/share", element: <RouteModuleElement routeModule={ShareQuestionModule} /> },
]);
}
11 changes: 11 additions & 0 deletions module/quest5TierEg/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>quest5TierEg remote</title>
</head>
<body>
<script type="module" src="./AppRoutes.tsx"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions module/quest5TierEg/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"type": "module",
"scripts": {
"build": "cross-env NODE_ENV=production react-router build",
"build:federation": "vite build",
"dev": "react-router dev",
"dev:federation": "vite",
"preview:federation": "vite preview --port 5176 --strictPort",
"start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
Expand Down Expand Up @@ -35,6 +38,7 @@
"tiny-invariant": "^1.3.3"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.4.1",
"@react-router/dev": "^7.6.2",
"@types/lodash": "^4.17.17",
"@types/node": "^20",
Expand Down
Loading