feat(portal-shell): implement portal-shell with apollo-router integration

M8: portal-shell unified frontend shell (Modular Monolith + micro-kernel).

- Apollo Client -> apollo-router (port 4010, RSC prefetch)

- 5 layouts: classic/focus/split/triple/canvas

- Registry + PluginLoader (dynamic import ssr:false)

- 3-layer props merge, Zustand PluginStore

- 4 widgets: grades/notification-bell/user-menu/class-selector

- config-service: new pluginConfig GraphQL resolver

- apollo-router: CORS + header propagation for portal-shell

- docker-compose.yml: portal-shell service block
This commit is contained in:
SpecialX
2026-07-15 08:06:09 +08:00
parent 47e950c664
commit 514e26ebb4
49 changed files with 2802 additions and 6 deletions

226
pnpm-lock.yaml generated
View File

@@ -182,7 +182,7 @@ importers:
version: 5.101.2(react@18.3.0)
'@urql/next':
specifier: ^1.1.0
version: 1.1.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))
version: 1.1.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))
clsx:
specifier: ^2.1.0
version: 2.1.1
@@ -194,7 +194,7 @@ importers:
version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
nuqs:
specifier: ^1.19.0
version: 1.19.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))
version: 1.19.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))
react:
specifier: ^18.3.0
version: 18.3.0
@@ -303,6 +303,82 @@ importers:
specifier: ^4.2.0
version: 4.2.4
apps/portal-shell:
dependencies:
'@apollo/client':
specifier: ^3.11.0
version: 3.14.1(@types/react@18.3.31)(graphql-ws@6.1.0(graphql@16.14.2)(ws@8.21.0))(graphql@16.14.2)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)(subscriptions-transport-ws@0.11.0(graphql@16.14.2))
'@edu/hooks':
specifier: workspace:*
version: link:../../packages/hooks
'@edu/ui-components':
specifier: workspace:*
version: link:../../packages/ui-components
'@edu/ui-tokens':
specifier: workspace:*
version: link:../../packages/ui-tokens
graphql:
specifier: ^16.8.0
version: 16.14.2
next:
specifier: ^14.2.0
version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
react:
specifier: ^18.3.0
version: 18.3.0
react-dom:
specifier: ^18.3.0
version: 18.3.0(react@18.3.0)
swr:
specifier: ^2.2.0
version: 2.4.2(react@18.3.0)
zustand:
specifier: ^4.5.0
version: 4.5.0(@types/react@18.3.31)(react@18.3.0)
devDependencies:
'@testing-library/jest-dom':
specifier: ^6.4.0
version: 6.9.1
'@testing-library/react':
specifier: ^16.0.0
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
'@types/node':
specifier: ^22.0.0
version: 22.20.1
'@types/react':
specifier: ^18.3.0
version: 18.3.31
'@types/react-dom':
specifier: ^18.3.0
version: 18.3.7(@types/react@18.3.31)
'@vitejs/plugin-react':
specifier: ^4.3.0
version: 4.3.0(vite@5.0.0(@types/node@22.20.1)(terser@5.49.0))
autoprefixer:
specifier: ^10.4.0
version: 10.5.2(postcss@8.5.18)
eslint:
specifier: ^9.0.0
version: 9.39.5(jiti@2.7.0)
eslint-config-prettier:
specifier: ^9.1.0
version: 9.1.2(eslint@9.39.5(jiti@2.7.0))
jsdom:
specifier: ^25.0.0
version: 25.0.0
postcss:
specifier: ^8.4.0
version: 8.5.18
tailwindcss:
specifier: ^3.4.0
version: 3.4.19(tsx@4.23.1)(yaml@2.9.0)
typescript:
specifier: ^5.6.0
version: 5.6.2
vitest:
specifier: ^2.0.0
version: 2.1.0(@types/node@22.20.1)(jsdom@25.0.0)(msw@2.15.0(@types/node@22.20.1)(typescript@5.6.2))(terser@5.49.0)
apps/student-portal:
dependencies:
'@edu/contracts':
@@ -343,7 +419,7 @@ importers:
version: 3.20.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)
nuqs:
specifier: ^1.19.0
version: 1.19.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))
version: 1.19.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))
react:
specifier: ^18.3.0
version: 18.3.0
@@ -1648,6 +1724,24 @@ packages:
peerDependencies:
graphql: 14.x || 15.x || 16.x
'@apollo/client@3.14.1':
resolution: {integrity: sha512-SgGX6E23JsZhUdG2anxiyHvEvvN6CUaI4ZfMsndZFeuHPXL3H0IsaiNAhLITSISbeyeYd+CBd9oERXQDdjXWZw==}
peerDependencies:
graphql: ^15.0.0 || ^16.0.0
graphql-ws: ^5.5.5 || ^6.0.3
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc
subscriptions-transport-ws: ^0.9.0 || ^0.11.0
peerDependenciesMeta:
graphql-ws:
optional: true
react:
optional: true
react-dom:
optional: true
subscriptions-transport-ws:
optional: true
'@apollo/federation-internals@2.14.2':
resolution: {integrity: sha512-6+de7KrV53x3I+RNjTaFzjT6INmI1dJ1uLbgPhVBcBEk9lbJ9BwyOWwBYpMuVFAgEuqMHb0/2qy/gme93j/ciA==}
engines: {node: '>=18'}
@@ -5822,6 +5916,22 @@ packages:
resolution: {integrity: sha512-VSdkwnJRr8Yv9UgB2aXB3VUPWwd6Oqnn0hycFwhg9pZgWxJXb7JmhsiXe9tmpMwjHFxli12PGcz9aI63YYloGQ==}
engines: {node: '>=18.0.0'}
'@wry/caches@1.0.1':
resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==}
engines: {node: '>=8'}
'@wry/context@0.7.4':
resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==}
engines: {node: '>=8'}
'@wry/equality@0.5.7':
resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==}
engines: {node: '>=8'}
'@wry/trie@0.5.0':
resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==}
engines: {node: '>=8'}
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -8890,6 +9000,9 @@ packages:
resolution: {integrity: sha512-YYamqKu48bZCSTJKSWLLO4SSk8tKN2Gg2z1sJZVzHJYVObMO/xQpIzAh6re9HCMHRdB1dJvBjJH18DW7xYOicg==}
engines: {node: ^22 || ^21 || ^20 || ^18 || ^16}
optimism@0.18.1:
resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -9408,6 +9521,17 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
rehackt@0.1.0:
resolution: {integrity: sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==}
peerDependencies:
'@types/react': '*'
react: '*'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
remedial@1.0.8:
resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==}
@@ -9969,6 +10093,11 @@ packages:
swap-case@2.0.2:
resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==}
swr@2.4.2:
resolution: {integrity: sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
symbol-observable@1.2.0:
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
engines: {node: '>=0.10.0'}
@@ -10211,6 +10340,10 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
ts-invariant@0.10.3:
resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==}
engines: {node: '>=8'}
ts-log@2.2.3:
resolution: {integrity: sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w==}
@@ -10494,6 +10627,11 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -10851,6 +10989,12 @@ packages:
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
engines: {node: '>=12.20'}
zen-observable-ts@1.2.5:
resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==}
zen-observable@0.8.15:
resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==}
zip-stream@6.0.1:
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
engines: {node: '>= 14'}
@@ -10948,6 +11092,30 @@ snapshots:
dependencies:
graphql: 16.14.2
'@apollo/client@3.14.1(@types/react@18.3.31)(graphql-ws@6.1.0(graphql@16.14.2)(ws@8.21.0))(graphql@16.14.2)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)(subscriptions-transport-ws@0.11.0(graphql@16.14.2))':
dependencies:
'@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2)
'@wry/caches': 1.0.1
'@wry/equality': 0.5.7
'@wry/trie': 0.5.0
graphql: 16.14.2
graphql-tag: 2.12.7(graphql@16.14.2)
hoist-non-react-statics: 3.3.2
optimism: 0.18.1
prop-types: 15.8.1
rehackt: 0.1.0(@types/react@18.3.31)(react@18.3.0)
symbol-observable: 4.0.0
ts-invariant: 0.10.3
tslib: 2.8.1
zen-observable-ts: 1.2.5
optionalDependencies:
graphql-ws: 6.1.0(graphql@16.14.2)(ws@8.21.0)
react: 18.3.0
react-dom: 18.3.0(react@18.3.0)
subscriptions-transport-ws: 0.11.0(graphql@16.14.2)
transitivePeerDependencies:
- '@types/react'
'@apollo/federation-internals@2.14.2(graphql@16.14.2)':
dependencies:
'@types/uuid': 9.0.8
@@ -15912,7 +16080,7 @@ snapshots:
transitivePeerDependencies:
- graphql
'@urql/next@1.1.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))':
'@urql/next@1.1.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(urql@4.2.0(@urql/core@5.0.0(graphql@16.14.2))(react@18.3.0))':
dependencies:
next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
react: 18.3.0
@@ -16131,6 +16299,22 @@ snapshots:
'@whatwg-node/promise-helpers': 1.3.2
tslib: 2.8.1
'@wry/caches@1.0.1':
dependencies:
tslib: 2.8.1
'@wry/context@0.7.4':
dependencies:
tslib: 2.8.1
'@wry/equality@0.5.7':
dependencies:
tslib: 2.8.1
'@wry/trie@0.5.0':
dependencies:
tslib: 2.8.1
'@xtuc/ieee754@1.2.0': {}
'@xtuc/long@4.2.2': {}
@@ -19414,7 +19598,7 @@ snapshots:
gauge: 3.0.2
set-blocking: 2.0.0
nuqs@1.19.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)):
nuqs@1.19.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)):
dependencies:
mitt: 3.0.1
next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0)
@@ -19474,6 +19658,13 @@ snapshots:
opossum@8.4.0: {}
optimism@0.18.1:
dependencies:
'@wry/caches': 1.0.1
'@wry/context': 0.7.4
'@wry/trie': 0.5.0
tslib: 2.8.1
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -20043,6 +20234,11 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
rehackt@0.1.0(@types/react@18.3.31)(react@18.3.0):
optionalDependencies:
'@types/react': 18.3.31
react: 18.3.0
remedial@1.0.8: {}
remove-trailing-separator@1.1.0: {}
@@ -20675,6 +20871,12 @@ snapshots:
dependencies:
tslib: 2.8.1
swr@2.4.2(react@18.3.0):
dependencies:
dequal: 2.0.3
react: 18.3.0
use-sync-external-store: 1.6.0(react@18.3.0)
symbol-observable@1.2.0: {}
symbol-observable@4.0.0: {}
@@ -20969,6 +21171,10 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-invariant@0.10.3:
dependencies:
tslib: 2.8.1
ts-log@2.2.3: {}
ts-morph@24.0.0:
@@ -21208,6 +21414,10 @@ snapshots:
dependencies:
react: 18.3.0
use-sync-external-store@1.6.0(react@18.3.0):
dependencies:
react: 18.3.0
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
@@ -21658,6 +21868,12 @@ snapshots:
yocto-queue@1.2.2: {}
zen-observable-ts@1.2.5:
dependencies:
zen-observable: 0.8.15
zen-observable@0.8.15: {}
zip-stream@6.0.1:
dependencies:
archiver-utils: 5.0.2